Skip to content

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.

ContractResponsibilityLive address
TideHookThe 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
TideMirrorThe 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
TideArtPure, 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
TreasuryBuyback-burn sink. Holds queued TIDE and burns it on the guardian’s command. Has no withdrawal function — the only outward action is burning.0x076f29063199DB470D260E5cE2cb560Af98cfBd3
BaseHookVendored 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 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. TideHook is the ERC-20. balanceOf, transfer, approve, and the rest live at the hook address.
  • The ERC-721 face (Tide-LP, symbol TIDE-LP) is TideMirror, reached as hook.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 once
uint256 public constant UNIT = 1 ether; // 1 whole TIDE ⇆ 1 Tide-LP NFT

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

The split has one rule that matters for integrators:

  • User-facing NFT actions go to the mirror. Call transferFrom, safeTransferFrom, approve, and setApprovalForAll on hook.mirror(). Each forwards to the hook (e.g. transferFromhandleNFTTransfer(from, to, tokenId, msg.sender)), which moves both the NFT state and the matching 1 UNIT of 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 onlyHook emitters (emitTransfer, emitApproval, emitApprovalForAll) so that Transfer/Approval/ApprovalForAll are logged from the canonical ERC-721 address that indexers watch. A mint is Transfer(0x0 → owner).
  • The hook’s handleNFT* functions are onlyMirror — never call them from a client. They take a trusted caller argument injected by the mirror; calling them directly would let you spoof it. The hook’s nft* 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.

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) │ └──────────┘
└─────────┘
  • TideHookPoolManager: 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 own unlock callback.
  • TideHookTideArt: nftTokenURI(tokenId) delegates to TideArt.tokenURI(tokenId, seed), where seed is the deterministic per-token seed (seedOf). The art is fully on-chain and pure.
  • TideHookTreasury: expired lottery prizes (and the no-eligible-winner fallback) route forfeited TIDE to the Treasury. The guardian calls executeBuyback(), which calls back into hook.burn(amount) (gated TreasuryOnly) to reduce supply.
  • TideHookTideMirror: 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.

There is exactly one pool, and the hook owns the entire liquidity position in it. The pool key is fixed and exposed via poolKey():

FieldValueNotes
currency0address(0)Native ETH
currency1address(this)The TIDE token (the hook)
feeLPFeeLibrary.DYNAMIC_FEE_FLAGMarks the pool as dynamic-fee, which is what lets _beforeSwap override the fee per swap
tickSpacingTICK_SPACING = 200
hooksaddress(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 schedule
p.afterSwap = true; // reserved; a no-op today

On 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 architecture is what makes the trust story enforceable rather than promised:

  • Fixed supply. SUPPLY is 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() (reverts AlreadySeeded afterward). 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) and transferGuardian(). 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.