Foundry Fuzz Testing:让测试自动寻找 Solidity Bug

Fuzz Testing(模糊测试)

普通测试是我们自己指定输入:

perl 复制代码
function test_Add() public {
    counter.setNumber(10);
    assertEq(counter.number(), 10);
}

但 Solidity 合约可能面对成千上万种输入。

难道每一种情况都手动测试?

显然不现实。

Fuzz Testing 的思路就是:

让测试框架自动生成大量不同的输入,尝试找到程序中的异常情况。


一、什么是 Fuzz Testing?

假设我们有一个简单的函数:

ini 复制代码
function withdraw(uint256 amount) public {
    require(amount <= balance[msg.sender], "insufficient balance");

    balance[msg.sender] -= amount;
}

传统测试可能只测试:

ini 复制代码
amount = 10
amount = 100
amount = 1000

但真正的问题可能出现在:

ini 复制代码
amount = 0
amount = 1
amount = type(uint256).max

甚至某个非常特殊的数值。

Fuzz Testing 就是让框架自动尝试:

erlang 复制代码
0
1
10
100
999
...

然后检查:

无论输入是什么,程序是否始终满足我们定义的规则?


二、Foundry 的 Fuzz Test

Foundry 的 Fuzz Test 非常简单。

只需要给测试函数增加参数:

perl 复制代码
function testFuzz_SetNumber(uint256 x) public {
    counter.setNumber(x);

    assertEq(counter.number(), x);
}

注意这里:

复制代码
uint256 x

并不是我们手动传入。

而是:

Foundry 自动生成 x

执行:

bash 复制代码
forge test

Foundry 会自动运行大量不同的输入。


三、Fuzz Testing 的核心

普通测试:

perl 复制代码
function test_SetNumber() public {
    counter.setNumber(100);

    assertEq(counter.number(), 100);
}

相当于:

复制代码
固定输入 → 执行 → 检查

Fuzz Testing:

perl 复制代码
function testFuzz_SetNumber(uint256 x) public {
    counter.setNumber(x);

    assertEq(counter.number(), x);
}

相当于:

markdown 复制代码
自动生成输入
      ↓
   执行合约
      ↓
   检查断言
      ↓
发现失败?
      ↓
保存导致失败的输入

所以 Fuzz Test 最重要的不是:

"运行很多次"

而是:

尝试寻找能够破坏程序性质的输入。


四、一个真正容易出现 Bug 的例子

假设我们写了一个 Token:

ini 复制代码
contract Token {
    mapping(address => uint256) public balance;

    function deposit(uint256 amount) public {
        balance[msg.sender] += amount;
    }

    function withdraw(uint256 amount) public {
        require(
            balance[msg.sender] >= amount,
            "insufficient balance"
        );

        balance[msg.sender] -= amount;
    }
}

我们希望保证:

复制代码
withdraw 之后
余额不能变成负数

可以写:

scss 复制代码
function testFuzz_Withdraw(uint256 depositAmount) public {
    token.deposit(depositAmount);

    uint256 beforeBalance = token.balance(address(this));

    token.withdraw(depositAmount);

    assertEq(
        token.balance(address(this)),
        beforeBalance - depositAmount
    );
}

这里:

复制代码
depositAmount

由 Foundry 自动生成。


五、Fuzz 最有价值的地方:边界值

很多 Bug 并不是出现在正常输入,而是出现在:

边界条件。

例如:

ini 复制代码
uint256 x;

它的范围是:

复制代码
0 ~ 2^256 - 1

普通测试很难覆盖:

lua 复制代码
0
1
type(uint256).max
type(uint256).max - 1

而这些值恰恰可能触发:

  • 整数溢出
  • 下溢
  • 除零
  • 边界判断错误
  • 权限判断错误
  • 价格计算错误

所以 Fuzz Testing 特别适合 Solidity。


六、Foundry 会保存失败的输入

假设我们的代码存在 Bug:

java 复制代码
function calculate(uint256 x) public pure returns (uint256) {
    return 100 / x;
}

然后测试:

scss 复制代码
function testFuzz_Calculate(uint256 x) public {
    calculate(x);
}

如果 Foundry 找到了:

ini 复制代码
x = 0

导致:

csharp 复制代码
division by zero

测试失败时,Foundry 会把导致失败的输入报告出来。

这非常重要。

因为我们不需要自己猜:

"到底哪个输入导致 Bug?"

Foundry 会告诉我们:

ini 复制代码
Counterexample:
x = 0

这就是 Counterexample(反例)


七、Fuzz Testing ≠ 随机乱测

这是理解 Fuzz Testing 很重要的一点。

它不是简单地:

复制代码
随机生成一个数字
随机生成一个数字
随机生成一个数字

然后碰运气。

测试框架会围绕输入空间进行探索,并在发现失败时给出能够复现问题的输入。

因此一个好的 Fuzz Test,本质上是在描述:

程序必须永远满足什么性质?

例如:

perl 复制代码
function testFuzz_Deposit(uint256 amount) public {
    token.deposit(amount);

    assertEq(
        token.balance(address(this)),
        amount
    );
}

这里真正测试的是:

ini 复制代码
对于任意 amount:

deposit(amount)
之后

balance == amount

也就是:

ini 复制代码
∀ amount,balance == amount

八、可以同时 Fuzz 多个参数

并不是只能测试一个参数:

vbnet 复制代码
function testFuzz_Transfer(
    uint256 amount,
    address to
) public {
    ...
}

Foundry 可以同时改变:

css 复制代码
amount
to

从而测试更多输入组合。

例如:

ini 复制代码
amount = 0
to = address(0)

amount = 1
to = 某地址

amount = max uint256
to = 某地址

这对于 Token、DeFi 等合约尤其有价值。


九、Fuzz + Cheatcode

前面学习的 Cheatcode 可以和 Fuzz Testing 一起使用。

例如:

scss 复制代码
function testFuzz_Withdraw(
    uint256 amount
) public {
    vm.deal(address(this), 100 ether);

    token.deposit{value: 100 ether}();

    ...
}

还可以:

ini 复制代码
vm.prank(user);

配合:

scss 复制代码
function testFuzz_Transfer(
    uint256 amount
) public {
    vm.prank(alice);

    token.transfer(bob, amount);
}

于是测试可以同时模拟:

diff 复制代码
不同用户
+
不同金额
+
不同时间
+
不同余额

测试能力会强很多。


十、Fuzz Testing 最适合测试什么?

特别适合下面这些场景:

1. 数值计算

复制代码
价格
利率
手续费
数量
兑换比例

2. 边界条件

复制代码
0
1
最大值
最小值

3. Token

arduino 复制代码
transfer
approve
mint
burn

4. DeFi

复制代码
swap
deposit
withdraw
liquidity
interest

5. 状态变化

例如:

css 复制代码
调用 A
调用 B
调用 C

之后检查:

复制代码
状态是否仍然满足某个规则

十一、普通测试和 Fuzz Testing 怎么选择?

可以简单理解:

类型 输入 目的
Unit Test 手动指定 验证具体场景
Fuzz Test 自动生成 寻找边界和异常输入
Invariant Test 自动生成调用序列 验证系统长期不变量

例如:

scss 复制代码
// 普通测试
function test_Deposit100() public {
    token.deposit(100);
}

// Fuzz
function testFuzz_Deposit(uint256 amount) public {
    token.deposit(amount);
}

普通测试关注:

这个场景对不对?

Fuzz Testing 关注:

换成各种输入还对不对?

而下一层的 Invariant Testing 则关注:

不断调用各种函数之后,系统规则还能不能保持?


十二、Fuzz Testing 的核心思想

其实可以把它总结成一句话:

复制代码
普通测试:
我告诉你测试什么输入。

Fuzz Testing:
你自己寻找可能出问题的输入。

而测试代码负责定义:

复制代码
什么叫"正确"?

例如:

perl 复制代码
assertEq(balanceAfter, balanceBefore + amount);

或者:

scss 复制代码
assertGe(balance, 0);

或者:

scss 复制代码
assertTrue(totalSupply <= MAX_SUPPLY);

断言写得越好,Fuzz Testing 越有价值。


总结

Foundry Fuzz Testing 的核心就是:

java 复制代码
测试函数增加参数
        ↓
Foundry 自动生成输入
        ↓
执行 Solidity 合约
        ↓
检查 assert
        ↓
发现 Bug
        ↓
给出 Counterexample

记住三个关键词:

vbnet 复制代码
Fuzz Test
   ↓
自动生成输入

Property
   ↓
定义程序必须满足的规则

Counterexample
   ↓
找到违反规则的具体输入

所以 Fuzz Testing 并不是"帮你写测试",而是:

你负责告诉它什么是正确的,它负责尽可能寻找错误。

相关推荐
IT_陈寒1 小时前
SpringBoot自动配置失效时我差点把电脑扔了
前端·人工智能·后端
两点王爷10 小时前
SpringBoot 项目集成 GeoServer 源码,实现地图服务发布
java·spring boot·后端
leoZ23111 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
海上彼尚12 小时前
Cursor 模型的强度实测排行
前端·人工智能·后端
GoGeekBaird13 小时前
从「跑完即销毁」到「用完再销毁」:云沙箱的一次进化
后端·github·ai编程
孫治AllenSun14 小时前
【LangChain4J-09】开发学习知识点
spring boot·后端·学习
lisin-lee-cooper14 小时前
简单聊聊 Spring 框架源码
java·后端·spring
复方金银花颗粒14 小时前
reactor和proactor的好处和坏处。为什么要用reactor而不用 proactor?
后端·信号处理
IT_陈寒15 小时前
JavaScript的这个隐式转换特性差点让我加班到凌晨
前端·人工智能·后端