本文作者:小驹[1]
本文列举了 foundry 中常用的命令,方便以后查询使用。
官方网站:getfoundry.sh[2]
在 mac 环境下,使用下面命令进行安装
curl -L https://foundry.paradigm.xyz | bash
source ~/.zshrc
# 每次执行foundryup时,都会下载最新的cast,anvil,forge程序
foundryup
foundry 系列的工具,主要包含三大组件,分别对应不同的功能,下面会每个组件依次试用。
😀 我的 ETH alchemy的RPC接点 https://eth-mainnet.g.alchemy.com/v2/*****
export ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/******
cast 是 Foundry 用于执行以太坊 RPC 调用的命令行工具。您可以进行智能合约调用
、发送交易或检索任何类型的链数据
!
cast 与 web3 交互的小工具,即使不是代码开发的人员也会经常使用该工具与链上数据进行查询等交互。
cast rpc eth_blockNumber --rpc-url=$ETH_RPC_URL
😀 cast支持环境变量ETH_RPC_URL,将RPC节点设置到环境变量ETH_RPC_URL中。对带有--rpc-url的参数的cast命令中,可直接从环境变量中直接读取,不需要在命令中体现。
查询区块高度-cast rpc eth_blockNumber
cast rpc eth_blockNumber --rpc-url=$ETH_RPC_URL
"0xebc18f"
(base) ➜ ~ cast --to-dec "0xebc18f"
15450511
(base) ➜ ~ cast --to-dec 0xebc18f
15450511
查询区块信息-cast block
(base) ➜ ~ cast block 15450511 --rpc-url=$ETH_RPC_URL
baseFeePerGas 18648783904
difficulty 12266510444604275
extraData 0x706f6f6c696e2e636f6d21bb45000ef0fc7e9d
gasLimit 29941438
gasUsed 28701300
transactions: [
0x1ac18cdb12a6cb7022823fef4e2bc64fa959352af58507e057fc27f62d1e23a7
0x1b0032cb42ade1add87a25f367b4142ebe627771abc936f2a6f403bcd50e6dc5
0x28afc8b0659d88ffb03b803b01eb573690b6e3b70a0c1cf941d7f6fafc146465
查询交易信息-cast tx <交易 hash>
(base) ➜ ~ cast tx 0xd38950f391b91fef3daaf516d86470a1552461539bdba5ace230b942d5237974 --rpc-url=$ETH_RPC_URL
blockHash 0xd73fb0230f3ab6e8a8c9ba5698c1ec7beb5aa23175e1231560b5d507b748a7ea
blockNumber 15450511
from 0x796ed889d874dEeE8fE495F6c245765cf7db193B
gas 96677
gasPrice 19654542987
hash 0xd38950f391b91fef3daaf516d86470a1552461539bdba5ace230b942d5237974
input 0xa0712d680000000000000000000000000000000000000000000000000000000000000002
nonce 0
r 0x31c9c3e6d7cd7058025a4b7cf2c17355ac856902c20fdff6b83d2c134d66ea2f
s 0x1775d3a1002467e05cbabc23219e18b27e9ac29dd0dbdafc165bbab052f7ba23
to 0xc93f78f08c7E9526C78Da56Cba1DEE8287baCb27
transactionIndex 4
v 1
value 0
交易回执查询-cast receipt <receipt_hash>
base) ➜ ~ cast receipt 0xd38950f391b91fef3daaf516d86470a1552461539bdba5ace230b942d5237974 --rpc-url=$ETH_RPC_URL
blockHash 0xd73fb0230f3ab6e8a8c9ba5698c1ec7beb5aa23175e1231560b5d507b748a7ea
blockNumber 15450511
contractAddress
cumulativeGasUsed 564786
effectiveGasPrice 19654542987
gasUsed 86319
logs [{"address":"0xc93f78f08c7e9526c78da56cba1dee8287bacb27","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000000000000000000000000000
使用 --json 以json格式返回数据,使用管道输入给jq进行处
cast receipt 0xd38950f391b91fef3daaf516d86470a1552461539bdba5ace230b942d5237974 --rpc-url=$ETH_RPC_URL --json | jq
🤣 jq 工具的使用 jq 一个灵活的轻量级命令行 JSON 处理器,jq 用于处理 JSON 输入,将给定过滤器应用于其 JSON 文本输入并在标准输出上将过滤器的结果生成为 JSON。
查询 calldata 数据-cast pretty-calldata <十六进制数据>
pretty-calldata 命令会取出 <十六进制数据>中的前 4 个字节,从在线网站的数据库(https://sig.eth.samczsun.com/[3])中比对 4 字节的 selector 对应的函数原型,并将 <十六进制数据>中的后面部分的数据按照函数原型进行格式化输出。
查看input数据
(base) ➜ ~ cast tx 0x3574c7c9b34df46d7476c5a8e9fb48b2bf007df7d5d021ef9aa79983f4b13f92 --rpc-url=$ETH_RPC_URL input
0xa9059cbb0000000000000000000000007f1949e62203a83ad6e6be0a819f93e580054f9d000000000000000000000000000000000000000000038e8f7792d79767800000
(base) ➜ ~ cast pretty-calldata 0xa9059cbb0000000000000000000000007f1949e62203a83ad6e6be0a819f93e580054f9d000000000000000000000000000000000000000000038e8f7792d79767800000
Possible methods:
- transfer(address,uint256)
------------
可以通过 cast 4byte <十六进制数据> 在查询函数 selector 对应的函数原型。
# 查询0xa9059cbb selector对应的函数原型
(base) ➜ ~ cast 4byte 0xa9059cbb
transfer(address,uint256)
# 使用keccak计算函数原型对应的hash,可以发现hash的前4个字节就是selector
(base) ➜ ~ cast keccak "transfer(address,uint256)"
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
(base) ➜ ~ cast sig "transfer(address,uint256)"
0xa9059cbb
查询 topic 日志对应的函数原型-cast 4byte-event
(base) ➜ ~ cast 4byte-event 0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
Deposit(address,uint256)
cast run 命令
以****defi 直接价格操纵经典案例-tcrToken 被黑事件中的****交易为例,可参考https://learnblockchain.cn/article/4491[4]
对应的交易为0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154[5]
(base) ➜ ~ cast run 0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154
使用帮助
(base) ➜ ~ cast wallet -h
cast-wallet
Wallet management utilities.
USAGE:
cast wallet <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
SUBCOMMANDS:
address Convert a private key to an address. [aliases: a, addr]
help Print this message or the help of the given subcommand(s)
new Create a new random keypair. [aliases: n]
sign Sign a message. [aliases: s]
vanity Generate a vanity address. [aliases: va]
verify Verify the signature of a message. [aliases: v]
创建钱包
通过 cast wallet new 创建新的钱包
(base) ➜ ~ cast wallet new
Successfully created new keypair.
Address: 0x382B0Db462165Bc1b78B355eBB747E2F378bC711
直接跟目录名,将钱包保存到 keystore 目录中
(base) ➜ cast_basic cast wallet new keystore
Insert secret:
Created new encrypted keystore file: `/Users/mamaogang/Nextcloud/code/eth_test/foundry/cast_basic/keystore/8c0cb584-95aa-4f63-924d-d8c5ab92f1bf`\nPublic Address of the key: 0xb18A7BC0c376CB3be07CCC883900b61d8e33ce8B
签名-cast wallet sign
ENS 功能-cast resolve-name 和 cast lookup-address
(base) ➜ cast_basic cast resolve-name vatalik.eth
0x7d66bD3dA15e079495989dc8139379784146afeD
(base) ➜ cast_basic cast lookup-address 0x7d66bD3dA15e079495989dc8139379784146afeD
Error:
ens name not found: 7d66bd3da15e079495989dc8139379784146afed.addr.reverse
在使用查看源代码功能之前,需要设置 ETHERSCAN_API_KEY 的环境变量
export ETHERSCAN_API_KEY=NZMQ7KC5CD5BND19KMBQFA3BI3QJUTG53V
WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2et
export WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
查看源代码-cast etherscan-source
cast etherscan-source
WETH 的源代码。
使用-d 参数,将结果保存到指定目录下。
(base) ➜ cast_basic cast etherscan-source $WETH -d weth_source
(base) ➜ cast_basic vi weth_source/WETH9/WETH9.sol
调用合约函数-cast call
cast call $WETH "balanceOf(address)" 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e
(base) ➜ cast_basic cast --to-dec 0x00000000000000000000000000000000000000000000bdb51a04b5aa8eb6431e
895868000762793410577182
cast index 根据 KEY_TYPE 的类型和 KEY,及 SLOT_NUMBER 计算出存储位置
。
帮助说明
问题:计算0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e
账户在 $WETH token 中的余额,可以使用两种方式取得。
常规函数调用方式
采用合约函数调用的方式,可以看到该账户下有 bdb51a04b5aa8eb6431e 个 WETH.
(base) ➜ cast_basic cast call $WETH "balanceOf(address)" 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e
0x00000000000000000000000000000000000000000000bdb51a04b5aa8eb6431e
读取合约 slot 存储方式
先根据 WETH 的源代码,分析得到 balanceOf 状态变量位于第 3 个 slot,如何获得源代码?可以通过cast etherscan-source $WETH -d 目录
命令来获得。源代码如下:
pragma solidity ^0.4.18;
contract WETH9 {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
function() public payable {
deposit();
}
通过 slot 来读取
# 先计算出KEY_TYPE为address,KEY为0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e,slot为3,所对应的存储位置。
(base) ➜ cast_basic cast index address 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e 3
0x1f8193c3f94e8840dc3a6dfc0bc012432d338ef33c4f3e4b3aca0d6d3c5a09b6
# 取出对应存储位置的原始数据,因为为address=>int,所以取出来就没int
(base) ➜ cast_basic cast storage $WETH 0x1f8193c3f94e8840dc3a6dfc0bc012432d338ef33c4f3e4b3aca0d6d3c5a09b6
0x00000000000000000000000000000000000000000000bdb51a04b5aa8eb6431e
查询合约的存储 slot 的原始数据-cast storage
查询合约的存储 slot 中的原始数据。
帮助文档
从 abi 生成 interface-cast interface <abi 文件或者合约地址>
使用帮助
以 WBNB 为例
在https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c#code[6]中复制 abi 并保存到 wbnb.abi 文件中,使用下列命令生成接口。
cast interface wbnb.abi
也可以直接跟某个地址
(base) ➜ cast_basic echo $WETH
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
cast interface $WETH
编码解码-cast —to-xxx 系统函数
cast --to-hex
cast --to-dec
cast --to-wei
cast --to-uint 如cast --to-uint 100000 ether 将10000转成ether的单位。
cast --to-bytes32
cast --to-ascii
cast --from-wei
cast --format-bytes32-string
直接运行效果
模拟从主网 fork-casat —fork-url=$ETH_RPC_URL
使用fork-casat —fork-url=$ETH_RPC_URL可以模拟主网
anvil 常用的命令参数
—accounts=账户的数量
—balance=每个账户的余额
—fork-block-number=区块高度
特殊的 RPC 方法-anvil*等同于 hardhat***
anvil_impersonateAccount
anvil_setStorageAt
forge init <dir_name>
forge init —template <template_path> <dir_name>
看下当前目录的结构
(base) ➜ forge_basic tree -L 2
.
└── hello-foundry
├── foundry.toml
├── lib
├── script
├── src
└── test
配置设置
# 打印所有的配置
forge config
# 打印基础的配置
forge config --basic
# 生成新的基础配置
forge config > foundry.toml
对应的编译命令为
forge build
forge build -w 实时写代码,实时编译
😀 通常会在tmux中开两个pane。第一个pane用于查看实时编码情况,使用-w实时监控; 第二个pane中编写代码,每次修改完代码后,保存后,第一个panel就会实时显示编译是否通过。
# 可以使用使用-v级别、-vv级别、-vvv级别进行日志的打印
forge test -v /-vv / -vvv
# 使用-w进行监视模式
forge test -v /-vv / -vvv -w 使用监视模式
测试分类
有个牛逼的功能。标准库里有个 vm 实例,可以通过 vm 改变虚拟机的状态。
日志打印通常有两种方法:
console2.log(”hello world”)
。注意,使用打日志的方法的方法时,如果使用forge test
无法展示打印的日志,记得要—vvv
以上才能打印出来,一个 v 时显示不出来
emit log(”hello world”);
使用console2.log(”hello world”);
也是同样的效果。
cheatcode,可以在 test 合约中使用 vm 变量修改 vm 的状态。
block.timestamp
变量block.number
变量下一次
调用的 msg.sender,只改变下一次调用,其他的调用会恢复回来。如果后面的调用也一直保持修改,使用vm.startPrank(alice); vm.stopPrank();
vm.warp(1641070800);
emit log_uint(block.timestamp);
vm.startPrank(alice);
vm.stopPrank();
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
address public alice;
Helper public h;
function setUp() public {
counter = new Counter();
alice = address(1);
h = new Helper();
counter.setNumber(0);
}
function testVm() public {
console2.log("before cheatcode:", h.whoCalled());
vm.startPrank(alice);
console2.log("after cheatcode:", h.whoCalled());
vm.stopPrank();
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumberOne() public {
counter.setNumber(1);
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
contract Helper {
function whoCalled() public view returns(address) {
return msg.sender;
}
}
vm.deal 修改 balance 示例
vm.deal(alice, 1 ether) //改变 alice 地址的原生代币的余额为 1 ether
vm.rollFork() 到指定的区块高度。
function testVmFork() public {
string memory MAINNET_RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/*******l";
uint256 forkId = vm.createFork(MAINNET_RPC_URL);
vm.selectFork(forkId);
console2.log("cur blocknum:", block.number);
vm.rollFork(15531500);
console2.log("after blocknum:", block.number);
}
vm.ffi 调用外部命令
使用 vm.ffi 时,在启动 forge test 时,需要添加 —ffi 参数。如
forge test -vvv -w --fork-url=$ETH_RPC_URL --ffi
测试代码
function testffi() public {
// 使用keccak256函数计算出hash1
string memory aMessage = "abc";
bytes32 hash1 = keccak256(abi.encodePacked(aMessage));
console2.logBytes32(hash1);
// 使用vm.ffi计算出hash2
string[] memory cmds = new string[](3 "] memory cmds = new string[");
cmds[0] = "cast";
cmds[1] = "keccak";
cmds[2] = aMessage;
bytes memory ffiResult = vm.ffi(cmds);
bytes32 hash2 = abi.decode(ffiResult, (bytes32));
console2.logBytes32(hash2);
// 比较hash1和hash2是相同的。
assertEq(hash1, hash2);
}
为每个测试用例的 gas 使用创建快照。主要用于在开发过程中对 gas 费的优化。
常与forge snapshot —diff
一起使用,-diff 参数会与上次的快照对比 gas 费的对比。
在 5.5 中,可以通过 vm.deal 来修改原生代币的余额,那么在编写测试用例时,怎样才能修改 ERC20 代币的余额呢?可以一起通过编写一个 ERC20 的代币,并使用 foundry 来修改 ERC20 代币的余额的测试用例。
yarn 安装@openzeppelin/contracts
yarn add @openzeppelin/contracts
配置 config,foundry.toml 文件,将 lib 中加入node_modules
libs = ['lib','node_modules']
使用 forge remappings 查看当前的 remappings
forge remappings >remappings.txt
将当前 remappings 保存到 remappings.txt 文件中,
@openzeppelin/=node_modules/@openzeppelin/
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src
🤣 如果foundry.toml文件中的libs=[’lib’] 没有包含node_modules的话,使用forge remappings 产生的remappings.txt就不会包含@openzeppelin这一行了。
使用标准的 cheatcode 函数 deal
deal(address(dai), alice, 10000e18);
assertEq(dai.balanceOf(alice), 10000e18);
完整的演示代码
contract CounterTest is Test {
Counter public counter;
address public alice;
Helper public h;
IERC20 public dai;
function setUp() public {
counter = new Counter();
alice = address(1);
h = new Helper();
counter.setNumber(0);
dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
}
function testDaiDeal() public {
console2.log("before deal, Alice Dai balance is",2);
deal(address(dai), alice, 1001 ether);
console2.log("after deal, Alice Dai balance is", alice.balance);
}
如果使用 forge test -vvv -w 时,可以看到测试不会通过,测试会失败,出错内容为"EvmError: Revert”,如下所示
出错的原因是,因为dai合约没有在测试环境中部署
。如果不想部署 dai 合约,我们可以通过fork-url
的方式直接使用主网的 dai 合约。
使用主网的 dai 合约测试的话,使用forge test -vvv -w -fork-url=$ETH_RPC_URL
,fork 主网到本地进行测试。使用该命令就可以测试成功
。
上面 fork-url 时,是直接通过 forge 调用的参数传递进去的,有没有办法在代码直接进行 fork-url?
如果在代码中可以实现 fork-url 的话,我们就可以直接在代码针对不同的测试网络编写不同的测试用例,在测试用例中就可以覆盖全网络。
通过 vm.envAddress 函数可以从 vm 中读取环境变量
vm.envAddress(string calldata, string calldata) 取得 vm 中的地址。
在代码中进行 fork 的主要代码
string memory rpc = vm.envString("ETH_RPC_URL");
uint256 mainnet = vm.createFork(rpc);
vm.selectFork(mainnet);
IERC20 public dai;
function setUp() public {
counter = new Counter();
alice = address(1);
h = new Helper();
counter.setNumber(0);
// dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
dai = IERC20(vm.envAddress("DAI"));
console2.log("DAI address:", address(dai));
}
function testDaiDeal() public {
string memory rpc = vm.envString("ETH_RPC_URL");
uint256 mainnet = vm.createFork(rpc);
vm.selectFork(mainnet);
console2.log("before deal, Alice Dai balance is", alice.balance);
deal(address(dai), alice, 1001 ether);
console2.log("after deal, Alice Dai balance is", alice.balance);
}
https://book.getfoundry.sh/
https://www.youtube.com/watch?v=EXYeltwvftw&t=6s
https://sig.eth.samczsun.com/
[1]
小驹: https://learnblockchain.cn/people/9625
[2]
getfoundry.sh: http://getfoundry.sh
[3]
https://sig.eth.samczsun.com/: https://sig.eth.samczsun.com/
[4]
https://learnblockchain.cn/article/4491: https://learnblockchain.cn/article/4491
[5]
0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154: https://cn.etherscan.com/tx/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154
[6]
https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c#code: https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c#code