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 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 low bits of its deployed address (mined via CREATE2 salt) and must agree with getHookPermissions(); a callback the hook
$ npx -y skills add omermaksutii/RugProof --skill v4-hook-permission-flags-mismatch --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/v4-hook-permission-flags-mismatchContext preview
The summary Claude sees to decide when to auto-load this skill.
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 low bits of its deployed address (mined via CREATE2 salt) and must agree with getHookPermissions(); a callback the hook
name: v4-hook-permission-flags-mismatch description: 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 low bits of its deployed address (mined via CREATE2 salt) and must agree with getHookPermissions(); a callback the hook implements but whose flag bit is unset is never invoked, and a flag set without a real implementation makes pool initialization revert in Hooks.validateHookPermissions. Activate on any BaseHook/IHooks contract, getHookPermissions overrides, or hook address mining.
Trigger on any of:
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({ beforeSwap: true, afterSwap: false, /* ...all else false */ });
}
function afterSwap(...) external override returns (bytes4, int128) {
_accrueFees(...); // ← real logic, but afterSwap flag is FALSE
return (this.afterSwap.selector, 0);
}The pool reads permissions from the hook *address bits*, not from the function table. With the `AFTER_SWAP` bit unset, the PoolManager never calls `afterSwap`; `_accrueFees` silently never runs. **Signal:** a callback is implemented (non-reverting body) but its corresponding permission is `false` / the address bit is unmined.
return Hooks.Permissions({ beforeAddLiquidity: true, /* ... */ });
// but beforeAddLiquidity is NOT overridden → BaseHook reverts HookNotImplemented`Hooks.validateHookPermissions` checks that each set address bit corresponds to an implemented callback; a set flag with no override makes `PoolManager.initialize` revert, bricking the pool. **Signal:** permission `true` (or address bit set) with no matching overridden function, or the default `BaseHook` stub left in place (reverts `HookNotImplemented`).
beforeSwapReturnDelta: false // but beforeSwap returns a non-zero BeforeSwapDelta
If `beforeSwap` returns a non-zero delta while `BEFORE_SWAP_RETURNS_DELTA` is unset, the manager ignores the delta (or reverts), stranding the accounting the hook tried to apply. **Signal:** a callback returns a non-zero `BeforeSwapDelta`/`int128` while its `*ReturnDelta` permission is false.
address hook = address(uint160(0x...0040)); // only BEFORE_SWAP bit // getHookPermissions() also claims afterSwap → mismatch at validateHookPermissions
**Signal:** the mined deployment address low bits don't equal the `getHookPermissions()` struct.
| Pattern | Severity | Notes | |---|---|---| | Implemented callback with unset flag → silently skipped | **High** | Fees/limits/guards never run | | Flag set, no implementation → init reverts | **High** | Pool un-initializable (DoS) | | returnDelta flag mismatch → delta ignored | **High** | Stranded accounting, see [[v4-hook-delta-accounting]] | | Address bits disagree with getHookPermissions | **High** | Deterministic init revert | | Cosmetic flag set but callback is a true no-op | **Low** | Wasted gas only |
1. **Single source of truth** — derive the deploy salt from `getHookPermissions()` (e.g. `HookMiner.find` with the exact flag set) so address bits and the struct can't drift. 2. **Assert at construction** — `Hooks.validateHookPermissions(this, getHookPermissions())` in the constructor to fail fast on a wrong address. 3. **Implement exactly the flagged callbacks** — every `true` has an override; every override has a `true`. Remove dead callbacks or set their flag. 4. **Set the matching `*ReturnDelta` flag** whenever a callback can return a non-zero delta. 5. **Test against the real PoolManager** init path, not a mock that skips validation.
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 reentrancy in Uniswap V4 hooks via the PoolManager unlock/lock callback. V4 uses a singleton PoolManager with transient lock state; all pool mutations…