Skip to content

Uniswap V4 Hooks — What the Next DEX Paradigm Looks Like

Posted on:February 9, 2026 at 10:00 AM

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:

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_INITIALIZE
Bit 158: AFTER_INITIALIZE
Bit 157: BEFORE_ADD_LIQUIDITY
Bit 156: AFTER_ADD_LIQUIDITY
Bit 155: BEFORE_REMOVE_LIQUIDITY
Bit 154: AFTER_REMOVE_LIQUIDITY
Bit 153: BEFORE_SWAP
Bit 152: AFTER_SWAP
Bit 151: BEFORE_SWAP_RETURNS_DELTA
Bit 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 bits
function 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: MIT
pragma 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 transaction
afterSwap hook: check if any orders crossed during the swap
→ finalize the order, release funds to the seller

TWAMM (Time-Weighted Average Market Maker)

Long-term trades executed incrementally over time to minimize price impact. Implemented as:

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 opportunity
afterSwap hook: send additional fee revenue to LP positions proportionally

KYC/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 list

The Singleton and Flash Accounting

V4’s PoolManager singleton holds all token balances across all pools. When you swap ETH for USDC, the PoolManager atomically:

  1. Reduces your ETH balance
  2. Increases the pool’s ETH balance
  3. Reduces the pool’s USDC balance
  4. 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:

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.