The Uniswap v4 hook
CovenantHook is the brain of the protocol. The entire mechanism is defined by three callbacks;
everything else is parameters.
The callbacks
| Callback | Phase | What it does |
|---|---|---|
beforeSwap | pre-settlement | Detects buy/sell direction, levies the 3% ETH tax, absorbs the covenant queue on buys, and retains 20% of the net on sells — all via returned deltas. |
afterSwap | post-settlement | On sells: skims the creator's ETH share from the realized output and books the covenant (size, frozen discount, expiry) into the queue. |
expireMany / expireOne | permissionless | Converts matured covenant debt plus its share of reserve ETH into the locked protocol-owned LP position. |
The hook returns deltas, so it needs the v4 permissions beforeSwapReturnDelta and
afterSwapReturnDelta (the latter is required to take ETH out of a sell's realized output).
Delta accounting
Covenant is exact-input only (ExactOutputNotSupported reverts exact-output swaps). The delta
convention:
- BUY —
beforeSwapreturnsdeltaSpecified = +(tax + ethUsed)anddeltaUnspecified = -tokensFromCovenant. The tax is accrued as an ETH claim for the creator (flushed byflushTax, which the router calls after each swap); absorbed ETH becomes the vault's claim; absorbed CVN is delivered by burning the vault's claims. - SELL —
beforeSwapreturnsdeltaSpecified = +covenantTokens(the retention). The 3% ETH tax is skimmed from the realized output inafterSwap(hookDeltaUnspecified = +ethToCreator) and paid to the creator immediately.
Immutable configuration
The hook's configuration is fixed at deployment — there are no admin setters:
| Immutable | Meaning |
|---|---|
creator | The 3% tax recipient and seed operator. Cannot be changed. |
token | currency1 — the CVN token. |
queue | The CovenantQueue address. |
vault | The CovenantVault address. |
poolInitializer | The only account allowed to bind the canonical pool. |
The poolInitializer front-run guard
The hook's address is a mined CREATE2 address known before the pool exists. Without a guard, an
open afterInitialize could be front-run between deploy and initialize — binding the hook to a junk
fee tier or price and bricking the deployment. So afterInitialize requires
sender == poolInitializer. (The Queue and Vault use the same deployer-only initHook pattern.)
Constants
uint256 public constant RETENTION_BPS = 2_000; // 20% of net-of-tax sell retained
uint256 public constant BASE_DISCOUNT_BPS = 500; // 5% floor
uint256 public constant MAX_DISCOUNT_BPS = 2_000; // 20% cap
uint256 public constant TAX_BPS = 300; // 3% buy & sell, in ETH, to creator
uint256 public constant BPS = 10_000;
The contracts are immutable — no upgrades, no admin keys. See all parameters in Parameters, and note the documented implementation choices in Known risks.