example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
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
Claude Code
$ npx -y skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock --agent claude-code
Codex
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a codex -y
Cursor
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a cursor -y
Gemini CLI
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a gemini-cli -y
GitHub Copilot
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a github-copilot -y
OpenCode
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a opencode -y
Windsurf
$ npx skills add omermaksutii/RugProof --skill v4-hook-reentrancy-via-unlock -a windsurf -y
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
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the…
Use for authorized security assessment of REST, GraphQL, WebSocket, or SOAP APIs, including discovery, authentication,…
Extract and decrypt Windows DPAPI-protected secrets (Credential Manager, browser logins/cookies, Wi-Fi credentials, KeePass keys)…
Clean and finalize authorized natural-language text intended for readers by auditing suspicious invisible Unicode and rewriting…