Skip to content

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.

NetworkAddress
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.

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).

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.

FunctionTypeForwards to (hook)
ownerOf(uint256 tokenId)viewnftOwnerOf(tokenId)
balanceOf(address owner)viewnftBalanceOf(owner)
tokenURI(uint256 tokenId)viewnftTokenURI(tokenId)
getApproved(uint256 tokenId)viewnftGetApproved(tokenId)
isApprovedForAll(address owner, address operator)viewnftIsApprovedForAll(owner, operator)
supportsInterface(bytes4 id)pure— (answered locally)
approve(address to, uint256 tokenId)mutatinghandleNFTApprove(to, tokenId, msg.sender)
setApprovalForAll(address operator, bool approved)mutatinghandleNFTSetApprovalForAll(operator, approved, msg.sender)
transferFrom(address from, address to, uint256 tokenId)mutatinghandleNFTTransfer(from, to, tokenId, msg.sender)
safeTransferFrom(address from, address to, uint256 tokenId)mutatingcalls safeTransferFrom(..., "")
safeTransferFrom(address from, address to, uint256 tokenId, bytes data)mutatingtransferFrom + 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.

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(bytes4 id) is answered directly on the mirror (it needs no hook state) and returns true for:

Interface IDStandard
0x01ffc9a7ERC-165
0x80ac58cdERC-721
0x5b5e139fERC-721 Metadata

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:

EventSignature
TransferTransfer(address indexed from, address indexed to, uint256 indexed tokenId)
ApprovalApproval(address indexed owner, address indexed approved, uint256 indexed tokenId)
ApprovalForAllApprovalForAll(address indexed owner, address indexed operator, bool approved)
ErrorMeaning
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.

  • 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. The handleNFT* functions are onlyMirror and 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).