Foundry 作弊码
大多数时候,仅仅测试您的智能合约输出是不够的。 为了操纵区块链的状态,以及测试特定的 reverts 和事件 Events,Foundry 附带了一组作弊码(Cheatcodes)。
在使用 Foundry 对合约进行调试的过程中,可以使用 Foundry 内置的强大的作弊码(CheatCodes)功能。
可以通过使用 Forge
标准库里面的 Test
合约中提供的 vm
示例来使用操作码。
断言部分(Assertions)
expectRevert(下面的一行代码必须回滚)
使用 Foundry 对合约进行调试时,有些时候要检验不满足设定条件时,交易是否能正确回滚revert
,这个时候,就可以使用 expectRevert
作弊码
expectRevert
作弊码要求其下一行的代码**必须发生revert
**(忽略vm
(虚拟机)里面的代码^2),否则,在终端进行调试时,调试报错。
Signature
1 | function expectRevert() external; |
1 | function expectRevert(bytes4 message) external; |
1 | function expectRevert(bytes calldata message) external; |
使用方法举例:
1 | function testRevert() public{ |
可以使用在命令行使用下面的命令:
1 | forge test --mt testRevert |
上面的 -mt
是 --match-test
的简写,详情参考[FoundryTest调试](./FoundryTest调试.md##forge test 参数)
对于使用后面两种方法较为复杂:
举例:
1 | function testLowLevelCallRevert() public { |
要使用expectRevert
和字符串,将其转换为字节数组。
1 | vm.expectRevert(bytes("error message")); |
要使用expectRevert
和没有参数的自定义错误类型,使用其选择器。
1 | vm.expectRevert(MyContract.CustomError.selector); |
要使用expectRevert
和带有参数的自定义错误类型,请对错误类型进行ABI编码。
1 | vm.expectRevert( |
如果需要断言函数在没有消息的情况下回滚,可以使用expectRevert(bytes(""))
。
1 | function testExpectRevertNoReason() public { |
如果需要断言函数回滚了一个四字符的消息,例如AAAA
,可以这样做:
1 | function testFourLetterMessage() public { |
如果使用expectRevert("AAAA")
,则使用了expectRevert(bytes4 msg)
的重载,导致行为不同。
您还可以在单个测试中有多个expectRevert()
检查。
1 | function testMultipleExpectReverts() public { |
1 | function testTest() public { |
expectEmit(测试事件是否触发)
签名:
1 | function expectEmit ( |
1 | function expectEmit( |
在当前函数结束之前,断言特定的日志(event
)被触发(emit
)
- 调用作弊码时,指定我们应该检查第一个、第二个、第三个主题(
topic
)以及日志数据(log data1
)。作弊码失踪检查topic0
- 在当前函数执行结束之前,触发我们应该触发的事件
- 执行调用
如果事件在当前范围内不可用(例如,我们使用接口或外部合约),可以使用相同的事件签名自定义事件。
这里由两种签名:
- 不检查发送者地址:断言主题是否匹配,但是不检查触发地址
- 带有地址:断言主题匹配并断言地址匹配
顺序很重要:
如果我们调用
expectEmit
并触发了一个事件,那么下一个被触发的事件必须与我们的预期一致
示例:
没有地址的签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14event Transfer(address indexed from, address indexed to, uint256 ammount);
function testERC20EmitsTransfer() public {
// 只有`from`和`to`在ERC20的“Transfer”事件中是 inded,
// 所以我们专门检查topics 1和2(默认情况下总是检查topic 0),
// 以及数据(`amount`)。
vm.expectEmit(true, true, false, true);
// 我们出发的事件:我们预期触发的的事件:
emit MyToken.Transfer(address(this), address(1), 10);
// 执行调用
myToken.transfer(address(1). 10);
}有签名的地址:
1
2
3
4
5
6
7
8
9event Transfer(address indexed from, address indexed to, uint256 amount);
function testERC20EmitsTransfer() public {
// 通过将地址作为第五个参数传递来检测时间的触发者是否为预期的触发者
vm.expectEmit(true, true, false, true, address(myToken));
emit myToken.Transfer(address(this), address(1), 10);
myToken.transfer(address(1), 10);
}断言在单次调用中触发多个事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14function testERC20EmitsBatchTransfer() public {
// 声明多个预期的转账事件
for (uint256 i = 0; i< users.length; i++) {
vm.expectEmit(true, true, true, true);
emit Transfer(address(this), users[i], 10);
}
// 我们还预期一个自定义的 `BatchTransfer(uint256 numverOfTransfers)` event。
vm.expectEmit(false, false, false, false);
emit BatchTransfer(users.length);
// 执行调用
myToken.batchTransfer(users, 10);
}
Environment(环境)
prank
可以将msg.sender
设置为指定地址,用于下一次调用。所以在使用 Foundry
调试合约的时候,可以通过prank
来明确知道是谁发起了交易
Signature
1 | function prank(address) external; |
注意:这只有在调试中生效,并且只适用于 foundry
需要设置origin
的时候,也可以使用上面的prank
(传入两个地址,第一个是msg.sender
,后一个为tx.origin
)
在下面有一个例子,联合deal
、makeAddr
使用[^1]
还存在类似于startBroadcast()
和stopBroadcast()
类似用法的startPrank()
和stopPrank()
一组函数,这组函数之间的交易,都会被视作是传入的虚假地址发起的交易:
1 | function testTest() public { |
warp
Signature
1 | function warp(uint256) external; |
描述
设置block.timestamp
举例
1 | vm.warp(1641070800); |
rool
Signature
1 | function roll(uint256) external; |
描述
设置当前的区块高度
举例
1 | vm.roll(100); |
deal(交易)
将一个地址的余额设置为一个新的余额
signature
1 | function deal(address who, uint256 newBalance) external; |
1 | function deal(address token, address to, uint256 give) external; // 貌似弃用 |
1 | function deal(address token, address to, uint256 give, bool adjust) external; // 貌似弃用 |
同样可以联合 prank
使用[^1]
recordLogs
告诉虚拟机开始记录所有发出的事件。要访问它们,需要使用getRecordedLogs
signature
1 | function recordLogs() external; |
举例
1 | /// event LogCompleted( |
getRecordedLogs
Signature
1 | struct Log { |
描述
获取recordLogs
记录的触发的事件。该函数在调用时会消耗记录的日志。
例子
1 | /// event LogTopic1( |
txGasPrice
设置 tx.gasprice
的值(实际交易中的 Gas 价格)
Signature
1 | function txGasPrice(uint256) external; |
1 | function txGasPrice(uint256 newGasPrice) external; |
External(外部)
envUint
将环境变量读取为uint256
或者uint256[]
Signature
1 | function envUint(string calldata key) external returns (uint256 value); |
1 | function envUint(string calldata key, string calldata delimiter) external returns(uint256[] memory values); |
Tips:
- 如果值以
0x
开头,它将被解释为十六进制,否则,将其视为十六进制。 - 对于数组,可以指定用于分割参数与值的分隔符
delimiter
例子:
单个值
已拥有环境变量:
1
UINT_VALUES=115792089237316195423570985008687907853269984665640564039457584007913129639935
1
2
3
4strinf memory key = "UINT_VALUE";
uint256 expected = type(uint256).max;
uint256 output = vm.envUint(key);
assert(output == expected);数组
已拥有环境变量:
1
UINT_VALUES=0,0x0000000000000000000000000000000000000000000000000000000000000000
1
2
3
4
5string memory key = "UINT_VALUES";
string memory delimiter = ",";
uint256[2] memory expected = [type(uint256).min, type(uint256).min];
uint256[] memory output = cheats.envUint(key, delimiter);
assert(keccak256(abi.encodePacked((output))) == keccak256(abi.encodePacked((expected))));
Files(文件)
描述:
forge-std
提供的作弊码可以用于访问系统文件。
注意:默认情况下,不允许访问文件系统,需要设置foundry.toml
:
1 | fs_permission = [{access = "read", path = "./images/"}] |
来自Foundry官方文档:
1
2
3
4
5
6
7
8
9
10
11 # 配置允许触及文件系统的作弊码的权限,例如 `vm.writeFile`
# `access` 限制了通过作弊码可以访问 `path` 的方式
# `read-write` | `true` => 允许读取和写入访问(`vm.readFile` + `vm.writeFile`)
# `none`| `false` => 没有访问权限
# `read` => 只允许读取访问(`vm.readFile`)
# `write` => 只允许写入访问 (`vm.writeFile`)
# 通过列表列出了被认为是路径的路径,例如:'./' 表示项目根目录
# 默认情况下,不授予任何文件系统访问权限,并且不允许任何路径
以下示例仅启用对项目目录的读取权限:
'fs_permissions = [{ access = "read", path = "./"}]'
fs_permissions = [] # 默认值:禁用所有文件作弊码
Signature
1 | // 将文件的整个内容读取为字符串,(路径) => (数据) |
Std Cheats(非 cheatCode,可以独立使用)
makeAddr
创造一个用户来发起交易,我们可以传入一个名称,他会返回一个地址,来自forge-std
文件夹
Signature
1 | function makeAddr(string memory name) internal returns(address addr); |
使用方法:
1 | address alice = makeAddr('alice'); |
[^1]: 可以配合上面的 prank
使用
1 | address USER = makeAddr("user"); |
hoax
合并了 prank
和 deal
功能,将一个地址设置为prank
然后deal
赋予一些ETH
Signature
1 | function hoax(address who) public; |
1 | function hoax(address who, uint256 give) public; |
1 | function hoax(address who, address origin) public; |
1 | function hoax(address who, address origin, uint256 give) public; |
–未完待续–