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 that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta, take/mint, donate, settle/sync) is tracked in the manager's transient nonzeroDeltaCount; if the books aren't flat when
$ npx -y skills add omermaksutii/RugProof --skill v4-hook-delta-accounting --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/v4-hook-delta-accountingContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta, take/mint, donate, settle/sync) is tracked in the manager's transient nonzeroDeltaCount; if the books aren't flat when
name: v4-hook-delta-accounting description: Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta, take/mint, donate, settle/sync) is tracked in the manager's transient nonzeroDeltaCount; if the books aren't flat when unlock returns, the whole transaction reverts (CurrencyNotSettled), and mismatched take/settle/donate either strands hook funds in the manager or lets a swap leave with unpaid debt. Activate on hooks returning deltas, calling take/settle/mint/burn/donate, or custom unlockCallback accounting.
Trigger on any of:
function afterSwap(address, PoolKey calldata key, ..., BalanceDelta, bytes calldata)
external override returns (bytes4, int128)
{
poolManager.take(key.currency0, address(this), feeAmount); // ← creates a -debt for the hook
return (this.afterSwap.selector, 0); // ← returns 0 delta, never settles
}`take` debits the hook's currency balance in the manager; with no matching `settle`/returned delta, `nonzeroDeltaCount != 0` and the entire `unlock` reverts `CurrencyNotSettled` — every swap on the pool reverts. **Signal:** `take`/`mint` without a balancing `settle`/`burn` or a non-zero returned `hookDelta` accounting for it.
return (this.afterSwap.selector, int128(feeAmount)); // claims to owe the pool feeAmount... // ...but the hook never `sync` + `settle`s those tokens into the manager
A positive `hookDelta` says "the hook owes the pool X"; if the hook never actually transfers and settles X, the books don't flatten → revert, or in the inverse direction the swapper leaves with unpaid debt. **Signal:** non-zero returned delta with no corresponding `sync`/`settle` (or `take`) of the same currency/amount.
poolManager.donate(key, amount0, amount1, ""); // adds to pool reserves... // hook forgot to settle the donated tokens it owes
`donate` increases what the hook owes the pool; the tokens must be `settle`d. Imbalanced donate strands funds or reverts. **Signal:** `donate` without settling the donated amounts, or `take` of donated funds with no offsetting credit.
BeforeSwapDelta d = toBeforeSwapDelta(int128(amtSpecified), 0); // wrong: specified vs unspecified swapped
`BeforeSwapDelta` packs (specified, unspecified) deltas; swapping the two halves or the sign mis-accounts the swap and either reverts or hands the swapper free output. **Signal:** `toBeforeSwapDelta` arguments in the wrong slot/sign, or specified-delta not reconciled with the actual swap amount.
| Pattern | Severity | Notes | |---|---|---| | take/mint with no matching settle → CurrencyNotSettled | **High** | Pool-wide swap DoS | | Returned hookDelta not backed by settle → revert or free funds | **High** | Loss or DoS | | Donate without settling owed tokens | **High** | Stranded funds / revert | | BeforeSwapDelta sign/slot error | **High** | Mis-accounted swap, value leak | | Hook over-settles (pays more than owed) | **Medium** | Hook self-loss, no swapper gain | | Deltas always net to zero within callback | **Info** | Correct accounting |
1. **Net every delta to zero before unlock returns** — for each currency the hook touches, pair `take` with `settle` (or a correct returned `hookDelta`). 2. **sync → transfer → settle** — call `poolManager.sync(currency)`, transfer the tokens in, then `settle()` so the manager credits the exact owed amount. 3. **Use the official delta helpers** — `toBeforeSwapDelta(specified, unspecified)` with correct argument order and signs; reconcile `specified` against `params.amountSpecified`. 4. **Assert flatness in tests** — after a swap, assert `poolManager.currencyDelta(hook, currency) == 0` for every currency. 5. **Settle donations** — every `donate` must be followed by transferring + settling the donated amounts.
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 whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the hook's permissions live in the…
Detect reentrancy in Uniswap V4 hooks via the PoolManager unlock/lock callback. V4 uses a singleton PoolManager with transient lock state; all pool mutations…