本文作者:Tiny 熊[1]
Foundry 是一个全新的 EVM 开发环境。有了 Solidity-native 测试能力(使用原生的 Solidity 编写测试),强大的命令行工具和高性能的 Rust 工具,Foundry 更值得大家学习,翻译一篇 Foundry 的使用指南文章。
安装
官方安装指南可以在这里[2]找到。不过我自己在用foundryup
获得anvil
时遇到了麻烦,所以如果你想在 bash/zsh shell 上从源码构建,请使用以下方法。注意你需要先安装有git
和cargo
才能进行源代码构建。
git clone https://github.com/foundry-rs/foundry &&
cd foundry &&
cargo install --path ./cli --bins --locked --force &&
cargo install --path ./anvil --locked --force
如果你选择了从源码构建,那么就需要花费给你一些精力,编译器有很多事情要做。
Foundry 包含的组件
Foundry 由三个不同的命令行工具(CLI)组成,包括forge
,cast
,和anvil
。首先,我们先了解下这些工具,然后用他们来构建和测试一个智能合约。
Cast
Cast 是一个 CLI 工具,用于对兼容以太坊虚拟机(EVM)的区块链进行 RPC 调用。使用cast
,我们可以进行合约调用,查询数据,并处理编码和解码。cast
有很多的子命令,所以要想获得完整的参考,请看 Foundry 书中的cast[3]一节!
要执行合约调用而不发布交易,我们可以使用call
子命令:
cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
"balanceOf(address)(uint256)"
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
--rpc-url https://rpc.flashbots.net
这对0xC0...c2
(WETH)地址执行balanceOf(address)
查询,传递0xf3...66
地址作为参数,并将返回数据解码为uint256
类型的值。在这个和下面的cast
子命令中,我们明确地使用了 Flashbots RPC,因为默认是http://localhost:8545
。
要查询一个地址的以太余额,我们可以使用balance
子命令:
cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
--rpc-url https://rpc.flashbots.net
我们也可以用 ENS 名称查询余额。
代码语言:javascript复制cast balance yourmom.eth --rpc-url https://rpc.flashbots.net
要提取合约上的合约状态槽内容,我们可以使用storage
子命令。
cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0
--rpc-url https://rpc.flashbots.net
这将从0xC0...c2
(WETH)地址中获得存储槽0
内容。
Forge
Forge 是一个 CLI 工具,用于构建、测试、模糊测试、部署和验证 Solidity 合约。Forge 同样有很多的子命令,所以这里是参考文档[4]! 顺便说一下,如果你是来学习教程的,不需要运行这些命令,所有东西都包含在后面 创建 Foundry 仓库
部分。
一个常用的子命令是 init
,它可以初始化一个新的版本库:
forge init my_gigabrain_protocol
install
子命令允许你安装指定版本的依赖项:
forge install Rari-Capital/solmate@v6
这将安装由Rari-Capital
拥有的solmate
软件库,特别是v6
。注意,@v6
是可选的。
为了运行测试,我们可以使用以下方法:
代码语言:javascript复制forge test
Anvil
Anvil 是一个 CLI 工具,用于运行本地 EVM 区块链。它可以与 ganache 和 hardhat 节点相媲美,但好像更快。
要启动 Anvil,只需使用 avil 命令:
代码语言:javascript复制anvil
你也可以指定一些参数, 用-v
来显示详细日志,如果用-fork-url <FORK_URL>
指定 URL 来分叉一个公共网络,等等。要查看 anvil
选项的完整列表,可以使用以下命令:
anvil -h
创建 Foundry 仓库
初始化
为了开始工作,如上所述,我们将使用以下命令:
代码语言:javascript复制forge init my_token && cd my_token
这将创建一个my_token
目录,初始化一个 git 仓库,添加一个 GitHub 工作流目录,安装forge-std
包,生成一个foundry.toml
文件,一个test
目录,一个src
目录,最后进入到my_token
目录。
让我们继续,通过运行下面的程序删除现有的合约:
代码语言:javascript复制rm src/Contract.sol
现在,首先让我们看看foundry.toml
文件,自动生成的文件应该是这样的:
[default]
src = 'src'
out = 'out'
libs = ['lib']
这将合约源代码目录设置为src
,编译器输出目录设置为out
,库的重新映射设置为lib
。
安装依赖
现在我们将安装一个依赖关系。我们可以使用<所有者>/<存储库>
模式来安装Rari-Capital
的solmate
。
forge install Rari-Capital/solmate
这样就把solmate
安装到了lib
目录。现在让我们在src/MyToken.sol
中创建一个 ERC20 合约。
我们可以导入并使用solmate
的 ERC20 实现,如下:
// SPDX-License-Identifier: AGPLv3
pragma solidity ^0.8.13;
import {ERC20} from "solmate/tokens/ERC20.sol";
error NotMinter();
contract MyToken is ERC20 {
address public immutable minter;
// ERC20(name, symbol, decimals)
contructor() ERC20("MyToken", "MyT", 18) {
minter == msg.sender;
}
function mint(uint256 amount) external {
if (msg.sender != minter) revert NotMinter();
_mint(minter, amount);
}
}
所以这是一个符合 ERC20 标准的代币,有一个简单的访问控制功能,用于铸币控制。
注意solmate/tokens/ERC20.sol
导入是根据foundry.toml
文件中指定的libs/
目录重新映射的。
如果你使用 VSCode 并得到错误信息,可以尝试在项目根目录下创建一个remappings.txt
文件并添加以下内容:
solmate/=lib/solmate/src/
forge-std/=lib/forge-std/src
编译
让我们继续编译合约:
代码语言:javascript复制foundry build
测试
现在我们需要写一些测试,因为一个掌管着不可忽视的价值的智能合约实际上是一个非自愿的 bug 赏金。不要让自己成为一个教训。:)
为了写测试,在test
中创建一个文件。让我们创建一个名为test/MyToken.t.sol
的文件。.t.sol
表示这将是一个测试文件:
// SPDX-License-Identifier: AGPLv3
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {MyToken} from "../MyToken.sol";
contract MyTokenTest {
MyToken internal myToken;
address internal constant alice = address(1);
address internal constant bob = address(2);
function setUp() external {
vm.prank(alice);
myToken = new MyToken();
}
function testMint() external {
vm.prank(alice);
myToken.mint(1);
assertEq(myToken.balanceOf(alice), 1);
}
function testFailMint() external {
vm.prank(bob);
myToken.mint(1);
}
}
这里有一些代码需要解释:
从 forge-std
导入的 Test
默认是在 forge 创建 repo 时一起的生成的。它包括一个内部的vm
变量,这是一个 cheat-code
运行器。
vm
包括一系列强大的“作弊代码”,例如:时间戳操纵 、将字节码刻在地址上、存储槽读写等。要查看所有这些代码,请看参考这里的介绍[5]。在这个例子中,我们使用vm.prank(address)
,其中的address
参数将是下一个外部合约调用的msg.sender
。
要测试一个函数,在运行测试的函数的名称前加上test
。在本例中,我们正在测试mint
,所以我们可以叫它testMint
。如果它不是以test
开头,它将不会在forge test
上运行。assertEq
函数用来断言两个值是相等的。在Test
合约中还声明了其他断言,可以在这里[6]找到参考。
要测试还原(revert),在函数名前加上testFail
。在本例中,我们期望vm.prank(bob)
失败,因为在setUp
中,MyToken
的minter
被vm.prank(alice)
设置成了alice
。
现在我们来运行测试:
代码语言:javascript复制forge test
一切都应该通过。如果你需要调试函数调用,请在测试命令中加入 -vvvv
(verbosity 4)。
forge test -vvvv
如果你需要从测试内部记录变量,你可以在Test
中声明的事件。使用emit log_uint(uint)
记录数字,你可以用emit log_named_uint(string,uint)
来进行标记。要显示事件日志,在运行forge test
时使用-vv
(verbosity 2)。
本地部署
为了在本地部署合约,我们需要首先启动一个 anvil 实例。
代码语言:javascript复制anvil
当本地 devnet 开始运行时,这应该会打印出一些账户。让 anvil 运行实例单独使用一个终端窗口。
现在让我们从 anvil
的输出中获取第一个账户的私钥,并将其设置为$PRIV_KEY
环境变量。这不是必须的,它只是保持事情清晰。
export
PRIV_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
注意这些私钥是确定的,并且是公开的,所以在公共网络上投入资金之前要三思而后行。
现在验证私钥是否可以访问:
代码语言:javascript复制echo $PRIV_KEY
如果它打印出私钥,你就可以了。
现在把合约部署到本地 devnet 上:
代码语言:javascript复制forge create src/MyToken.sol:MyToken --private-key=$PRIV_KEY
这将加载到环境中的私钥,使用src/MyToken.sol
文件中MyToken
创建合约。请注意语法使用了明确指定的文件名称和合约名称:
forge create <filename>:<contractname> ...
在这种情况下,我们不需要构造器参数,但如果需要,则在最后传递--constructor-args
标志,并写出构造器参数,用空格隔开:
forge create Filename.sol:Contractname
--private-key $PRIV
--constructor-args arg0 arg1 arg2
运行后,你应该在终端上打印出类似这样的东西。注意,你的合约地址和交易哈希值可能不同。
代码语言:javascript复制Deployer: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Deployed to: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Transaction hash: 0x3f849ddc766b4851fed8798a26aa6fe527c74cd9d6f2639ec289f5e11ceaa3b5
很好,现在我们可以使用 cast 来试用我们新创建的代币了。
同样,为了保持整洁,在终端环境中把你的合约地址导出为$CON_ADDRESS
。如果你的地址不同,只需在下面的命令中替换它。
export CON_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3
现在我们来查询 token 的.name()
。
cast call $CON_ADDRESS "name():(string)"
注意我们在函数签名中包括:(string)
。这是为了帮助cast
解码返回的数据,否则我们会得到一个巨大的十六进制字符串。
现在让我们使用部署该函数的同一私钥来调用 mint 函数。如果你使用任何其他的私钥则会失败,因为这是mint
函数中的逻辑。
cast send --private-key $PRIV_KEY $CON_ADDRESS "mint(uint256)" 1
这会给我们的账户铸造 1 个代币,你也会注意到交易数据已经打印到屏幕上。真不错。
作为本地部署的最后验证,检查你账户的余额。如果你使用的私钥不是由 anvil 提供的,你可以随时使用以下方法:
代码语言:javascript复制cast wallet address --private-key $PRIV_KEY
再一次,为了方便,只需将钱包地址添加到环境中:
代码语言:javascript复制export WALLET=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
现在进行余额查询。
代码语言:javascript复制cast call $CON_ADDRESS "balanceOf(address):uint256" $WALLET
这应该返回 1。
公共网络部署
部署在公共网络的工作原理与上述相同,当然是使用你自己的私钥、RPC 端点、链 ID。
合约代码验证
要用 Etherscan 验证你公开部署的合约,请使用以下命令:
代码语言:javascript复制forge verify-contract
--chain $CHAIN_ID
--compiler-version $COMPILER_VERSION
$CON_ADDRESS src/MyToken.sol:MyToken $ETHERSCAN_API_KEY
总结
Foundry 很强大,而且我们还只是从表面上看到了代工厂的兔子洞有多深。我希望这篇文章对你的编程和智能合约之旅有所帮助,并一如既往地做好黑客工作