example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
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,
Claude Code
$ npx -y skills add omermaksutii/RugProof --skill stylus-rust --agent claude-code
Codex
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a codex -y
Cursor
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a cursor -y
Gemini CLI
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a gemini-cli -y
GitHub Copilot
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a github-copilot -y
OpenCode
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a opencode -y
Windsurf
$ npx skills add omermaksutii/RugProof --skill stylus-rust -a windsurf -y
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
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…