This example demonstrates how to use CosmFuzz to fuzz a CW20 token contract.
Let's fuzz the cw20-base contract from the cw-plus repository.
cd ~/cw-pluscargo cosmfuzz init --contracts cw20-baseThis 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 = 5Edit 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
}
}
}
}cargo cosmfuzz buildOutput:
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
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,
};cargo cosmfuzz fuzzThe 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
After fuzzing for a while (Ctrl+C to stop):
cargo cosmfuzz coverageThis generates a coverage report showing which code paths were explored.
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-seedsTo reduce the corpus to minimal interesting test cases:
cargo cosmfuzz minimizeFor fuzzing multiple interacting contracts (e.g., CW20 + Staking):
cargo cosmfuzz init --contracts cw20-base cw20-stakingThen 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
}
}- Start simple: Begin with basic invariants and add complexity
- Focus on state invariants: Properties that should always hold
- Check cross-contract interactions: Ensure contracts interact correctly
- Use meaningful seeds: Bootstrap with known good/bad inputs
- Run for extended periods: Fuzzing improves with time
- Monitor coverage: Ensure you're testing all code paths
- Fix bugs iteratively: Fix found bugs and re-fuzz
- 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