Skip to main content

The v4 hook

CovenantHook implements four Uniswap v4 callbacks:

CallbackRole
afterInitializeBinds the canonical pool and computes the launch band
beforeAddLiquidityBlocks external liquidity — the position stays protocol-owned
beforeSwapLevies the tax; on a sell, withholds the 20% — unless the seller is exempt
afterSwapSettles the ETH tax and registers the bond

A sell is exempt from the retention when two conditions hold. First, the actor must be provably the seller, by either of two anchors. Either the swap arrived through the official router, wired once through initRouter: the swap's sender is supplied by Uniswap itself and cannot be forged, and the router always encodes its own caller as the seller. Or the actor sent out at least the amount being sold in the same transaction: the token reports every outbound move to CovenantBonds, which tallies it for the length of the transaction, and consumeExemption spends that tally — so one debit justifies exactly one exemption. Second, the seller's remaining allowance — exemptAllowanceOf, five times their commit principal minus what previous exempt sales consumed — must cover the whole amount. afterSwap burns the allowance in the same transaction that granted it, so the two can never disagree.

Naming somebody else's wallet in hook data satisfies neither anchor: that wallet did not move. Neither does signing the transaction. Sharing a transaction proves nothing about whose tokens are leaving, so a contract running inside a holder's transaction must never inherit their allowance — the allowance and the tokens have to belong to the same wallet.

Two consequences follow, and both are stated rather than glossed. A contract a seller routes a sale through sits inside a transaction where the seller really is debiting CVN, so it can aim that one debit at a sale of its own; it is bounded to one by the spend, and a contract already holding a token approval can take the tokens outright, which is worse. And Uniswap v4 settles after the hook has decided, so a route that pays the pool straight out of the seller's wallet at the end has moved nothing yet when the question is asked, and that sale pays the retention. Routes that take the tokens first — the official router, and the ordinary aggregator shape — are unaffected.

Everything the hook cannot vouch for gets the default mechanism, and the 20% comes back as a bond rather than disappearing. That is the fail-safe direction throughout: an unwired router and an untouched balance both exempt nobody.

The allowance is carried by the position, not by the address: when a commit bond changes hands, the share of the quota it has already spent moves with it. Without that, a single transfer to a fresh wallet would reopen a full 5x, and "lifetime" would mean nothing.

The launch band

The seeded position runs from the floor tick up to the launch price exactly. With spot sitting on the upper bound, the position holds only tokens — so the launch needs no paired ETH, and buyers bring it themselves.

The launch price must sit exactly on the tick grid. If it does not, spot lands above the band and the first buyer walks a dead, zero-liquidity gap for free. Deployment is rejected outright in that case.

Atomic launch

initializeAndSeed creates the pool and seeds it in one transaction. Splitting the two leaves the pool live with zero liquidity, and a v4 swap against zero liquidity consumes no input — meaning anyone, holding nothing at all, could move the price for free and dictate what price the creator seeded at. Measured before the fix: 0.01 ETH bought the entire seeded supply.

seed also takes the expected price and reverts if it moved, as a second layer.

Guards

  • Exact-input only. Exact-output orders revert.
  • Partial fills rejected. A sell that only partly executes reverts, which removes the gap between the amount requested and the amount actually traded.
  • Callbacks are PoolManager-only.
  • The pool binds once, to one key, with ETH as currency0 and CVN as currency1.