crypto
Foundry的基本使用总结
本文列举了foundry中常用的命令,方便以后查询使用。
一. 为什么要用foundry
- 全面支持solidity,可有效减少上下文切换
- 与hardhat+ethers组合工具相比,hardhat+ethers合约使用solidity,而部署测试等使用 js或者ts。而对于foundry工具,合约、部署、测试等都使用solidity,不需要在多种编程语言之间进行切换。
- 功能更齐全。如cast命令可以直接从etherscan下载源代码,可以直接从abi 生成interface等功能。
- 运行速度更快。
二. 软件安装方法
官方网站:getfoundry.sh
在mac环境下,使用下面命令进行安装
curl -L https://foundry.paradigm.xyz | bash source ~/.zshrc # 每次执行foundryup时,都会下载最新的cast,anvil,forge程序 foundryup
foundry系列的工具,主要包含三大组件,分别对应不同的功能,下面会每个组件依次试用。
- forge:主要用来开发、编译、部署合约。
- cast:执行以太坊 RPC 调用的命令行工具
- anvil:本地模拟节点环境,类似于ganache-cli的功能。
三. cast使用
😀 我的 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命令中,可直接从环境变量中直接读取,不需要在命令中体现。
3.1 查询功能
查询区块高度-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。
- 下载https://github.com/stedolan/jq/releases
- 移动到 /usr/local/bin,并命名成jq,设置成可执行属性。
查询calldata数据-cast pretty-calldata <十六进制数据>
pretty-calldata 命令会取出 <十六进制数据>中的前4个字节,从在线网站的数据库(https://sig.eth.samczsun.com/)中比对4字节的selector对应的函数原型,并将 <十六进制数据>中的后面部分的数据按照函数原型进行格式化输出。
查看input数据 (base) ➜ ~ cast tx 0x3574c7c9b34df46d7476c5a8e9fb48b2bf007df7d5d021ef9aa79983f4b13f92 --rpc-url=$ETH_RPC_URL input 0xa9059cbb0000000000000000000000007f1949e62203a83ad6e6be0a819f93e580054f9d000000000000000000000000000000000000000000038e8f7792d79767800000 (base) ➜ ~ cast pretty-calldata 0xa9059cbb0000000000000000000000007f1949e62203a83ad6e6be0a819f93e580054f9d000000000000000000000000000000000000000000038e8f7792d79767800000 Possible methods: - transfer(address,uint256) ------------ [0]: 0000000000000000000000007f1949e62203a83ad6e6be0a819f93e580054f9d [1]: 000000000000000000000000000000000000000000038e8f7792d79767800000
可以通过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)
3.2 交易模拟-cast run
cast run命令
以defi直接价格操纵经典案例-tcrToken被黑事件中的交易为例,可参考https://learnblockchain.cn/article/4491
对应的交易为0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154
(base) ➜ ~ cast run 0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154
3.3 钱包相关功能-cast wallet
使用帮助
(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
3.4 合约相关功能
在使用查看源代码功能之前,需要设置ETHERSCAN_API_KEY的环境变量
export ETHERSCAN_API_KEY=NZMQ7KC5CD5BND19KMBQFA3BI3QJUTG53V
WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2et
export WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
查看源代码-cast etherscan-source
cast etherscan-source WETH 查看
WETH查看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
查询合约的slot的存储位置-cast index
cast index 根据KEY_TYPE的类型和KEY,及SLOT_NUMBER计算出存储位置
。
帮助说明
问题:计算0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e
账户在$WETH token中的余额,可以使用两种方式取得。
- 常规函数调用方式
- 读取合约slot存储方式
- 常规函数调用方式
- 采用合约函数调用的方式,可以看到该账户下有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中复制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
四. anvil使用
直接运行效果
模拟从主网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-智能合约开发框架
5.1 初始化项目-forge init
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
5.2 编译-forge build
对应的编译命令为
forge build forge build -w 实时写代码,实时编译
😀 通常会在tmux中开两个pane。 第一个pane用于查看实时编码情况,使用-w实时监控; 第二个pane中编写代码,每次修改完代码后,保存后,第一个panel就会实时显示编译是否通过。
5.3 自动化测试-forge test
# 可以使用使用-v级别、-vv级别、-vvv级别进行日志的打印 forge test -v /-vv / -vvv # 使用-w进行监视模式 forge test -v /-vv / -vvv -w 使用监视模式
测试分类
- 简单测试
- fuzz
- 不变量测试
有个牛逼的功能。标准库里有个vm实例,可以通过vm改变虚拟机的状态。
5.4 日志打印
日志打印通常有两种方法:
- console模块。如
console2.log(”hello world”)
。 - emit log方法。如emit log(”hello world”);
注意,使用打日志的方法的方法时,如果使用forge test
无法展示打印的日志,记得要—vvv
以上才能打印出来,一个v时显示不出来
emit log(”hello world”);
使用console2.log(”hello world”);
也是同样的效果。
5.5 cheatcode修改vm状态
cheatcode,可以在test合约中使用vm变量修改vm的状态。
- vm.warp() 修改vm中的
block.timestamp
变量 - vm.roll() 修改vm中的
block.number
变量 - vm.prank(address) 改变
下一次
调用的msg.sender,只改变下一次调用,其他的调用会恢复回来。 - 如果后面的调用也一直保持修改,使用
vm.startPrank(alice); vm.stopPrank();
- deal(address who, uint256 newBalance) 改变who地址的余额。
vm.warp-修改timestamp示例
vm.warp(1641070800); emit log_uint(block.timestamp);
vm.startPrank-修改msg.sender示例
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函数计算出hash1string memory aMessage = "abc"; bytes32 hash1 = keccak256(abi.encodePacked(aMessage)); console2.logBytes32(hash1); // 使用vm.ffi计算出hash2string[] memory cmds = new string[](3); 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); }
5.6 forge snapshot-快照功能
为每个测试用例的gas使用创建快照。主要用于在开发过程中对gas费的优化。
常与forge snapshot —diff
一起使用,-diff 参数会与上次的快照对比gas费的对比。
六.代码示例
6.1 如何修改ERC20代币的余额呢?
在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主网到本地进行测试。使用该命令就可以测试成功
。
6.2. 如何在代码中进行fork-url
上面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); }
参考
喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。
发布评论…