教程区块链区块链技术ch1313.3 合约测试策略:单元、集成、模糊与 Gas 报告

本页目录

"不测试的合约就是漏洞。" 在 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(`❌ name:{name}:{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: passed/{passed}/{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.function:{r.function}:{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

评论加载中…

发表评论

0/2000