Skip to content
Security
Skill

/cosmwasm

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

From plugin
rugproof
952 skills23 agents45 commands4 hooks
Install
$ npx -y skills add omermaksutii/RugProof --skill cosmwasm --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/cosmwasm

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

cosmwasm.SKILL.md
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 (Rust) detection

When this applies

  • Any `.rs` file importing `cosmwasm_std` (`use cosmwasm_std::{...}`)
  • Macros/attrs `#[entry_point]`, `#[cw_serde]`; enums `ExecuteMsg`, `InstantiateMsg`, `QueryMsg`, `MigrateMsg`, `SudoMsg`
  • `cw_storage_plus` types `Map`, `Item`, `IndexedMap`; `DepsMut`/`Deps`, `MessageInfo`, `Env`
  • `SubMsg`, `Reply`, `reply()`, `set_contract_version`, `cw2`

CosmWasm has no `msg.sender`-gated visibility and no implicit auth. Every `execute` branch is callable by anyone unless the handler checks `info.sender`.

Detection patterns

Missing info.sender authorization in execute (CRITICAL)

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.

Unbounded map iteration → gas exhaustion DoS (HIGH)

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`.

reply_id confusion / unvalidated submessage reply (HIGH)

#[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.

migrate admin backdoor / unguarded migrate (HIGH)

#[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.

addr_validate vs raw string address (MEDIUM-HIGH)

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.

Unchecked info.funds (HIGH)

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.

Uint128 / Uint256 overflow & raw arithmetic (HIGH)

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`.

Query reentrancy / cross-contract query trust (MEDIUM)

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.

Severity rubric

| 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

Read more
Ships withrugproof

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

Get the whole plugin

Other skills on rugproof.