v4 constraints
Uniswap v4 shapes this design more than the economics do. Three constraints in particular decide what the hook can and cannot be.
Permissions
A v4 hook's permissions are encoded in the low bits of its own address, so they are fixed at deployment and cannot be changed. Assay claims exactly five — beforeInitialize, afterInitialize, beforeSwap, afterSwap, and afterSwapReturnDelta — giving mask 0x30C4. The address is CREATE2-mined to carry it.
It claims no liquidity permissions at all. A liquidity provider can always withdraw; the hook is not in that path and structurally cannot block it.
Dynamic fee handshake
A pool must be created with the dynamic-fee flag for a hook to override its fee. beforeInitialize rejects any pool that was not — one of only two places the hook ever reverts, and it runs before any liquidity exists.
The other rejection is the currency binding. v4 hooks are permissionless, so anyone can create a pool naming this one. The reference oracle describes exactly one token pair, so a pool of unrelated assets would be priced against a reference for something else entirely, while the hook reported that reading as fresh. Refusing at creation is the only point where refusal costs nothing.
Hot path
beforeSwap must never revert. A revert there is a denial of service against every liquidity provider in the pool, so every failure path degrades to a quoted fee instead — a stale reference quotes the ceiling rather than erroring.
| Path | Measured | Budget |
|---|---|---|
| Ordinary swap | 16,180 | 20,000 |
| Block boundary, live feed | 52,849 | 55,000 |
| Extreme dislocation | 28,617 | 55,000 |
Reading a Chainlink feed costs roughly 20,774 gas, which is why the reference is cached in a single packed storage slot and refreshed at most once per block. The common path makes no external call at all.
Where the hook's activity is visible
A hook is never the recipient of a transaction. A swap goes to a router, the router calls the PoolManager, and the PoolManager calls the hook — so on any block explorer the hook's Transactions tab is permanently empty, and always will be. Nothing has gone wrong when it looks that way.
The activity is real; it is visible as SwapAssayed events on the hook, and as internal transactions from the PoolManager. The Markets page reads those events directly, and the swap and simulation surfaces link each transaction to the explorer as it confirms.
One field is deliberately not shown anywhere: sender. In beforeSwap that argument is the router, not the trader — it is the same address on every swap that routes the same way. Treating it as a user identity is a well-known way to build a hook that can be trivially spoofed, so nothing here reads it as one.