Known limitations
TIDE documents its own risks in the open. Honesty is a feature here: this page is the complete inventory of every limitation that the codebase and SECURITY.md actually support, written plainly so you can size your position with full information.
For what the protocol cannot do to you (the no-rug invariants), see trust-model.md. This page covers the sharp edges that remain even given that trust model.
Testnet-only status
Section titled “Testnet-only status”The only live deployment is on Ethereum Sepolia (chain id 11155111), see ../deployment/addresses.md. This is a deliberate dress rehearsal: no real funds are at stake, and it lets the full lifecycle be watched end to end before any production launch.
The live Sepolia deployment runs compressed lifecycle durations so the full lifecycle can be watched in real time, instead of the production durations:
| Immutable | Live Sepolia | Production target |
|---|---|---|
feeWindow1 | 300 s (5 min) | 300 s (5 min) |
feeWindow2 | 480 s (8 min) | 480 s (8 min) |
vestingDuration | 300 s (~5 min) | 259_200 s (72 h) |
prizeActivationWindow | 600 s (~10 min) | 172_800 s (48 h) |
These four durations are constructor immutables and are part of the hook’s CREATE2 init code, so changing any of them changes the mined hook address. Going to mainnet therefore re-mines and redeploys the hook — the current Sepolia addresses are not the mainnet addresses. The dapp reads these durations live from the chain (feeWindow1, feeWindow2, vestingDuration, prizeActivationWindow), so it renders correctly under either set of values without hardcoding.
Accepted pool-initialization griefing vector
Section titled “Accepted pool-initialization griefing vector”This is a known limitation, deliberately carried for the current testnet phase and documented in the open.
The launch is performed by the one-shot seed() call, which does a single IPositionManager.multicall that (1) calls initializePool(key, sqrtPriceX96) on the Uniswap v4 PoolManager and (2) mints the full-supply position into that pool. The pool key is fixed and public:
PoolKey({ currency0: Currency.wrap(address(0)), // native ETH currency1: Currency.wrap(address(this)), // TIDE fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, tickSpacing: TICK_SPACING, // 200 hooks: IHooks(address(this))})The problem: initializePool on the v4 PoolManager is permissionless, and a v4 pool can only be initialized once. Because the hook address (and therefore the entire PoolKey) is known before seed() runs, an attacker can front-run the deployer and call initializePool for that exact key first, at a price of their choosing. After that the deployer’s own seed() multicall can no longer initialize the same pool — the launch is griefed at the initialization step.
Why it is accepted for now:
- This is a liveness/launch-integrity griefing risk, not a path to seize user funds. It cannot rug a seeded pool; it only interferes with getting the pool seeded.
- The mitigating choice (initializing and seeding atomically, or otherwise hardening the launch transaction) was weighed against the cost of carrying it on testnet, and the team chose to leave it as a documented, deliberately unaddressed limitation for the current stage.
prevrandao-based lottery randomness
Section titled “prevrandao-based lottery randomness”The forfeited-vesting lottery draws its winner with on-chain pseudo-randomness, not a verifiable random function. The draw seed is:
keccak256(abi.encode(block.prevrandao, block.timestamp, ++_drawNonce, ex1, minted))and the draw probes up to LOTTERY_PROBES = 128 token ids looking for a live, eligible owner.
Two distinct limitations follow from this:
- Validator-biasable randomness.
block.prevrandaois influenced by the proposing validator. For a sufficiently large forfeit, a validator could bias the draw at the margin. This is accepted for what is a low-value, ludic mechanic; a hardened version would use a verifiable source such as Chainlink VRF. - Variable lottery gas (observed live). Because the RNG decides how many probes actually run before finding an eligible winner, the gas cost of a sale that triggers the lottery is variable. A gas estimate can under-provision, and a sale that triggers the draw can revert with out-of-gas — this was observed in live testing. It needs to be flattened to constant gas before any mainnet launch.
Buyback MEV (sandwichable claims)
Section titled “Buyback MEV (sandwichable claims)”When you claim, your ETH-denominated fees are converted to TIDE by an in-pool buyback rather than paid out as ETH — see claim = in-pool buyback. That buyback derives its slippage floor from the spot price (getSlot0) with a fixed band:
MAX_BUYBACK_SLIPPAGE_BPS = 1000 // 10%minOut = expected * (10_000 - 1000) / 10_000;Because minOut comes from spot price with a 10% band, large claims are sandwichable. The mitigation today is behavioral — keep per-claim amounts small. A future hardening would price the buyback off a TWAP oracle, or let the caller supply their own minOut.
Silent no-op writes (handled, not hidden)
Section titled “Silent no-op writes (handled, not hidden)”Several claim/vesting writes are designed to no-op on-chain rather than revert when there is nothing to do — for example calling pokeFees() when no new fees have accrued, withdrawVested() when nothing has unlocked, or claim/processOwed when you are owed nothing. The transaction succeeds but changes no state.
This is not a vulnerability, but it is a usability sharp edge: a naive UI would show a “success” toast for a transaction that did nothing. The dapp handles this by inspecting the emitted events in the receipt and surfacing an honest “nothing to do” message instead of a false success:
| Action | Event checked | Message when absent |
|---|---|---|
claim / claimMany | OwedProcessed or Claimed | ”No fees were available to claim yet.” |
pokeFees | FeesPoked | ”No new fees were available to sync.” |
withdrawVested | VestWithdrawn | ”Nothing was unlocked to withdraw yet.” |
processOwed | OwedProcessed | ”You had no owed fees to convert.” |
If you interact with the contracts directly (not through the dapp), be aware that a mined, successful transaction does not by itself confirm that fees were claimed, synced, or withdrawn — check for the corresponding event.
Other accepted trade-offs
Section titled “Other accepted trade-offs”These are deliberate design choices, disclosed here so they are not surprises.
Liquidity-fragmentation / fee evasion
Section titled “Liquidity-fragmentation / fee evasion”The degressive fee binds only liquidity in the hooked pool. A competing TIDE pool (Uniswap V2/V3, or a hookless v4 pool) lets trades route around the fee. This is inherent to every hook-fee token. The only complete fix — an ERC20 transfer tax — is explicitly rejected, because it flags as a scam on detectors (Honeypot.is, GoPlus, De.Fi) and breaks composability (CEX deposits, bridges, other DeFi). The defense is economic and social: all protocol liquidity stays single-sided in the canonical pool and never seeds a competitor, so alternative pools are shallow and aggregators route size to the deep canonical venue. See fee schedule.
Single-sided launch price impact
Section titled “Single-sided launch price impact”The pool launches single-sided — the entire supply and zero ETH in one position whose range sits above the initial tick. The initial price is set solely by sqrtPriceX96 at seed time; there is no oracle and no external price source enforcing fairness — the curve is the price. First buyers walk the price along the range, and the position can never be re-ranged. A careless or malicious owner can launch at a bad price (but cannot rug the position afterward). Holders cannot withdraw the underlying; they exit only by selling TIDE into the same pool. This is the single most consequential parameter choice in the system. See trust-model.md for why it is not a custody power.
Vesting clock restart on new claims
Section titled “Vesting clock restart on new claims”A new claim re-locks the still-locked remainder over a fresh vestingDuration (an intentional anti-claim-and-dump measure). A withdrawal of already-vested TIDE does not restart the clock. This is disclosed behavior, not a bug — see vesting.
External dependencies
Section titled “External dependencies”TIDE trusts canonical Uniswap v4 infrastructure — PoolManager, PositionManager, and Permit2 — plus Solady’s ERC20 and the vendored src/base/BaseHook.sol. A bug or compromise in any of these is out of TIDE’s control and is part of your own risk assessment.