← ClaudeAtlas

v4-hook-reentrancy-via-unlocklisted

Detect reentrancy in Uniswap V4 hooks via the PoolManager unlock/lock callback. V4 uses a singleton PoolManager with transient lock state; all pool mutations happen inside an unlockCallback. A hook that makes external calls during beforeSwap/afterSwap/before*Liquidity (to tokens with hooks, arbitrary routers, or user-controlled contracts) can be re-entered, and because the manager is already unlocked the attacker can recursively swap/modify liquidity against stale hook state. Activate on any V4 hook performing external calls inside a callback, or custom unlockCallback logic.
iktok90-design/ai-smart-contract-auditor · ★ 36 · AI & Automation · score 80
Install: claude install-skill iktok90-design/ai-smart-contract-auditor
# Uniswap V4 hook reentrancy via unlock detection ## When this applies Trigger on any of: - Hook callbacks (`beforeSwap`, `afterSwap`, `beforeAddLiquidity`, `afterRemoveLiquidity`, `beforeDonate`, ...) that perform external calls - External calls to ERC-777 / ERC-1155 / callback-bearing tokens, arbitrary routers, or user-supplied addresses inside a callback - A hook that itself calls `poolManager.unlock(...)` or `swap`/`modifyLiquidity`/`take`/`settle` re-entrantly - Custom `unlockCallback` implementations - Hook state (fee accumulators, TWAP buffers, custom accounting) read/written across an external call within one callback - `safeTransfer` / `transferFrom` of tokens that invoke recipient hooks during settlement ## Detection patterns ### External call before state finalize inside a callback (HIGH) ```solidity function afterSwap(address, PoolKey calldata key, ..., int128) external override returns (bytes4, int128) { uint256 reward = _pending[key.toId()]; rewardToken.safeTransfer(msg.sender, reward); // ← ERC-777 hook re-enters here _pending[key.toId()] = 0; // ← cleared AFTER the external call return (this.afterSwap.selector, 0); } ``` During the transfer the recipient re-enters `swap` (manager is unlocked), triggering `afterSwap` again while `_pending` is still non-zero → double reward. **Signal:** hook state mutated after an external call inside a callback, with the PoolManager unlocked (CEI violated in hook context). ### Recurs