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 cross-contract state inconsistency — two or more contracts sharing a token, oracle, or price feed where one mutates and another reads stale, cached state that drifts from source of truth, non-atomic multi-contract updates, accounting that assumes synchronized state, and
$ npx -y skills add omermaksutii/RugProof --skill cross-contract-state --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/cross-contract-stateContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect cross-contract state inconsistency — two or more contracts sharing a token, oracle, or price feed where one mutates and another reads stale, cached state that drifts from source of truth, non-atomic multi-contract updates, accounting that assumes synchronized state, and
name: cross-contract-state description: Detect cross-contract state inconsistency — two or more contracts sharing a token, oracle, or price feed where one mutates and another reads stale, cached state that drifts from source of truth, non-atomic multi-contract updates, accounting that assumes synchronized state, and reads during callbacks. Activate whenever a system spans multiple contracts that must agree on a value but update at different times.
Trigger on any of:
// Vault.deposit() transfers to Strategy, then: uint256 tvl = strategy.totalAssets(); // ← strategy hasn't accounted the deposit yet shares = amount * totalSupply / tvl; // wrong denominator
**Signal:** B reads A while A's update is incomplete (or vice versa). Share/price math uses a denominator that's about to change, letting an attacker mint mispriced shares. This is the read-only-reentrancy family generalized to ordinary call ordering.
uint256 public cachedPrice; // updated by poke()
function valueOf(uint256 amt) external view returns (uint256) {
return amt * cachedPrice / 1e18; // ← may be hours stale
}**Signal:** a mirrored value with no freshness guarantee or no atomic refresh-before-use. Trades/loans price off a cache that diverged from the source contract.
registry.setActive(id, true); // external call here can observe registry=active but vault=uninitialized vault.initialize(id);
**Signal:** state spread across contracts is updated in sequence; a reentrant or interleaved call observes a half-applied transaction (one contract updated, the other not). Invariants that span both contracts are temporarily violated.
Two contracts both read the same oracle/token but at different blocks or via different cached copies, then reconcile assuming equality. **Signal:** `assert(A.totalSupply() == B.mirroredSupply())`-style assumptions with independent update paths.
A hook/plugin invoked mid-operation calls back and reads the core contract's not-yet-finalized accounting — the Curve/Balancer read-only reentrancy shape, but also plain composability (e.g. an ERC-4626 vault read by a money-market during the vault's own deposit).
| Pattern | Severity | Notes | |---|---|---| | Stale cross-contract read in share/price math | **High** | Mispriced mint/redeem | | Read-during-callback of unfinalized state | **High** | Read-only reentrancy class | | Non-atomic multi-contract invariant break | **High** | Half-applied state observable | | Cached value drift used for valuation | **High** | Stale pricing | | Synchronized-state assumption across modules | **Medium** | Reconciliation error |
1. **Single source of truth** — derive values from one authoritative contract at read time; avoid mirrored copies, or make the mirror push-updated atomically in the same tx. 2. **Update-then-read ordering** — ensure A finalizes accounting before B reads; apply CEI across the call boundary, not just within a function. 3. **Atomic multi-contract updates** — batch via a single entrypoint / multicall that completes all writes before any external observation; guard with a system-wide reentrancy lock. 4. **Freshness gates on caches** — `require(block.timestamp - lastUpdate <= maxAge)` or refresh-on-read. 5. **Reentrancy-guard the read path too** (Balancer `ensureNotInVaultContext` / Curve lock) when callbacks can observe state.
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…