本指南详细阐述了如何使用Foundry框架进行Solidity智能合约的模糊测试(fuzz testing)。文章首先介绍了什么是不变性(invariant),然后分别讲解了无状态和有状态的模糊测试的实现,并通过代码示例展示了相关实现步骤。最后强调了模糊测试在保证智能合约安全性方面的重要性。
学习如何使用 Foundry 框架编写 Solidity 智能合约模糊测试(fuzzing)。编写测试、使用恶作剧地址并通过 forge 执行它们。
本文将教你如何编写 Solidity 智能合约模糊测试(fuzzing),以帮助你编写更安全的协议并发掘代码中的问题。
智能合约的模糊测试是智能合约安全的新标准,是任何开发者在将代码部署到区块链之前的必备工具。市场上有很多可用于执行模糊测试的工具,今天的博客将深入探讨与 Foundry 的模糊测试。
如果你不知道 Solidity 智能合约模糊测试不变性测试是什么,请务必查看我们专门的文章,介绍该技术的细微差别,包含简单示例和类比。
如果你从未编写过一行代码,请查看我们终极的 区块链开发者课程,从零开始到专家。
在开始智能合约模糊测试之前,让我们快速了解什么是“不变性(invariant)”,因为这是我们在设置模糊测试时需要牢记的关键组成部分。
不变性(invariant)
是一种条件,系统必须始终满足,无论合约的状态或输入如何。
在去中心化金融(DeFi)中,一个不错的不变性可能是:
Foundry 将不变性测试定义为 有状态模糊测试(stateful fuzz test)。然而,这个定义并不完全准确,因为我们可以对 不变性
执行任何测试,如以下各节所示。
要在 Solidity 智能合约上使用 Foundry 执行无状态模糊测试,变量的状态将在每次运行时被遗忘,让我们考虑一个简单示例 - 如果你想跟随代码,可以启动一个新的 Foundry 项目。
forge init
现在,让我们创建一个称为 SimpleDapp 的智能合约,该合约将允许用户存入和提取资金,合约代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
/// @title SimpleDapp
/// @notice 此合约允许用户存入和提取 ETH
contract SimpleDapp {
mapping(address => uint256) public balances;
/// @notice 将 ETH 存入合约
/// @dev 此函数将 ETH 存入合约并更新映射 balances.abi
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice 从合约提取 ETH
/// @dev 此函数将从合约中提取 ETH 并更新映射 balances。
/// @param _amount 要提取的 ETH 数量
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "余额不足");
balances[msg.sender] -= _amount;
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "提取失败");
}
}
不变性
或系统对该合约始终必须保持的属性是: 用户永远不能提取超过他们存入的资金。
在 Foundry 中设置此 Solidity 合约的无状态模糊测试很简单,我们需要像通常一样在该框架中创建一个测试。
首先,我们需要使用基本导入并定义一个设置函数。
// SPDX-License-Identifier: MIT
import {Test} from "forge-std/Test.sol";
import {SimpleDapp} from "../src/SimpleDapp.sol";
pragma solidity ^0.8.23;
/// @title SimpleDapp 合约测试
/// @notice 此合约实现对 SimpleDapp 的模糊测试
contract SimpleDappTest is Test {
SimpleDapp simpleDapp;
address public user;
///@notice 通过部署 SimpleDapp 设置测试
function setUp() public {
simpleDapp = new SimpleDapp();
user = address(this);
}
}
之后,我们可以轻松设置此合约的测试。将它们变成 无状态模糊测试 的关键是,不使用代码中烧录的测试参数,而是将它们设置为输入参数;这样,Foundry 将自动开始对它们施加随机输入数据。
/// @notice 存款和提取功能的模糊测试
/// @dev 测试用户无法提取超过他们存入的资金这一不变性
/// @param depositAmount 存入的 ETH 数量
/// @param withdrawAmount 提取的 ETH 数量
function testDepositAndWithdraw(
// 我们将 depositAmount 和 withdrawAmount 设置为输入参数 👇👇👇
uint256 depositAmount,
uint256 withdrawAmount
)
public
payable
// Foundry 将为输入参数生成随机值 👆👆👆
{
// 确保用户有足够的以太坊来覆盖存款
uint256 initialUserBalance = 100 ether;
vm.deal(user, initialUserBalance);
// 仅当用户的余额足够时才尝试存款
if (depositAmount <= initialUserBalance) {
simpleDapp.deposit{value: depositAmount}();
if (withdrawAmount <= depositAmount) {
simpleDapp.withdraw(withdrawAmount);
assertEq(
simpleDapp.balances(user),
depositAmount - withdrawAmount,
"提取后的余额应与预期值匹配"
);
} else {
// 预计因余额不足而回滚
vm.expectRevert("余额不足");
simpleDapp.withdraw(withdrawAmount);
}
}
}
在此测试中,模糊器将对 depositAmount 和 withdrawAmount 这两个变量尝试随机值。如果提取金额超过存入金额,测试将失败;让我们使用以下命令尝试一下:
test --mt testDepositAndWithdraw -vvv
如预期,这将抛出一个错误,说明不变性条件被违反,因为所有的存款和提取都是随机的;将会出现提取值大于存款值的情况。
正如你所看到的,除了用来破坏我们功能的数字还有另一个参数:“runs” - 这表示模糊器在找到 反例(CounterExample)
之前经过的随机生成输入的数量。如果模糊器测试成千上万的潜在 反例
,而没有一个有效,因为没有错误,那么你可能会无休止地等待。
为解决此问题,我们可以设置模糊器在停止之前将尝试的最大运行次数;在 Foundry 中,我们需要访问配置文件 foundry.toml
。
然后,我们可以设置一个名为 [fuzz]
的新参数,并手动声明最大运行次数。最终结果将如下所示。
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
## 查看更多配置选项 https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[fuzz]
runs = 1000
对于这种类型的测试,变量的状态在多次运行中被记住,我们需要在 Foundry 中进行一些独特的配置以使其工作。
让我们探索一个新合约称为 AlwaysEven.sol
的不同示例;这次,我们为一个名为 alwaysEvenNumber
的变量设置了不变性,其条件是该变量必须始终保持为偶数,绝对不能为奇数。
因此,合约如下所示。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
contract AlwaysEven {
uint256 public alwaysEvenNumber;
uint256 public hiddenValue;
function setEvenNumber(uint256 inputNumber) public {
if (inputNumber % 2 == 0) {
alwaysEvenNumber += inputNumber;
}
// 此条件会打破必定为偶数的不变性
if (hiddenValue == 8) {
alwaysEvenNumber = 3;
}
// 我们在函数结束时将 hiddenValue 设置为 inputNumber
// 在有状态的场景中,此值将在下一次调用中记住
hiddenValue = inputNumber;
}
}
我们还包括另一个名为 hiddenValue
的变量,该变量可以将 alwaysEvenNumber
的值更改为奇数三。由于变量的状态将被记住,这很可能会打破不变性条件。
首先,我们需要从标准 forge-std
库进行额外导入,如下所示:
import {StdInvariant} from "forge-std/StdInvariant.sol";
我们需要将其作为我们的测试合约的一部分进行继承,如下所示:
contract AlwaysEvenTestStateful is StdInvariant, Test {}
从 StdInvariant.sol
我们获得一个新函数 targetContract
。这将允许我们定义将要测试的合约。
有趣的是,通过定义目标合约,Foundry 将自动开始随机执行所有合约函数并设置随机输入参数。要定义目标合约,我们需要在 setup
函数中进行设置。
function setUp() public {
targetContract(address(SelectedContract));
}
最后,我们可以设置测试。这一次,我们不需要为测试包含输入参数,函数将会自动执行,因此我们需要断言语句。最终结果如下所示:
// SPDX-License-Identifier: MIT
import {Test} from "forge-std/Test.sol";
import {AlwaysEven} from "../src/AlwaysEven.sol";
// 我们需要从 forge-std 导入不变性合约
import {StdInvariant} from "forge-std/StdInvariant.sol";
pragma solidity ^0.8.12;
contract AlwaysEvenTestStateful is StdInvariant, Test {
AlwaysEven alwaysEven;
function setUp() public {
alwaysEven = new AlwaysEven();
targetContract(address(alwaysEven));
}
function invariant_testsetEvenNumber() public view {
assert(alwaysEven.alwaysEvenNumber() % 2 == 0);
}
}
当模糊器开始对函数施加随机数据时,它最终会设置输入参数为 8,从而使得不变条件被打破并导致错误。让我们使用以下命令运行测试:
forge test --mt invariant_testsetEvenNumber -vvv
我们将得到这样的结果。
关于不同类型的测试,术语可能会令人困惑。 Foundry 通常将不变性测试归类为 有状态模糊测试(stateful fuzz test),尽管我们可以对任何使用 不变性
的测试执行,从单元测试到任何模糊测试。
为了澄清这些区别,这里有一张由 Nisedo 制作的详细图表,列出了各种测试类型。
因此,请记住,你可以定义一个 不变性
- 或者系统必须始终保持的属性,并对其执行任何测试。Foundry 要求你在有状态不变模糊测试中使用关键字 invariant
,但这并不意味着它是唯一类型的不变性测试。
采用模糊和不变性测试超越了智能合约开发中的标准实践—这是一个必要条件。
我们希望你喜欢这篇关于使用 Foundry 框架进行 Solidity 智能合约不变性模糊测试的指南。虽然存在很多工具,但 Foundry 的优势在于其能够快速开发智能合约。
如果你想尝试这段代码,请不要忘记查看 GitHub 上的合约源代码。
- 原文链接: cyfrin.io/blog/smart-con...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!