跳到主要内容

模糊测试模板

使用 Foundry 对智能合约进行模糊测试的实用模板。

概述

模糊测试(或基于属性的测试)会自动生成随机输入,以针对不变量测试您的合约。

项目结构

fuzzing-template/
├── src/
│ ├── Token.sol
│ └── Vault.sol
├── test/
│ ├── Token.t.sol
│ ├── TokenFuzz.t.sol
│ └── VaultInvariant.t.sol
└── foundry.toml

配置

foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[fuzz]
runs = 1000
max_test_rejects = 100000

[invariant]
runs = 256
depth = 15
fail_on_revert = true

模糊测试

基础模糊测试

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenFuzzTest is Test {
Token token;

function setUp() public {
token = new Token("Test", "TST", 1000000);
}

/// forge-config: default.fuzz.runs = 10000
function testFuzzTransfer(address to, uint256 amount) public {
// 假设有效输入
vm.assume(to != address(0));
vm.assume(to != address(this));
vm.assume(amount <= token.balanceOf(address(this)));

uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(to, amount);

// 不变量
assertEq(
token.balanceOf(address(this)),
balanceBefore - amount
);
assertEq(token.balanceOf(to), amount);
}
}

不变量测试

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Vault.sol";

contract VaultHandler is Test {
Vault public vault;
address[] public actors;

constructor(Vault _vault) {
vault = _vault;
}

function deposit(uint256 actorIndex, uint256 amount) public {
actorIndex = bound(actorIndex, 0, actors.length - 1);
amount = bound(amount, 0, 1000 ether);

address actor = actors[actorIndex];
vm.deal(actor, amount);

vm.prank(actor);
vault.deposit{value: amount}();
}

function withdraw(uint256 actorIndex, uint256 amount) public {
actorIndex = bound(actorIndex, 0, actors.length - 1);
address actor = actors[actorIndex];

uint256 balance = vault.balanceOf(actor);
amount = bound(amount, 0, balance);

vm.prank(actor);
vault.withdraw(amount);
}
}

contract VaultInvariantTest is Test {
Vault vault;
VaultHandler handler;

function setUp() public {
vault = new Vault();
handler = new VaultHandler(vault);

// 添加参与者
handler.actors().push(address(0x1));
handler.actors().push(address(0x2));
handler.actors().push(address(0x3));

// 将处理器设为不变量测试的目标
targetContract(address(handler));
}

/// forge-config: default.invariant.runs = 256
/// forge-config: default.invariant.depth = 15
function invariant_totalSupplyEqualsBalance() public {
assertEq(
vault.totalSupply(),
address(vault).balance
);
}

function invariant_userBalancesLessThanTotal() public {
for (uint i = 0; i < handler.actors().length; i++) {
address actor = handler.actors(i);
assertLe(
vault.balanceOf(actor),
vault.totalSupply()
);
}
}
}

最佳实践

1. 使用 bound() 进行范围约束

function testFuzz(uint256 x) public {
x = bound(x, 1, 100); // 约束到 [1, 100]
// ...
}

2. 使用 vm.assume() 设置前提条件

function testFuzz(address user) public {
vm.assume(user != address(0));
vm.assume(user.code.length == 0);
// ...
}

3. 创建处理器合约

处理器合约包装您的协议,并为不变量测试提供有界的随机操作。

4. 定义清晰的不变量

好的不变量:

  • 总供应量等于余额总和
  • 合约余额大于等于总存款
  • 用户余额小于等于总供应量

5. 使用幽灵变量

在处理器中跟踪额外状态:

contract Handler {
uint256 public ghost_depositSum;
uint256 public ghost_withdrawSum;

function deposit(uint256 amount) public {
// ...
ghost_depositSum += amount;
}
}

运行模糊测试

# 运行模糊测试
forge test --match-contract Fuzz

# 运行不变量测试
forge test --match-contract Invariant

# 详细输出
forge test --match-contract Invariant -vvvv

# 带 Gas 报告
forge test --match-contract Fuzz --gas-report

解释结果

成功

Test result: ok. 1000 passed

失败

Failing test:
testFuzzTransfer(address,uint256)

Counterexample:
to=0x0000000000000000000000000000000000000000
amount=1

高级技术

基于字典的模糊测试

address[] public dictionary = [
address(0),
address(this),
address(token)
];

function testFuzz(uint256 index) public {
address target = dictionary[bound(index, 0, dictionary.length - 1)];
// ...
}

有状态模糊测试

在模糊测试运行之间保持状态以测试复杂场景。

资源

📢 Share this article