Skip to content

TideHook

TideHook is the heart of TIDE. A single contract is, simultaneously:

  • the TIDE ERC-20 token (name() = "Tide", symbol() = "TIDE"),
  • the Uniswap V4 hook for the canonical ETH/TIDE pool,
  • the LP owner of the one protocol-owned V4 position, and
  • the fee router that distributes swap fees pro-rata to NFT holders.

It also deploys and wires the TideMirror (the Tide-LP ERC-721 face), TideArt (on-chain SVG), and Treasury in its constructor. For the full architecture, see Architecture overview; for every event and custom error, see Events & custom errors.

All constants are public (so they are readable on-chain) except ACC_SCALE and the buyback slippage cap, which are private.

ConstantTypeValueMeaning
SUPPLYuint25610_000 ether (10000e18)Fixed total supply, minted once in the constructor. Never inflates.
UNITuint2561 ether (1e18)One whole TIDE = one Tide-LP NFT = a 1/10000 share of the pool.
TICK_SPACINGint24200Tick spacing of the canonical pool.
FEE_TIER_1uint24250_000First fee tier in pips (1e6 = 100%): 25%.
FEE_TIER_2uint24100_000Second fee tier: 10%.
FEE_FINALuint2450_000Steady-state fee: 5% forever.
ACC_SCALEuint256 (private)1e12Fixed-point scale of the per-share fee accumulators.
MAX_BUYBACK_SLIPPAGE_BPSuint256 (private)1000Slippage floor on the in-pool claim buyback: 10%.

The pool itself is keyed with the dynamic-fee flag (LPFeeLibrary.DYNAMIC_FEE_FLAG), which is what lets the hook override the fee per swap. Inside _beforeSwap, the per-swap override is returned OR’d with LPFeeLibrary.OVERRIDE_FEE_FLAG. See Degressive launch fee.

The four lifecycle durations are constructor immutables — there is no setter. Because they are part of the CREATE2 init code, changing any of them changes the mined hook address. The dapp reads them live from chain, so the same UI renders correctly whether durations are compressed (Sepolia) or full (mainnet). See Addresses & network for the live values.

ImmutableTypeProduction defaultMeaning
feeWindow1uint32300 (5 min)t < feeWindow1 → 25% fee.
feeWindow2uint32480 (8 min)feeWindow1 ≤ t < feeWindow2 → 10% fee; t ≥ feeWindow2 → 5%.
vestingDurationuint64259_200 (72 h)Linear vesting length applied to all claims.
prizeActivationWindowuint64172_800 (48 h)Window a lottery winner has to activate a prize before it expires to the Treasury.

Other immutables: POSM (V4 PositionManager), PERMIT2, mirror (TideMirror), art (TideArt), treasury (Treasury).

State variableTypeMeaning
seededbooltrue once seed() has run. The pool is one-shot.
launchTimeuint64Timestamp the pool was seeded; drives the fee schedule. 0 until seeded.
totalSharesuint256Count of live (minted, not burned) NFTs; the fee-distribution denominator.
accFeesPerShareETHuint256Cumulative ETH fees per share, scaled by ACC_SCALE. Monotonic.
accFeesPerShareTIDEuint256Cumulative TIDE fees per share, scaled by ACC_SCALE. Monotonic.
hookPositionTokenIduint256The V4 PositionManager token id of the protocol-owned position.
globalTickLowerint24Lower tick of the seeded range.
globalTickUpperint24Upper tick of the seeded range.
pendingETH / pendingTIDEmapping(address => uint256)Raw, not-yet-converted fees owed to a user.
vestsmapping(address => Vest)Per-user linear vesting position (denominated in TIDE).
pendingPrize / prizeAwardedAtmapping(address => uint256)Lottery prize amount and award timestamp.

Every function below is view (or pure) and free to call before connecting a wallet. Reverting functions are noted.

FunctionReturnsMeaning
name()string"Tide".
symbol()string"TIDE".
totalSupply()uint256Circulating TIDE (Solady ERC-20; starts at SUPPLY = 10000e18, only ever decreases via burns).
balanceOf(address)uint256TIDE balance of an address (Solady ERC-20).
totalSharesuint256Live NFT count = fee denominator.
totalMinted()uint256NFTs ever minted (monotonic; ignores burns).
FunctionReturnsMeaning
seededboolWhether the pool has been seeded.
launchTimeuint64Seed/launch timestamp (0 pre-launch).
currentFee()uint24The fee in pips applying to a swap right now (250_000 / 100_000 / 50_000). Returns FEE_FINAL pre-launch.
feeWindow1 / feeWindow2uint32Fee-tier window lengths (seconds).
vestingDurationuint64Vesting length (seconds).
prizeActivationWindowuint64Prize activation window (seconds).
accFeesPerShareETH / accFeesPerShareTIDEuint256Per-share fee accumulators (scaled by ACC_SCALE = 1e12).
poolKey()PoolKeyThe canonical V4 pool key (currency0 = address(0) native ETH, currency1 = address(this), dynamic fee, tickSpacing = 200, hooks = this).
globalTickLower / globalTickUpperint24Seeded range ticks.
hookPositionTokenIduint256PositionManager token id of the protocol position.

See 72-hour linear vesting and Claim = in-pool buyback.

FunctionReturnsMeaning
nftBalanceOf(address owner)uint256Live Tide-LP NFT count held (_ownedLength).
ownedTokensOf(address owner)uint256[]The owner’s owned tokenId array (view-only unpack).
claimableNow(address user)uint256TIDE withdrawable right now (claimable + linearly-vested-not-yet-withdrawn).
lockedOf(address user)uint256TIDE still locked (not yet vested).
vestEndsAt(address user)uint256Timestamp the current locked tranche finishes vesting (0 if nothing locked).
owedOf(address user)(uint256 ethOwed, uint256 tideOwed)Raw fees owed but not yet converted+vested = (pendingETH[user], pendingTIDE[user]).
prizeStatus(address user)(uint256 amount, uint256 expiresAt, bool expired)Lottery prize state. See The forfeited-vesting lottery.
FunctionReturnsMeaning
pendingFees(uint256 tokenId)(uint256 owedETH, uint256 owedTIDE)Fees owed to a tokenId since its last checkpoint. Excludes un-poked fees still inside the V4 position — call pokeFees() first for the freshest figure. Returns (0, 0) for a non-existent id.
nftOwnerOf(uint256 tokenId)addressOwner of a Tide-LP NFT. Reverts InvalidTokenId if none.
nftBalanceOf(address owner)uint256(also listed above) Live NFT count.
nftGetApproved(uint256 tokenId)addressSingle-token approval.
nftIsApprovedForAll(address o, address s)boolOperator approval.
nftTokenURI(uint256 tokenId)stringOn-chain art data-URI via TideArt. Reverts InvalidTokenId if none.
seedOf(uint256 tokenId)bytes32Deterministic art seed (pure function of (id, this)). Reverts InvalidTokenId if none.
FunctionGuardEffect
seed(uint160 sqrtPriceX96, int24 tickLower, int24 tickUpper, uint128 liquidity) returns (uint256 tokenId)onlyOwner, one-shotInitialize the pool and mint the single full-supply V4 position. Reverts AlreadySeeded on a second call.
pokeFees()none (permissionless)Pull accrued swap fees out of the V4 position into the per-share accumulators. Idempotent; early-returns if not seeded, totalShares == 0, or the pool is currently unlocked.
claim(uint256 tokenId)nonReentrantPoke fees, harvest that NFT’s accrual to its owner, then convert+vest only the caller’s owed.
claimMany(uint256[] tokenIds)nonReentrantPoke fees, harvest a batch to their owners, then convert+vest the caller’s owed.
processOwed()nonReentrantConvert+vest the caller’s owed without needing an NFT (for owed accrued via NFT moves/burns).
withdrawVested()nonReentrantWithdraw the linearly-unlocked portion of the caller’s vesting position (paid in TIDE). Does not restart the vesting clock.
activatePrize()nonReentrantActivate the caller’s pending lottery prize → re-vests it. Reverts NoActivatablePrize if none or the window passed.
expirePrize(address winner)nonReentrant, permissionlessRoute an un-activated, expired prize to the Treasury. Reverts NoActivatablePrize if still activatable.
burn(uint256 amount)Treasury-onlyBurn TIDE held by the Treasury. Reverts TreasuryOnly for any other caller; only ever burns Treasury-held supply.

The hook also inherits Solady ERC20 (transfer, transferFrom, approve, permit, …) and Solady Ownable (transferOwnership, renounceOwnership, …), and exposes receive() to accept native ETH for pool flows.

Mirror-only functions — never call from a client

Section titled “Mirror-only functions — never call from a client”

The hook exposes three onlyMirror mutating functions that perform NFT authorization. They take an explicit trusted caller argument injected by the mirror and are not meant to be called directly by clients.

function handleNFTTransfer(address from, address to, uint256 tokenId, address caller) external; // onlyMirror, nonReentrant
function handleNFTApprove(address spender, uint256 tokenId, address caller) external; // onlyMirror
function handleNFTSetApprovalForAll(address operator, bool approved, address caller) external; // onlyMirror

getHookPermissions() declares beforeSwap and afterSwap live. _beforeSwap overrides the LP fee per the degressive schedule (the hook’s own buyback, where sender == address(this), is fee-exempt); _afterSwap is a no-op reserved for future accounting. Both are onlyPoolManager via the vendored BaseHook. The ETH→TIDE buyback runs inside the hook’s own _unlockCallback, reachable only via the hook’s unlock (PoolManager-restricted).