Back to Blog 

Shipping a Uniswap v4 hook? Start with a fast pre launch pass in Krait, then request a scoped audit quote for the manual review. This guide breaks down the four hook attack patterns behind every public v4 exploit, each with the smallest viable vulnerable function and the smallest viable exploit.
Uniswap v4 hook security review
Four patterns cover every public hook exploit. Catch them before liquidity hits mainnet.
Reentrancy in callbacks, permission flag mismatches, direct-call bypass, and custom-accounting rounding drift have cost more than \$20M on-chain. Use Krait to catch the obvious gaps early, then move into a Zealynx audit scope once the design is stable.
Callback access control
Custom accounting deltas
Hook reentrancy edge cases
The four patterns, by realized loss
1. Reentrancy via beforeSwap / afterSwap callbacks
2. Permission / flag bypass in the hook address bits
3. Donation griefing and direct-call bypass
4. Custom-accounting drift from rounding and sign errors
Uniswap v4 hooks have produced two real exploits worth more than $20 million combined, multiple critical audit findings on hooks the Uniswap Foundation itself commissioned, and a long tail of high-severity findings across every audit firm with a public reports page. The architecture is twelve months old at mainnet and the pattern is already clear.
Most surveys of v4 hook security catalog ten or twelve attack classes. That is useful for vocabulary and not useful for an auditor sitting in front of a 600-line hook contract on Friday afternoon. Four patterns cover every public exploit and audit finding worth knowing about. This article walks through them with the smallest viable vulnerable function and the smallest viable exploit for each, anchored to real on-chain losses where they exist.
If you have not read the Uniswap v4 security guide or built a first hook before, start there. This piece assumes you know what a hook is and what
BeforeSwapDelta does.The trust boundary moved
In v2 and v3 the PoolManager equivalents were monolithic. If Uniswap was secure, your integration was secure. The v4 PoolManager is a kernel, and the hook is userland code that the kernel calls at 14 lifecycle points:
beforeInitialize, afterInitialize, beforeAddLiquidity, afterAddLiquidity, beforeRemoveLiquidity, afterRemoveLiquidity, beforeSwap, afterSwap, beforeDonate, afterDonate, plus four "returns delta" variants that let the hook adjust accounting.That userland code can take custody of funds, modify accounting deltas, re-enter the PoolManager, and silently bypass invariants the protocol thought it was enforcing. The trust model is now a three-way negotiation between the PoolManager, the hook, and whatever protocol the hook is embedded in. The pieces of the v4 architecture that make hooks powerful (custom curves, hook fees, NoOp swaps) are the same pieces that make hook security hard. If the singleton architecture and flash accounting are new to you, the v4 security guide covers them in depth.
The four patterns that follow are ordered by realized loss, descending.

Pattern 1: reentrancy via beforeSwap / afterSwap
v2 and v3 closed reentrancy as a serious AMM bug class. v4 reopened it, not because the PoolManager itself is reentrant (it locks via transient storage), but because the hook is called with the PoolManager unlocked, and the hook is allowed to make external calls. The hook author is sitting inside the unlock window without always realizing it.
Certora's threat model phrases the principle cleanly: the problem is not the external call, it is making the external call while internal state is inconsistent. A hook that updates state after transferring tokens, that calls into a user-supplied token contract, that emits a callback to an arbitrary subscriber, has every CEI failure mode the EVM has ever produced.
The minimal vulnerable function
1contract RewardHook is BaseHook {2 mapping(address => uint256) public rewards;3 IERC20 public immutable rewardToken;45 function _afterSwap(6 address sender,7 PoolKey calldata,8 SwapParams calldata,9 BalanceDelta delta,10 bytes calldata11 ) internal override returns (bytes4, int128) {12 uint256 reward = _calculateReward(delta);1314 // Interaction BEFORE effect: the classic CEI failure.15 rewardToken.transfer(sender, reward);16 rewards[sender] += reward;1718 return (BaseHook.afterSwap.selector, 0);19 }20}
If
rewardToken is an ERC-777 (or any token with a transfer callback), or if the hook is composed with a downstream contract that fires its own callback, sender is re-entered. The attacker's contract uses the re-entry to trigger another swap through the PoolManager. The PoolManager itself is fine, it is happy to take another unlock call. But the hook's rewards mapping has not been updated yet, and the same reward is credited twice.
The minimal exploit
1contract ReentrantAttacker {2 IPoolManager pm;3 PoolKey key;4 uint256 hits;56 function tokensReceived(address, address, uint256, bytes calldata) external {7 if (hits < 5) {8 hits++;9 pm.unlock(abi.encode(SwapAction(key, 1e18)));10 }11 }1213 function unlockCallback(bytes calldata data) external returns (bytes memory) {14 SwapAction memory a = abi.decode(data, (SwapAction));15 pm.swap(a.key, SwapParams(true, int256(a.amount), MIN_SQRT_PRICE + 1), "");16 // settle deltas...17 return "";18 }19}
Five recursive swaps, five reward accruals, one settlement. The hook handed out rewards without taking custody of the corresponding obligations.
The defense
Two things, both required:
1modifier nonReentrant() {2 require(_locked == 1, "Reentrant");3 _locked = 2;4 _;5 _locked = 1;6}78function _afterSwap(...) internal override nonReentrant returns (bytes4, int128) {9 uint256 reward = _calculateReward(delta);10 rewards[sender] += reward; // effect first11 rewardToken.transfer(sender, reward); // interaction last12 return (BaseHook.afterSwap.selector, 0);13}
The reentrancy guard is on the hook, not the PoolManager. The PoolManager's lock protects the kernel, not your state. If the hook tracks anything across calls, it needs its own guard. Note that view functions are not exempt: a hook that exposes pricing or reward data through a getter is also exposed to read-only reentrancy if that getter reads mid-update state.
Pattern 2: permission/flag bypass
Permissions are encoded in the least significant 14 bits of the hook contract's deployment address. This is the hook address mining scheme: each bit corresponds to a lifecycle hook. Bit 13 is
BEFORE_INITIALIZE_FLAG, bit 12 is AFTER_INITIALIZE_FLAG, down to bit 0 which is AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG. The PoolManager checks the bit with a single bitwise AND in hasPermission:1function hasPermission(IHooks self, uint160 flag) internal pure returns (bool) {2 return uint160(address(self)) & flag != 0;3}

This is fast and gas-efficient. It is also entirely dependent on the deployer mining an address whose bit pattern matches the functions actually implemented. Three things go wrong:
Flag set, function not implemented. The hook address says "I implement
afterSwap" but the contract has no afterSwap function (or it reverts). Every swap on every pool using this hook reverts. Whole pool, DoSed.Function implemented, flag not set. The hook has an
afterSwap function with critical fee-distribution logic. The deployer did not mine AFTER_SWAP_FLAG into the address. The PoolManager never calls it. The fee logic silently does not run, and nobody notices until a discrepancy shows up weeks later. Sorella's L2 Angstrom had exactly this: the AFTER_SWAP_RETURNS_DELTA_FLAG was missing, so once protocol fees were enabled, every swap reverted.Constructor validation skipped. The v4-periphery
BaseHook calls Hooks.validateHookPermissions(this, getHookPermissions()) in its constructor, comparing every flag bit in the address against the Permissions struct the hook itself declares. A hand-rolled hook that does not inherit BaseHook (or OpenZeppelin's BaseHook) can deploy with whatever mismatched bits the deployer accidentally produced.The minimal vulnerable function
1contract ProtocolFeeHook is IHooks {2 // Implements afterSwap but does NOT inherit BaseHook3 // and does NOT mine AFTER_SWAP_RETURNS_DELTA_FLAG into the address.45 function getHookPermissions() public pure returns (Hooks.Permissions memory p) {6 p.afterSwap = true;7 p.afterSwapReturnDelta = true; // declared in struct, NOT in address bits8 }910 function afterSwap(11 address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata12 ) external returns (bytes4, int128) {13 return (IHooks.afterSwap.selector, int128(protocolFee));14 }15}
The struct says one thing. The address says another. If
validateHookPermissions is not called in the constructor, the deployment goes through and the hook ships.The minimal exploit
There is no exploit here in the offensive sense. The attack is the deployment itself. Once the hook is bound to a pool, one of the following is true:
- All swaps revert (flag set, no function).
- Critical hook logic is silently skipped (function set, no flag).
- The hook reverts on a specific lifecycle event the operator never tested for.
In each case, depending on what the hook was supposed to do, the result is either a permanent DoS of the pool or a silent loss of value over time. The PoolManager has no way to know which one is happening because both look correct from its perspective.
The defense
Inherit
BaseHook. From v4-periphery:1abstract contract BaseHook is IHooks, ImmutableState {2 constructor(IPoolManager _manager) ImmutableState(_manager) {3 validateHookAddress(this);4 }56 function getHookPermissions() public pure virtual returns (Hooks.Permissions memory);78 function validateHookAddress(BaseHook _this) internal pure virtual {9 Hooks.validateHookPermissions(_this, getHookPermissions());10 }11}
Use the
HookMiner library from v4-periphery to find a salt that produces an address with the bit pattern matching your declared Permissions. Treat any code path that constructs a hook without going through BaseHook (or OpenZeppelin's hardened equivalent) as a high-severity finding in review. For the mechanics of mining a matching address, see the first hook walkthrough.Pattern 3: donation griefing and direct-call bypass
The PoolManager's entrypoints are public. Anyone can call
donate, modifyLiquidity, or swap directly. The hook only sees what flows through the lifecycle callbacks it has enabled. If a hook tracks state that depends on what happens to the pool, and it has not enabled the before* hooks for every entrypoint that can change that state, an outsider can mutate the pool around the hook's back.
This pattern produced a critical finding in the OpenZeppelin audit of the Uniswap Foundation's own
LiquidityPenaltyHook. The hook was designed to charge a penalty on Just-In-Time liquidity providers when they remove liquidity within a configured block window. The penalty is computed in afterRemoveLiquidity. The auditors found that calling increaseLiquidity first triggers Uniswap v4's automatic fee collection, which credits all accrued fees to the user and resets the fee state. After the reset, the subsequent removal calculates a penalty against zero, and no penalty fires.The exact fix in the rc.2 audit was to add
beforeAddLiquidity to the hook's permissions and use it to track fee collection that occurs during liquidity additions. The penalty was bypassable purely because the hook only watched one of the two PoolManager entrypoints that affected its state.Donations are a second source of this class.
PoolManager.donate increases fee growth for in-range LPs. A hook that uses fee growth as input to any internal calculation (rebate accrual, dynamic fee basis, anti-MEV tax, custom incentive distribution) can have that input manipulated by anyone calling donate directly.The minimal vulnerable function
1contract RebateHook is BaseHook {2 mapping(PoolId => uint256) public feeGrowthSnapshot;34 function _beforeSwap(5 address sender, PoolKey calldata key, SwapParams calldata, bytes calldata6 ) internal override returns (bytes4, BeforeSwapDelta, uint24) {7 PoolId id = key.toId();8 uint256 currentFeeGrowth = _poolFeeGrowth(id);9 uint256 delta = currentFeeGrowth - feeGrowthSnapshot[id];1011 // Rebate the swapper proportionally to fee growth accrued since last swap.12 _payRebate(sender, delta);1314 feeGrowthSnapshot[id] = currentFeeGrowth;15 return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);16 }17}
Get funded for your audit
Core grants cover up to $32k. Growth and Builder tiers available. Rolling applications.
No spam. Unsubscribe anytime.
The hook does not implement
beforeDonate. The BEFORE_DONATE_FLAG is not in its address.The minimal exploit
1// Attacker calls PoolManager.donate directly, then swaps a dust amount.2pm.unlock(abi.encode(GriefAction(key)));34function unlockCallback(bytes calldata data) external returns (bytes memory) {5 GriefAction memory g = abi.decode(data, (GriefAction));6 // Inflate fee growth without going through the hook.7 pm.donate(g.key, 100e18, 100e18, "");8 // Trigger the hook's snapshot logic with a dust swap.9 pm.swap(g.key, SwapParams(true, 1, MIN_SQRT_PRICE + 1), "");10 // ...settle deltas, collect rebate.11}
The attacker pays $200 of liquidity to the pool in
donate. The next swap, however small, sees a delta of 200e18 in fee growth and pays out a rebate sized to that. If the rebate budget comes from a treasury, the attacker drains the treasury by repeatedly donating-and-swapping. If it comes from accrued protocol fees, they drain those.The defense
The heuristic: for every state variable the hook reads or updates, ask which PoolManager entrypoints can change a quantity that state depends on. Confirm a hook function gates each one. For the rebate hook above, the fix is either:
- Implement
beforeDonatethat updatesfeeGrowthSnapshotso donations cannot inflate the next swap's rebate, or - Implement
beforeDonatethat always reverts, locking the pool to in-flow donations.
For the
LiquidityPenaltyHook family, the fix is symmetric: enable beforeAddLiquidity and track the fee collection that happens during liquidity additions.v4 gives the hook 14 lifecycle hooks. Most hooks ship using two or three. The other 11 are the side door.
Pattern 4: custom-accounting drift
BeforeSwapDelta is a packed int256: the upper 128 bits are the specified delta, the lower 128 bits are the unspecified delta. afterSwap can return an int128 hookDelta. The PoolManager tracks all of these in transient storage via NonzeroDeltaCount and reverts at the end of the unlock window if any BalanceDelta is unsettled. That sounds safe, and on the surface it is: you cannot silently lose money to an unsettled balance because the transaction reverts.The danger is the inverse. A hook that thinks it is settling correctly and is actually rounding the wrong way is solvent for one swap and insolvent on the next. The PoolManager has no way to detect that. The off-by-one is in the hook's own math, not in the kernel's bookkeeping.
This is the pattern that killed Bunni V2 in September 2025. Bunni was a Uniswap v4 hook implementing a custom Liquidity Distribution Function that rebalanced pools after every trade. The
withdraw function was intended to round idle balance down to favor the pool. Due to a subtle sign in the calculation, it rounded the other way. The attacker used flash loans to perform precisely-sized swaps that pushed the LDF across rebalance thresholds, accumulating rounding errors trade after trade. Total losses: $2.4M on Ethereum, $5.9M on Unichain, $8.4M combined. Bunni shut down permanently. The protocol had been audited by Trail of Bits and Cyfrin pre-deployment.There are four sub-patterns to know:
Sign convention errors. Specified vs unspecified deltas, exact-input vs exact-output. The Uniswap dev docs explicitly flag "asymmetric swap handling between exact-input and exact-output" as a class. A hook that handles exact-input correctly but inverts a sign on exact-output is exploitable on demand.
Rounding direction. The Bunni class.
amount * 100 / 10000 rounds toward zero, which favors the caller. (amount * 100 + 9999) / 10000 rounds up, which favors the pool. Pick the wrong one and every swap leaks value in a direction the attacker can amplify.Dust accumulation. Unsettled small balances after a swap. The PoolManager reverts on nonzero delta count to prevent loss, but the hook can DoS itself if it does not call
clear() on the dust. Guardian Audits' Gamma V4 Limit Orders review found exactly this: H-01 was a dust-based DoS that locked the hook.Sync-before-unlock. Calling
PoolManager.sync() before unlock reverts in older v4-core builds. Pashov found this in Bunni V2 (H-01 in the August review). The constraint has been relaxed in recent versions of the PoolManager, but if you are auditing a hook against a pinned older v4-core, it is still a live finding.
The minimal vulnerable function
1contract HookFeeHook is BaseHook {2 uint256 public constant FEE_BPS = 100; // 1%34 function _beforeSwap(5 address, PoolKey calldata, SwapParams calldata params, bytes calldata6 ) internal override returns (bytes4, BeforeSwapDelta, uint24) {7 // Exact-input swap: amountSpecified is negative.8 int128 specifiedAmount = int128(params.amountSpecified);9 int128 absAmount = specifiedAmount < 0 ? -specifiedAmount : specifiedAmount;1011 // BUG: rounds toward zero. On every swap, the hook keeps slightly less12 // than 1%, so the swapper receives slightly more than they should.13 int128 fee = (absAmount * 100) / 10000;1415 return (16 BaseHook.beforeSwap.selector,17 toBeforeSwapDelta(fee, 0), // fee on specified token, none on unspecified18 019 );20 }21}
The bug is the rounding.
100 / 10000 rounds down. For a 1 wei amount, fee is zero. For a 99 wei amount, fee is zero. The hook collects nothing on small swaps and is short on integer division of larger ones.The minimal exploit
1// Multicall 10,000 swaps of a size chosen to maximize the rounding leak.2// Each swap pays effectively less than the intended 1% fee.3// At Bunni-scale, the leak is the entire protocol.4for (uint i = 0; i < 10000; i++) {5 pm.unlock(abi.encode(SwapAction(key, OPTIMAL_AMOUNT)));6}
The exploit is not glamorous. It is a loop. The point of the pattern is that the math error is microscopic per swap and arithmetic at scale.
The defense
Three things:
- Round in the pool's favor on every direction. Write the fee formula to round up on the input side and round down on the output side. Add a test that asserts the hook is at least as solvent after each swap as before, for every combination of
(zeroForOne, exactInput). - Treat the LDF and any custom curve as a separate audit scope. Bunni was audited and still shipped a critical rounding bug. Custom curves require differential testing against a reference AMM, property-based fuzzing of the curve's invariants, and ideally formal proofs that every state transition preserves solvency.
- Explicit
clear()on dust. Any path where the hook holds a non-zero residual balance and does not explicitly settle it is a DoS vector at best and a value leak at worst.
For hooks that defend against MEV via dynamic fees, the same rounding discipline applies. See the MEV protection deep-dive for the kinds of custom-accounting hooks this class affects, and the AMM security foundations series for the invariant-preservation reasoning that underpins all of it.
A real exploit walk-through: Cork Protocol, $12M
Cork Protocol was exploited on May 28, 2025 for 3,761 wstETH, roughly $12M. The exploit chained Pattern 2 (permission/access-control gap) with Pattern 3 (direct-call bypass). It is the cleanest illustration in v4 of how a single missing modifier becomes an eight-figure loss.
Cork's
CorkHook contract implemented beforeSwap. The function was supposed to be callable only by the PoolManager. The version of the v4-periphery BaseHook that Cork forked predated the onlyPoolManager check; an upstream commit on February 6, 2025 added the modifier, but Cork was on a pre-February fork and did not pull it in.Anyone with the PoolManager unlocked could call
CorkHook.beforeSwap directly with arbitrary hookData. The attacker created a fake Cork market using a real Depeg Swap token as the redemption asset, called pm.unlock, and from inside the unlock callback called the legitimate CorkHook.beforeSwap with hand-crafted hook data. The hook executed CorkCall against the legitimate market with the attacker-supplied parameters, crediting the attacker 3,761 weETH-DS tokens as if they had deposited that amount. They had not. They then redeemed those tokens for wstETH and bridged.Two patterns, one transaction:
- Pattern 2.
onlyPoolManagerwas missing. The hook was callable by anyone. - Pattern 3. The hook trusted
hookDatafrom the caller and used it to drive cross-market state changes. Even withonlyPoolManager, if the trust-but-don't-verify onhookDatahad remained, the attack might have routed through a malicious pool. The combination is what made it cheap.
The defense was a one-line modifier. The cost was $12M.
Audit checklist, ordered by realized loss
Run this checklist top-to-bottom on any v4 hook before review concludes. Items are ordered by the dollar amount the corresponding bug has cost on-chain or in audited critical findings.
- Does every external hook function carry
onlyPoolManager(or equivalent)? The Cork exploit is one missing modifier. ($12M.) - Does the hook track state that can be modified by a PoolManager entrypoint not covered by an enabled
before*hook? OpenZeppelin's audit of the Uniswap Foundation's LiquidityPenaltyHook found a critical of exactly this shape; the fix was addingbeforeAddLiquidityto the permissions. ($8.4M paid by Bunni in a different but adjacent class; the LiquidityPenaltyHook bug was caught pre-deployment.) - Do all custom-accounting deltas round in the pool's favor across every combination of swap direction and exact-input/exact-output? Bunni's rounding direction error was the entire root cause. ($8.4M.)
- Does the hook inherit
BaseHook(v4-periphery or OpenZeppelin), so that the address bits are validated againstgetHookPermissions()in the constructor? Hand-rolled hooks consistently produce flag/function mismatches. - Are external calls inside hook callbacks protected by both checks-effects-interactions and an explicit reentrancy guard on the hook itself? The PoolManager's lock does not protect the hook's state.
- Is
PoolKeyvalidated to constrain the hook to a known set of pools, currencies, and fee tiers? Certora's Doppler review found a critical (C-01) where insufficient pool key validation made a coordinator contract drainable. - Is dust accumulation explicitly handled with
clear()? Guardian Audits' Gamma V4 Limit Orders review (H-01) found a dust-based DoS. - If any inline assembly handles dynamic ABI-encoded parameters, is the calldata offset actually fixed? The z0r0z V4 Router lost $42K because a fixed assembly offset assumption did not hold for non-canonical ABI encoding. Hook-adjacent rather than hook-internal, but the same pattern recurs in custom router and aggregator code wrapping hooks.
What this all adds up to
v4 hooks are powerful for the same reason they are dangerous: the protocol delegates execution to user code at every meaningful lifecycle point. Every hook is a new smart contract with the trust level of the protocol it is attached to. The cheapest mistakes to ship (a missing modifier, a wrong rounding direction, a flag bit one off in the deployment address) are the most expensive when exploited.
For more on the broader v4 architecture, see the Zealynx Uniswap v4 security guide and the first hook walkthrough. For the AMM foundations that v3 closed and v4 reopened, see AMM security foundations Part 1 and Part 2, and the Uniswap v3 deep dive for the concentrated-liquidity model v4 inherits.
For deeper survey-style reading on v4 hook security, Cyfrin's V4 Hooks Security Deep Dive catalogs the broader pattern space with extensive solodit links. Certora's V4 threat modeling covers the reentrancy first principles in more depth. The Uniswap Labs "Known Effects of Hook Permissions" PDF is required reading for anyone deploying a hook to mainnet.
Get a Uniswap v4 hook review before mainnet
If you are shipping a custom hook, a PoolManager integration, or a singleton-aware router, the highest-risk failures are rarely visible in local happy-path tests. Callback ordering, flash accounting assumptions, fee logic, custom-accounting rounding, and missing access-control modifiers all deserve human review before your first real liquidity lands.
Start with a fast pre launch pass in Krait to catch the obvious hook issues, then request a Uniswap v4 security review for the manual audit path. If you need a deeper scope first, review our smart contract audit services.
Want to stay ahead with more in-depth analyses like this? Subscribe to our newsletter and ensure you don't miss out on future insights.
FAQ: Uniswap v4 hook attacks
1. What is BeforeSwapDelta in Uniswap v4?
BeforeSwapDelta is a packedint256 value a hook returns from beforeSwap to adjust a swap's accounting. The upper 128 bits hold the specified delta (the token the swapper named an exact amount for) and the lower 128 bits hold the unspecified delta (the other token). A hook uses it to take a fee, run a custom curve, or skip the swap entirely. The Pool Manager nets every delta in transient storage and reverts the transaction if anything is left unsettled, so the risk is never a silently lost balance — it is a hook whose own math rounds the wrong way and quietly leaks value, as covered in Pattern 4 above. See the BalanceDelta glossary entry for the related settlement type.
2. How does hook address mining work in Uniswap v4?
A v4 hook's permissions are encoded in the least significant 14 bits of its contract address, one bit per lifecycle callback (beforeSwap, afterSwap, beforeAddLiquidity, and so on). The Pool Manager decides whether to call a given callback with a single bitwise AND against the address — no storage read. Because you cannot choose a contract address freely, deployers use a mining tool such as HookMiner to brute-force a CREATE2 salt that produces an address whose bits match the callbacks the hook actually implements. A mismatch between the bits and the code is a bug class on its own: see hook address mining and Pattern 2.
3. Why doesn't the Pool Manager's lock protect a hook from reentrancy?
The Pool Manager guards its own state with a transient-storage lock that is held for the duration of anunlock call. But hooks are invoked while that lock is open — the whole point of the unlock window is to let the caller (and its hooks) run a sequence of pool actions before settling. So the lock protects the kernel's bookkeeping, not your hook's storage. If your hook makes an external call (a token transfer, a callback to a subscriber) before updating its own state, an attacker can re-enter and trigger another swap while your reward or fee mapping is stale. The fix is an explicit reentrancy guard on the hook plus strict checks-effects-interactions ordering.
4. What is JIT (just-in-time) liquidity and why do hooks penalize it?
Just-in-time liquidity is an MEV strategy where a bot adds a large, tightly-concentrated liquidity position in the same block as a known large swap, captures most of that swap's fees, then removes the position immediately — all in one transaction, with near-zero exposure to price risk. It dilutes the fees earned by long-term liquidity providers. Anti-JIT hooks such as the Uniswap Foundation'sLiquidityPenaltyHook charge a penalty when liquidity is removed within a configured block window. As Pattern 3 shows, those penalties are only as strong as the set of lifecycle callbacks the hook watches. More detail in the JIT liquidity attack glossary entry.
5. What is checks-effects-interactions (CEI) and why does it matter for hooks?
Checks-effects-interactions is an ordering discipline for smart contract functions: first run all checks (requires, validations), then apply all effects (update your own storage), and only then perform interactions (external calls such as token transfers or callbacks). Violating it — transferring tokens before updating a balance, for instance — is the root cause of most reentrancy exploits. It matters acutely for v4 hooks because the hook runs inside the Pool Manager's open unlock window, so an external call made before an effect lets an attacker re-enter and act on stale hook state. Pattern 1 is a CEI failure in its purest form.6. How much have Uniswap v4 hook exploits cost, and which were the biggest?
The two largest realized losses to date are Cork Protocol (~\$12M on May 28, 2025, from a missingonlyPoolManager access-control modifier combined with trusting attacker-supplied hookData) and Bunni V2 (~\$8.4M in September 2025, from a rounding-direction error in a custom Liquidity Distribution Function — \$2.4M on Ethereum and \$5.9M on Unichain). Both protocols had been audited before deployment, which is the central lesson: hook bugs are subtle, architecture-specific, and survive generalist review. Several more critical findings — on the Foundation's own LiquidityPenaltyHook and AntiSandwichHook, and on Doppler — were caught pre-deployment. See our Uniswap v4 security guide for the full architectural context.
Glossary
Quick reference for key terms used in this article:
| Term | Definition |
|---|---|
| Pool Manager | The singleton contract that manages all v4 pools, liquidity, and swaps, and calls hooks at lifecycle points. |
| Hooks | External smart contracts that execute custom logic at specific points in a pool's lifecycle. |
| Hook Address Mining | Brute-forcing a CREATE2 salt so a hook's address bits encode the exact permissions it implements. |
| BalanceDelta | The net token balance change from a pool operation, settled at the end of the unlock window. |
| Flash Accounting | Tracking balance deltas in transient storage and settling only the final net amount. |
| Reentrancy Attack | Recursively calling back into a contract before its state updates complete. |
| JIT Liquidity Attack | Adding concentrated liquidity right before a large swap to capture fees with minimal risk. |
| Singleton Architecture | Design pattern where all pools are managed within one unified contract. |
Get funded for your audit
Core grants cover up to $32k. Growth and Builder tiers available. Rolling applications.
No spam. Unsubscribe anytime.
