Development of Custom Hooks for Uniswap v4
Uniswap v4 changed architecture radically: a single singleton contract PoolManager manages all pools, and logic extensions go through hooks. These aren't just callbacks. Hooks get control over critical points in a pool's lifecycle: before and after initialization, before and after swaps, before and after adding/removing liquidity. A correctly written hook enables limit orders, dynamic pricing, fee rebates, MEV-capture—without forking the protocol. An incorrect one blocks the pool or becomes an attack vector.
Hook Architecture: Critical Understanding Before Writing Code
Flags and Permissions
A hook's address encodes permissions in bits. Bits 0-7 determine which callbacks are active: BEFORE_SWAP_FLAG, AFTER_SWAP_FLAG, BEFORE_ADD_LIQUIDITY_FLAG, etc. If a hook declares getHookPermissions() with flag afterSwap: true, but the deployment address doesn't contain the corresponding bit, PoolManager reverts on pool initialization.
This means: the hook contract's address is not arbitrary. You need CREATE2 deployment with salt iteration until the correct bits in the address align. For a complex hook with 4-5 flags, salt hunting is a separate task solved with an off-chain script.
PoolKey and Pool Isolation
Each pool in v4 is identified by PoolKey: {currency0, currency1, fee, tickSpacing, hooks}. The hook address is part of the pool identifier. Two pools with identical tokens and fee but different hooks are different pools with different liquidity positions. This means: liquidity cannot be "migrated" between hooks without full withdrawal and re-entry.
Transient Storage and EIP-1153
V4 actively uses EIP-1153 transient storage—storage that clears at the end of a transaction. It's cheaper than SSTORE/SLOAD and ideal for temporary state within a transaction (e.g., a flag that "a swap is in progress"). Hooks can use transient storage for reentrancy protection without persistent storage overhead.
Typical Hook Use Cases and Complexities
Dynamic Fee Hook
The most popular request: fees that change based on volatility. Logic in afterSwap: calculate deviation from TWAP; if >threshold, raise the fee for the next swap via poolManager.updateDynamicLPFee().
Problem: TWAP must be stored in-hook. Using a Uniswap v3 TWAP oracle as reference means an external call from afterSwap, adding 3-5k gas to every swap. Alternative: your own rolling TWAP in hook storage, updated in afterSwap. Cheaper but requires bootstrap period and edge case handling for early swaps.
Second nuance: updateDynamicLPFee only works if the pool initialized with FEE_DYNAMIC_FLAG. This flag must be set in the fee field of PoolKey at pool creation. Miss it, and the contract deploys, the pool is created, but the hook doesn't work. You cannot replay it.
Limit Order Hook
beforeSwap checks if there are pending limit orders in the current tick range. If yes, executes them as part of the swap. Implementation: a mapping tick => orders[], traversal on tick crossing.
Main risk: unbounded loop over orders on a tick. If 500 orders accumulate on one tick, one swap crossing it costs >1M gas and hits block limit. Defense: order count limits per tick + batch execution via a separate keeper function for accumulated orders.
MEV Capture via AfterSwap Fee Redistribution
Idea: if a swap caused significant price movement (suspected MEV), redirect part of the fee to a compensation pool for LP. afterSwap calculates price impact; if above threshold, sends extra payment to a vault.
Technical complexity: afterSwap receives delta—balance changes. You must calculate price impact from delta and the pool's initial state. Capture that initial state in beforeSwap and store it in transient storage—so in afterSwap you can compare. This is a classic pattern for pairing beforeX/afterX hooks.
Development Tools
Foundry—the only reasonable choice for v4 hooks. The v4-core repository is written for Foundry; tests too. forge test --fork-url <mainnet> lets you test your hook against real PoolManager state.
v4-template from Uniswap—the starting point. Contains proper HookMiner setup for CREATE2 deployment, basic BaseHook abstractions, test examples.
Slither with custom detectors for v4—verify flag correctness, check for storage collisions with PoolManager slots.
Common Hook Development Mistakes
| Mistake | Consequence | Solution |
|---|---|---|
| Incorrect bits in address | Pool won't initialize | CREATE2 + HookMiner before deployment |
External call in beforeSwap without reentrancy guard |
Possible reentrancy via hook | nonReentrant + transient storage lock |
| Unbounded loop in order book | DoS via gas limit | Limit orders per tick + keeper |
Using SSTORE in hot path |
+20k gas per swap | Transient storage (EIP-1153) |
Mutating PoolKey in hook |
Impossible—PoolKey is immutable | Design logic without changing key |
Development Process
Specification (2-3 days). Formalize hook behavior at each lifecycle point. What invariants must hold? "Total fee always ≥ base fee," "limit order never executes at worse price than stated."
Development (5-7 days). Foundry + v4-template. CREATE2 deployment script with HookMiner. Property-based tests via Echidna on key invariants.
Fork Testing (2-3 days). Tests against real mainnet state: pool initialization, series of swaps, edge cases (empty pool, single-sided liquidity, large price impact).
Audit and Gas Profile. Slither + manual review. Gas snapshot via forge snapshot—compare gas cost of swap with and without hook. Acceptable overhead for most cases: <10k gas per swap.
Timeline Estimates
Simple hook (dynamic fee or whitelist): 1 week including tests. Medium-complexity hook (limit orders, MEV capture): 2-3 weeks. Complex system with several interacting hooks: 4+ weeks.
Pricing calculated individually after discussing required mechanics.







