Architecture overview
TIDE is a small set of contracts arranged around a single idea: the token contract is the liquidity position. There is no staking contract, no vault, no LP-token wrapper. The whole fixed supply lives inside one Uniswap V4 position owned by the hook itself, and holding the token is what entitles you to a pro-rata cut of that position’s swap fees.
This page maps the five contracts, explains the DN404 split that makes one position present both an ERC-20 and an ERC-721 face, and notes the Uniswap V4 specifics that tie them together. For the live on-chain values, see Addresses & network.
The five contracts
Section titled “The five contracts”| Contract | Responsibility | Live address |
|---|---|---|
TideHook | The ERC-20 TIDE token, the Uniswap V4 hook, the owner of the single LP position, the fee router (accrual + claim), the vesting engine, and the lottery. All canonical state lives here. | 0xF6F88E408Ea5df8a809a3b4232b9Ef7f2a9d40c0 |
TideMirror | The canonical ERC-721 Tide-LP (TIDE-LP). A stateless DN404-style mirror reached as hook.mirror(); it presents the 721 surface and emits the 721 events, forwarding every mutation to the hook. | 0x4c08F0B24254BE677924C5655Ca1706Feb8259F4 |
TideArt | Pure, on-chain generative SVG. tokenURI(tokenId, seed) is deterministic and pure — no storage, no IPFS, no server. Split out of the hook to keep TideHook under the EIP-170 size limit. | 0xE08Cb2BDa6517573FD6DB93B300f54CBcd5fdcf4 |
Treasury | Buyback-burn sink. Holds queued TIDE and burns it on the guardian’s command. Has no withdrawal function — the only outward action is burning. | 0x076f29063199DB470D260E5cE2cb560Af98cfBd3 |
BaseHook | Vendored Uniswap V4 hook scaffolding (abstract). Routes the V4 callbacks to onlyPoolManager internal virtuals and restricts the unlock callback to the PoolManager. TideHook inherits it. | (inherited by TideHook; not separately deployed) |
Detailed reference pages: TideHook · TideMirror · TideArt · Treasury. Every event and custom error is enumerated in Events & custom errors.
The DN404 split: one position, two faces
Section titled “The DN404 split: one position, two faces”The defining structural choice is a DN404-style mirror (inspired by Vectorized’s DN404). A single share of state is presented through two contracts at two different addresses:
- The ERC-20 face (
TIDE) is the hook itself.TideHookis the ERC-20.balanceOf,transfer,approve, and the rest live at the hook address. - The ERC-721 face (
Tide-LP, symbolTIDE-LP) isTideMirror, reached ashook.mirror(). It is a separate contract address — this is the address wallets and marketplaces treat as “the NFT collection.”
// Two addresses, one shared ledger:// ERC-20 TIDE → TideHook (the hook itself)// ERC-721 Tide-LP → hook.mirror() (a separate contract)The two are kept in lockstep by the UNIT threshold. UNIT is 1 ether (1e18), so every whole TIDE you hold materializes exactly one Tide-LP NFT:
uint256 public constant SUPPLY = 10_000 ether; // 10,000 TIDE, minted onceuint256 public constant UNIT = 1 ether; // 1 whole TIDE ⇆ 1 Tide-LP NFTThe invariant the protocol drives toward is nftBalanceOf(user) == balanceOf(user) / UNIT (floor). Buying TIDE across a whole-UNIT boundary mints an NFT; selling or transferring below one burns the tail NFT, concentrating fees onto the remaining live shares. Because SUPPLY / UNIT == 10_000, each whole NFT is a 1/10,000 share of the single pool, and that share is the unit of fee accounting.
Where actions and events go
Section titled “Where actions and events go”The split has one rule that matters for integrators:
- User-facing NFT actions go to the mirror. Call
transferFrom,safeTransferFrom,approve, andsetApprovalForAllonhook.mirror(). Each forwards to the hook (e.g.transferFrom→handleNFTTransfer(from, to, tokenId, msg.sender)), which moves both the NFT state and the matching1 UNITof TIDE. - Canonical 721 events originate from the mirror’s address. Because the mirror is stateless, the hook mutates balances and then calls back into the mirror’s
onlyHookemitters (emitTransfer,emitApproval,emitApprovalForAll) so thatTransfer/Approval/ApprovalForAllare logged from the canonical ERC-721 address that indexers watch. A mint isTransfer(0x0 → owner). - The hook’s
handleNFT*functions areonlyMirror— never call them from a client. They take a trustedcallerargument injected by the mirror; calling them directly would let you spoof it. The hook’snft*functions (nftOwnerOf,nftBalanceOf,nftTokenURI, …) are safe read-only views you may call directly.
See TideMirror for the full forwarding table and TideArt for how nftTokenURI resolves art.
How the pieces relate at runtime
Section titled “How the pieces relate at runtime” swaps ┌──────────────────────────────┐ PoolManager ◀────────▶│ TideHook │ (Uniswap V4) │ ERC-20 TIDE · V4 hook · │ ▲ │ LP position owner · │ │ single pool │ fee router · vesting · │ │ ETH / TIDE │ lottery │ │ └──┬───────────┬─────────────┬──┘ │ forfeits/expiry │ art │ 721 face │ │ ▼ ▼ ▼ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ └──────────────│ Treasury│ │ TideArt │ │TideMirror│ buyback-burn │ (burn- │ │ (pure, │ │ (Tide-LP │ (ETH→TIDE) │ only) │ │ on-chain│ │ ERC-721)│ └─────────┘ │ SVG) │ └──────────┘ └─────────┘TideHook↔PoolManager: every swap calls the hook’s_beforeSwap, which overrides the LP fee with the current degressive tier. Fees accrue inside the position;pokeFees()pulls them out into per-share accumulators. Claims and Treasury buybacks swap ETH→TIDE through the same pool via the hook’s ownunlockcallback.TideHook→TideArt:nftTokenURI(tokenId)delegates toTideArt.tokenURI(tokenId, seed), whereseedis the deterministic per-token seed (seedOf). The art is fully on-chain andpure.TideHook→Treasury: expired lottery prizes (and the no-eligible-winner fallback) route forfeited TIDE to the Treasury. The guardian callsexecuteBuyback(), which calls back intohook.burn(amount)(gatedTreasuryOnly) to reduce supply.TideHook↔TideMirror: the round-trip described above — user calls land on the mirror, forward to the hook, and the hook calls back to emit canonical 721 events.
Uniswap V4 specifics
Section titled “Uniswap V4 specifics”There is exactly one pool, and the hook owns the entire liquidity position in it. The pool key is fixed and exposed via poolKey():
| Field | Value | Notes |
|---|---|---|
currency0 | address(0) | Native ETH |
currency1 | address(this) | The TIDE token (the hook) |
fee | LPFeeLibrary.DYNAMIC_FEE_FLAG | Marks the pool as dynamic-fee, which is what lets _beforeSwap override the fee per swap |
tickSpacing | TICK_SPACING = 200 | |
hooks | address(this) | The hook is its own pool’s hook |
The hook declares exactly two permissions in getHookPermissions():
p.beforeSwap = true; // override the LP fee per the degressive launch schedulep.afterSwap = true; // reserved; a no-op todayOn every swap, _beforeSwap returns the fee for the current tier OR-ed with LPFeeLibrary.OVERRIDE_FEE_FLAG. Because the fee is enforced inside the hook at the PoolManager level, it is router-agnostic and trustless: every swap through the canonical pool pays it regardless of which router or aggregator initiated the trade, and there is no setter to change the schedule. The protocol’s own buyback (sender == address(this)) is fee-exempt.
The no-rug shape, structurally
Section titled “The no-rug shape, structurally”The architecture is what makes the trust story enforceable rather than promised:
- Fixed supply.
SUPPLYis minted once in the constructor. There is no mint path; the owner cannot inflate. Supply only ever goes down, via Treasury buyback-burn. - One privileged action. The owner’s only special power is the one-shot
seed()(revertsAlreadySeededafterward). Post-seed there is no path to move, withdraw, or migrate the position, no fee-skim, no pause, and no proxy/upgrade. - The Treasury cannot pay anyone out. Its only state-changing externals are
executeBuyback()(burn) andtransferGuardian(). There is no withdrawal function — “the dev holds the button, not the cashbox.”
For the full treatment, see Trust model & no-rug and the honestly-documented gaps in Known limitations.