使用Foundry进行智能合约模糊测试的完整指南

  • cyfrin
  • 发布于 2024-10-16 14:21
  • 阅读 19

本指南详细阐述了如何使用Foundry框架进行Solidity智能合约的模糊测试(fuzz testing)。文章首先介绍了什么是不变性(invariant),然后分别讲解了无状态和有状态的模糊测试的实现,并通过代码示例展示了相关实现步骤。最后强调了模糊测试在保证智能合约安全性方面的重要性。

学习如何使用 Foundry 框架编写 Solidity 智能合约模糊测试(fuzzing)。编写测试、使用恶作剧地址并通过 forge 执行它们。

本文将教你如何编写 Solidity 智能合约模糊测试(fuzzing),以帮助你编写更安全的协议并发掘代码中的问题。

智能合约的模糊测试是智能合约安全的新标准,是任何开发者在将代码部署到区块链之前的必备工具。市场上有很多可用于执行模糊测试的工具,今天的博客将深入探讨与 Foundry 的模糊测试。

如果你不知道 Solidity 智能合约模糊测试不变性测试是什么,请务必查看我们专门的文章,介绍该技术的细微差别,包含简单示例和类比。

如果你从未编写过一行代码,请查看我们终极的 区块链开发者课程,从零开始到专家。

在开始智能合约模糊测试之前,让我们快速了解什么是“不变性(invariant)”,因为这是我们在设置模糊测试时需要牢记的关键组成部分。

在你的 Solidity 智能合约中定义不变性

不变性(invariant) 是一种条件,系统必须始终满足,无论合约的状态或输入如何。

在去中心化金融(DeFi)中,一个不错的不变性可能是:

  • 投票数不能超过注册选民的数目。
  • 用户永远不能提取超过他们存入的资金。
  • 公平彩票只能有一个赢家。

Foundry 将不变性测试定义为 有状态模糊测试(stateful fuzz test)。然而,这个定义并不完全准确,因为我们可以对 不变性 执行任何测试,如以下各节所示。

无状态 Solidity 智能合约模糊测试

要在 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

如预期,这将抛出一个错误,说明不变性条件被违反,因为所有的存款和提取都是随机的;将会出现提取值大于存款值的情况。

这张图片显示了使用 Foundry 进行无状态模糊测试的结果

正如你所看到的,除了用来破坏我们功能的数字还有另一个参数:“runs” - 这表示模糊器在找到 反例(CounterExample) 之前经过的随机生成输入的数量。如果模糊器测试成千上万的潜在 反例,而没有一个有效,因为没有错误,那么你可能会无休止地等待。

为解决此问题,我们可以设置模糊器在停止之前将尝试的最大运行次数;在 Foundry 中,我们需要访问配置文件 foundry.toml

这张图片显示了配置测试的 Foundry dot 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

有状态 Solidity 智能合约模糊测试

对于这种类型的测试,变量的状态在多次运行中被记住,我们需要在 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 进行有状态 Solidity 智能合约模糊测试的结果

关于 Solidity 智能合约模糊测试术语的注意事项

关于不同类型的测试,术语可能会令人困惑。 Foundry 通常将不变性测试归类为 有状态模糊测试(stateful fuzz test),尽管我们可以对任何使用 不变性 的测试执行,从单元测试到任何模糊测试。

为了澄清这些区别,这里有一张由 Nisedo 制作的详细图表,列出了各种测试类型。

这张图片显示了不同类型的 Solidity 智能合约测试

因此,请记住,你可以定义一个 不变性 - 或者系统必须始终保持的属性,并对其执行任何测试。Foundry 要求你在有状态不变模糊测试中使用关键字 invariant,但这并不意味着它是唯一类型的不变性测试。

结论

采用模糊和不变性测试超越了智能合约开发中的标准实践—这是一个必要条件。

我们希望你喜欢这篇关于使用 Foundry 框架进行 Solidity 智能合约不变性模糊测试的指南。虽然存在很多工具,但 Foundry 的优势在于其能够快速开发智能合约。

如果你想尝试这段代码,请不要忘记查看 GitHub 上的合约源代码。

  • 原文链接: cyfrin.io/blog/smart-con...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.