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 中分别运行刚刚的单元测试和模糊测试:

  • 单元测试——通过

    image-20240109162901764

  • 模糊测试
    image-20240109162951480

可以看到,成功找到了问题所在。

接下来,将源码中的第一个 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;
    }
}

再次进行模糊测试,结果如下:
image-20240109164505195

可以看到这次模糊测试成功,后面的括号中runs: 256 runs 对应的数字就是运行的次数,这代表:在这里做了 256 种不同的随机输入

在 Foundry 种,可以在 foundry.toml 文件中添加如下一个部分来更改运行次数:

[fuzz]
runs = 10000

进行上述修改后,我们再次运行模糊测试,结果如下:

image-20240109164856186

运行次数很重要,更多的运行次数意味着能进行更多的随机输入,更多的用例。以及更大的概率捕捉 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:

image-20240109170929760

可见,对于复杂的代码,使用某个非常具体的场景是很容易被忽略的。

要在 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 已经找到了这个存在问题的序列:

image-20240109171833818

可以看到,在我们的合约中,使用参数 7 调用 doStuff,然后再次使用某个随机数调用我们的合约时(因为无论调用哪个数都会出现问题,所以后面的数据就不重要了)

说明

  • Foundry 使用不变量这个词来描述这种有状态的模糊测试
  • 无状态的模糊测试是将随机数据提供给函数的输入,以查看是否破坏了某种不变性
  • 有状态的模糊测试是将随机数据和随机函数调用提供给系统,以查看是否破坏了它们

在 Foundry 中,模糊测试是无状态的模糊测试,不变量是有状态的模糊测试

Foundry Fuzzing = Stateless fuzzing
Foundry Invariant = Stateful fuzzing

所以当人们在讨论 Foundry 中的 Invariant 时,他们通常是在讨论有状态的模糊测试,如果他们在讨论 Foundry 中的 Fuzzing,那么它们是在讨论 无状态的模糊测试

实际项目中可能需要模糊测试的点

  • 新代币的发行少于通货膨胀率
  • 在随机抽奖中只能有一个赢家
  • 有人不应该从协议中拿出比他们投入更多的钱

清楚你的模糊器如何选择随机数据

他不会能够覆盖每一个可能的数据输入,所以了解所用的模糊器如何选择随机数据是一个很重要的点

目前比较好用的模糊器:

  • Trailer Bits
  • SlashOptic

上述二者是比较好用的两个,当然还有 Rift Jesus