Foundation reading: First explore UniswapV2 interactively and UniswapV3 concentrated liquidity before diving into V4 hooks architecture.
Uniswap V3 gave liquidity providers concentrated ranges. Uniswap V4 gives everyone programmability. Hooks let you attach arbitrary logic to any pool action — before a swap, after a swap, when LP positions change, when a pool is created. If V3 was the iPhone (powerful but fixed), V4 is the App Store — an extensible platform where the protocol team provides the rails and the ecosystem builds the applications.
This post covers the V4 hook system technically: the architecture, the lifecycle, real examples, and the security implications.
Table of contents
Open Table of contents
What Changed From V3 to V4
V3 architecture: Each liquidity pair deploys a separate contract. 100,000 pools = 100,000 contracts. Every pool has the same logic.
V4 architecture: A single PoolManager singleton manages all pools. Pools are identified by a PoolKey (token pair, fee, tick spacing, hook address). Liquidity stays in one contract — dramatically reducing gas for multi-hop swaps.
The hook address is embedded in the pool identity. A pool with hook 0x0000…0000 (zero address) behaves exactly like V3. A pool with a custom hook address runs that hook’s code at specific lifecycle points.
The Hook Lifecycle
V4 hooks fire at six points in the pool lifecycle:
Pool Creation: beforeInitialize() → [pool created] → afterInitialize()
Swap: beforeSwap() → [swap executed] → afterSwap()
LP Actions: beforeAddLiquidity() → [liquidity added] → afterAddLiquidity() beforeRemoveLiquidity() → [liquidity removed] → afterRemoveLiquidity()Your hook contract implements whichever of these it needs. The hook can:
- Inspect the current state (pool price, tick, liquidity)
- Modify the swap parameters (change fee, modify amounts)
- Execute arbitrary external calls
- Revert the entire action (acting as a guard)
Hook Permissions: The Flag System
Not every hook implements all callbacks. V4 encodes which callbacks are active in the hook address itself — specifically, in the most significant (leading) bits of the uint160 address:
Bit 159 (highest): BEFORE_INITIALIZEBit 158: AFTER_INITIALIZEBit 157: BEFORE_ADD_LIQUIDITYBit 156: AFTER_ADD_LIQUIDITYBit 155: BEFORE_REMOVE_LIQUIDITYBit 154: AFTER_REMOVE_LIQUIDITYBit 153: BEFORE_SWAPBit 152: AFTER_SWAPBit 151: BEFORE_SWAP_RETURNS_DELTABit 150: AFTER_SWAP_RETURNS_DELTA...A hook that only implements beforeSwap and afterSwap must be deployed to an address where the corresponding high bits are set. This is achieved via CREATE2 with a salt — you mine for an address with the right bit pattern in the leading bytes.
// The PoolManager validates hook permissions against address bitsfunction validateHookPermissions( IHooks self, Hooks.Permissions memory permissions) internal pure { if (permissions.beforeSwap != (uint160(address(self)) & Hooks.BEFORE_SWAP_FLAG != 0)) { revert HookAddressNotValid(address(self)); } // ... validate all permissions}This is elegant: the hook’s capabilities are verifiable from its address alone, without reading contract storage.
Building a Hook: Dynamic Fee Example
A common use case: dynamic fees that adjust based on volatility. Higher volatility → higher fee (compensates LPs for increased IL risk).
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";import {Hooks} from "v4-core/src/libraries/Hooks.sol";import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";import {PoolKey} from "v4-core/src/types/PoolKey.sol";import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";import {BeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol";
contract DynamicFeeHook is BaseHook { using PoolIdLibrary for PoolKey;
// Volatility state per pool mapping(PoolId => uint24) public currentFee; mapping(PoolId => int24[]) private recentTicks; mapping(PoolId => uint256) private lastUpdate;
uint24 constant MIN_FEE = 100; // 0.01% uint24 constant MAX_FEE = 10_000; // 1% uint256 constant UPDATE_INTERVAL = 1 hours;
constructor(IPoolManager _manager) BaseHook(_manager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: true, // Initialize fee beforeAddLiquidity: false, afterAddLiquidity: false, beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: true, // Update fee before each swap afterSwap: true, // Record tick after swap for volatility beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false, }); }
function afterInitialize( address, PoolKey calldata key, uint160, int24, bytes calldata ) external override onlyByPoolManager returns (bytes4) { currentFee[key.toId()] = MIN_FEE; return BaseHook.afterInitialize.selector; }
function beforeSwap( address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata ) external override onlyByPoolManager returns (bytes4, BeforeSwapDelta, uint24) { PoolId poolId = key.toId();
// Update fee based on recent volatility uint24 fee = computeDynamicFee(poolId); currentFee[poolId] = fee;
// Return the override fee (third return value) return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), fee); }
function afterSwap( address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata ) external override onlyByPoolManager returns (bytes4, int128) { PoolId poolId = key.toId();
// Record current tick for volatility computation (, int24 currentTick,,) = poolManager.getSlot0(poolId); recentTicks[poolId].push(currentTick);
// Keep only last 10 ticks if (recentTicks[poolId].length > 10) { // Shift array (gas-inefficient for demo; use circular buffer in production) for (uint i = 0; i < recentTicks[poolId].length - 1; i++) { recentTicks[poolId][i] = recentTicks[poolId][i + 1]; } recentTicks[poolId].pop(); }
return (BaseHook.afterSwap.selector, 0); }
function computeDynamicFee(PoolId poolId) internal view returns (uint24) { int24[] storage ticks = recentTicks[poolId]; if (ticks.length < 2) return MIN_FEE;
// Compute average tick movement (volatility proxy) uint256 totalMovement = 0; for (uint i = 1; i < ticks.length; i++) { int24 movement = ticks[i] - ticks[i - 1]; totalMovement += movement < 0 ? uint24(-movement) : uint24(movement); }
uint256 avgMovement = totalMovement / (ticks.length - 1);
// Map tick movement to fee // 1 tick = 0.01% price move // 0-10 ticks avg = low volatility = MIN_FEE // 100+ ticks avg = high volatility = MAX_FEE if (avgMovement < 10) return MIN_FEE; if (avgMovement > 100) return MAX_FEE;
return MIN_FEE + uint24(((avgMovement - 10) * (MAX_FEE - MIN_FEE)) / 90); }}Hook Examples: What V4 Enables
Limit Orders via Range Orders
V3 already supports range orders (positions entirely above/below current price). V4 hooks can implement proper limit orders with automatic execution:
beforeSwap hook: check if any limit orders would be filled at the current price → execute them as part of the same transactionafterSwap hook: check if any orders crossed during the swap → finalize the order, release funds to the sellerTWAMM (Time-Weighted Average Market Maker)
Long-term trades executed incrementally over time to minimize price impact. Implemented as:
beforeSwap: Execute the portion of all pending long-term orders due since the last swap- Maintains virtual order state in the hook contract
MEV Redistribution
Hooks can capture MEV and redistribute it to LPs:
beforeSwap hook: check if this is a sandwich attack (price moved significantly in prev block) → charge additional fee proportional to the MEV opportunityafterSwap hook: send additional fee revenue to LP positions proportionallyKYC/Compliance Gating
For regulated pools:
beforeAddLiquidity hook: require caller to present a KYC proof (e.g., a Sismo attestation or on-chain KYC badge)beforeSwap hook: verify caller is not on sanctions listThe Singleton and Flash Accounting
V4’s PoolManager singleton holds all token balances across all pools. When you swap ETH for USDC, the PoolManager atomically:
- Reduces your ETH balance
- Increases the pool’s ETH balance
- Reduces the pool’s USDC balance
- Increases your USDC balance
All in one contract. For multi-hop swaps (ETH → USDC → DAI), the intermediate USDC never actually moves — only the net ETH and DAI are settled. This is the flash accounting system, enabled by EIP-1153 (transient storage) in the Cancun upgrade.
Internal accounting uses ERC-6909 — a minimal multi-token standard (simpler than ERC-1155) — for tracking claim balances within the PoolManager. External LP position management is handled by peripheral contracts (like PositionManager) which use ERC-721 tokens, similar to V3’s NonfungiblePositionManager. The ERC-6909 claims enable:
- Cheaper gas for internal token settlement
- Batch operations across multiple pools in a single transaction
- Deferred settlement — tokens only move when you explicitly collect
Security Considerations
Hooks execute arbitrary code with direct access to pool state during swaps. This creates new attack surfaces:
Hook as reentrancy vector: A malicious hook could reenter the PoolManager during a callback. V4 addresses this with a lock mechanism — only one operation can hold the PoolManager lock at a time.
Hook upgrade risk: If a hook is upgradeable, the pool’s behavior can change after LPs deposit. Immutable hooks are safer; if upgradeable, the governance mechanism matters enormously.
Oracle manipulation via hooks: A hook that modifies swap amounts can manipulate the pool price without moving through the normal price impact curve. Protocols using V4 TWAP oracles must account for hook-modified swaps.
Gas DoS: A hook that consumes a lot of gas on beforeSwap increases the cost of every swap in that pool. Hook developers should benchmark and LPs should evaluate hook gas consumption before choosing pools.
V4 represents a fundamental architectural shift: from Uniswap as an AMM to Uniswap as a permissionless financial infrastructure layer. The protocol team provides the execution engine and accounting primitives; the hooks ecosystem provides the strategies. It’s the right design for the next decade of DeFi — and it will take a full cycle to discover all the interesting things you can build with it.