Foundry模糊测试入门
Foundry模糊测试入门
关于模糊测试,需要首先考虑在我们的代码中系统的不变量/属性 是什么,这样我们就可以编写一些有状态和无状态的模糊测试
定义:
Fuzz 测试(模糊测试),是将随机数据提供给系统,以尝试破坏系统
举例:
以下是一个要进行模糊测试的合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyContract {
uint256 public shouldAlwaysBeZero = 0;
uint256 private hiddenValue = 0;
function doStuff(uint256 data) public {
if (data == 2) {
shouldAlwaysBeZero = 1;
}
if (hiddenValue == 7) {
shouldAlwaysBeZero = 1;
}
hiddenValue = data;
}
}
这段合约只是简单的模拟,在大型项目中可能出现的情况。其中的shouldAlwaysBeZero
变量应始终为0
。
而现在的合约源码就是没有经过 Fuzz 测试,需要我们对齐进行 Debug(虽然一眼能看出来问题,实际中没那么容易看出来)
接下来,我们就要通过调试,尝试破环我们的不可变变量(shouldAlwaysBeZero
)
现在。一个简单的单元测试常规来说是这样的:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {MyContract} from "../src/MyContract.sol";
import {Test} from "forge-std/Test.sol";
contract UnitTest is Test {
MyContract exampleContract;
function setUp() public {
exampleContract = new MyContract();
}
function testIAlwaysGetZero() public {
uint256 data = 0; // 传递一个数据点
exampleContract.doStuff(data); // 调用该函数
assert(exampleContract.shouldAlwaysBeZero() == 0); // 进行断言
}
}
通过这种方式,我们可能认为我们的代码已经覆盖了,但是,查看我们的源合约能够发现:当data
为 2 时,我们的不可变变量就发生了变化。对于这个源合约来说,这是显然易见的,但是对于一些逻辑比较复杂的合约,就没这么容易直接看出来了。
那么,我们不能(闲的)手动将可能出现的情况手动一次次的进行单元测试,我们需要一种程序化的方法来找到这个场景,这个场景就是 Fuzz 模糊测试。
在 Foundry 中书写一个模糊测测和单元测试很像:
function testIAlwaysGetZero(uint256 data) public {
// uint256 data = 0;
exampleContract.doStuff(data);
assert(exampleContract.shouldAlwaysBeZero() == 0);
}
对刚刚的函数进行简单的修改,现在这就是 模糊测试 了。接下来,我们在 Foundry 中分别运行刚刚的单元测试和模糊测试:
单元测试——通过
模糊测试
可以看到,成功找到了问题所在。
接下来,将源码中的第一个 if 判断注释掉,即:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyContract {
uint256 public shouldAlwaysBeZero = 0;
uint256 private hiddenValue = 0;
function doStuff(uint256 data) public {
// if (data == 2) {
// shouldAlwaysBeZero = 1;
// }
if (hiddenValue == 7) {
shouldAlwaysBeZero = 1;
}
hiddenValue = data;
}
}
再次进行模糊测试,结果如下:
可以看到这次模糊测试成功,后面的括号中runs: 256
runs
对应的数字就是运行的次数,这代表:在这里做了 256 种不同的随机输入
在 Foundry 种,可以在 foundry.toml
文件中添加如下一个部分来更改运行次数:
[fuzz]
runs = 10000
进行上述修改后,我们再次运行模糊测试,结果如下:
运行次数很重要,更多的运行次数意味着能进行更多的随机输入,更多的用例。以及更大的概率捕捉 Bug。
简单模糊测试回顾
在进行 Fuzz 之前,首先要找到的就是系统中的不变量。这必须是始终不变的
- 理解这个系统中的不变性
- 编写一个 Fuzz 测试,通过随机输入来尝试打破这个不变性
接下来接着看我们的源合约,当 hiddenValue = 7 的时候,我们的不变量也发生了变化,而将 hiddenValue 设置为 7 需要首先调用 doStuff,按照我们刚刚写的模糊测试,我们是无法找到这个 Bug的,因为我们刚刚的模糊测试是无状态的模糊测试,其上一次的运行结果将被丢弃
针对上述情况,我们就可以进行有状态模糊测试。有状态模糊测试是上一次运行的结束状态时下一次的起始状态
有状态模糊测试
当我们的单元测试写成下面的样子时,我们发现我们的源合约存在 Bug。
function test1() public {
uint256 data = 7;
exampleContract.doStuff(data);
assert(exampleContract.shouldAlwaysBeZero() == 0);
// Use the same contract
data = 0;
exampleContract.doStuff(data);
assert(exampleContract.shouldAlwaysBeZero() == 0);
}
测试结果——发现了我们源合约中的 Bug:
可见,对于复杂的代码,使用某个非常具体的场景是很容易被忽略的。
要在 Foundry 中编写有状态的模糊测试,需要使用 invariant
关键字,并且他需要一些安装
在 Foundry 中,我们首先要到导入这一部分,并在我们的测试合约中继承,接下来,我们要告诉 Foundry 要调用哪个合约的随机函数(由于我们只有一个合约和一个函数,我们将告诉 Foundry 我的合约应该被调用,它可以调用我的合约的任何函数):
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {MyContract} from "../src/MyContract.sol";
import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
contract UnitTest is StdInvariant, Test {
MyContract exampleContract;
function setUp() public {
exampleContract = new MyContract();
targetContract(address(exampleContract));
}
function invariant_testAlwaysIsZero() public {
assert(exampleContract.shouldAlwaysBeZero() == 0);
}
}
接下来,我们运行这个测试,发现 Foundry 已经找到了这个存在问题的序列:
可以看到,在我们的合约中,使用参数 7 调用 doStuff,然后再次使用某个随机数调用我们的合约时(因为无论调用哪个数都会出现问题,所以后面的数据就不重要了)
说明:
- Foundry 使用不变量这个词来描述这种有状态的模糊测试
- 无状态的模糊测试是将随机数据提供给函数的输入,以查看是否破坏了某种不变性
- 有状态的模糊测试是将随机数据和随机函数调用提供给系统,以查看是否破坏了它们
在 Foundry 中,模糊测试是无状态的模糊测试,不变量是有状态的模糊测试
Foundry Fuzzing = Stateless fuzzing
Foundry Invariant = Stateful fuzzing
所以当人们在讨论 Foundry 中的 Invariant 时,他们通常是在讨论有状态的模糊测试,如果他们在讨论 Foundry 中的 Fuzzing,那么它们是在讨论 无状态的模糊测试
实际项目中可能需要模糊测试的点
- 新代币的发行少于通货膨胀率
- 在随机抽奖中只能有一个赢家
- 有人不应该从协议中拿出比他们投入更多的钱
清楚你的模糊器如何选择随机数据
他不会能够覆盖每一个可能的数据输入,所以了解所用的模糊器如何选择随机数据是一个很重要的点
目前比较好用的模糊器:
- Trailer Bits
- SlashOptic
上述二者是比较好用的两个,当然还有 Rift Jesus