说明
有关Foundry的基本使用,已经在Foundry入门以及实战部署中做过简单的介绍和演示。
cast
cast主要是和链交互的工具。官方文档
RPC请求
cast rpc eth_blockNumber --rpc-url=$ETH_RPC_URL,表示请求以太坊的区块高度。
区块查询
cast block-number --rpc-url=$ETH_RPC_URL表示查询以太坊的区块高度,等价于cast rpc eth_blockNumber --rpc-url=$ETH_RPC_URL。cast block block-number --rpc-url=$ETH_RPC_URL表示查询以太坊的区块高度对应的区块信息。cast block latest表示查询最新的区块信息。
交易查询
cast tx tx-hash --rpc-url=$ETH_RPC_URL表示查询以太坊的交易信息。cast receipt tx-hash --rpc-url=$ETH_RPC_URL表示查询以太坊的交易回执信息。cast tx tx-hash gasPrice --rpc-url=$ETH_RPC_URL表示查询以太坊的交易的gasPrice。其他的字段也可以查询。cast tx tx-hash --rpc-url=$ETH_RPC_URL --json以json格式输出交易信息。cast pretty-calldata calldata, 解析交易的calldata。1
2
3
4
5
6
7$ cast pretty-calldata 0xa9059cbb000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d00000000000000000000000000000000000000000000000000174b37380cea000
Possible methods:
- transfer(address,uint256)
------------
[0]: 000000000000000000000000e78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0
[1]: 0000000000000000000000000000000000000000000000000174b37380cea000cast 4byte selector,通过selector查询对应的方法。1
2
3
4$ cast 4byte 0xa9059cbb
Possible methods:
- transfer(address,uint256)cast keccak function-sig通过function-sig计算selector。1
2
3$ cast keccak "transfer(address,uint256)"
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049bcast sig function-sig, 通过function-sig计算selector。1
2
3$ cast sig "transfer(address,uint256)"
0xa9059cbbcast 4buye-event event_hash, 根据event hash获得event的信息。1
2
3
4$ cast 4byte-event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
Possible events:
- Approval(address,address,uint256)cast run tx-hash, 模拟运行,运行结果会显示调用栈信息,速度可能会比较慢,因为要下载访问链上的数据。1
2
3
4
5
6
7$ cast run 0x0e0b0c0d0f0e0b0c0d0f0e0b0c0d0f0e0b0c0d0f0e0b0c0d0f0e0b0c0d0f0e0b
Traces:
[29962] 0xc02a…6cc2::transfer(0x40950267d12e979ad42974be5ac9a7e452f9505e, 105667789681831058)
├─ emit Transfer(param0: 0xc564ee9f21ed8a2d8e7e76c085740d5e4c5fafbe, param1: 0x40950267d12e979ad42974be5ac9a7e452f9505e, param2: 105667789681831058)
└─ ← 0x0000000000000000000000000000000000000000000000000000000000000001
Transaction successfully executed.
Gas used: 51618cast run tx-hash --debug调试链上合约的情况。
合约交互
cast etherscan-source contract_address --etherscan-api-key=$ETHERSCAN_API_KEY -d /path/to/dir,通过etherscan的api获取合约的源码, 保存到/path/to/dir目录下。1
$ cast etherscan-source 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --etherscan-api-key=$ETHERSCAN_API_KEY
cast storage address contract_address stroage_index, 查询合约的stroage_index的值。1
2$ cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0
0x577261707065642045746865720000000000000000000000000000000000001acast interface /path/to/abifile通过abi文件获取合约的接口
账户管理
cast account new,创建一个新的账户。cast account new path/to/file, 创建一个新的账户,并保存到指定的路径。cast wallet sign "abc" -i, 通过私钥签名交易,-i表示通过交互式输入私钥。cast wallet verify
编码解码
cast --to-dec 0x1234,表示将十六进制转换为十进制cast --to-hex, 将十进制换转为十六进制cast --to-unit,cast --to-weicast --to-rip, 序列化cast --from-rip, 反序列化
anvil
anvil在本地模拟节点,同时会在本地生成10个账户。anvil --fork-url=$ETH_RPC_URL, fork其他网络节点。
forge
forge init hello-foundry,初始化一个名为hello-foudry的项目。1
2
3
4
5
6
7
8
9
10
11
12
13$forge init hello-foundry
└── hello-foundry
├── foundry.toml
├── lib
│ └── forge-std
├── script
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.solforge build,编译合约,编译结果会保存到out目录下。forge build -w,watch模式,如果sol文件发生变化,会自动编译。
依赖管理
forge install <gh_user/gh_repo>, 安装依赖。比如forge install openzeppelin/openzeppelin-contracts --no-commit
forge test
在开发过程中使用forget test对合约进行测试,这也是foundry最为强大的功能之一。
以Counter合约为例,Counter.t.sol文件内容如下:
1 | pragma solidity ^0.8.13; |
测试代码需要注意两点:
- 测试合约都会继承
Test合约,测试一般都会有setUp(),每个测试方法运行之前会调用setUp()方法 - 所有的测试函数都是以
test开头的,测试函数的参数是public,测试函数的返回值是public - 每个测试方法是相互独立的,互不影响
- 对于存在参数的测试方法,是一个模糊测试,会随机选择参数调用测试
forge test --vvv,显示test函数的详细信息;
普通测试
普通测试是指测试合约的方法,不涉及交易,不涉及账户,不涉及gas,也不需要参数
1 | ```solidity |
模糊测试
模糊测试是指测试合约的方法,不涉及交易,不涉及账户,不涉及gas,但是需要参数,参数是由foundry框架自动输入。
1 | function testSetNumber(uint256 x) public { |
invariant测试
invariant测试,顾名思义就是不变量测试。invariant测试的方法需要以invariant。测试框架会随意调用合约中的方法,看看是否是有不满足不变量的情况。
1 | function invariant_sometest() public { |
在上述例子中,我们测试了counter.number()是否小于uint128的最大值。实际测试结果:
1 | Failing tests: |
会发现当counter.number()大于uint128的最大值时,就会出现不满足不变量的情况,测试用例不通过。如果修改为如下:
1 | function invariant_sometest() public { |
合约中的number最大值必然不会超过uint256的最大值,测试用例就会通过。
ffi测试
主要是用于本地合约算法算出来的内容和外部工具计算出来的内容是否一致,从而验证算法的有效性。ffi测试需要注意两点:
- ffi测试需要借助与
vm.ffi(cmd),用于执行外部命令,返回结果。 - 测试时,需要加上
--ffi参数。例如forge test -vvv --ffi可以看到测试通过,表示外部工具计算出来的结果和合约中的结果一致。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function testCheatcode() public {
string memory message = "abc";
bytes32 hash = keccak256(abi.encodePacked(message));
console2.logBytes32(hash);
string[] memory cmd = new string[](3);
// 传入外部命令
cmd[0] = "cast";
cmd[1] = "keccak";
cmd[2] = message;
// 执行外部命令
bytes memory result = vm.ffi(cmd);
bytes32 hash1 = abi.decode(result, (bytes32));
assertEq(hash, hash1);
}
---------------------output----------
[PASS] testCheatcode() (gas: 8555)
Logs:
0x4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45
Test result: ok. 1 passed; 0 failed; finished in 16.28ms
Cheatcode
官方参考文档
foundry在测试用例中为我们提供了一些内置的合约,可以方便我们打印一些信息。常用的是console2和emit log。比如:
1 | function test_log() public { |
输出结果如下:
1 | [PASS] test_log() (gas: 4939) |
foundry在测试用例中,可以利用vm变量,修改合约中的状态。vm中存在很多方法:
vm.warp(int).改变block timestamp1
2
3
4
5
6
7
8
9
10function testCheatcode() public {
console2.log("before",block.timestamp);
vm.warp(1000); // 改变block timestamp
console2.log("after",block.timestamp);
}
---------------------output----------
[PASS] testCheatcode() (gas: 6887)
Logs:
before 1
after 1000vm.roll(int)改变 block number1
2
3
4
5
6
7
8
9
10function testCheatcode() public {
console2.log("before",block.number);
vm.roll(1000); // 改变block number
console2.log("after",block.number);
}
---------------------output----------
[PASS] testCheatcode() (gas: 6887)
Logs:
before 1
after 1000vm.prank()改变下一次合约调用的msg.sender1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34contract CounterTest is Test {
Counter public counter;
Helper public h;
address public alice;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
h = new Helper();
alice = address(1);
}
function testCheatcode() public {
vm.prank(alice);
address caller = h.whocalled();
console2.log("caller", caller);
address caller2 = h.whocalled();
console2.log("caller2", caller2);
}
}
contract Helper {
function whocalled() public returns (address) {
return msg.sender;
}
}
---------------------output----------
Running 1 test for test/Counter.t.sol:CounterTest
[PASS] testCheatcode() (gas: 14908)
Logs:
caller 0x0000000000000000000000000000000000000001
caller2 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84startPrank(address),stopPrank()包含在这两个语句之间的合约调用的msg.sender都会被改变1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function testCheatcode() public {
vm.startPrank(alice);
address caller = h.whocalled();
console2.log("caller", caller);
address caller2 = h.whocalled();
console2.log("caller2", caller2);
vm.stopPrank();
address caller3 = h.whocalled();
console2.log("caller3", caller3);
}
---------------------output----------
[PASS] testCheatcode() (gas: 14908)
Logs:
caller 0x0000000000000000000000000000000000000001
caller2 0x0000000000000000000000000000000000000001
caller3 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84vm.deal(address,1ether),修改address余额为1ether1
2
3
4
5
6
7
8
9
10
11function testCheatcode() public {
console2.log("before:",alice.balance);
vm.deal(alice,1 ether);
console2.log("after:",alice.balance);
}
---------------------output----------
Running 1 test for test/Counter.t.sol:CounterTest
[PASS] testCheatcode() (gas: 9481)
Logs:
before: 0
after: 1000000000000000000deal(address token,address to,uint256 give), 给用户发送某个erc20的token.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
Helper public h;
address public alice;
IERC20 public dai;
function setUp() public {
counter = new Counter();
counter.setNumber(1);
h = new Helper();
alice = address(10086);
dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
}
function testCheatcode() public {
console2.log("before:",dai.balanceOf(alice));
deal(address(dai),alice,1 ether);
console2.log("after:",dai.balanceOf(alice));
}
}
contract Helper {
function whocalled() public returns (address) {
return msg.sender;
}
}因为我们没有在本地部署dai合约,所以我们在进行
forge test时,需要使用链上的rpc。1
2
3
4
5
6
7forge test -vvv --fork-url=$ETH_RPC_URL
---------------------output----------
Running 1 test for test/Counter.t.sol:CounterTest
[PASS] testCheatcode() (gas: 153636)
Logs:
before: 0
after: 1000000000000000000vm.envString(env_name)从环境变量中获取内容1
2
3
4
5
6
7
8function testCheatcode() public {
string memory rpc = vm.envString("ETH_RPC_URL");
console2.log("rpc from env",rpc);
}
---------------------output----------
[PASS] testCheatcode() (gas: 7311)
Logs:
rpc from env your_rpc_urlvm.selectFork(forkId)在这条语句后面都是多会处在forkId所在的rpc的环境里。使用selectFork好处就是可以在一份测试代码里对不同的rpc测试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function testCheatcode() public {
string memory rpc = vm.envString("ETH_RPC_URL");
console2.log("rpc from env",rpc);
uint256 mainnet = vm.createFork(rpc);
vm.selectFork(mainnet);
console2.log("before:",dai.balanceOf(alice));
deal(address(dai),alice,1 ether);
console2.log("after:",dai.balanceOf(alice));
}
---------------------output----------
forge test -vvv # 由于我们已经在代码中通过selectFork指定了rpc,所以测试时就不需要再指定rpc了
[PASS] testCheatcode() (gas: 156409)
Logs:
rpc from env your_rpc_url
before: 0
after: 1000000000000000000vm.rollFork(block_number)指定需要fork的区块高度1
2
3
4
5
6
7
8
9
10
11function testCheatcode() public {
string memory rpc = vm.envString("ETH_RPC_URL");
uint256 mainnet = vm.createFork(rpc);
vm.selectFork(mainnet);
vm.rollFork(150000);
console2.log(block.number);
}
---------------------output----------
[PASS] testCheatcode() (gas: 7976)
Logs:
150000vm.expectEmit(bool,bool,bool,bool)表示对event中的index0,index1,index2,data进行测试。如果不需要测试,就将bool设置为false.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40pragma solidity 0.8.10;
import "forge-std/Test.sol";
contract EmitContractTest is Test {
event Transfer(address indexed from, address indexed to, uint256 amount);
function testExpectEmit() public {
ExpectEmit emitter = new ExpectEmit();
// Check that topic 1, topic 2, and data are the same as the following emitted event.
// Checking topic 3 here doesn't matter, because `Transfer` only has 2 indexed topics.
vm.expectEmit(true, true, false, true);
// The event we expect
emit Transfer(address(this), address(1337), 1337);
// The event we get
emitter.t();
}
function testExpectEmitDoNotCheckData() public {
ExpectEmit emitter = new ExpectEmit();
// Check topic 1 and topic 2, but do not check data
vm.expectEmit(true, true, false, false);
// The event we expect
emit Transfer(address(this), address(1337), 1338);
// The event we get
emitter.t();
}
}
contract ExpectEmit {
event Transfer(address indexed from, address indexed to, uint256 amount);
function t() public {
emit Transfer(msg.sender, address(1337), 1337);
}
}
---------------------output----------
[PASS] testEmit() (gas: 72582)
Test result: ok. 1 passed; 0 failed; finished in 324.91µs测试通过。
vm.expectRevert(message),用于判断revert的错误是否和message一致,这种主要是用于测试revert报错的内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19contract CounterTest is Test {
Helper public h;
function setUp() public {
h = new Helper();
}
function testRevert() public {
vm.expectRevert("some reason");
h.revertIt();
}
}
contract Helper {
function revertIt() public {
revert("some reason");
}
}
---------------------output----------
[PASS] testRevert() (gas: 8321)
Test result: ok. 1 passed; 0 failed; finished in 295.85µs测试通过,表示revert的原因与预期的一致。
forget script
常规使用
社区的规范,是将script的脚本宣布全部放在scripts目录下,以.s.sol结尾。
- 和测试脚本一样,也会存在一个
setUp()方法。函数运行前会调用setUp()函数。 - 默认创建的项目中的
scipts中会存在一个Counter.s.sol的脚本,我们可以直接使用。 - 运行方式
forge script script/xxx.s.sol例如,我们修改为:1
2
3
4
5
6
7
8
9
10
11
12// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
contract CounterScript is Script {
function setUp() public {}
function run() public {
vm.broadcast();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18contract CounterScript is Script {
function setUp() public {
console2.log("in setUp");
}
function run() public {
console2.log("in run");
}
}
---------------------output----------
$ forge script scripts/Counter.s.sol
Script ran successfully.
Gas used: 25870
== Logs ==
in setUp
in run
指定运行函数
除此之外,forge script还可以通过--sig指定运行函数。如下所示:
1 | contract CounterScript is Script { |
传入参数
如果我们需要运行的方法需要参数,我们也可以手动传入参数。这一点与测试函数不一样,测试函数是采用fuzzing测试。
1 | contract CounterScript is Script { |
部署合约
目前forge script 最常用的功能是用于部署合约。如下所示:
1 | contract CounterScript is Script { |
vm.startBroadcast()和vm.stopBroadcast();中的调用信息会被记录下来。根据这些调用信息,将合约部署上链。forge script script/Counter.s.sol -vvvv --rpc-url=http://127.0.0.1:8545 在本地部合约。查看调用详情:
1 | Traces: |
如果确认没有问题,需要实际部署合约时,需要加上--broadcast参数。如下所示:
1 | forge script script/Counter.s.sol -vvvv --rpc-url=rpc-url --broadcast --private-key=private-key |
如果在本地成功部署之后。forge script运行的结果是:
1 | Waiting for receipts. |
anvil的结果会输出:
1 | Transaction: 0x98f1137d22797fa42197038cde25c43b3038c66cb3adbe0ebb88c1ab7a69b186 |
forge script结果输出和anvil的输出是一致的。所有的部署信息会保存在broadcast目录下。最终以json的格式保存,比如本次部署的结果就是:
1 | { |
调试合约
在部署合约之前,我们需要先调试合约。调试合约的命令是forge script script/Counter.s.sol --debug。调用完成之后就会出来一个字符界面的调试窗口。从上到下以此是:
- 最上面的是evm字节码PC寄存器,每一步消耗的gas
- 其次是evm stack保存的数据
- 然后是evm memory保存的数据
- 最后对应的就是sol代码
通过上下键运行选择字节码运行。通过这种方式学习evm字节码以及字节码的优化是非常有帮助的。这条命令和cast run tx-hash --debug调试界面是一样的。只不过通过forge script 调试的是本地合约。cast调试的是链上合约。
forge inspect
默认情况下,forge build的输出结果都会保存在output目录下。如果需要查看output目录下的合约的信息,可以使用forge inspect命令。如下所示:
forge inspect Counter mi,查看合约的方法标识符1
2
3
4
5
6$ forge inspect Counter mi
{
"increment()": "d09de08a",
"number()": "8381f58a",
"setNumber(uint256)": "3fb5c1cb"
}forge inspect Counter storage查看storage1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20$ forge inspect Counter storage
{
"storage": [
{
"astId": 23201,
"contract": "src/Counter.sol:Counter",
"label": "number",
"offset": 0,
"slot": "0",
"type": "t_uint256"
}
],
"types": {
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}forge inspect Counter abi, 查看合约的abi。
其他
forge test --gas-report查看每个方法消耗的gas情况;forge install <gh_user/gh_repo>安装依赖forge update <dep>更新依赖forge remove <dep>删除依赖forge coverage查看测试覆盖率forge fmt格式化代码