MSG Chain CosmWasm 合约模糊测试与形式化验证指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目录
- 概述
- Rust 单元测试基础
- Fuzz Testing 与 cargo-fuzz
- 特性测试 (Property-based Testing)
- CosmWasm VM 仿真测试
- 形式化验证
- CI 集成
- 常见漏洞测试模式
- 附录
1. 概述
1.1 为什么智能合约需要模糊测试
智能合约一旦部署即不可篡改,漏洞可能导致不可逆的资金损失。CosmWasm 合约虽然受益于 Rust 的内存安全性,但逻辑漏洞仍然存在:
- 整型溢出/下溢(release 模式下 Rust 静默回绕)
- 权限检查遗漏
- 重入攻击(通过 SubMsg + Reply)
- 价格预言机操纵
- 精度损失与舍入错误
模糊测试(Fuzz Testing)通过生成大量随机输入来探索合约的状态空间,能够发现传统单元测试覆盖不到的边界情况。
1.2 模糊测试 vs 单元测试 vs 集成测试
| 维度 | 单元测试 | 集成测试 | 模糊测试 |
|---|---|---|---|
| 输入来源 | 开发者手动构造 | 有限的场景组合 | 自动生成随机输入 |
| 覆盖范围 | 已知路径 | 多合约交互 | 边界/异常/未预期 |
| 断言方式 | 期望结果对比 | 场景验证 | 断言不变量/Crash 检测 |
| 运行频率 | 每次提交 | 每次提交 | 定期/CI 夜间 |
| 发现漏洞类型 | 逻辑错误 | 交互错误 | 内存/溢出/边界/拒绝服务 |
1.3 形式化验证概述
形式化验证使用数学方法证明合约的正确性。不同于测试(只能证明存在 bug),形式化验证可以证明不存在特定类型的 bug。
在 CosmWasm 生态中,形式化验证的层次:
合约源代码 (Rust)
│
├── K Framework 语义模型 → 全合约验证
├── Rust 类型系统 → 编译时安全
├── 不变量断言 → 运行时检查
└── Model Checking → 状态空间穷举
1.4 MSG Chain 环境速查
Chain ID: msg-chain-1
Bech32 前缀: msg
代币精度: 18 位小数 (umsg)
签名算法: Dilithium-5 (后量子)
存储引擎: BadgerDB (LSM-Tree)
共识: Round-Robin + DAR
出块时间: ~5 秒
1.5 可用工具栈
| 工具 | 用途 | 适用阶段 |
|---|---|---|
cargo test |
单元测试 | 开发/CI |
cargo clippy |
静态分析 | 开发/CI |
cargo fuzz |
覆盖率引导模糊测试 | 开发/CI 夜间 |
proptest |
属性基测试 | 开发/CI |
cw-multi-test |
多合约集成测试 | 开发/CI |
cosmwasm-vm |
全节点仿真测试 | 部署前 |
K Framework |
形式化验证 | 审计/关键合约 |
make test |
Go 端测试 | CI |
make test-quantum |
量子密码测试 | CI |
make ci-contracts |
合约编译+测试 | CI |
2. Rust 单元测试基础
2.1 标准测试模块结构
每个 CosmWasm 合约必须在 src/contract.rs 或专用的 tests/ 中包含完整的测试覆盖。
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{from_binary, Addr, Binary, Uint128, coins};
use crate::contract::{instantiate, execute, query};
use crate::msg::{
InstantiateMsg, ExecuteMsg, QueryMsg,
GetBalanceResponse, TransferResponse,
};
use crate::error::ContractError;
use crate::state::{Config, State, CONFIG, BALANCES};
}
2.2 辅助函数模式
fn setup_contract() -> (
cosmwasm_std::OwnedDeps<
cosmwasm_std::MemoryStorage,
cosmwasm_std::testing::MockApi,
cosmwasm_std::testing::MockQuerier,
>,
cosmwasm_std::Env,
) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &coins(1000, "umsg")),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "MSG Test Token".to_string(),
token_symbol: "MTT".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
(deps, env)
}
fn mint_tokens(
deps: &mut cosmwasm_std::OwnedDeps<
cosmwasm_std::MemoryStorage,
cosmwasm_std::testing::MockApi,
cosmwasm_std::testing::MockQuerier,
>,
to: &str,
amount: Uint128,
) {
execute(
deps.as_mut(),
mock_env(),
mock_info("admin", &[]),
ExecuteMsg::Mint {
to: to.to_string(),
amount,
},
)
.unwrap();
}
2.3 标准测试场景
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info("admin", &coins(1000, "umsg"));
let res = instantiate(
deps.as_mut(),
env,
info,
InstantiateMsg {
owner: "admin".to_string(),
token_name: "MSG Test".to_string(),
token_symbol: "MST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
assert_eq!(res.attributes.len(), 4);
assert_eq!(res.attributes[0].value, "instantiate");
assert_eq!(res.attributes[1].value, "admin");
assert_eq!(res.attributes[2].value, "MSG Test");
let config = CONFIG.load(&deps.storage).unwrap();
assert_eq!(config.owner, "admin");
assert_eq!(config.token_name, "MSG Test");
assert_eq!(config.token_symbol, "MST");
}
#[test]
fn transfer_updates_balances() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &coins(1000, "umsg")),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::new(5000),
)
.unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("alice", &[]),
ExecuteMsg::Transfer {
recipient: "bob".to_string(),
amount: Uint128::new(1000),
},
)
.unwrap();
let alice_bal = BALANCES
.load(&deps.storage, &Addr::unchecked("alice"))
.unwrap();
let bob_bal = BALANCES
.load(&deps.storage, &Addr::unchecked("bob"))
.unwrap();
assert_eq!(alice_bal, Uint128::new(4000));
assert_eq!(bob_bal, Uint128::new(1000));
}
2.4 错误路径测试
#[test]
fn transfer_insufficient_balance_fails() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::new(100),
)
.unwrap();
let err = execute(
deps.as_mut(),
env,
mock_info("alice", &[]),
ExecuteMsg::Transfer {
recipient: "bob".to_string(),
amount: Uint128::new(200),
},
)
.unwrap_err();
assert_eq!(
err,
ContractError::InsufficientBalance {
balance: Uint128::new(100),
required: Uint128::new(200),
}
);
}
#[test]
fn unauthorized_mint_fails() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
let err = execute(
deps.as_mut(),
env,
mock_info("attacker", &[]),
ExecuteMsg::Mint {
to: "attacker".to_string(),
amount: Uint128::new(1_000_000),
},
)
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
}
2.5 边界条件测试
#[test]
fn transfer_max_uint128_handling() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::MAX,
)
.unwrap();
let err = execute(
deps.as_mut(),
env,
mock_info("alice", &[]),
ExecuteMsg::Transfer {
recipient: "bob".to_string(),
amount: Uint128::new(1),
},
)
.unwrap();
assert!(err.is_ok());
}
#[test]
fn transfer_zero_amount() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::new(100),
)
.unwrap();
let res = execute(
deps.as_mut(),
env,
mock_info("alice", &[]),
ExecuteMsg::Transfer {
recipient: "bob".to_string(),
amount: Uint128::zero(),
},
)
.unwrap();
assert_eq!(res.attributes.len(), 3);
}
#[test]
fn self_transfer() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::new(500),
)
.unwrap();
execute(
deps.as_mut(),
env,
mock_info("alice", &[]),
ExecuteMsg::Transfer {
recipient: "alice".to_string(),
amount: Uint128::new(100),
},
)
.unwrap();
let bal = BALANCES
.load(&deps.storage, &Addr::unchecked("alice"))
.unwrap();
assert_eq!(bal, Uint128::new(500));
}
2.6 Gas 边界测试
#[test]
fn test_gas_heavy_operation() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Test".to_string(),
token_symbol: "TST".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
let many_recipients: Vec<String> = (0..100)
.map(|i| format!("user{}", i))
.collect();
let err = execute(
deps.as_mut(),
env,
mock_info("admin", &[]),
ExecuteMsg::BatchTransfer {
recipients: many_recipients,
amount: Uint128::new(10),
},
);
match err {
Ok(_) => {}
Err(ContractError::GasLimitExceeded { .. }) => {}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
2.7 测试覆盖率要求
在 Cargo.toml 中配置覆盖率工具:
[dev-dependencies]
tarpaulin = "0.27"
# 生成覆盖率报告
cargo tarpaulin --ignore-tests --out Html --output-dir coverage
# 在 CI 中验证覆盖率
cargo tarpaulin --ignore-tests --fail-under 80
覆盖率目标:
| 指标 | 最低要求 | 目标 |
|---|---|---|
| 函数覆盖率 | 90% | 95%+ |
| 行覆盖率 | 85% | 92%+ |
| 分支覆盖率 | 80% | 90%+ |
| 错误路径覆盖 | 70% | 85%+ |
3. Fuzz Testing 与 cargo-fuzz
3.1 概述
cargo-fuzz 使用 libFuzzer 引擎,通过覆盖率引导(coverage-guided)的方式自动生成输入,探索程序中所有可达路径。对于 CosmWasm 合约,fuzz 测试可以发现:
- 反序列化崩溃(panic in
from_binary) - 整数溢出/下溢
- 断言失败(unreachable 路径)
- gas 耗尽攻击面
- 存储损坏
3.2 环境安装
# 安装 cargo-fuzz
cargo install cargo-fuzz
# 验证安装
cargo fuzz --help
# 在合约项目中初始化 fuzz 目录
cargo fuzz init
初始化后,项目结构变为:
my_contract/
├── Cargo.toml
├── src/
│ ├── contract.rs
│ ├── msg.rs
│ ├── state.rs
│ └── error.rs
├── fuzz/
│ ├── Cargo.toml # fuzz 依赖配置
│ └── targets/
│ └── fuzz_target_1.rs
3.3 Fuzz 依赖配置
# fuzz/Cargo.toml
[package]
name = "my_contract-fuzz"
version = "0.0.1"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer_sys = "0.4"
cosmwasm_std = { version = "1.5", features = ["stargate"] }
cosmwasm-storage = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# 被测试的合约
my_contract = { path = ".." }
3.4 基础 Fuzz Target:Instantiate + Execute 模糊测试
// fuzz/targets/fuzz_execute.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, from_binary};
use my_contract::contract::{instantiate, execute, query};
use my_contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fuzz_target!(|data: &[u8]| {
if let Ok(msg) = from_binary::<ExecuteMsg>(&data.into()) {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
owner: "admin".to_string(),
token_name: "Fuzz".to_string(),
token_symbol: "FUZZ".to_string(),
initial_supply: Uint128::new(1_000_000),
};
if instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
init_msg,
)
.is_err()
{
return;
}
let random_funds = coins(
(data.len() % 1000) as u128,
"umsg",
);
let _ = execute(
deps.as_mut(),
env,
mock_info("user", &random_funds),
msg,
);
}
});
3.5 结构体 Fuzz Target
// fuzz/targets/fuzz_instantiate_params.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use my_contract::contract::instantiate;
fuzz_target!(|data: &[u8]| {
let mut deps = mock_dependencies();
let env = mock_env();
if let Ok(owner_str) = String::from_utf8(data.to_vec()) {
let truncated = if owner_str.len() > 128 {
&owner_str[..128]
} else {
&owner_str
};
let msg = my_contract::msg::InstantiateMsg {
owner: truncated.to_string(),
token_name: "FuzzTest".to_string(),
token_symbol: "FT".to_string(),
initial_supply: cosmwasm_std::Uint128::new(
data.len() as u128,
),
};
let _ = instantiate(
deps.as_mut(),
env,
mock_info("fuzzer", &[]),
msg,
);
}
});
3.6 多步操作 Fuzz Target
// fuzz/targets/fuzz_multi_step.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, from_binary, Uint128};
use my_contract::contract::{instantiate, execute};
use my_contract::msg::{ExecuteMsg, InstantiateMsg};
const MAX_STEPS: usize = 100;
fuzz_target!(|data: &[u8]| {
let mut deps = mock_dependencies();
let env = mock_env();
if instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "MultiStep".to_string(),
token_symbol: "MSTEP".to_string(),
initial_supply: Uint128::new(1_000_000_000),
},
)
.is_err()
{
return;
}
let chunks: Vec<&[u8]> = data.chunks(64).collect();
for (step_count, chunk) in chunks.iter().enumerate().take(MAX_STEPS) {
if let Ok(msg) = from_binary::<ExecuteMsg>(chunk) {
let sender = format!("user_{}", step_count % 10);
let funds = coins(
(chunk[0] as u128) * 1000,
"umsg",
);
if execute(
deps.as_mut(),
env.clone(),
mock_info(&sender, &funds),
msg,
)
.is_err()
{
continue;
}
}
}
});
3.7 Fuzz 测试 Execute Message 枚举变种
// fuzz/targets/fuzz_execute_variants.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, Uint128, Addr};
use my_contract::contract::{instantiate, execute};
use my_contract::msg::{ExecuteMsg, InstantiateMsg};
use my_contract::state::BALANCES;
fuzz_target!(|data: &[u8]| {
if data.len() < 4 {
return;
}
let mut deps = mock_dependencies();
let env = mock_env();
let variant = data[0] % 5;
if instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "FuzzVariant".to_string(),
token_symbol: "FV".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.is_err()
{
return;
}
BALANCES
.save(
deps.as_mut().storage,
&Addr::unchecked("alice"),
&Uint128::new(u128::from_be_bytes(
data[..16].try_into().unwrap_or([0u8; 16]),
)),
)
.unwrap();
let amount = Uint128::new(u128::from_be_bytes(
data[..16].try_into().unwrap_or([0u8; 16]),
));
let msg = match variant {
0 => ExecuteMsg::Transfer {
recipient: format!("user_{}", data[1] % 10),
amount,
},
1 => ExecuteMsg::Mint {
to: format!("user_{}", data[2] % 10),
amount,
},
2 => ExecuteMsg::Burn {
from: format!("user_{}", data[3] % 10),
amount,
},
3 => ExecuteMsg::Approve {
spender: format!("spender_{}", data[1] % 5),
amount,
},
4 => ExecuteMsg::TransferFrom {
owner: "alice".to_string(),
recipient: format!("recipient_{}", data[2] % 5),
amount,
},
_ => return,
};
let _ = execute(
deps.as_mut(),
env,
mock_info("alice", &coins(0, "umsg")),
msg,
);
});
3.8 Fuzz 边界条件
// fuzz/targets/fuzz_edge_cases.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, Uint128};
use my_contract::contract::{instantiate, execute};
use my_contract::msg::{ExecuteMsg, InstantiateMsg};
fuzz_target!(|data: &[u8]| {
let mut deps = mock_dependencies();
let env = mock_env();
if instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Edge".to_string(),
token_symbol: "EDGE".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.is_err()
{
return;
}
let edge_amounts = [
Uint128::zero(),
Uint128::new(1),
Uint128::new(u128::MAX),
Uint128::new(1_000_000_000_000_000_000_000_000u128),
Uint128::new(340282366920938463463374607431768211455u128),
];
for &amount in &edge_amounts {
let addr = String::from_utf8_lossy(
&data[..data.len().min(64)],
);
let msg = ExecuteMsg::Transfer {
recipient: addr.to_string(),
amount,
};
let _ = execute(
deps.as_mut(),
env.clone(),
mock_info("alice", &coins(0, "umsg")),
msg,
);
}
});
3.9 Fuzz 测试 SubMsg 与 Reply
// fuzz/targets/fuzz_submsg.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, from_binary, Uint128, Binary, Reply,
SubMsgResult, SubMsgResponse,
};
use my_contract::contract::reply;
fuzz_target!(|data: &[u8]| {
let mut deps = mock_dependencies();
let env = mock_env();
let reply_id = u64::from_be_bytes(
data[..8].try_into().unwrap_or([0u8; 8]),
);
let (result, _error_msg) = if data.len() > 8 && data[8] % 2 == 0 {
let payload = if data.len() > 16 {
Binary::from(&data[16..])
} else {
Binary::from(b"{}")
};
(
SubMsgResult::Ok(SubMsgResponse {
data: Some(payload),
events: vec![],
}),
String::new(),
)
} else {
(
SubMsgResult::Error(
String::from_utf8_lossy(&data[8..]).to_string(),
),
String::new(),
)
};
let msg = Reply {
id: reply_id,
result,
};
let _ = reply(deps.as_mut(), env, msg);
});
3.10 运行 Fuzz 测试
# 基本运行(使用默认配置)
cargo fuzz run fuzz_execute
# 指定运行时间
cargo fuzz run fuzz_execute -- -max_total_time=300
# 指定最大输入大小
cargo fuzz run fuzz_execute -- -max_len=4096
# 限制内存使用
cargo fuzz run fuzz_execute -- -rss_limit_mb=2048
# 显示更多统计
cargo fuzz run fuzz_execute -- -print_final_stats=1
# 多核运行
cargo fuzz run fuzz_execute -- -jobs=4
# 使用语料库(corpus)
cargo fuzz run fuzz_execute fuzz/corpus/fuzz_execute/
# 合并语料库(减少冗余)
cargo fuzz run fuzz_execute -- -merge=1 fuzz/corpus/fuzz_execute/
3.11 Fuzz 测试配置
# fuzz/.options
[libfuzzer]
max_len = 8192
rss_limit_mb = 4096
timeout = 30
print_final_stats = 1
detect_leaks = 1
artifact_prefix = "fuzz/artifacts/"
3.12 处理 Crash 回放
# 重现 crash
cargo fuzz run fuzz_execute fuzz/artifacts/fuzz_execute/crash-*
# 缩减 crash 输入
cargo fuzz run fuzz_execute fuzz/artifacts/fuzz_execute/crash-* -- -minimize_crash=1
# 分析 panic 栈
RUST_BACKTRACE=1 cargo fuzz run fuzz_execute fuzz/artifacts/crash-xxx
3.13 Crash 修复流程
// 当 fuzz 发现 crash 后,记录并修复
//
// 1. 复现 crash:
// cargo fuzz run fuzz_execute fuzz/artifacts/crash-abc123
//
// 2. 编写回归测试:
#[test]
fn fuzz_regression_crash_abc123() {
let crash_input = include_bytes!("../fuzz/artifacts/crash-abc123");
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Reg".to_string(),
token_symbol: "R".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.unwrap();
if let Ok(msg) = from_binary::<ExecuteMsg>(crash_input) {
let result = execute(
deps.as_mut(),
env,
mock_info("user", &coins(0, "umsg")),
msg,
);
assert!(result.is_ok() || result.is_err());
}
}
//
// 3. 将 crash 输入添加到语料库
// cp fuzz/artifacts/crash-abc123 fuzz/corpus/fuzz_execute/
//
// 4. 验证修复
// cargo fuzz run fuzz_execute -- -max_total_time=60
3.14 Fuzz 测试覆盖率分析
# 生成覆盖率报告
cargo fuzz coverage fuzz_execute
# 使用 grcov 生成详细报告
grcov fuzz/coverage --output-type html --output-path fuzz-coverage-report
3.15 MSG Chain 专属 Fuzz Target
// fuzz/targets/fuzz_msg_chain.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, Uint128, from_binary};
use my_contract::contract::{instantiate, execute, query};
use my_contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fuzz_target!(|data: &[u8]| {
let mut deps = mock_dependencies();
let env = mock_env();
if instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &coins(1_000_000, "umsg")),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "MSGChainFuzz".to_string(),
token_symbol: "MCF".to_string(),
initial_supply: Uint128::new(1_000_000),
},
)
.is_err()
{
return;
}
let amounts = [
Uint128::new(0),
Uint128::new(1),
Uint128::new(u64::MAX as u128),
Uint128::new(u128::MAX),
];
let recipients = [
"msg1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5f6k4nq",
];
for &amnt in &amounts {
let msg = ExecuteMsg::Transfer {
recipient: recipients[0].to_string(),
amount: amnt,
};
let _ = execute(
deps.as_mut(),
env.clone(),
mock_info("user", &coins(0, "umsg")),
msg,
);
}
let _ = query(
deps.as_ref(),
env,
QueryMsg::GetBalance {
address: String::from_utf8_lossy(data).to_string(),
},
);
});
4. 特性测试 (Property-based Testing)
4.1 概述
特性测试(Property-based Testing)不同于单元测试手动构造输入输出对,而是声明不变量并由工具自动生成输入来验证不变量是否成立。
在 CosmWasm 合约中,典型的不变量包括:
- "转账不会改变总供应量"
- "权限操作只能由授权用户执行"
- "DID 停用后不可重新激活"
- "余额永不为负"
4.2 安装 proptest
# Cargo.toml
[dev-dependencies]
proptest = "1.4"
4.3 基础不变量测试
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{from_binary, Uint128, Addr, coins};
use crate::contract::{instantiate, execute};
use crate::msg::{ExecuteMsg, InstantiateMsg};
use crate::state::{BALANCES, TOTAL_SUPPLY};
proptest! {
#[test]
fn transfer_never_changes_total_supply(
sender_balance in 100u128..1_000_000u128,
transfer_amount in 1u128..=sender_balance,
) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "PropTest".to_string(),
token_symbol: "PROP".to_string(),
initial_supply: Uint128::new(1_000_000),
},
).unwrap();
TOTAL_SUPPLY.save(
deps.as_mut().storage,
&Uint128::new(1_000_000),
).unwrap();
BALANCES.save(
deps.as_mut().storage,
&Addr::unchecked("sender"),
&Uint128::new(sender_balance),
).unwrap();
let supply_before = TOTAL_SUPPLY.load(&deps.storage).unwrap();
let result = execute(
deps.as_mut(),
env,
mock_info("sender", &[]),
ExecuteMsg::Transfer {
recipient: "recipient".to_string(),
amount: Uint128::new(transfer_amount),
},
);
if sender_balance >= transfer_amount {
prop_assert!(result.is_ok());
let supply_after = TOTAL_SUPPLY.load(&deps.storage).unwrap();
prop_assert_eq!(
supply_before, supply_after,
"Total supply must remain constant after transfer"
);
}
}
}
}
4.4 状态机测试
#[cfg(test)]
mod state_machine_tests {
use proptest::prelude::*;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{Uint128, Addr, coins};
use crate::contract::{instantiate, execute};
use crate::msg::{ExecuteMsg, InstantiateMsg};
use crate::state::{BALANCES, TOTAL_SUPPLY};
#[derive(Clone, Debug)]
enum Action {
Mint { to: String, amount: u128 },
Burn { from: String, amount: u128 },
Transfer { from: String, to: String, amount: u128 },
}
fn arb_action() -> impl Strategy<Value = Action> {
prop_oneof![
(any::<String>(), 0..10_000u128).prop_map(|(to, amount)|
Action::Mint { to, amount }),
(any::<String>(), 0..10_000u128).prop_map(|(from, amount)|
Action::Burn { from, amount }),
(any::<String>(), any::<String>(), 0..10_000u128)
.prop_map(|(from, to, amount)|
Action::Transfer { from, to, amount }),
]
}
proptest! {
#[test]
fn state_machine_invariants(
actions in proptest::collection::vec(arb_action(), 1..50),
) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "SM".to_string(),
token_symbol: "SM".to_string(),
initial_supply: Uint128::new(1_000_000),
},
).unwrap();
for (i, action) in actions.iter().enumerate() {
let sender = format!("user_{}", i % 5);
let msg = match action {
Action::Mint { to, amount } =>
ExecuteMsg::Mint { to: to.clone(), amount: Uint128::new(*amount) },
Action::Burn { from, amount } =>
ExecuteMsg::Burn { from: from.clone(), amount: Uint128::new(*amount) },
Action::Transfer { from: _, to, amount } =>
ExecuteMsg::Transfer { recipient: to.clone(), amount: Uint128::new(*amount) },
};
let _ = execute(
deps.as_mut(),
env.clone(),
mock_info(&sender, &coins(0, "umsg")),
msg,
);
if let Ok(supply) = TOTAL_SUPPLY.load(&deps.storage) {
prop_assert!(supply <= Uint128::new(1_000_000_000_000_000u128));
}
}
}
}
}
4.5 不变量:余额守恒
#[cfg(test)]
mod invariance_tests {
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{Uint128, Addr, coins};
use crate::contract::{instantiate, execute};
use crate::msg::*;
use crate::state::{BALANCES, TOTAL_SUPPLY};
#[test]
fn total_supply_equals_sum_of_balances() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "Inv".to_string(),
token_symbol: "INV".to_string(),
initial_supply: Uint128::new(1_000_000),
},
).unwrap();
BALANCES.save(deps.as_mut().storage, &Addr::unchecked("alice"), &Uint128::new(500_000)).unwrap();
BALANCES.save(deps.as_mut().storage, &Addr::unchecked("bob"), &Uint128::new(500_000)).unwrap();
TOTAL_SUPPLY.save(deps.as_mut().storage, &Uint128::new(1_000_000)).unwrap();
for _ in 0..10 {
let _ = execute(
deps.as_mut(), env.clone(), mock_info("alice", &[]),
ExecuteMsg::Transfer { recipient: "bob".to_string(), amount: Uint128::new(1000) },
);
}
let supply = TOTAL_SUPPLY.load(&deps.storage).unwrap();
let alice_bal = BALANCES.load(&deps.storage, &Addr::unchecked("alice")).unwrap_or(Uint128::zero());
let bob_bal = BALANCES.load(&deps.storage, &Addr::unchecked("bob")).unwrap_or(Uint128::zero());
assert_eq!(supply, alice_bal.checked_add(bob_bal).unwrap());
}
}
4.6 不变量:权限不变
#[test]
fn unauthorized_calls_always_fail() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "AuthCheck".to_string(),
token_symbol: "AUTH".to_string(),
initial_supply: Uint128::new(1_000_000),
},
).unwrap();
let unauthorized = vec![
ExecuteMsg::Mint { to: "attacker".to_string(), amount: Uint128::new(1000) },
ExecuteMsg::Pause {},
ExecuteMsg::Unpause {},
ExecuteMsg::UpdateOwner { new_owner: "attacker".to_string() },
];
for msg in unauthorized {
let result = execute(
deps.as_mut(), env.clone(), mock_info("attacker", &[]), msg
);
assert!(result.is_err(), "Unauthorized action should fail");
}
}
4.7 DID 生命周期不变量
#[test]
fn did_lifecycle_invariant() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
owner: "admin".to_string(),
token_name: "DIDCheck".to_string(),
token_symbol: "DID".to_string(),
initial_supply: Uint128::new(1_000_000),
},
).unwrap();
let mut was_created = false;
let mut was_deactivated = false;
let operations = vec![
ExecuteMsg::CreateDID { did_id: "did:msg:agent:test-1".to_string(), controller: "controller".to_string(), verification_methods: vec![], services: vec![], metadata: None },
ExecuteMsg::CreateDID { did_id: "did:msg:agent:test-1".to_string(), controller: "controller".to_string(), verification_methods: vec![], services: vec![], metadata: None },
ExecuteMsg::DeactivateDID { did_id: "did:msg:agent:test-1".to_string() },
ExecuteMsg::DeactivateDID { did_id: "did:msg:agent:test-1".to_string() },
];
for msg in operations {
let result = execute(
deps.as_mut(), env.clone(), mock_info("controller", &[]), msg.clone()
);
match msg {
ExecuteMsg::CreateDID { .. } => {
if !was_created { assert!(result.is_ok()); was_created = true; }
else { assert!(result.is_err()); }
}
ExecuteMsg::DeactivateDID { .. } => {
if was_created && !was_deactivated { assert!(result.is_ok()); was_deactivated = true; }
else { assert!(result.is_err()); }
}
_ => {}
}
}
}
4.8 使用 proptest 策略生成有效消息
#[cfg(test)]
mod proptest_strategies {
use proptest::prelude::*;
use cosmwasm_std::{Uint128, Addr, coins};
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use crate::contract::{instantiate, execute};
use crate::msg::*;
use crate::state::BALANCES;
fn valid_addr() -> impl Strategy<Value = String> {
prop::string::string_regex("msg1[a-zA-Z0-9]{38}").expect("Invalid regex")
}
fn reasonable_amount() -> impl Strategy<Value = Uint128> {
(0u128..10_000_000_000u128).prop_map(Uint128::new)
}
proptest! {
#[test]
fn transfer_with_valid_addresses(
sender in valid_addr(),
recipient in valid_addr(),
amount in reasonable_amount(),
) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "S".to_string(), token_symbol: "S".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
BALANCES.save(deps.as_mut().storage, &deps.api.addr_validate(&sender).unwrap(), &Uint128::new(10_000_000_000u128)).unwrap();
let result = execute(
deps.as_mut(), env, mock_info(&sender, &[]),
ExecuteMsg::Transfer { recipient, amount },
);
prop_assert!(result.is_ok());
}
}
}
4.9 配置 proptest 执行参数
fn custom_proptest_config() -> ProptestConfig {
ProptestConfig {
cases: 10000,
max_shrink_iters: 1000,
timeout: 300,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::WithSource("proptest-regressions"),
)),
..ProptestConfig::default()
}
}
proptest! {
#![proptest_config = "custom_proptest_config()"]
#[test]
fn high_volume_transfer_test(amount in 1u128..1_000_000u128) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "HV".to_string(), token_symbol: "HV".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
BALANCES.save(deps.as_mut().storage, &Addr::unchecked("alice"), &Uint128::new(1_000_000)).unwrap();
for i in 0..100 {
let r = format!("user_{}", i);
let res = execute(
deps.as_mut(), env.clone(), mock_info("alice", &[]),
ExecuteMsg::Transfer { recipient: r, amount: Uint128::new(amount) },
);
if amount * (i as u128 + 1) <= 1_000_000 { prop_assert!(res.is_ok()); }
else { prop_assert!(res.is_err()); break; }
}
}
}
5. CosmWasm VM 仿真测试
5.1 概述
cosmwasm-vm 是 CosmWasm 的官方虚拟机实现,它使用 Wasmer 运行时执行编译后的 Wasm 合约。相比于 mock_dependencies,VM 测试:
- 使用真实的 Wasm 字节码
- 执行真实的 gas 计量
- 提供沙箱隔离
- 测试序列化/反序列化
在 MSG Chain 上,VM 测试用于部署前的最终验证。
5.2 安装与配置
[dev-dependencies]
cosmwasm-vm = "1.5"
确保 Wasm 合约已编译:
cargo build --target wasm32-unknown-unknown --release
5.3 基础 VM 测试
#[cfg(test)]
mod vm_tests {
use cosmwasm_vm::testing::{
mock_backend, mock_env, mock_info,
};
use cosmwasm_vm::{
Instance, Options, ContractResult,
};
use cosmwasm_std::{coins, from_slice, Uint128, Binary};
const WASM_BYTES: &[u8] =
include_bytes!("../../target/wasm32-unknown-unknown/release/my_contract.wasm");
#[test]
fn vm_instantiate_and_query() {
let backend = mock_backend(&[]);
let options = Options {
gas_limit: 10_000_000_000,
..Options::default()
};
let mut instance = Instance::from_code(
WASM_BYTES, backend, options, Default::default(),
).expect("Failed to create VM instance");
let env = mock_env();
let info = mock_info("admin", &coins(1000, "umsg"));
let msg = serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(),
token_name: "VM Test".to_string(),
token_symbol: "VMT".to_string(),
initial_supply: Uint128::new(1_000_000),
}).unwrap();
let res: ContractResult<Binary> = instance
.instantiate(env.clone(), info, msg)
.expect("Instantiate should succeed");
let gas_used = 10_000_000_000 - instance.get_gas_left();
println!("Gas used for instantiate: {}", gas_used);
assert!(gas_used < 1_000_000, "Instantiate used too much gas");
instance.recycle().unwrap();
}
}
5.4 VM Gas 计量测试
#[test]
fn vm_gas_budget_test() {
let backend = mock_backend(&[]);
let tight_options = Options {
gas_limit: 500_000,
..Options::default()
};
let mut instance = Instance::from_code(
WASM_BYTES, backend, tight_options, Default::default(),
).expect("Failed to create VM instance");
let env = mock_env();
let msg = serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(),
token_name: "GasTest".to_string(),
token_symbol: "GAS".to_string(),
initial_supply: Uint128::new(1_000_000),
}).unwrap();
match instance.instantiate(env, mock_info("admin", &coins(1000, "umsg")), msg) {
Ok(_) => println!("Contract instantiated within gas limit"),
Err(e) => {
if e.to_string().contains("gas") {
println!("Expected: Out of gas");
} else {
panic!("Unexpected error: {}", e);
}
}
}
instance.recycle().unwrap();
}
#[test]
fn vm_gas_cost_breakdown() {
let backend = mock_backend(&[]);
let mut instance = Instance::from_code(
WASM_BYTES, backend,
Options { gas_limit: 10_000_000_000, ..Options::default() },
Default::default(),
).unwrap();
let env = mock_env();
let gas_before = instance.get_gas_left();
instance.instantiate(
env.clone(),
mock_info("admin", &coins(1000, "umsg")),
serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(), token_name: "GBD".to_string(),
token_symbol: "GBD".to_string(), initial_supply: Uint128::new(1_000_000),
}).unwrap(),
).unwrap();
let instantiate_cost = gas_before - instance.get_gas_left();
println!("Instantiate cost: {}", instantiate_cost);
let gas_before2 = instance.get_gas_left();
instance.execute(
env.clone(), mock_info("admin", &[]),
serde_json::to_vec(&ExecuteMsg::Mint { to: "user".to_string(), amount: Uint128::new(1000) }).unwrap(),
).unwrap();
let execute_cost = gas_before2 - instance.get_gas_left();
println!("Execute (mint) cost: {}", execute_cost);
assert!(instantiate_cost < 5_000_000, "Instantiate gas too high");
assert!(execute_cost < 2_000_000, "Execute gas too high");
instance.recycle().unwrap();
}
5.5 VM Load Testing
#[test]
fn vm_load_test_high_throughput() {
use std::time::Instant;
let backend = mock_backend(&[]);
let options = Options {
gas_limit: 100_000_000_000,
..Options::default()
};
let mut instance = Instance::from_code(
WASM_BYTES, backend, options, Default::default(),
).unwrap();
let env = mock_env();
instance.instantiate(
env.clone(), mock_info("admin", &coins(1000, "umsg")),
serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(), token_name: "LoadTest".to_string(),
token_symbol: "LOAD".to_string(), initial_supply: Uint128::new(1_000_000_000),
}).unwrap(),
).unwrap();
let num_transfers = 100;
let start = Instant::now();
for i in 0..num_transfers {
let _ = instance.execute(
env.clone(), mock_info("admin", &[]),
serde_json::to_vec(&ExecuteMsg::Mint {
to: format!("bulk_user_{}", i),
amount: Uint128::new(1000),
}).unwrap(),
).unwrap();
}
let duration = start.elapsed();
let tps = num_transfers as f64 / duration.as_secs_f64();
println!("Processed {} txns in {:?} ({:.0} tps)", num_transfers, duration, tps);
instance.recycle().unwrap();
}
5.6 VM 边界条件测试
#[test]
fn vm_edge_cases() {
let backend = mock_backend(&[]);
let result = Instance::from_code(
&[], backend.clone(), Options::default(), Default::default(),
);
assert!(result.is_err(), "Empty wasm should fail");
let invalid_wasm = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
let result = Instance::from_code(
&invalid_wasm, backend,
Options { gas_limit: 0, ..Options::default() },
Default::default(),
);
match result {
Ok(mut inst) => {
let r = inst.instantiate(
mock_env(), mock_info("admin", &coins(1000, "umsg")),
serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(), token_name: "Z".to_string(),
token_symbol: "Z".to_string(), initial_supply: Uint128::new(1),
}).unwrap(),
);
match r {
Ok(_) => {}
Err(e) => println!("Zero gas result: {}", e),
}
inst.recycle().unwrap();
}
Err(_) => {}
}
}
5.7 VM 跨合约查询模拟
#[test]
fn vm_cross_contract_query() {
use cosmwasm_vm::testing::MockQuerier;
let mut querier = MockQuerier::new(&[]);
querier.update_wasm(|query| {
match query {
cosmwasm_vm::WasmQuery::Smart { contract_addr, msg } => {
if contract_addr == "oracle" {
Ok(ContractResult::Ok(
serde_json::to_vec(&serde_json::json!({"price": "1000000"})).unwrap().into(),
))
} else {
Err("Unknown contract")
}
}
_ => Err("Unsupported query"),
}
});
let backend = mock_backend(&[]).set_querier(querier);
let mut instance = Instance::from_code(
WASM_BYTES, backend,
Options { gas_limit: 10_000_000_000, ..Options::default() },
Default::default(),
).unwrap();
instance.instantiate(
mock_env(), mock_info("admin", &coins(1000, "umsg")),
serde_json::to_vec(&InstantiateMsg {
owner: "admin".to_string(), token_name: "OracleTest".to_string(),
token_symbol: "ORA".to_string(), initial_supply: Uint128::new(1_000_000),
}).unwrap(),
).unwrap();
instance.recycle().unwrap();
}
6. 形式化验证
6.1 概述
形式化验证使用数学方法证明合约满足特定规范。对于 CosmWasm 合约,形式化验证可以分为:
| 方法 | 工具 | 覆盖范围 | 成本 |
|---|---|---|---|
| Rust 类型系统 | 编译器 | 类型安全 | 免费 |
| 运行时断言 | debug_assert! |
运行时路径 | 低 |
| 不变量编码 | #[cfg(test)] |
测试路径 | 低 |
| 符号执行 | KLEE / SymCC | 路径探索 | 中 |
| 模型检查 | TLA+ / NuSMV | 状态空间 | 高 |
| 定理证明 | Coq / Isabelle | 全合约 | 极高 |
6.2 Rust 类型层形式化
利用 Rust 的类型系统和 Uint128、Decimal 等安全类型防止整型错误:
use cosmwasm_std::{Uint128, Decimal, StdResult, StdError};
#[derive(Clone, Debug, PartialEq)]
pub struct Collateral(Uint128);
#[derive(Clone, Debug, PartialEq)]
pub struct Debt(Uint128);
impl Collateral {
pub fn new(amount: Uint128) -> Self { Collateral(amount) }
pub fn value_at_price(&self, price: &Decimal) -> StdResult<Uint128> {
self.0.checked_mul(price).map_err(|_| StdError::generic_err("collateral overflow"))
}
}
impl Debt {
pub fn new(amount: Uint128) -> Self { Debt(amount) }
pub fn is_healthy(&self, collateral_value: Uint128) -> bool {
collateral_value >= self.0 * Uint128::new(150) / Uint128::new(100)
}
}
pub fn evaluate_position(collateral: Collateral, debt: Debt, price: &Decimal) -> StdResult<bool> {
let cv = collateral.value_at_price(price)?;
Ok(debt.is_healthy(cv))
}
6.3 运行时断言
pub fn transfer(
deps: DepsMut,
info: MessageInfo,
recipient: String,
amount: Uint128,
) -> StdResult<Response> {
debug_assert!(!amount.is_zero(), "Amount must not be zero");
let sender_balance = BALANCES.load(deps.storage, &info.sender)?
.checked_sub(amount)
.map_err(|_| StdError::generic_err("Insufficient balance"))?;
let recipient_addr = deps.api.addr_validate(&recipient)?;
let recipient_balance = BALANCES.load(deps.storage, &recipient_addr)
.unwrap_or(Uint128::zero());
let sum_before = sender_balance.checked_add(recipient_balance).unwrap();
BALANCES.save(deps.storage, &info.sender, &sender_balance)?;
BALANCES.save(deps.storage, &recipient_addr, &recipient_balance.checked_add(amount).map_err(|_| StdError::generic_err("overflow"))?)?;
debug_assert_eq!(sum_before, sender_balance.checked_add(recipient_balance).unwrap());
Ok(Response::new().add_attribute("action", "transfer"))
}
6.4 合约级不变量规范
/// ## 全局不变量
/// I1: total_supply == sum(balances[all addresses])
/// I2: total_supply <= MAX_SUPPLY
/// I3: balance[addr] >= 0 (由 Uint128 保证)
///
/// Transfer preserves total_supply
/// post: total_supply' == total_supply
/// Mint 增加 total_supply
/// pre: caller == owner AND total_supply + amount <= MAX_SUPPLY
pub fn verify_state_invariants(storage: &dyn Storage) -> StdResult<()> {
let supply = TOTAL_SUPPLY.load(storage)?;
if supply > CONFIG.load(storage)?.max_supply {
return Err(StdError::generic_err("supply exceeds max"));
}
Ok(())
}
6.5 状态转换模型检测
#[derive(Clone, Debug, PartialEq)]
pub enum ContractState { Init, Active, Paused, Migrating }
/// Init -> Active -> Paused <-> Active
/// Active -> Migrating, Paused -> Migrating
pub fn transition_to(
storage: &mut dyn Storage, current: &ContractState, target: ContractState,
) -> StdResult<ContractState> {
let allowed = matches!((current, &target),
(ContractState::Init, ContractState::Active)
| (ContractState::Active, ContractState::Paused)
| (ContractState::Paused, ContractState::Active)
| (ContractState::Active, ContractState::Migrating)
| (ContractState::Paused, ContractState::Migrating)
);
if !allowed {
return Err(StdError::generic_err(format!("Illegal transition")));
}
STATE.save(storage, &target)?;
Ok(target)
}
#[cfg(test)]
mod state_transition_tests {
use super::*;
use cosmwasm_std::testing::mock_dependencies;
#[test]
fn test_all_transitions() {
let mut deps = mock_dependencies();
let legal = vec![
(ContractState::Init, ContractState::Active),
(ContractState::Active, ContractState::Paused),
(ContractState::Paused, ContractState::Active),
(ContractState::Active, ContractState::Migrating),
(ContractState::Paused, ContractState::Migrating),
];
for (from, to) in legal {
STATE.save(deps.as_mut().storage, &from).unwrap();
assert!(transition_to(deps.as_mut().storage, &from, to).is_ok());
}
let illegal = vec![
(ContractState::Init, ContractState::Paused),
(ContractState::Init, ContractState::Migrating),
(ContractState::Active, ContractState::Init),
(ContractState::Migrating, ContractState::Active),
];
for (from, to) in illegal {
STATE.save(deps.as_mut().storage, &from).unwrap();
assert!(transition_to(deps.as_mut().storage, &from, to).is_err());
}
}
}
6.6 K Framework 集成
# K Framework 语义模型
# CosmWasm 合约在 K Framework 中的形式化验证:
#
# 1. 定义 CosmWasm 语义
# - Wasm 执行模型
# - 存储操作语义
#
# 2. 合约规范 (K 规则)
# rule [transfer-preserves-supply]:
# <k> transfer(From, To, Amount) => ... </k>
# requires BFrom >=Int Amount
# ensures S == #supply
#
# 3. 验证命令
# kprove --spec contract-spec.k contract-semantics.k
#
# 可用工具:
# - K Framework 5.0+ (https://github.com/runtimeverification/k)
# - K-CosmWasm (社区开发中)
6.7 穷举验证(小范围)
#[test]
fn proof_checked_sub_never_panics() {
for a in 0u128..500u128 {
for b in 0u128..500u128 {
let result = Uint128::new(a).checked_sub(Uint128::new(b));
if a >= b { assert!(result.is_ok()); }
else { assert!(result.is_err()); }
}
}
}
6.8 不变量宏辅助
macro_rules! ensure_invariants {
($storage:expr) => {
if let Err(e) = assert_all_invariants($storage) {
return Err(StdError::generic_err(
format!("Invariant failed: {}", e)
));
}
};
}
pub fn assert_all_invariants(storage: &dyn Storage) -> StdResult<()> {
let supply = TOTAL_SUPPLY.load(storage)?;
if supply > CONFIG.load(storage)?.max_supply {
return Err(StdError::generic_err("supply exceeds max"));
}
Ok(())
}
7. CI 集成
7.1 GitHub Actions Fuzz 工作流
name: Contract Fuzz Testing
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
push:
branches: [main, develop]
paths:
- 'contracts/cosmwasm/all/**'
env:
RUST_TOOLCHAIN: nightly
jobs:
fuzz:
name: Fuzz ${{ matrix.fuzz-target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fuzz-target:
- fuzz_execute
- fuzz_multi_step
- fuzz_edge_cases
steps:
- uses: actions/checkout@v4
- name: Set up Rust nightly
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
**/target
**/fuzz/target
key: ${{ runner.os }}-fuzz-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Build Wasm
run: cargo build --target wasm32-unknown-unknown --release
- name: Run fuzz target
run: |
cargo fuzz run ${{ matrix.fuzz-target }} \
-- -max_total_time=600 -rss_limit_mb=4096 -print_final_stats=1
continue-on-error: true
timeout-minutes: 15
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes-${{ matrix.fuzz-target }}-${{ github.sha }}
path: fuzz/artifacts/
7.2 属性测试 CI 工作流
name: Property-Based Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
proptest:
name: Property Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Run property tests
run: cargo test property_tests -- --nocapture
env:
PROPTEST_CASES: '1000'
- name: Run invariance tests
run: cargo test invariance_tests -- --nocapture
7.3 覆盖率 CI
name: Test Coverage
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
coverage:
name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Generate coverage
run: cargo tarpaulin --ignore-tests --out Xml --fail-under 80
- name: Upload to Codecov
uses: codecov/codecov-action@v4
7.4 回归测试套件
#[cfg(test)]
mod fuzz_regression_tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, Uint128, coins};
use crate::contract::{instantiate, execute};
use crate::msg::*;
#[test]
fn regression_burn_overflow() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "R".to_string(), token_symbol: "R".to_string(), initial_supply: Uint128::new(0) },
).unwrap();
let result = execute(
deps.as_mut(), env, mock_info("user", &[]),
ExecuteMsg::Burn { from: "user".to_string(), amount: Uint128::new(100) },
);
assert!(result.is_err());
}
#[test]
fn regression_empty_address() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "R".to_string(), token_symbol: "R".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
let result = execute(
deps.as_mut(), env, mock_info("admin", &[]),
ExecuteMsg::Transfer { recipient: "".to_string(), amount: Uint128::new(100) },
);
assert!(result.is_err());
}
}
7.5 Gas Benchmark CI
name: Gas Benchmark
on:
push:
branches: [main]
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Build Wasm
run: cargo build --target wasm32-unknown-unknown --release
- name: Run gas benchmarks
run: cargo test vm_gas_cost_breakdown -- --nocapture | tee gas-report.txt
- name: Upload gas report
uses: actions/upload-artifact@v4
with:
name: gas-benchmark-${{ github.sha }}
path: gas-report.txt
7.6 MSG Chain CI 完整流水线
name: MSG Chain Full CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: clippy, rustfmt
- name: Clippy
run: cargo clippy -- -D warnings
- name: Rust fmt
run: cargo fmt -- --check
- name: Go lint
run: make lint
test:
name: Tests
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Go tests
run: make test
- name: Quantum tests
run: make test-quantum
- name: Contract CI
run: make ci-contracts
proptest:
name: Property Tests
needs: [lint]
runs-on: ubuntu-latest
steps:
- name: Run property tests
run: cargo test property_tests -- --nocapture
env:
PROPTEST_CASES: '500'
fuzz:
if: github.event_name == 'schedule'
name: Fuzz Tests
needs: [test]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [fuzz_execute, fuzz_multi_step]
steps:
- uses: actions/checkout@v4
- name: Set up Rust nightly
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Fuzz
run: cargo fuzz run ${{ matrix.target }} -- -max_total_time=300
continue-on-error: true
coverage:
name: Coverage
needs: [test]
runs-on: ubuntu-latest
steps:
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Coverage
run: cargo tarpaulin --ignore-tests --out Xml --fail-under 80
8. 常见漏洞测试模式
8.1 重入攻击测试 (VULN-01)
#[test]
fn reentrancy_protection_test() {
let mut deps = mock_dependencies();
let env = mock_env();
STATE.save(deps.as_mut().storage, &State {
balances: Uint128::new(1000),
locked: false,
}).unwrap();
let res = withdraw(deps.as_mut(), env.clone(), mock_info("user", &coins(0, "umsg")), Uint128::new(500));
if let Ok(_) = res {
let state = STATE.load(&deps.storage).unwrap();
assert_eq!(state.balances, Uint128::new(500));
let reentry = withdraw(deps.as_mut(), env, mock_info("user", &coins(0, "umsg")), Uint128::new(600));
assert!(reentry.is_err());
}
}
8.2 整数溢出/下溢测试 (VULN-03)
#[test]
fn overflow_attack_simulation() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "OFT".to_string(), token_symbol: "OFT".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
BALANCES.save(deps.as_mut().storage, &Addr::unchecked("victim"), &Uint128::new(100)).unwrap();
let result = execute(
deps.as_mut(), env.clone(), mock_info("victim", &[]),
ExecuteMsg::Transfer { recipient: "attacker".to_string(), amount: Uint128::new(101) },
);
assert!(result.is_err(), "Over-transfer must be caught");
BALANCES.save(deps.as_mut().storage, &Addr::unchecked("whale"), &Uint128::MAX).unwrap();
let result = execute(
deps.as_mut(), env, mock_info("admin", &[]),
ExecuteMsg::Mint { to: "whale".to_string(), amount: Uint128::new(1) },
);
assert!(result.is_err() || result.is_ok());
}
8.3 访问控制测试 (VULN-02)
#[cfg(test)]
mod access_control_proptest {
use proptest::prelude::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::Uint128;
use crate::contract::{instantiate, execute};
use crate::msg::*;
proptest! {
#[test]
fn only_owner_can_mint(caller in any::<String>(), amount in 0u128..1_000_000u128) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "ACL".to_string(), token_symbol: "ACL".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
let result = execute(
deps.as_mut(), env, mock_info(&caller, &[]),
ExecuteMsg::Mint { to: "user".to_string(), amount: Uint128::new(amount) },
);
if caller == "admin" { prop_assert!(result.is_ok()); }
else { prop_assert!(result.is_err()); }
}
}
}
8.4 预言机操纵测试 (VULN-07)
#[test]
fn oracle_manipulation_simulation() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "O".to_string(), token_symbol: "O".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
PRICE.save(deps.as_mut().storage, &"umsg".to_string(), &Uint128::new(100)).unwrap();
let result = execute(
deps.as_mut(), env.clone(), mock_info("user", &coins(0, "umsg")),
ExecuteMsg::Swap { token_in: "umsg".to_string(), amount_in: Uint128::new(1000), min_amount_out: Uint128::new(9) },
);
assert!(result.is_ok(), "Normal swap should succeed");
PRICE.save(deps.as_mut().storage, &"umsg".to_string(), &Uint128::new(10000)).unwrap();
let result = execute(
deps.as_mut(), env, mock_info("attacker", &coins(0, "umsg")),
ExecuteMsg::Swap { token_in: "umsg".to_string(), amount_in: Uint128::new(1), min_amount_out: Uint128::new(50) },
);
match result {
Ok(_) => {}
Err(e) => assert!(e.to_string().contains("price") || e.to_string().contains("slippage")),
}
}
8.5 精度损失测试 (VULN-19)
#[test]
fn precision_loss_detection() {
let total_reward = Uint128::new(1000);
let stakers = vec![
(Uint128::new(333)),
(Uint128::new(333)),
(Uint128::new(334)),
];
let total_staked = Uint128::new(1000);
let mut distributed = Uint128::zero();
for &stake in &stakers {
let share = Decimal::from_ratio(stake, total_staked);
distributed = distributed.checked_add(total_reward * share).unwrap();
}
if distributed != total_reward {
let remainder = total_reward.checked_sub(distributed).unwrap();
println!("Precision loss: {} remainder", remainder);
}
}
#[test]
fn division_before_multiplication_loss() {
for numerator in 1u128..=100u128 {
for denominator in 1u128..=100u128 {
let amt = Uint128::new(1000);
let ratio = Decimal::from_ratio(Uint128::new(numerator), Uint128::new(denominator));
let decimal_result = amt * ratio;
let int_result = Uint128::new(amt.u128() * numerator / denominator);
assert_eq!(decimal_result, int_result);
}
}
}
8.6 Gas Griefing 测试 (VULN-12)
#[test]
fn gas_griefing_detection() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "G".to_string(), token_symbol: "G".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
let many_users: Vec<String> = (0..1000).map(|i| format!("user_{}", i)).collect();
let result = execute(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
ExecuteMsg::BatchProcess { users: many_users },
);
match result {
Ok(_) => {}
Err(e) => assert!(
e.to_string().contains("gas") || e.to_string().contains("limit")
),
}
}
8.7 存储碰撞测试 (VULN-04)
#[test]
fn storage_collision_detection() {
let mut deps = mock_dependencies();
ITEM_A.save(deps.as_mut().storage, &"admin_a".to_string()).unwrap();
ITEM_B.save(deps.as_mut().storage, &"admin_b".to_string()).unwrap();
let val_a: String = ITEM_A.load(&deps.storage).unwrap();
let val_b: String = ITEM_B.load(&deps.storage).unwrap();
if val_a == "admin_b" {
panic!("STORAGE COLLISION: ITEM_A and ITEM_B share the same key!");
}
assert_eq!(val_a, "admin_a");
assert_eq!(val_b, "admin_b");
}
8.8 跨合约查询测试 (VULN-15)
#[test]
fn cross_contract_query_safety() {
let mut deps = mock_dependencies();
let env = mock_env();
CONFIG.save(deps.as_mut().storage, &Config {
owner: "admin".to_string(),
trusted_oracles: vec!["msg1oracle".to_string()],
max_supply: Uint128::new(1_000_000),
}).unwrap();
let result = execute(
deps.as_mut(), env.clone(), mock_info("user", &[]),
ExecuteMsg::GetPrice { asset: "umsg".to_string(), oracle: "msg1trusted".to_string() },
);
match result {
Ok(_) => {}
Err(e) => assert!(e.to_string().contains("oracle") || e.to_string().contains("trusted")),
}
}
8.9 签名重放测试 (VULN-09)
#[test]
fn signature_replay_protection() {
let mut deps = mock_dependencies();
let env = mock_env();
let permit = Permit {
signer: "alice".to_string(),
nonce: 1,
deadline: 9999999999,
message: vec![].into(),
signature: vec![].into(),
};
let res1 = execute_permit(deps.as_mut(), env.clone(), mock_info("relayer", &[]), permit.clone());
if res1.is_ok() {
let res2 = execute_permit(deps.as_mut(), env, mock_info("relayer", &[]), permit);
assert!(res2.is_err(), "Replay must fail");
}
}
8.10 无限制铸币测试 (VULN-17)
#[test]
fn unchecked_minting_detection() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(), env.clone(), mock_info("admin", &[]),
InstantiateMsg { owner: "admin".to_string(), token_name: "M".to_string(), token_symbol: "M".to_string(), initial_supply: Uint128::new(1_000_000) },
).unwrap();
let supply_before = TOTAL_SUPPLY.load(&deps.storage).unwrap_or(Uint128::zero());
let _ = execute(
deps.as_mut(), env.clone(), mock_info("attacker", &[]),
ExecuteMsg::Mint { to: "attacker".to_string(), amount: Uint128::new(999_999_999) },
);
let supply_after = TOTAL_SUPPLY.load(&deps.storage).unwrap_or(Uint128::zero());
if supply_after > supply_before && supply_after > Uint128::new(1_000_000) {
panic!("UNCHECKED MINTING: non-admin was able to mint tokens!");
}
}
9. 附录
A. 工具安装速查
# Rust 工具链
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
rustup component add clippy rustfmt
# 模糊测试
cargo install cargo-fuzz
# 覆盖率
cargo install cargo-tarpaulin
# 安全审计
cargo install cargo-audit
cargo install cargo-geiger
# Wasm 优化
cargo install wasm-opt
# Go 工具
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install github.com/securego/gosec/v2/cmd/gosec@latest
B. Makefile 测试目标
# 标准测试
make test # Go pkg 测试
make test-quantum # Dilithium-5 密码学测试
make ci-contracts # 所有合约编译 + cargo test
make test-coverage # 覆盖率报告
# CI 流水线
make ci-tests # 完整测试流水线
make ci-security # 安全审计 (gosec + cargo audit)
make lint # 代码质量检查
C. Cargo.toml 依赖速查
[dependencies]
cosmwasm-std = { version = "1.5", features = ["stargate"] }
cosmwasm-storage = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1"
[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"
proptest = "1.4"
tarpaulin = "0.27"
libfuzzer_sys = "0.4"
D. 常见 Fuzz Crash 信号解释
| 信号 | Fuzz 输出 | 含义 |
|---|---|---|
| SIGSEGV | SEGV on unknown address |
内存访问越界 |
| SIGABRT | Aborted |
断言失败 assert!() |
| SIGILL | Illegal instruction |
非法指令 |
| OOM | Out of memory |
内存超限 |
| Timeout | Timeout |
执行超时 > 30s |
| Stack Overflow | Stack overflow |
无限递归 |
E. MSG Chain 20 漏洞类别速查
| ID | 漏洞名称 | 严重等级 | 模糊测试可检测 |
|---|---|---|---|
| VULN-01 | 重入攻击 | 🔴 Critical | ✅ |
| VULN-02 | 未授权访问 | 🔴 Critical | ✅ |
| VULN-03 | 整数溢出/下溢 | 🟠 High | ✅ |
| VULN-04 | 存储碰撞 | 🟠 High | ✅ |
| VULN-05 | Reply 处理漏洞 | 🟠 High | ✅ |
| VULN-06 | IBC 超时漏洞 | 🟠 High | ❌ (需集成测试) |
| VULN-07 | 价格预言机操纵 | 🔴 Critical | ✅ |
| VULN-08 | 闪电贷攻击 | 🟠 High | ✅ |
| VULN-09 | 签名重放 | 🟠 High | ✅ |
| VULN-10 | 随机数预测 | 🟠 Medium | ✅ |
| VULN-11 | 抢先交易/MEV | 🟠 High | ❌ |
| VULN-12 | Gas Griefing | 🟡 Medium | ✅ |
| VULN-13 | 不安全地址验证 | 🟡 Medium | ✅ |
| VULN-14 | Funds 验证遗漏 | 🟡 Medium | ✅ |
| VULN-15 | 不安全跨合约查询 | 🟠 High | ✅ |
| VULN-16 | 不安全 IBC 包验证 | 🟠 High | ❌ |
| VULN-17 | 无限制代币增发 | 🔴 Critical | ✅ |
| VULN-18 | 权限提升 | 🔴 Critical | ✅ |
| VULN-19 | 精度损失 | 🟡 Medium | ✅ |
| VULN-20 | Agent API 滥用 | 🟠 High | ✅ |
F. Fuzz 测试用例清单
| Target 文件 | 测试重点 | 建议运行时间 |
|---|---|---|
fuzz_execute |
通用 Execute 反序列化 | 5 min |
fuzz_instantiate_params |
Instantiate 参数边界 | 3 min |
fuzz_multi_step |
多步操作状态积累 | 10 min |
fuzz_execute_variants |
枚举变种覆盖 | 5 min |
fuzz_edge_cases |
极值边界 | 3 min |
fuzz_submsg |
SubMsg/Reply 安全 | 5 min |
fuzz_msg_chain |
MSG Chain 专属场景 | 5 min |
G. 参考资源
- CosmWasm 官方文档
- cargo-fuzz 文档
- proptest 文档
- K Framework
- MSG Chain 安全审计清单
- MSG Chain 合约模板库
- MSG Chain 测试与 CI/CD 指南
本文档是 MSG Chain 开发指南系列的一部分。
如有问题,请提交 Issue 至 MSG Chain 官方仓库。
