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 CosmWasm (Rust) contracts — missing info.sender authorization in execute handlers, unbounded map iteration → gas/DoS, reply/submessage reply_id confusion, migrate admin backdoors, addr_validate vs raw string addresses, unchecked info.funds, Uint128
Claude Code
$ npx -y skills add omermaksutii/RugProof --skill cosmwasm --agent claude-code
Codex
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a codex -y
Cursor
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a cursor -y
Gemini CLI
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a gemini-cli -y
GitHub Copilot
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a github-copilot -y
OpenCode
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a opencode -y
Windsurf
$ npx skills add omermaksutii/RugProof --skill cosmwasm -a windsurf -y
name: cosmwasm description: Detect bug classes specific to CosmWasm (Rust) contracts — missing info.sender authorization in execute handlers, unbounded map iteration → gas/DoS, reply/submessage reply_id confusion, migrate admin backdoors, addr_validate vs raw string addresses, unchecked info.funds, Uint128 overflow, query reentrancy, and migration/version state. Activate on any `.rs` file with `use cosmwasm_std`, `#[entry_point]`, `ExecuteMsg`, `InstantiateMsg`, `QueryMsg`, `cw_storage_plus`, or `DepsMut`.
CosmWasm has no `msg.sender`-gated visibility and no implicit auth. Every `execute` branch is callable by anyone unless the handler checks `info.sender`.
ExecuteMsg::SetConfig { admin } => {
let mut cfg = CONFIG.load(deps.storage)?;
cfg.admin = admin; // ← no info.sender check
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::new())
}**Signal:** an `ExecuteMsg` arm that mutates privileged state (`admin`, `owner`, `mint`, `withdraw`, `pause`) with no `if info.sender != cfg.admin { return Err(Unauthorized) }`. Anyone can dispatch any message.
let all: Vec<_> = BALANCES
.range(deps.storage, None, None, Order::Ascending) // ← whole map
.collect::<StdResult<_>>()?;
for (addr, bal) in all { /* mutate each */ }**Signal:** `.range(.., None, None, ..)` / `.keys(..)` over a user-growable `Map` inside `execute`, or a loop whose length attackers control. Each entry costs gas; an attacker inflates the map until the handler always runs out of gas (permanent freeze). Paginate with `start_after` + `limit`.
#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
let res = msg.result.unwrap(); // ← assumes success; panics on err
// no match on msg.id → all replies handled identically
parse_instantiate_response_data(&res.data.unwrap())?;
Ok(Response::new())
}**Signal:** a `reply` handler that doesn't `match msg.id { ... }` against known reply IDs, or `.unwrap()`s `msg.result`/`msg.result.data`, or trusts `SubMsgResult` data without checking it came from the expected submessage. Different submessages can land in the same reply with attacker-influenced data.
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
CONFIG.save(deps.storage, &Config { admin: msg.new_admin })?; // ← arbitrary rewrite
Ok(Response::new()) // no version/sender guard
}**Signal:** `migrate` that overwrites owner/config from `MigrateMsg` without checking `set_contract_version`/`get_contract_version` (downgrade or replay), or a centralization risk where the code-admin can swap logic+state at will. Even with chain-level migrate auth, document who holds the admin.
let recipient = msg.to; // ← String, never validated BALANCES.save(deps.storage, &Addr::unchecked(&recipient), &amt)?; // ← unchecked
**Signal:** `Addr::unchecked(...)` on user input, or storing/using a `String` address without `deps.api.addr_validate(&s)?`. Unvalidated/un-normalized addresses split balances across casing/bech32 variants and break invariants.
ExecuteMsg::Deposit {} => {
let amount = msg.amount; // ← trusts a field, not info.funds
CREDIT.save(deps.storage, &info.sender, &amount)?;
Ok(Response::new())
}**Signal:** crediting a deposit from a message field instead of `info.funds`, or not asserting `info.funds` contains the expected denom AND amount (`must_pay`/`one_coin` from `cw_utils`). Also flag handlers that ignore unexpected attached funds.
let total = a + b; // ← Uint128 Add can overflow → error, let scaled = price * qty; // but `as`/u128 casts wrap silently
**Signal:** `Uint128`/`Uint256` `+`/`-`/`*` where overflow should be a domain error vs a panic, OR casting to/from primitive `u128`/`u64` with `as`/`.u128()` in accounting. Prefer `checked_add`/`checked_sub`/`checked_mul` returning `OverflowError`.
let price: PriceResp = deps.querier.query_wasm_smart(oracle, &QueryMsg::Price {})?;**Signal:** acting on a `query_wasm_smart` result from a user-supplied contract address, or assuming queries are side-effect-free guarantees (a malicious queried contract returns adversarial data). Validate the oracle/contract address against an allowlist.
| Pattern | Severity | Notes | |---|---|---| | Execute arm mutating privileged state, no `info.sender` check | **Critical** | Anyone seizes admin/funds | | Unbounded `.range(None,None)` over user-growable map | **High** | Permanent gas-DoS freeze | | `reply` without `match msg.id` / unwrap of result | **High** | Cross-submsg data confusion | | `migrate` overwrites owner/config unguarded | **High** | Logic+state backdoor | | Deposit credited from field, not `info.funds` | **High** | Free credit / fund theft | | `Uint128` overflow / lossy primitive cast | **High** | Value-dependent | | `Addr::unchecked` on user input | **Medium** | A
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…