Foundry 教程: 用Solidity编写ERC-20测试用例

极快的测试,不再使用 BigNumber.js,只有 Solidity 代码

也许你是编程新手,并且刚刚开始学习 Solidity?对你来说,一个恼人的问题可能是,你基本上需要学习第二种语言(JavaScript/TypeScript)来编写测试。这无疑是一个缺点,现在随着新的foundry框架的出现,这个缺点已经消失了。

更新: 登链社区组织翻译了Foundry 文档: https://learnblockchain.cn/docs/foundry/i18n/zh/

但是,即使你精通JavaScript,一般来说,把所有的东西都放在同一个技术栈里会更好。使用foundry可以极大地帮助你用更少的代码行编写测试,而且再也不会被BigNumber.js / bn.js所困扰。

foundry是用Rust编写的,速度极快。尽管它很新,但在生产中也是非常可用的。如果我今天要开始一个新的项目,我一定会用Foundry来尝试。

Foundry Meme

感谢devtooligan提供的图片。

所以我们来实现一个ERC-20并写一些测试。创建 ERC20 合约,你也可以参考社区的这篇文章如何创建并部署ERC20代币

1. 安装Foundry

安装foundry的具体步骤将取决于你的系统。我在 Mac OS 上使用 zsh 作为终端的所需命令在下边。对于其他系统,请查看指南这里。这将给我们带来两个新的二进制文件:forgecast

$ curl -L https://foundry.paradigm.xyz | bash
$ source ~/.zshrc
$ brew install libusb
$ foundryup

2. 创建一个新的项目

要创建一个新的项目,我们现在可以使用forge init。你可以创建一个空项目,或者从一个模板开始。

我发现一个很好的模板:

$ forge init --template https://github.com/FrankieIsLost/forge-template

这将包括一些我们要使用的测试工具。

或者使用我创建的模板,其中包含本帖中所有的示例代码,见最后的说明。

3. 实现一个ERC-20

现在让我们创建一个ERC-20合约,并对其进行一些测试。首先让我们安装Openzeppelin合约并更新std库。使用forge,这可以通过以下方式完成:

$ forge install OpenZeppelin/openzeppelin-contracts@v4.5.0
$ forge update foundry-rs/forge-std

现在将该库添加到现有的remappings文件中:

forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/

现在使用Openzeppelin合约来创建一个新的合约,只要把现有的文件重命名为MyERC20.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";

contract MyERC20 is ERC20 {
    constructor() ERC20("Name", "SYM") {
        this;
    }
}

4. 创建一个测试基础设置

现在在文件MyErc20.t.sol中,我们可以创建一个基础设置(Setup)。在foundry中,有一个setUp函数,可以定义它来使合约进入不同的状态并创建一些地址。除了ERC20合约本身,我们还将从forge-std、ds-test和utils导入一些东西。

把相应的测试文件重命名为MyErc20.t.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {console} from "forge-std/console.sol";
import {stdStorage, StdStorage, Test} from "forge-std/Test.sol";

import {Utils} from "./utils/Utils.sol";
import {MyERC20} from "../MyERC20.sol";

contract BaseSetup is MyERC20, DSTest {
    Utils internal utils;
    address payable[] internal users;

    address internal alice;
    address internal bob;

    function setUp() public virtual {
        utils = new Utils();
        users = utils.createUsers(5);

        alice = users[0];
        vm.label(alice, "Alice");
        bob = users[1];
        vm.label(bob, "Bob");
    }
}

对于基本的setUp函数,我们只需使用模板中已有的utils函数。它们允许我们创建一些持有以太币的用户地址。让我们称第一个地址为Alice,第二个为Bob。

我们可以使用Vm contract来修改EVM的一些低级别的东西,例如给一个地址贴上标签,这样在堆栈跟踪中我们就可以很容易地用标签来识别它。

现在,让我们创建一些设置来转移代币......

5. 代币转移设置

现在我们可以创建一个转账代币的设置(Setup)。类似于JavaScript mocha测试中的 beforeEachdescribe的设置,当现在所有的设置都使用 Solidity ,我们可以编写一个公共的 setUp函数和合约。在setUp函数中不要忘记调用BaseSetup的 setUp

而且使用 console.log! 可以在堆栈追踪中打印日志,可以用console.log记录你当前所处的场景类型。

contract WhenTransferringTokens is BaseSetup {
    uint256 internal maxTransferAmount = 12e18;

    function setUp() public virtual override {
        BaseSetup.setUp();
        console.log("When transferring tokens");
    }

    function transferToken(
        address from,
        address to,
        uint256 transferAmount
    ) public returns (bool) {
        vm.prank(from);
        return this.transfer(to, transferAmount);
    }
}

而我们现在也有了一个简单的转账函数,可以在测试中使用。注意,为了让vm.prank工作,你必须进行实际调用,使用this.transfer而不是只使用transfer

6. 代币转账测试

我们创建两个场景:

  • 一个有足够的资金进行转账
  • 一个是资金不足下进行转账

setUp中,不要忘记调用之前的setUp, 你也可以使用super(),但我更喜欢明确的方式。然后我们可以使用ds-test库中的断言帮助函数。它将给你几个断言助手:用于断言相等(assertEq),小于(assertLe)和大于(assertGe),包括我们将使用的带decimals的标记的选项。

场景1:

contract WhenAliceHasSufficientFunds is WhenTransferringTokens {
  uint256 internal mintAmount = maxTransferAmount;

  function setUp() public override {
    WhenTransferringTokens.setUp();
    console.log("When Alice has sufficient funds");
    _mint(alice, mintAmount);
  }

  function itTransfersAmountCorrectly(
    address from,
    address to,
    uint256 amount
  ) public {
    uint256 fromBalance = balanceOf(from);
    bool success = transferToken(from, to, amount);

    assertTrue(success);
    assertEqDecimal(
      balanceOf(from),
      fromBalance - amount, decimals()
    );
    assertEqDecimal(
      balanceOf(to),
      transferAmount, decimals()
    );
  }

  function testTransferAllTokens() public {
    uint256 t = maxTransferAmount;
    itTransfersAmountCorrectly(alice, bob, t);
  }

  function testTransferHalfTokens() public {
    uint256 t = maxTransferAmount / 2;
    itTransfersAmountCorrectly(alice, bob, amount);
  }

  function testTransferOneToken() public {
    itTransfersAmountCorrectly(alice, bob, 1);
  }
}

场景2:

contract WhenAliceHasInsufficientFunds is WhenTransferringTokens {
  uint256 internal mintAmount = maxTransferAmount - 1e18;

  function setUp() public override {
    WhenTransferringTokens.setUp();
    console.log("When Alice has insufficient funds");
    _mint(alice, mintAmount);
  }

  function itRevertsTransfer(
    address from,
    address to,
    uint256 amount,
    string memory expRevertMessage
  ) public {
    vm.expectRevert(abi.encodePacked(expRevertMessage));
    transferToken(from, to, amount);
  }

  function testCannotTransferMoreThanAvailable() public {
    itRevertsTransfer({
      from: alice,
      to: bob,
      amount: maxTransferAmount,
      expRevertMessage: "[...] exceeds balance"
    });
  }

  function testCannotTransferToZero() public {
    itRevertsTransfer({
      from: alice,
      to: address(0),
      amount: mintAmount,
      expRevertMessage: "[...] zero address"
    });
  }
}

7. 模拟调用

vm也允许你模拟一个调用。例如,你可以模拟一个向bob转账和金额的调用,并返回false。你还可以使用clearMockedCalls()来清除模拟。

function testTransferWithMockedCall() public {
    vm.prank(alice);
    vm.mockCall(
        address(this),
        abi.encodeWithSelector(
            this.transfer.selector,
            bob,
            maxTransferAmount
        ),
        abi.encode(false)
    );
    bool success = this.transfer(bob, maxTransferAmount);
    assertTrue(!success);
    vm.clearMockedCalls();
}

8. 直接检索数据

你也可以使用stdStorage功能,直接从状态中检索数据。例如,要直接从状态中读取余额,首先计算存储槽,如下代码所示。然后使用vm.load将其加载出来:

using stdStorage for StdStorage;

function testFindMapping() public {
    uint256 slot = stdstore
        .target(address(this))
        .sig(this.balanceOf.selector)
        .with_key(alice)
        .find();
    bytes32 data = vm.load(address(this), bytes32(slot));
    assertEqDecimal(uint256(data), mintAmount, decimals());
}

9. 模糊测试

你也可以在forge中使用模糊测试。只要做一个带有输入变量的测试函数,forge会自动为你进行模糊测试。如果你需要有一定的界限,你可以通过确切的输入类型来限制范围,或者使用vm.assumption来排除摸个值,和(或者)用modulo来限制输入到一个确切的范围:

function testTransferFuzzing(uint64 amount) public {
    vm.assume(amount != 0);
    itTransfersAmountCorrectly(
        alice,
        bob,
        amount % maxTransferAmount
    );
}

10. 运行测试

$ forge test -vvvvv

你可以在不同的verbose级别下运行forge测试。增加v的数量,最多5个。

  • 2: 打印所有测试的日志
  • 3: 打印失败的测试的执行堆栈
  • 4: 打印所有测试的执行堆栈,以及失败测试的setup堆栈
  • 5: 打印所有测试的执行和setup堆栈

BigNumberJS Meme)

ERC-20 Forge 模板

到目前为止,更多项目已经取得了很好的经验切换他们的测试。

如果你也想试一试,想从上面的代码开始,请使用以下模板:

$ mkdir my-new-erc20 && cd my-new-erc20
$ forge init --template https://github.com/soliditylabs/forge-erc20-template

Solidity 编码快乐!

本翻译由 Decert.me 赞助支持。

Foundry 框架合约开发测试 测试对 Foundry 开发框架的了解,主要涉及的内容有编译、测试、部署、以及如何进行代码开源验证。
开始挑战
点赞 5
收藏 6
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO