Install
$ agentstack add skill-omermaksutii-rugproof-stylus-rust ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README — it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Stylus (Rust) detection
When this applies
- Any
.rsfile importingstylus_sdk(use stylus_sdk::{...}) - Macros
#[entrypoint],#[storage],sol_storage!,#[public],#[payable] - Host-IO calls:
evm::,msg::,block::,contract::,call::(Call::new,transfer_eth,RawCall) - Stylus types
StorageU256,StorageMap,StorageVec,alloy_primitives::U256/Address - A
Cargo.tomldeclaringstylus-sdkwithcrate-type = ["lib", "cdylib"]
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).
Detection patterns
Storage aliasing / stale local copy of EVM state (HIGH)
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.
Reentrancy via external call before state finalization (CRITICAL)
pub fn withdraw(&mut self) -> Result> {
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.
Panic / unwrap on attacker input = revert-DoS (HIGH)
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.
Release-mode integer wrapping (HIGH)
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).
#[payable] vs unguarded msg::value (HIGH)
#[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] / #[entrypoint] missing access control (HIGH)
#[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.
Lossy / truncating U256 conversions (MEDIUM-HIGH)
let n: u64 = amount.to::(); // ← truncates if amount > u64::MAX
let small: u128 = big_u256.wrapping_to();
Signal: .to::(), 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.
Host-IO host call assumptions (MEDIUM)
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]]).
Severity rubric
| 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 |
Remediation patterns
- CEI ordering — write all storage effects before
transfer_eth/Call. Re-read storage after any external call instead of trusting a cached local. checked_*arithmetic everywhere, or setoverflow-checks = trueunder[profile.release]inCargo.toml(accept the WASM size/gas cost).- Never
unwrap/expect/raw-index on untrusted input — propagateResult>and returnErr. - Gate privileged
#[public]methods with an explicitmsg::sender()owner/role check; consider a reentrancy guard storage bool for callback paths. - Make value-receiving methods
#[payable]and validatemsg::value()bounds; reject unexpected value in non-payable methods. - Use
try_into()forU256narrowing and handle the error rather thanas/wrapping_to.
False-positive notes
checked_add(...).ok_or(...)?and similar are already safe — don't flag wrapping there.- A
#[public]method that is a pure view (&self) with no state writes and no value isn't an access-control issue. .unwrap()on a value the contract itself just constructed (not attacker-derived) is generally fine — focus on untrusted-input paths.- Stylus reentrancy is disabled unless the
reentrantfeature is enabled; still flag CEI violations since the feature flag is set per-project and callbacks can re-enter cross-contract.
Related
- [[reentrancy]] — Stylus shares the EVM external-call reentrancy surface
- [[integer-issues]] — release-mode wrapping + lossy U256 conversions
- [[access-control]] —
#[public]/#[entrypoint]has no implicit visibility gate
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: omermaksutii
- Source: omermaksutii/RugProof
- License: MIT
- Homepage: https://omermaksutii.github.io/RugProof
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.