Skip to content
Back to skills

Cosmwasm

ASecurity

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

  • 36 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 4, 2026
securityrustapi

Works with

  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add iktok90-design/ai-smart-contract-auditor --skill cosmwasm --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Cosmwasm?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Cosmwasm
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/iktok90-design-cosmwasm/badge)](https://www.skillsdirectory.com/skills/iktok90-design-cosmwasm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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)
```rust
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)
```rust
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)
```rust
#[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)
```rust
#[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)
```rust
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)
```rust
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)
```rust
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)
```rust
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** | Address-spoof / split state |
| Acting on query result from unvalidated contract | **Medium** | Oracle-trust dependent |

## Remediation patterns

1. **Authorize first** — every privileged `execute` arm starts with `ensure_eq!(info.sender, cfg.admin, ContractError::Unauthorized {})`.
2. **Paginate iteration** — `start_after` + bounded `limit` (e.g. 30); never `.range(None, None)` over attacker-growable maps in `execute`.
3. **Match reply IDs** — declare `const REPLY_X: u64`, `match msg.id { REPLY_X => ..., _ => Err(...) }`, and validate `SubMsgResult` before trusting its data.
4. **Guard migrate** — check `cw2::get_contract_version`, reject downgrades, and document the migrate-admin in centralization disclosures.
5. **Validate addresses** — `deps.api.addr_validate(&s)?`, never `Addr::unchecked` on user input.
6. **Verify funds** — use `cw_utils::must_pay`/`one_coin` against `info.funds`; reject unexpected denoms.
7. **`checked_*` math** on `Uint128`/`Uint256`; avoid `as`/`.u128()` casts in accounting.

## False-positive notes

- `Addr::unchecked` is acceptable in tests and when re-loading an address the contract itself validated and stored earlier.
- An `execute` arm that is intentionally permissionless (e.g. public `Claim {}` keyed by `info.sender`) needs no admin check — confirm it can only affect the caller's own state.
- `Uint128` `+`/`-` already returns an `OverflowError` (not a silent wrap) — the real risk is primitive `u128`/`as` casts and `Decimal` scaling; focus there.
- A bounded `.range(.., Some(limit))` is fine.

## Related

- [[access-control]] — CosmWasm has no implicit sender gate; every execute arm is public
- [[dos-vectors]] — unbounded map iteration is the canonical CosmWasm gas-freeze
- [[centralization-risk]] — migrate/admin powers concentrate upgrade + state control

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…