Skip to content

Latest commit

 

History

History
280 lines (218 loc) · 7.08 KB

File metadata and controls

280 lines (218 loc) · 7.08 KB

CosmFuzz Example: Fuzzing CW20 Contract

This example demonstrates how to use CosmFuzz to fuzz a CW20 token contract.

Setup

Let's fuzz the cw20-base contract from the cw-plus repository.

1. Navigate to the cw-plus workspace

cd ~/cw-plus

2. Initialize CosmFuzz

cargo cosmfuzz init --contracts cw20-base

This creates cosmfuzz-config.toml:

[cosmfuzz]
name = "cw-plus"
fuzz_dir = "/tmp/cosmfuzz"

[[contracts]]
name = "cw20-base"
path = "contracts/cw20-base"
entry_label = "cw20_base"

[fuzzing]
accounts = 5
native_denom = "uatom"
initial_balance = 1000
max_msg_count = 16
block_time_interval = 5

3. Add invariants to the contract

Edit contracts/cw20-base/src/lib.rs or contracts/cw20-base/src/contract.rs:

#[cfg(cosmfuzz)]
mod cosmfuzz {
    use super::*;
    use cosmwasm_std::{Addr, Uint128, StdResult};
    use cw_multi_test::App;
    use crate::msg::QueryMsg;

    pub fn invariant_total_supply_matches_balances(
        app: &App,
        cw20_base_addr: &Addr
    ) {
        // Query token info to get total supply
        let token_info: StdResult<cw20::TokenInfoResponse> = app.wrap().query_wasm_smart(
            cw20_base_addr,
            &QueryMsg::TokenInfo {}
        );

        if let Ok(info) = token_info {
            // Total supply should always be valid
            assert!(info.total_supply >= Uint128::zero());

            // Could add more checks:
            // - Sum of all balances equals total supply
            // - No balance exceeds total supply
            // - etc.
        }
    }

    pub fn invariant_balances_non_negative(
        app: &App,
        cw20_base_addr: &Addr
    ) {
        // Note: This is implicitly guaranteed by Uint128, but serves as an example
        // In practice, you might check:
        // - All balances in storage are valid
        // - No overflows occurred
        // - Allowances are sensible

        // Example: Query a balance and ensure it's valid
        let balance: StdResult<cw20::BalanceResponse> = app.wrap().query_wasm_smart(
            cw20_base_addr,
            &QueryMsg::Balance {
                address: app.api().addr_make("user0").to_string(),
            }
        );

        if let Ok(bal) = balance {
            assert!(bal.balance >= Uint128::zero());
        }
    }

    pub fn invariant_allowances_not_exceed_balance(
        app: &App,
        cw20_base_addr: &Addr
    ) {
        // Check that allowances granted don't exceed the owner's balance
        // This is a simplified example - in practice you'd iterate all allowances

        // Query allowance for a test pair
        let allowance: StdResult<cw20::AllowanceResponse> = app.wrap().query_wasm_smart(
            cw20_base_addr,
            &QueryMsg::Allowance {
                owner: app.api().addr_make("user0").to_string(),
                spender: app.api().addr_make("user1").to_string(),
            }
        );

        if let Ok(allow) = allowance {
            // Get the owner's balance
            let balance: StdResult<cw20::BalanceResponse> = app.wrap().query_wasm_smart(
                cw20_base_addr,
                &QueryMsg::Balance {
                    address: app.api().addr_make("user0").to_string(),
                }
            );

            if let Ok(bal) = balance {
                // Allowance shouldn't exceed balance (though it's allowed in the spec)
                // This is just a example invariant - adjust based on your requirements
                // In some protocols, allowance > balance is valid
            }
        }
    }
}

4. Build the fuzzing harness

cargo cosmfuzz build

Output:

Found configuration in: /home/user/cw-plus
Fuzz directory: /tmp/cosmfuzz/cw-plus

Discovering invariants...
  Scanning contract: cw20-base
    Found 3 invariant(s)
      - invariant_total_supply_matches_balances
      - invariant_balances_non_negative
      - invariant_allowances_not_exceed_balance

Generating fuzzing harness...
  ✓ Generated src/main.rs
  ✓ Generated Cargo.toml
  ✓ Generated env.sh
  ✓ Generated .gitignore

Building fuzzer with cargo ziggy...
[ziggy build output...]

✓ Build complete! Fuzzing harness ready at: /tmp/cosmfuzz/cw-plus

Run 'cargo cosmfuzz fuzz' to start fuzzing

5. REQUIRED: Customize the generated harness

The generated harness is at /tmp/cosmfuzz/cw-plus/src/main.rs.

You MUST customize the InstantiateMsg for your contracts. The generated harness includes TODO comments where you need to configure initialization parameters.

Edit the generated code to properly initialize your contract:

let cw20_base_init_msg = cw20_base_InstantiateMsg {
    name: "Fuzz Token".to_string(),
    symbol: "FUZZ".to_string(),
    decimals: 6,
    initial_balances: vec![
        cw20::Cw20Coin {
            address: accounts[0].to_string(),
            amount: Uint128::new(1000000),
        },
    ],
    mint: None,
    marketing: None,
};

6. Start fuzzing

cargo cosmfuzz fuzz

The fuzzer will:

  • Generate random inputs
  • Deserialize them into ExecuteMsg variants
  • Execute them on the contract
  • Check all invariants after each execution
  • Report any violations or panics

7. Generate coverage

After fuzzing for a while (Ctrl+C to stop):

cargo cosmfuzz coverage

This generates a coverage report showing which code paths were explored.

8. Add seeds (optional)

If you have specific test cases you want the fuzzer to learn from:

# Create a seeds directory
mkdir -p ~/my-seeds

# Add binary seeds (serialized ExecuteMsg)
# ... create your seeds ...

# Add them to the fuzzer
cargo cosmfuzz add-seeds ~/my-seeds

9. Minimize corpus

To reduce the corpus to minimal interesting test cases:

cargo cosmfuzz minimize

Multi-Contract Example

For fuzzing multiple interacting contracts (e.g., CW20 + Staking):

cargo cosmfuzz init --contracts cw20-base cw20-staking

Then add invariants to both contracts that can reference each other:

// In cw20-staking/src/lib.rs
#[cfg(cosmfuzz)]
mod cosmfuzz {
    use super::*;

    pub fn invariant_staked_tokens_are_locked(
        app: &App,
        cw20_base_addr: &Addr,
        cw20_staking_addr: &Addr,
    ) {
        // Verify that tokens staked in the staking contract
        // are actually locked in the CW20 contract
        // This requires querying both contracts
    }
}

Tips for Effective Fuzzing

  1. Start simple: Begin with basic invariants and add complexity
  2. Focus on state invariants: Properties that should always hold
  3. Check cross-contract interactions: Ensure contracts interact correctly
  4. Use meaningful seeds: Bootstrap with known good/bad inputs
  5. Run for extended periods: Fuzzing improves with time
  6. Monitor coverage: Ensure you're testing all code paths
  7. Fix bugs iteratively: Fix found bugs and re-fuzz

Common Invariants

  • Token supply equals sum of balances
  • No negative balances (enforced by types)
  • Allowances are valid
  • No unauthorized transfers
  • State transitions are valid
  • No funds are created or destroyed unexpectedly
  • Access control is enforced