example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
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
$ npx -y skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/v4-hook-reentrancy-via-unlockContext preview
The summary Claude sees to decide when to auto-load this skill.
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
name: v4-hook-reentrancy-via-unlock description: 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.
Trigger on any of:
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).
function beforeSwap(...) external override returns (bytes4, BeforeSwapDelta, uint24) {
router.call(userData); // user contract calls poolManager.swap again, re-entrant
...
}The transient lock is already held; the manager permits nested operations, so the hook's pre-swap invariants can be violated mid-flight. **Signal:** arbitrary/user-controlled external call inside a callback whose result feeds the same swap's accounting.
function getTwap(PoolKey calldata key) external view returns (uint256) {
return _twap[key.toId()]; // read by a victim mid-callback, before this hook updates it
}A consumer reads the hook's oracle while the hook is mid-callback and its accumulator is stale. **Signal:** a public view exposing hook state that is updated inside a callback, readable during reentrancy. See [[reentrancy]].
| Pattern | Severity | Notes | |---|---|---| | State cleared after external call in callback → double-spend | **High** | Direct value extraction | | User-controlled call inside callback enabling nested swap | **High** | Invariant break mid-swap | | Read-only reentrancy on hook oracle/TWAP | **High** | Often missed, see [[oracle-manipulation]] | | External call to a fixed, trusted contract only | **Medium** | Bounded by trust assumption | | Callback with no external calls / pure accounting | **Info** | No reentrancy surface |
1. **CEI inside the hook** — finalize hook state (zero out pending, update accumulators) before any external call within a callback. 2. **Per-hook reentrancy guard** — a transient (`tstore`) guard on callbacks; the PoolManager's own lock does NOT protect hook-internal state. 3. **Avoid callback-bearing tokens** in callbacks, or pull/settle through the manager (`take`/`settle`) rather than raw transfers to arbitrary recipients. 4. **Guard view functions** that expose hook state used as a price, or serve a "settled" snapshot that only updates outside callbacks. 5. **No arbitrary external calls** inside callbacks; restrict to allowlisted, non-reentrant targets.
Rugproof your code before someone else does. 🌐 Live site: omermaksutii.github.io/RugProof 📦 Latest: v1.0.0 — 45 commands · 23 agents · 45 skills · 13 MCP servers · tested, offline-first, with rule packs, a benchmark, non-EVM coverage, and post-deploy
Repo: omermaksutii/RugProof
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove deliberately skip the EXTCODESIZE…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta,…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the hook's permissions live in the…