"不测试的合约就是漏洞。" 在 DeFi 中,一个未被测试的分支可能意味着百万美元损失。测试策略分三级:单元测试(每个函数孤立)、集成测试(协议间交互)、模糊测试(随机探索)。
13.3.1 测试金字塔
graph TD
subgraph Unit[单元测试 80%]
U1[Token transfer]
U2[Mint 权限]
U3[边界值: 0, max]
end
subgraph Integration[集成测试 15%]
I1[Token -> Aave 借贷]
I2[AMM swap -> 价格预言机]
end
subgraph Fuzz[模糊/分叉 5%]
F1[随机输入 invariant]
F2[Mainnet fork 复现]
end
U1 --> I1 --> F1
style Unit fill:#c8e6c9
style Integration fill:#fff3e0
style Fuzz fill:#ffebee
单元测试:Hardhat 模式
typescript
/**
* 单元测试核心模式:fixture + snapshot + 断言
* 模拟 Foundry 的 fuzz 概念,用 TypeScript 实现随机测试
*/
interface UnitTestResult {
name: string;
passed: boolean;
gas?: bigint;
error?: string;
}
class ContractTestSuite {
private tests: UnitTestResult[] = [];
async test(name: string, fn: () => Promise<boolean>, gasEstimate?: bigint) {
try {
const pass = await fn();
this.tests.push({ name, passed: pass, gas: gasEstimate });
console.log(`✅ ${name}`);
} catch (e) {
this.tests.push({ name, passed: false, error: String(e) });
console.log(`❌ {e}`);
}
}
report() {
const passed = this.tests.filter(t => t.passed).length;
const total = this.tests.length;
const avgGas = this.tests
.filter(t => t.gas !== undefined)
.reduce((a, t) => a + (t.gas || 0n), 0n);
console.log(`\n📊 Results: {total} passed`);
console.log(`⛽ Avg gas (sampled): ${avgGas}n`);
}
}
// 测试 ERC20 transfer 的边界情况
const suite = new ContractTestSuite();
// 基础功能
suite.test("transfer 到正常地址", async () => {
const balance = 1000n; const amount = 200n;
return balance - amount === 800n;
}, 52000n);
// 边界:零值
suite.test("transfer 0 金额不报错", async () => {
return true; // 合法: transfers 0 是有效的
});
// 边界:满额
suite.test("transfer 精确等于余额", async () => {
const balance = 100n; const amount = 100n;
return balance >= amount;
}, 52000n);
// 边界:溢出
suite.test("transfer 超过余额应拒绝", async () => {
const balance = 100n; const amount = 200n;
if (amount > balance) return true; // 正确行为:拒绝
return false;
}, 2200n); // revert 消耗较少 gas
// 随机模糊测试(简化版)
suite.test("fuzz 随机 transfer 金额 [0, 2^128]", async () => {
for (let i = 0; i < 20; i++) {
const balance = 1000000n;
const amount = BigInt(Math.floor(Math.random() * 10**18));
if (amount > balance) {
// 应该拒绝
continue;
}
// 接受,检查余额变化
}
return true;
});
suite.report();13.3.2 Foundry 模糊测试(Fuzzing)深度
solidity
// Foundry 模糊测试示例
contract InvariantTest is Test {
MyToken token;
function setUp() public {
token = new MyToken(1_000_000e18);
}
// 状态不变式:总供给永远不变(无 burn 时)
function invariant_totalSupply() public {
assertEq(
token.totalSupply(),
1_000_000e18,
"Total supply must be constant"
);
}
// 状态不变式:用户余额和 = 总供给(无铸造/销毁)
function invariant_sumBalancesEqTotal() public {
// 需要跟踪所有 holder(简化展示)
}
// 边界模糊:随机地址和金额
function testFuzzTransfer(address to, uint256 amount) public {
vm.assume(to != address(0));
vm.assume(to != address(this));
uint256 fromBal = token.balanceOf(address(this));
vm.assume(amount <= fromBal);
token.transfer(to, amount);
assertEq(token.balanceOf(to), amount);
assertEq(token.balanceOf(address(this)), fromBal - amount);
}
}13.3.3 Gas 报告与优化
typescript
/**
* Gas 报告分析:识别热点函数
*/
interface GasReport {
function: string;
calls: number;
avgGas: bigint;
minGas: bigint;
maxGas: bigint;
}
function analyzeGasOptimization(report: GasReport[]): string[] {
const bottlenecks: string[] = [];
for (const r of report) {
if (r.avgGas > 100000n) {
bottlenecks.push(`{r.avgGas} gas — 考虑优化存储访问`);
}
if (r.maxGas - r.minGas > r.avgGas / 10n) {
bottlenecks.push(`${r.function}: gas 波动大 — 检查分支逻辑`);
}
}
return bottlenecks;
}
// 示例输入
const sampleReport: GasReport[] = [
{ function: "transfer", calls: 1000, avgGas: 52000n, minGas: 51500n, maxGas: 65000n },
{ function: "swap", calls: 500, avgGas: 145000n, minGas: 120000n, maxGas: 210000n },
{ function: "stake", calls: 200, avgGas: 180000n, minGas: 175000n, maxGas: 182000n },
];
console.log(analyzeGasOptimization(sampleReport));
// 输出: swap 和 stake 可能超 100K,建议审查存储写入常见 Gas 优化策略
| 策略 | 节省 (估算) | 适用场景 |
|---|---|---|
calldata 代替 memory | -2000 / call | 外部函数参数 |
immutable 变量 | -2000 / read | 构造函数确定的常量 |
| 打包变量(共享 32 字节) | -2000 / slot | 多个 uint128/address/bool |
短路修饰(短路 &&) | -5000 | 复杂 require |
错误字符串改用 custom error | -200 / revert | 所有 revert 场景 |
> ← 13.2 本地网络 | 前往 → 13.4 脚本化部署 |*
评论
0评论加载中…