foundry的介绍

什么是foundry?

foundry是一个solidity智能合约开发工具。可以帮你管理依赖包,编译项目,运行测试脚本,还可以让你通过命令行工具或者script脚本和链上合约进行交互。和hardhat不同的地方是,hardhat我们还是主要用来开发大型的合约项目,但是foundry用来进行编写测试脚本我认为是非常方便的。主要的特点是可以直接使用solidity编写合约测试脚本,无需在JavaScript和solidity之间进行切换。虽然在hardhat也可以编写测试脚本,但是有时候需要切换语言或者对类型进行转换相对会比较麻烦。

安装和配置

Foundryup是foundry的安装器。我们可以直接通过终端命令行进行安装:

1
curl -L https://foundry.paradigm.xyz | bash

或者要是你喜欢也可以通过源码的方式进行编译安装

1
2
3
4
5
6
7
8
9
10
# clone the repository
git clone https://github.com/foundry-rs/foundry.git
cd foundry
# install Forge + Cast
cargo install --path ./cli --profile local --bins --force
# install Anvil
cargo install --path ./anvil --profile local --force
# install Chisel
cargo install --path ./chisel --profile local --force

初始化

我们可以通过forge命令行直接初始化新项目

1
forge init helle_foundry

初始化之后的大概是这么个目录结构:

1
2
3
4
5
6
7
8
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib 依赖的包文件目录
├── script 自定义的脚本目录
├── src 合约源码目录
└── test 测试脚本目录

特性和使用

编写测试脚本

测试脚本的基本代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract ContractBTest is Test {
uint256 testNumber;

function setUp() public {
testNumber = 42;
}

function test_NumberIs42() public {
assertEq(testNumber, 42);
}

function testFail_Subtract43() public {
testNumber -= 43;
}
}

在上面的例子中每个test函数都是相互独立的,setUp函数类似于我们的构造函数,会首先进行调用setUp函数进行初始化。在我们实际的操作中,可以直接导入需要测试的合约,然后在setUp中对合约进行初始化。我们只需要一个变量就能调用目标合约中的函数进行测试。

运行测试脚本的命令:

1
forge test --match-contract "ContractBTest(合约名称)" -vvv

debug

我们可以通过-vvv-vvvv进行数据跟踪。在打印出来的日志中,我们通过颜色来区分不同的错误类型(如果你的终端支持颜色显示的话):

  • 绿色 : 正常的调用
  • 红色 : 回滚的调用
  • 蓝色 : 作弊码的调用
  • 青色 : 日志
  • 黄色 : 合约部署
1
2
3
4
5
6
7
8
9
[24661] OwnerUpOnlyTest::testIncrementAsOwner()
├─ [2262] OwnerUpOnly::count()
│ └─ ← 0
├─ [20398] OwnerUpOnly::increment()
│ └─ ← ()
├─ [262] OwnerUpOnly::count()
│ └─ ← 1
└─ ← ()

fuzz测试

1
2
3
4
5
6
7
8
9
10
11
contract SafeTest is Test {

function testFuzz_Withdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}

比如上面的提款测试,普通的test测试函数需要自定义一个固定的amount金额,但是这个指定的amount不能覆盖到所有的情景,另一个选择就是使用testFuzz.fuzz测试的函数名和普通测试对比只是多了fuzz,需要注意的是所有的测试函数需要用test或者testFuzz开头否则是不会被认为是一个测试函数的。

console.log日志打印

在solidity中打印日志需要提前对日志内容的类型进行定义。foundry支持下面的一些数据类型进行日志打印,可以帮助我们进行debug:

  • console.logInt(int i)
  • console.logUint(uint i)
  • console.logString(string memory s)
  • console.logBool(bool b)
  • console.logAddress(address a)
  • console.logBytes(bytes memory b)
  • console.logBytes1(bytes1 b)
  • console.logBytes2(bytes2 b)
  • console.logBytes32(bytes32 b)
    对于一些更加复杂的数据结构,比如我们的目标合约中返回了一个struct类型的数据。我们可以直接使用targetContract.StructName对数据进行定义。

assert

foundry中主要有3种类型的assert

  • assert(conditon) 判断一个bool值
  • assertEq(a,b) 判断a,b两个值是否相等
  • assertTrue(condition,”reason”)
    1
    2
    3
    function testGetTotalShares() public {
    assertEq(prizePool.getTotalShares(), 220);
    }

时间和区块进行改变

某些合约中可能有时间间隔,比如一个stake asset操作之后,可能需要一个月的时间间隔之后才能进行下一次操作,这个时候我们可以直接去修改evm中的时间

1
vm.warp(block.timstamp + 30 days);

或者我们也可以对区块进行增加

1
vm.roll(block.number + 1);

vm.expectRevert

在某些测试中,对于条件不满足的情况下,合约逻辑中可能会存在一些revert的情况,我们需要对这些revert进行捕捉的时候可以用到vm.expertRevert

  • expectRevert()
  • expectRevert(bytes4 message)
  • expectRevert(bytes calldata message)
1
2
3
4
5
function testLowLevelCallRevert() public {
vm.expectRevert(bytes("error message"));
(bool revertsAsExpected, ) = address(myContract).call(myCalldata);
assertTrue(revertsAsExpected, "expectRevert: call did not revert");
}

有些合约对一些多次出现的错误可能把它定义为一个error类型,这个时候我们可以通过CustomError.selector对错误进行捕捉。

1
vm.expectRevert(CustomError.selector);
1
2
3
4
vm.expectRevert(
abi.encodeWithSelector(CustomError.selector, 1, 2)
);

对于那些直接revert并没有定义错误的情况下:

1
vm.expectRevert(bytes(""));

Follow我的推进行交流学习:https://twitter.com/coffiasse
TG:@coffiasd

Comments