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 bug classes specific to Arbitrum Stylus (Rust→WASM) contracts — storage aliasing & EVM state-cache coherence, msg::value / #[payable] handling, external-call reentrancy, panic-on-attacker-input DoS, release-mode integer wrapping, host-IO (evm::) misuse,
$ npx -y skills add omermaksutii/RugProof --skill stylus-rust --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/stylus-rustContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect bug classes specific to Arbitrum Stylus (Rust→WASM) contracts — storage aliasing & EVM state-cache coherence, msg::value / #[payable] handling, external-call reentrancy, panic-on-attacker-input DoS, release-mode integer wrapping, host-IO (evm::) misuse,
name: stylus-rust description: Detect bug classes specific to Arbitrum Stylus (Rust→WASM) contracts — storage aliasing & EVM state-cache coherence, msg::value / #[payable] handling, external-call reentrancy, panic-on-attacker-input DoS, release-mode integer wrapping, host-IO (evm::) misuse, #[entrypoint]/#[public] access control, and lossy U256 conversions. Activate on any `.rs` file with `use stylus_sdk`, `#[entrypoint]`, `#[storage]`, `sol_storage!`, `#[public]`, or `#[payable]`.
Stylus runs the SAME EVM state and shares the SAME external-call surface as Solidity. Rust safety does NOT remove EVM-level footguns — it adds new ones (panics, wrapping arithmetic, aliasing).
let mut bal = self.balances.get(from); // ← copies value out of storage do_external_call(); // callee may mutate self.balances self.balances.insert(from, bal - amount); // ← writes back STALE value
**Signal:** a `.get()` cached in a local, an intervening call/host-IO, then a `.set()`/`.insert()` of the stale local. Stylus storage reads are snapshots, not live references — re-read after any external call.
pub fn withdraw(&mut self) -> Result<(), Vec<u8>> {
let amt = self.balance.get(msg::sender());
call::transfer_eth(msg::sender(), amt)?; // ← control leaves contract
self.balance.setter(msg::sender()).set(U256::ZERO); // ← AFTER the call
Ok(())
}**Signal:** `transfer_eth` / `Call::new().call(...)` / `RawCall` before the storage write that zeroes the credited amount. Reentrancy is OFF by default but `Call::new()` and `transfer_eth` still re-enter; CEI still required.
let idx: usize = msg::data()[0].into(); let entry = self.items.get(idx).unwrap(); // ← panics → reverts whole tx
**Signal:** `.unwrap()`, `.expect()`, indexing `[i]`, or arithmetic that can panic on caller-controlled input. A Rust panic traps the WASM and reverts — fine for the victim caller, but if it's on a path others depend on (batch settlement, queue processing) it's a griefing DoS. Return `Result`/`Err`, never panic on untrusted input.
let total = a + b; // ← in --release this WRAPS, no panic, no revert self.supply.set(total);
**Signal:** bare `+`/`-`/`*` on `u64`/`u128`/`U256` in contract math. Stylus ships in release mode where overflow checks are OFF (wraps silently). `alloy` `U256` also wraps on `+`. Use `checked_add`/`checked_sub`/`checked_mul` (or `overflow-checks = true` in `Cargo.toml`).
#[public]
impl Token {
pub fn deposit(&mut self) { // ← NOT #[payable]
let v = msg::value(); // value forced to 0 by ABI? no —
self.credit.setter(msg::sender()).set(v);
}
}**Signal:** reading `msg::value()` in a non-`#[payable]` method (sent ETH gets stuck / call reverts depending on path), OR a `#[payable]` method that credits `msg::value()` without bounds, OR forgetting `msg::value()` is per-call (re-reads of it don't re-validate).
#[public]
impl Vault {
pub fn set_owner(&mut self, who: Address) { // ← anyone can call
self.owner.set(who);
}
}**Signal:** every method in a `#[public] impl` is externally callable. Admin/`set_*`/`mint`/`upgrade` methods with no `if msg::sender() != self.owner.get() { return Err(...) }` check. There is no implicit visibility gate — `#[public]` == external.
let n: u64 = amount.to::<u64>(); // ← truncates if amount > u64::MAX let small: u128 = big_u256.wrapping_to();
**Signal:** `.to::<u64>()`, `as u64`, `wrapping_to`, or `try_into().unwrap()` from `U256` to a narrow type used in accounting. Use `U256::try_into` and handle the error.
let ok = RawCall::new().call(target, &data)?; // ← gas, return-size, success
**Signal:** `RawCall`/`Call` results where success bool and return bytes aren't both checked, or hard-coded gas via `.gas(...)` that strands recipients. Same low-level-call hygiene as Solidity ([[unchecked-calls]]).
| Pattern | Severity | Notes | |---|---|---| | External call before state finalize (CEI broken) | **Critical** | Direct fund drain, same as Solidity | | Stale storage cache across external call | **High** | Aliasing reads are snapshots | | Release-mode wrapping in supply/balance math | **High** | Overflow checks off by default | | `unwrap`/index/panic on attacker input | **High** | Revert-DoS on shared paths | | `msg::value` mishandled vs `#[payable]` | **High** | Stuck or unbounded ETH | | `#[public]` admin method, no sender check | **High** | Anyone calls privileged fn | | Truncating `U256→u64/u128` in accounting | **Medium** | Value-dependent | | Unchecked `RawCall`/hardcoded gas | **Medium** | Recipient-dependent |
1. **CEI ordering** — write all storage effects before `transfer_eth`/`Call`. Re-read storage after any external call instead of trusting a cached local. 2. **`checked_*` arithmetic** everywhere, or set `overflow-checks = true` under `[profile.release]` in `Cargo.toml` (accept the WAS
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…