Skip to content

Trust model & no-rug

TIDE’s central security claim is structural, not a promise: no one — not even the deployer — has a code path to seize user funds. This page walks through exactly why, contract by contract, and draws a sharp line between the few things the privileged role can do and the much larger set it provably cannot.

This is the “no-rug” half of the story. For the honest counterweight (the risks, the audit status, the accepted grief vectors, and the things that depend on external code) read known-limitations.md. Both pages are required reading; neither is complete without the other.

#GuaranteeEnforced by
1Fixed supply, minted once. No mint function._mint(address(this), SUPPLY) in the constructor; no other mint path
2The LP is protocol-owned; no dev withdrawal path.seed() mints the full position to the hook itself; nothing can move it out
3The fee schedule is immutable and enforced inside the hook.Hardcoded constants + _beforeSwap override; no setter
4Claims route through the pool to the NFT’s owner.claim / claimMany pay the current owner; no admin “collect to treasury” route
5The Treasury guardian can only time a buyback-burn.executeBuyback() burns; there is no withdrawal function

1. Fixed supply — 10_000 ether, minted once, never again

Section titled “1. Fixed supply — 10_000 ether, minted once, never again”

The entire supply is minted to the hook in the constructor and is then fixed forever:

uint256 public constant SUPPLY = 10_000 ether;
uint256 public constant UNIT = 1 ether;
// ...in the constructor:
_mint(address(this), SUPPLY);

There is no mint function anywhere in TideHook. The owner cannot inflate the supply, dilute holders, or pre-mint a hidden allocation — the only _mint call lives in the constructor and runs exactly once at deploy. Supply can only ever move in one direction: down, via the Treasury burn (guarantee 5). With a UNIT of 1 ether, the maximum number of Tide-LP NFTs is SUPPLY / UNIT = 10000, so each whole NFT is a fixed 1/10000 share of the pool that can never be diluted by new issuance.

2. The LP is protocol-owned — there is no withdrawal path

Section titled “2. The LP is protocol-owned — there is no withdrawal path”

At launch the owner calls seed() exactly once. It initializes the pool and mints the entire supply into a single Uniswap v4 position owned by the hook contract itself:

function seed(
uint160 sqrtPriceX96,
int24 tickLower,
int24 tickUpper,
uint128 liquidity
) external onlyOwner returns (uint256 tokenId) {
if (seeded) revert AlreadySeeded();
// ...initializePool + modifyLiquidities minting SUPPLY to address(this)
}

seed() is onlyOwner and one-shot: a second call reverts with AlreadySeeded. After it runs:

  • The position NFT (hookPositionTokenId) and all the liquidity belong to the hook. There is no function on TideHook that lets the owner decrease, transfer, migrate, or re-range that position. The owner does not hold the position — the contract does, and the contract has no code to give it away.
  • Holders also cannot withdraw the underlying liquidity. There is no “redeem to ETH” path; the only way to exit is to sell TIDE into the same pool. This is a real constraint on holders (see known-limitations.md), and it is the same property that makes the position un-rug-able: nobody — owner or holder — has a drain.

The owner’s sole residual influence at seed is the launch price and tick range (sqrtPriceX96, tickLower, tickUpper). That is an economic choice, not a custody power — a bad launch price hurts buyers, but it still cannot let the owner take funds afterward.

3. The fee schedule is immutable and enforced at the PoolManager

Section titled “3. The fee schedule is immutable and enforced at the PoolManager”

The degressive launch fee (25%10%5%) is built from hardcoded constants:

uint24 public constant FEE_TIER_1 = 250_000; // 25%
uint24 public constant FEE_TIER_2 = 100_000; // 10%
uint24 public constant FEE_FINAL = 50_000; // 5%

Only the window lengths (feeWindow1, feeWindow2) are immutable constructor parameters — fixed at deploy, with no setter. The tiers themselves are compile-time constants. So once deployed, nobody, including the owner, can change the fee schedule.

Crucially, the fee is enforced inside the hook at swap time, not by a router or UI. currentFee() computes the active tier from block.timestamp - launchTime, and _beforeSwap returns it to the PoolManager as a per-swap override. Because the pool’s fee field is LPFeeLibrary.DYNAMIC_FEE_FLAG and the override happens at the protocol level, the fee is router-agnostic and trustless — every swap through the canonical pool pays it regardless of which router, aggregator, or UI initiated the trade. The owner cannot exempt themselves, front-run their own schedule, or skim a different rate.

For the full mechanism, see ../mechanics/fee-schedule.md.

4. Claims route through the pool — no privileged drain of fees

Section titled “4. Claims route through the pool — no privileged drain of fees”

Swap fees accrue in two streams (ETH and TIDE) into per-share accumulators. The only way fees leave the system is through the permissionless claim functions, and they always pay the NFT’s current owner — never the caller, never an admin sink:

FunctionGuardWho gets paid
pokeFees()none (permissionless)nobody — just harvests fees into accumulators
claim(tokenId)nonReentrantthe current owner of tokenId
claimMany(tokenIds)nonReentranteach NFT’s current owner
processOwed()nonReentrantthe caller (their own owed only)
withdrawVested()nonReentrantthe caller (their own vested TIDE)

The defining security property here is an absence: there is no onlyOwner or onlyGuardian “collect fees to treasury” function. Fees can only flow to NFT holders. A griefer who spams pokeFees() accomplishes nothing but wasting their own gas. And because claim pays the owner but processes only the caller’s owed buckets, you cannot restart a victim’s vesting clock by claiming their NFT.

See ../mechanics/claim-and-buyback.md for the claim flow.

5. The Treasury — the guardian holds the button, not the cash

Section titled “5. The Treasury — the guardian holds the button, not the cash”

Expired, un-activated lottery prizes route to the Treasury. The guardian (the dev) has exactly two powers, and neither is custodial:

function executeBuyback() external onlyGuardian {
uint256 bal = tide.balanceOf(address(this));
if (bal > 0) tide.burn(bal);
emit BuybackBurned(bal);
}
function transferGuardian(address to) external onlyGuardian {
if (to == address(0)) revert ZeroGuardian();
emit GuardianTransferred(guardian, to);
guardian = to;
}

executeBuyback() burns all TIDE the Treasury holds — supply reduction only, which raises every remaining holder’s share. transferGuardian() hands the role to another address (e.g. a multisig or timelock); the new guardian still can only burn.

There is no withdrawal function on the Treasury. receive() accepts ETH but no path sends it back out. The burn flows through the hook’s burn(amount), which is gated to the Treasury and can only touch the Treasury’s own balance:

function burn(uint256 amount) external {
if (msg.sender != address(treasury)) revert TreasuryOnly();
_burn(address(treasury), amount);
}

This is the “the dev holds the button, not the cash” property: the guardian controls the timing of supply reduction, never the custody of any funds. The contract is structurally incapable of seizing user value, which also keeps it clean for honeypot/rug detectors. The one minor residual edge — that the guardian knows the burn timing in advance — is disclosed in known-limitations.md.

The dev CANThe dev CANNOT
Call seed() once to initialize the pool and choose the launch price + tick rangeCall seed() again — reverts AlreadySeeded
Time executeBuyback() whenever they choose (burns Treasury TIDE → shrinks supply)Withdraw ETH or TIDE from the Treasury — no withdrawal function exists
Hand the guardian role to another address via transferGuardian()Withdraw, move, migrate, or re-range the LP position after seed
Use standard Solady Ownable functions (e.g. transfer/renounce ownership)Mint new TIDE — no mint function exists outside the constructor
Change the fee schedule or window lengths — no setter
Redirect, pause, or skim fees — fees flow only to NFT holders via claim
Pause, freeze, or upgrade the contracts — there is no proxy, no admin pause, no upgradeability

In short: post-seed(), there is no onlyOwner or onlyGuardian path that touches user value, except the Treasury burn — and the burn can only destroy the Treasury’s own TIDE, never extract anything to anyone.

Honesty about what was rejected is part of the trust model:

  • No transfer tax. It would be the only way to fully close fee-evasion via competing pools, but it flags as a scam on detectors and breaks composability (CEX deposits, bridges, other DeFi). Rejected on purpose.
  • No “emergency mode.” A dev-controlled freeze-and-recover switch was rejected outright — it is indistinguishable from a rug-pull backdoor.

The trade-off is explicit: TIDE accepts some fee-leakage and the inability to “save” funds in an emergency, in exchange for a contract with no custody and no kill switch. The cost of that choice — and every other honest caveat — is laid out in known-limitations.md.

The strongest form of trust here is the kind you don’t have to extend: read the verified source, confirm there is no mint, no fee setter, no LP withdrawal, and no Treasury drain, and you have verified the no-rug claim without trusting anyone’s word for it.