Building secure Uniswap v4 hooks

ErenayDev2 pts0 comments

Building secure Uniswap v4 hooks - The Trail of Bits BlogPage content

Uniswap v4 hooks let developers add custom behavior to pools, including dynamic fees, custom accounting, and external integrations. This flexibility moves some security responsibilities into application and hook code.<br>The Cork and Bunni exploits are two app-level incidents that show what can go wrong in that code. Together, they account for more than $20M in losses. Neither incident stemmed from a flaw in the Uniswap v4 core protocol or the PoolManager; both arose from application-specific authorization and accounting logic built around hooks.<br>After analyzing dozens of findings from Trail of Bits audits (including our Uniswap v4-core security review), public reports from other firms, and the Solodit database, I&rsquo;ve identified seven recurring failure patterns in application and hook code, including missing caller checks and accounting bugs that still satisfy the PoolManager&rsquo;s settlement invariant. Builders can use these patterns as a secure-development checklist; auditors can use them to focus their review.<br>What the PoolManager guarantees<br>If you&rsquo;re familiar with Uniswap v3, where each pool was a separate contract, v4 inverts the model. All pool state now lives in a singleton PoolManager contract, with each pool represented in its storage. Uniswap v4 adds hooks: independent contracts that execute custom logic at specific points in the swap and liquidity lifecycle.<br>Figure 1: Pools live inside the singleton PoolManager, and multiple pools can use the same hook contract.<br>Here&rsquo;s what a pool looks like in v4:<br>struct PoolKey {<br>Currency currency0;<br>Currency currency1;<br>uint24 fee;<br>int24 tickSpacing;<br>IHooks hooks;<br>}Figure 2: A pool's PoolKey includes both currencies, the fee, tick spacing, and the hook address (v4-core/src/types/PoolKey.sol).Notice that the hook address (IHooks hooks;) is part of the pool&rsquo;s identity. If you change any of these fields, you&rsquo;re talking to a different pool. This matters because trusting the wrong PoolKey means trusting the wrong pool.<br>v4 also introduces a session-based model that works like a flash loan. Your contract calls unlock() on the PoolManager, which triggers a callback into your code. At the end, the PoolManager checks that no unsettled currency deltas remain:<br>function unlock(bytes calldata data) external returns (bytes memory result) {<br>Lock.unlock();<br>// ... callback execution happens here ...<br>if (NonzeroDeltaCount.read() != 0) revert CurrencyNotSettled();<br>Lock.lock();<br>}Figure 3: Simplified PoolManager.unlock() flow: unlock the session, execute the callback, and revert unless all currency deltas settle to zero (v4-core/src/PoolManager.sol).Figure 4: A periphery or hook calls PoolManager.unlock(), handles unlockCallback(), and calls swap() inside the unlocked session.<br>The PoolManager enforces v4&rsquo;s protocol mechanics, including pool initialization rules, swap and liquidity math, hook-callback sequencing, and end-of-session settlement. Hook developers are responsible for validating the application-specific assumptions their hooks add.<br>Each hook must decide:<br>Who can call its privileged paths<br>Which pools are legitimate<br>How custom balances and deltas should be accounted for<br>Whether external integrations can fail or reenter safely<br>1. Anyone can call your hook<br>Hook callbacks are external functions on your contract. If you don&rsquo;t check the caller, an attacker can call those callbacks directly with malicious parameters. A loose unlockCallback path can also reach internal actions that should never be callable.<br>The fix: use BaseHook for hook entrypoints and SafeCallback for unlockCallback. Together, they enforce caller checks on the callback paths they cover:<br>modifier onlyPoolManager() {<br>if (msg.sender != address(poolManager))<br>revert NotPoolManager();<br>_;<br>}Figure 5: onlyPoolManager restricts hook callbacks to the configured PoolManager.Add an equivalent caller check only on paths those contracts don&rsquo;t cover.<br>Real-world example: The Cork exploit (~$12M, May 2025) shows why this check matters. Cork let data from an untrusted path reach hook logic that affected redemptions. That access-control gap, combined with a pricing issue elsewhere in the protocol, gave the attacker a way to drain funds.<br>2. Treating any pool as legitimate<br>Pool creation through the PoolManager is permissionless by default. Unless your hook restricts initialization in beforeInitialize, anyone can create a pool with your hook address attached. If your hook trusts a user-supplied PoolKey without validation, an attacker can route your logic through a malicious pool with currencies and parameters they choose.<br>An attacker-created pool presents two immediate risks. First, if your hook stores per-pool data keyed by PoolId, the new pool gets its own mapping slot. The attacker can influence values written through activity in that pool, and later accounting paths may treat those values as trusted. Second, currency0...

hook pool poolmanager rsquo hooks uniswap

Related Articles