Back to Blog

CosmWasm Security: The 8 Most Common Vulnerabilities

|Odin Scan Team
CosmWasm Security: The 8 Most Common Vulnerabilities

Rust eliminates memory safety bugs. No buffer overflows, no use-after-free, no null pointer dereferences. If you come from the Solidity world, that sounds like security paradise.

It is not paradise. It is just a different set of problems.

CosmWasm contracts running on Cosmos chains hold real value. The Osmosis DEX, Neutron, Mars Protocol, and dozens of other protocols run on CosmWasm. The vulnerabilities that affect them are Rust-specific, Cosmos-specific, and often invisible to auditors who learned their craft on EVM contracts.

These are the eight patterns that appear most often in CosmWasm security reviews.


1. Missing Input Validation

The most common finding in any CosmWasm audit. Functions accept amounts, addresses, and parameters without checking whether the values make sense.

The risk: Zero amounts can bypass fee logic. Empty or malformed addresses cause funds to be sent to invalid destinations. Out-of-range values corrupt contract state.

// Vulnerable: no validation
pub fn transfer(
    deps: DepsMut,
    recipient: String,
    amount: Uint128,
) -> Result<Response, ContractError> {
    BALANCES.update(deps.storage, &recipient, |b| -> StdResult<_> {
        Ok(b.unwrap_or_default().checked_add(amount)?)
    })?;
    Ok(Response::new())
}
// Safe: validate before touching state
pub fn transfer(
    deps: DepsMut,
    recipient: String,
    amount: Uint128,
) -> Result<Response, ContractError> {
    if amount.is_zero() {
        return Err(ContractError::InvalidAmount {});
    }
    if recipient.is_empty() {
        return Err(ContractError::InvalidRecipient {});
    }
    // addr_validate checks the bech32 format and prefix
    deps.api.addr_validate(&recipient)?;

    BALANCES.update(deps.storage, &recipient, |b| -> StdResult<_> {
        Ok(b.unwrap_or_default().checked_add(amount)?)
    })?;
    Ok(Response::new())
}

Validate: non-zero amounts, non-empty addresses, address format via deps.api.addr_validate, and any numeric bounds your business logic requires.


2. Missing Access Control

Public execute handlers that should be restricted. Admin functions callable by anyone. Ownership checks that can be bypassed.

The risk: An attacker calls a privileged function, changes critical parameters, drains funds, or takes ownership of the contract.

// Vulnerable: anyone can call this
pub fn set_config(
    deps: DepsMut,
    new_fee: Decimal,
) -> Result<Response, ContractError> {
    CONFIG.update(deps.storage, |mut c| -> StdResult<_> {
        c.fee = new_fee;
        Ok(c)
    })?;
    Ok(Response::new())
}
// Safe: check the caller
pub fn set_config(
    deps: DepsMut,
    info: MessageInfo,
    new_fee: Decimal,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    CONFIG.update(deps.storage, |mut c| -> StdResult<_> {
        c.fee = new_fee;
        Ok(c)
    })?;
    Ok(Response::new())
}

Every state-changing function needs an explicit caller check unless it is intentionally public. Document which functions are public and why.


3. Predictable Randomness

CosmWasm contracts cannot access a secure on-chain random number. Developers work around this in ways that produce predictable outputs attackers can game.

Common bad sources of randomness:

  • env.block.height
  • env.block.time
  • Transaction hashes
  • Combinations of the above

The risk: Lottery contracts, NFT minting with random traits, shuffle mechanics, and any game element that relies on randomness is gameable if the "random" value is predictable.

// Vulnerable: block time is known before the transaction
let seed = env.block.time.nanos() % options.len() as u64;
let winner = options[seed as usize].clone();

The fix: Use a commit-reveal scheme for user-influenced randomness, or integrate with a verifiable randomness oracle like Nois (the Cosmos-native VRF solution). Never use block data alone as entropy.

// Better: use Nois VRF for verifiable randomness
// The randomness is provided externally by the Nois oracle
// and cannot be predicted or manipulated by the block proposer
pub fn receive_randomness(
    deps: DepsMut,
    _env: Env,
    randomness: HexBinary,
    job_id: String,
) -> Result<Response, ContractError> {
    let randomness: [u8; 32] = randomness.to_array()?;
    // Use this randomness to resolve the pending game
    resolve_game(deps, job_id, randomness)
}

4. Storage Key Collisions

CosmWasm uses raw byte keys for storage. Two different maps or items that share the same key prefix will silently overwrite each other's data.

The risk: A write to one storage variable overwrites a completely different variable. State corruption, loss of funds, broken invariants.

// Vulnerable: both maps use the raw string "bal" as prefix
const USER_BALANCES: Map<&Addr, Uint128> = Map::new("bal");
const LP_BALANCES: Map<&Addr, Uint128> = Map::new("bal"); // collision!
// Safe: distinct, descriptive prefixes
const USER_BALANCES: Map<&Addr, Uint128> = Map::new("user_balances");
const LP_BALANCES: Map<&Addr, Uint128> = Map::new("lp_balances");

This is especially dangerous during contract upgrades, when new storage variables are added without checking for prefix conflicts with existing state. Maintain a storage layout document and check it during every migration.


5. Unbounded Loops

Iterating over a collection that can grow without bound causes transactions to run out of gas. If the loop is on a critical code path, the contract can become permanently locked.

The risk: An attacker can bloat the collection (by creating many entries) until the loop exceeds the gas limit, bricking any function that iterates over it.

// Vulnerable: iterates the entire map on every call
pub fn total_rewards(deps: Deps) -> StdResult<Uint128> {
    let total = REWARDS
        .range(deps.storage, None, None, Order::Ascending)
        .map(|item| item.map(|(_, v)| v))
        .try_fold(Uint128::zero(), |acc, v| Ok(acc.checked_add(v?)?))?;
    Ok(total)
}
// Safe: paginate or maintain a running total
pub fn total_rewards(
    deps: Deps,
    start_after: Option<Addr>,
    limit: u32,
) -> StdResult<Vec<(Addr, Uint128)>> {
    let limit = limit.min(MAX_PAGE_SIZE) as usize;
    let start = start_after.as_ref().map(Bound::exclusive);

    REWARDS
        .range(deps.storage, start, None, Order::Ascending)
        .take(limit)
        .collect()
}

The pattern: never iterate an unbounded collection in a single transaction. Use pagination for queries. For aggregates like totals, maintain a running sum in a separate storage item updated on every write.


6. Unchecked Address Validation (unchecked_addr)

CosmWasm provides two address types: Addr (validated) and String (unvalidated). Storing a raw String as an address, or creating an Addr via Addr::unchecked(), skips the bech32 validation that ensures the address is well-formed.

The risk: A malformed or attacker-controlled address string reaches contract storage. Funds sent to that address are lost. Authorization checks against that address pass or fail unexpectedly.

// Vulnerable: unchecked bypasses validation
let recipient = Addr::unchecked(info.sender.to_string());
// or: storing a raw String from user input
ADMIN.save(deps.storage, &new_admin_string)?;
// Safe: always validate before storing
let recipient = deps.api.addr_validate(&raw_address)?;
ADMIN.save(deps.storage, &recipient)?;

The rule: never use Addr::unchecked() on user-supplied input. Use it only for addresses that are compile-time constants you control, like contract addresses in test code.


7. Unsafe Math (unsafe_math)

Rust panics on integer overflow in debug mode but wraps silently in release mode unless you use checked arithmetic. CosmWasm's Uint128 type provides checked methods, but not every developer uses them consistently.

The risk: Arithmetic overflow causes a value to wrap around to zero or a small number. Fee calculations undercharge. Balance checks pass when they should fail. Amounts are credited incorrectly.

// Vulnerable: wraps silently on overflow in release builds
let new_balance = current_balance + deposit_amount;
// Safe: returns an error on overflow
let new_balance = current_balance
    .checked_add(deposit_amount)
    .map_err(|_| ContractError::Overflow {})?;

For every arithmetic operation involving user-supplied amounts or accumulated values, use checked_add, checked_sub, checked_mul, and checked_div. Never use the +, -, *, / operators on Uint128 values in production paths.


8. Unsaved Storage

The most CosmWasm-specific bug on this list. A developer loads a value from storage, modifies it in memory, and forgets to save it back. The state change silently disappears.

// Vulnerable: modification is never persisted
pub fn increment_counter(deps: DepsMut) -> Result<Response, ContractError> {
    let mut counter = COUNTER.load(deps.storage)?;
    counter.value += 1;
    // forgot to call COUNTER.save(deps.storage, &counter)?
    Ok(Response::new())
}
// Safe: always save after modifying
pub fn increment_counter(deps: DepsMut) -> Result<Response, ContractError> {
    let mut counter = COUNTER.load(deps.storage)?;
    counter.value = counter.value
        .checked_add(1)
        .map_err(|_| ContractError::Overflow {})?;
    COUNTER.save(deps.storage, &counter)?;
    Ok(Response::new())
}

Alternatively, use .update() which handles the load-modify-save cycle atomically:

COUNTER.update(deps.storage, |mut c| -> StdResult<_> {
    c.value = c.value.checked_add(1)?;
    Ok(c)
})?;

This bug is invisible at compile time and easy to miss in code review because the surrounding code looks correct. It shows up only when you notice state is not persisting between transactions.


The Common Thread

Seven of these eight vulnerabilities share a root cause: the developer wrote code that compiles, passes basic tests, and behaves correctly in the happy path. The bug only appears under adversarial input or edge conditions that unit tests rarely cover.

The eighth (unsaved storage) is a logic error that produces no error, just silent data loss.

This is why automated scanning matters for CosmWasm contracts specifically. The Rust compiler catches a huge class of bugs. The ones it does not catch are semantic: wrong authorization logic, wrong arithmetic method, wrong storage prefix. Those require pattern-based analysis on top of compilation.


Odin Scan includes dedicated CosmWasm rules for all eight vulnerability patterns listed here, plus additional checks for IBC callbacks, CW20/CW721 standard compliance, and migration safety. Scan your CosmWasm contracts with a free trial.