TideMirror (Tide-LP NFT)
TideMirror is the canonical ERC-721 contract for the Tide-LP collection. It is the address wallets, marketplaces, and indexers treat as “the NFT” — name is "Tide-LP", symbol is "TIDE-LP", and it emits every standard Transfer, Approval, and ApprovalForAll event.
It is reached as hook.mirror() — it is a separate contract from TideHook, which is the ERC-20 ("Tide" / TIDE). The two are the fungible and non-fungible faces of the same liquidity position, joined by a DN404-style mirror.
Where it lives
Section titled “Where it lives”| Network | Address |
|---|---|
Ethereum Sepolia (11155111) | 0x4c08F0B24254BE677924C5655Ca1706Feb8259F4 |
The mirror points back to the hook through one immutable:
address public immutable hook; // set once in constructor(address _hook)It is deployed by the hook’s constructor, so hook.mirror() always resolves to this contract, and TideMirror.hook always resolves back to the hook. See Addresses & network for the full deployment table.
How the mirror works
Section titled “How the mirror works”Because the mirror holds no state, it cannot decide who owns what — the hook does. Every user-facing mutation on the mirror forwards to a matching handleNFT* function on the hook, passing the original msg.sender as an explicit caller argument. The hook performs the authorization, mutates state, and — because it cannot emit events from the mirror’s address — calls back into the mirror’s onlyHook event emitters so the Transfer / Approval / ApprovalForAll logs originate from the canonical ERC-721 address.
A full transferFrom round-trip:
client └─▶ TideMirror.transferFrom(from, to, id) └─▶ hook.handleNFTTransfer(from, to, id, msg.sender) // onlyMirror — authorizes & moves state (+1 TIDE) └─▶ TideMirror.emitTransfer(from, to, id) // onlyHook — logs Transfer(from, to, id)Moving an NFT also moves 1 TIDE ERC-20 on the hook, keeping the nftBalanceOf == balanceOf / UNIT invariant intact (see Hold = LP (DN404)). A mint is the same round-trip with from = address(0) → Transfer(0x0, owner, id).
ERC-721 surface
Section titled “ERC-721 surface”All of the following live on TideMirror. The view functions forward to the hook’s nft* views; the mutating functions forward to the hook’s handleNFT* family.
| Function | Type | Forwards to (hook) |
|---|---|---|
ownerOf(uint256 tokenId) | view | nftOwnerOf(tokenId) |
balanceOf(address owner) | view | nftBalanceOf(owner) |
tokenURI(uint256 tokenId) | view | nftTokenURI(tokenId) |
getApproved(uint256 tokenId) | view | nftGetApproved(tokenId) |
isApprovedForAll(address owner, address operator) | view | nftIsApprovedForAll(owner, operator) |
supportsInterface(bytes4 id) | pure | — (answered locally) |
approve(address to, uint256 tokenId) | mutating | handleNFTApprove(to, tokenId, msg.sender) |
setApprovalForAll(address operator, bool approved) | mutating | handleNFTSetApprovalForAll(operator, approved, msg.sender) |
transferFrom(address from, address to, uint256 tokenId) | mutating | handleNFTTransfer(from, to, tokenId, msg.sender) |
safeTransferFrom(address from, address to, uint256 tokenId) | mutating | calls safeTransferFrom(..., "") |
safeTransferFrom(address from, address to, uint256 tokenId, bytes data) | mutating | transferFrom + receiver check |
ownerOf reverts on the hook side (InvalidTokenId) for an unminted or burned id. tokenURI returns the fully on-chain Base64 data URI rendered by TideArt.
safeTransferFrom receiver check
Section titled “safeTransferFrom receiver check”The 4-argument safeTransferFrom runs transferFrom first, then — only if the recipient has code (to.code.length > 0) — calls IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data). It reverts with NonERC721Receiver() if the call throws or returns the wrong selector. The 3-argument overload simply forwards with empty data.
supportsInterface
Section titled “supportsInterface”supportsInterface(bytes4 id) is answered directly on the mirror (it needs no hook state) and returns true for:
| Interface ID | Standard |
|---|---|
0x01ffc9a7 | ERC-165 |
0x80ac58cd | ERC-721 |
0x5b5e139f | ERC-721 Metadata |
Hook → mirror event emitters
Section titled “Hook → mirror event emitters”These three functions exist only so the hook can log ERC-721 events from the mirror’s address. They are gated onlyHook and revert OnlyHook() otherwise. Clients never call them.
function emitTransfer(address from, address to, uint256 tokenId) external onlyHook;function emitApproval(address owner, address approved, uint256 tokenId) external onlyHook;function emitApprovalForAll(address owner, address operator, bool approved) external onlyHook;Each emits the matching standard event:
| Event | Signature |
|---|---|
Transfer | Transfer(address indexed from, address indexed to, uint256 indexed tokenId) |
Approval | Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) |
ApprovalForAll | ApprovalForAll(address indexed owner, address indexed operator, bool approved) |
Custom errors
Section titled “Custom errors”| Error | Meaning |
|---|---|
OnlyHook() | An emit* callback was invoked by an address other than the hook. |
NonERC721Receiver() | A safeTransferFrom recipient contract rejected the token or returned the wrong selector. |
Authorization errors for transfers and approvals (e.g. NotOwnerOrApproved, TransferToZero, SelfTransferDisallowed, InvalidTokenId) are raised on the hook side inside the handleNFT* functions, not here. See Events & custom errors for the complete catalogue.
For integrators
Section titled “For integrators”- Treat
hook.mirror()as the NFT collection. All wallet, marketplace, and explorer interactions — reads and writes — go through this contract. - Read on the mirror or on the hook’s
nft*views; never write through the hook directly. ThehandleNFT*functions areonlyMirrorand must be reached via the mirror. - The NFT and the ERC-20 are linked. Acquiring a whole TIDE auto-mints one Tide-LP NFT; selling or transferring it moves the paired TIDE and can forfeit unvested rewards to the lottery (see Hold = LP (DN404) and The forfeited-vesting lottery).