Foundry的基本使用总结

  • 小驹
  • 更新于 2022-09-14 16:18
  • 阅读 6231

本文列举了foundry中常用的命令,方便以后查询使用。

Untitled.png 本文列举了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使用

<aside> 😀 我的 ETH alchemy的RPC接点 https://eth-mainnet.g.alchemy.com/v2/*****

export ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/**

</aside>

cast 是 Foundry 用于执行以太坊 RPC 调用的命令行工具。您可以进行智能合约调用发送交易或检索任何类型的链数据

cast与web3交互的小工具,即使不是代码开发的人员也会经常使用该工具与链上数据进行查询等交互。

cast rpc eth_blockNumber --rpc-url=$ETH_RPC_URL

<aside> 😀 cast支持环境变量ETH_RPC_URL,将RPC节点设置到环境变量ETH_RPC_URL中。 对带有--rpc-url的参数的cast命令中,可直接从环境变量中直接读取,不需要在命令中体现。

</aside>

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

Untitled 1.png

查询区块信息-cast block <blockNumer>


(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。

  1. 下载https://github.com/stedolan/jq/releases
  2. 移动到 /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 <topic>

Untitled 2.png


(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 &lt;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

Untitled 4.png

签名-cast wallet sign

Untitled 5.png 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

Untitled 6.png

3.4 合约相关功能

在使用查看源代码功能之前,需要设置ETHERSCAN_API_KEY的环境变量

export ETHERSCAN_API_KEY=NZMQ7KC5CD5BND19KMBQFA3BI3QJUTG53V

WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2et

export WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

查看源代码-cast etherscan-source

cast etherscan-source $WETH 查看$WETH的源代码。

使用-d参数,将结果保存到指定目录下。

(base) ➜  cast_basic cast etherscan-source $WETH -d weth_source
(base) ➜  cast_basic vi weth_source/WETH9/WETH9.sol

Untitled 7.png

调用合约函数-cast call

cast call $WETH "balanceOf(address)" 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e

(base) ➜  cast_basic cast --to-dec 0x00000000000000000000000000000000000000000000bdb51a04b5aa8eb6431e

895868000762793410577182

Untitled 8.png

查询合约的slot的存储位置-cast index

cast index 根据KEY_TYPE的类型和KEY,及SLOT_NUMBER计算出存储位置

帮助说明

Untitled 9.png

问题:计算0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e账户在$WETH token中的余额,可以使用两种方式取得。

  • 常规函数调用方式
  • 读取合约slot存储方式

    1. 常规函数调用方式

    采用合约函数调用的方式,可以看到该账户下有bdb51a04b5aa8eb6431e 个WETH.

    (base) ➜  cast_basic cast call $WETH  "balanceOf(address)" 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e
    0x00000000000000000000000000000000000000000000bdb51a04b5aa8eb6431e
  1. 读取合约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中的原始数据。

帮助文档

Untitled 10.png

从abi生成interface-cast interface <abi文件或者合约地址>

使用帮助

Untitled 11.png

以WBNB为例

https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c#code中复制abi并保存到wbnb.abi文件中,使用下列命令生成接口。

cast interface wbnb.abi

Untitled 12.png

也可以直接跟某个地址

(base) ➜  cast_basic echo $WETH
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

cast interface $WETH

Untitled 13.png

编码解码-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使用

直接运行效果

Untitled 14.png

模拟从主网fork-casat —fork-url=$ETH_RPC_URL

使用fork-casat —fork-url=$ETH_RPC_URL可以模拟主网

Untitled 15.png

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>

Untitled 16.png

看下当前目录的结构

(base) ➜  forge_basic tree -L 2
.
└── hello-foundry
    ├── foundry.toml
    ├── lib
    ├── script
    ├── src
    └── test

配置设置

# 打印所有的配置
forge config 

# 打印基础的配置
forge config --basic

# 生成新的基础配置
forge config > foundry.toml

Untitled 17.png

5.2 编译-forge build

对应的编译命令为

forge build
forge build -w 实时写代码,实时编译

<aside> 😀 通常会在tmux中开两个pane。 第一个pane用于查看实时编码情况,使用-w实时监控; 第二个pane中编写代码,每次修改完代码后,保存后,第一个panel就会实时显示编译是否通过。

</aside>

5.3 自动化测试-forge test

# 可以使用使用-v级别、-vv级别、-vvv级别进行日志的打印
forge test -v /-vv / -vvv

# 使用-w进行监视模式
forge test -v /-vv / -vvv -w 使用监视模式

测试分类

  • 简单测试
  • fuzz
  • 不变量测试

有个牛逼的功能。标准库里有个vm实例,可以通过vm改变虚拟机的状态。

5.4 日志打印

日志打印通常有两种方法:

  1. console模块。如console2.log(”hello world”)
  2. emit log方法。如emit log(”hello world”);

注意,使用打日志的方法的方法时,如果使用forge test无法展示打印的日志,记得要—vvv以上才能打印出来,一个v时显示不出来

emit log(”hello world”);

使用console2.log(”hello world”);也是同样的效果。

Untitled 18.png

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);

Untitled 19.png

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;
    }
}

Untitled 20.png

vm.deal修改balance示例

vm.deal(alice, 1 ether) //改变alice地址的原生代币的余额为1 ether

Untitled 21.png

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);
    }

Untitled 22.png

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);
        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);
    }

Untitled 23.png

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

<aside> 🤣 如果foundry.toml文件中的libs=[’lib’] 没有包含node_modules的话,使用forge remappings 产生的remappings.txt就不会包含@openzeppelin这一行了。

</aside>

使用标准的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”,如下所示

Untitled 25.png

出错的原因是,因为dai合约没有在测试环境中部署。如果不想部署dai合约,我们可以通过fork-url的方式直接使用主网的 dai合约。

使用主网的dai合约测试的话,使用forge test -vvv -w -fork-url=$ETH_RPC_URL ,fork主网到本地进行测试。使用该命令就可以测试成功

Untitled 26.png

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);
}

参考

https://book.getfoundry.sh/ https://www.youtube.com/watch?v=EXYeltwvftw&t=6s https://sig.eth.samczsun.com/

Foundry 框架合约开发测试 测试对 Foundry 开发框架的了解,主要涉及的内容有编译、测试、部署、以及如何进行代码开源验证。
开始挑战
点赞 9
收藏 15
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
小驹
小驹
区块链安全分析,欢迎私信沟通交流