使用 Foundry 开发环境

2022-11-07 10:00:47 浏览数 (1)

本文作者:Tiny 熊[1]

Foundry 是一个全新的 EVM 开发环境。有了 Solidity-native 测试能力(使用原生的 Solidity 编写测试),强大的命令行工具和高性能的 Rust 工具,Foundry 更值得大家学习,翻译一篇 Foundry 的使用指南文章。

安装

官方安装指南可以在这里[2]找到。不过我自己在用foundryup获得anvil时遇到了麻烦,所以如果你想在 bash/zsh shell 上从源码构建,请使用以下方法。注意你需要先安装有gitcargo才能进行源代码构建。

代码语言:javascript复制
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)组成,包括forgecast,和anvil。首先,我们先了解下这些工具,然后用他们来构建和测试一个智能合约。

Cast

Cast 是一个 CLI 工具,用于对兼容以太坊虚拟机(EVM)的区块链进行 RPC 调用。使用cast,我们可以进行合约调用,查询数据,并处理编码和解码。cast有很多的子命令,所以要想获得完整的参考,请看 Foundry 书中的cast[3]一节!

要执行合约调用而不发布交易,我们可以使用call子命令:

代码语言:javascript复制
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子命令:

代码语言:javascript复制
cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 
    --rpc-url https://rpc.flashbots.net

我们也可以用 ENS 名称查询余额。

代码语言:javascript复制
cast balance yourmom.eth --rpc-url https://rpc.flashbots.net

要提取合约上的合约状态槽内容,我们可以使用storage子命令。

代码语言:javascript复制
cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0 
    --rpc-url https://rpc.flashbots.net

这将从0xC0...c2(WETH)地址中获得存储槽0内容。

Forge

Forge 是一个 CLI 工具,用于构建、测试、模糊测试、部署和验证 Solidity 合约。Forge 同样有很多的子命令,所以这里是参考文档[4]! 顺便说一下,如果你是来学习教程的,不需要运行这些命令,所有东西都包含在后面 创建 Foundry 仓库部分。

一个常用的子命令是 init,它可以初始化一个新的版本库:

代码语言:javascript复制
forge init my_gigabrain_protocol

install子命令允许你安装指定版本的依赖项:

代码语言:javascript复制
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选项的完整列表,可以使用以下命令:

代码语言:javascript复制
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文件,自动生成的文件应该是这样的:

代码语言:javascript复制
[default]
src = 'src'
out = 'out'
libs = ['lib']

这将合约源代码目录设置为src,编译器输出目录设置为out,库的重新映射设置为lib

安装依赖

现在我们将安装一个依赖关系。我们可以使用<所有者>/<存储库>模式来安装Rari-Capitalsolmate

代码语言:javascript复制
forge install Rari-Capital/solmate

这样就把solmate安装到了lib目录。现在让我们在src/MyToken.sol中创建一个 ERC20 合约。

我们可以导入并使用solmate的 ERC20 实现,如下:

代码语言:javascript复制
// 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文件并添加以下内容:

代码语言:javascript复制
solmate/=lib/solmate/src/
forge-std/=lib/forge-std/src

编译

让我们继续编译合约:

代码语言:javascript复制
foundry build

测试

现在我们需要写一些测试,因为一个掌管着不可忽视的价值的智能合约实际上是一个非自愿的 bug 赏金。不要让自己成为一个教训。:)

为了写测试,在test中创建一个文件。让我们创建一个名为test/MyToken.t.sol的文件。.t.sol表示这将是一个测试文件:

代码语言:javascript复制
// 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中,MyTokenmintervm.prank(alice)设置成了alice

现在我们来运行测试:

代码语言:javascript复制
forge test

一切都应该通过。如果你需要调试函数调用,请在测试命令中加入 -vvvv (verbosity 4)。

代码语言:javascript复制
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环境变量。这不是必须的,它只是保持事情清晰。

代码语言:javascript复制
export 
PRIV_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

注意这些私钥是确定的,并且是公开的,所以在公共网络上投入资金之前要三思而后行。

现在验证私钥是否可以访问:

代码语言:javascript复制
echo $PRIV_KEY

如果它打印出私钥,你就可以了。

现在把合约部署到本地 devnet 上:

代码语言:javascript复制
forge create src/MyToken.sol:MyToken --private-key=$PRIV_KEY

这将加载到环境中的私钥,使用src/MyToken.sol文件中MyToken创建合约。请注意语法使用了明确指定的文件名称和合约名称:

代码语言:javascript复制
forge create <filename>:<contractname> ...

在这种情况下,我们不需要构造器参数,但如果需要,则在最后传递--constructor-args标志,并写出构造器参数,用空格隔开:

代码语言:javascript复制
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。如果你的地址不同,只需在下面的命令中替换它。

代码语言:javascript复制
export CON_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3

现在我们来查询 token 的.name()

代码语言:javascript复制
cast call $CON_ADDRESS "name():(string)"

注意我们在函数签名中包括:(string)。这是为了帮助cast解码返回的数据,否则我们会得到一个巨大的十六进制字符串。

现在让我们使用部署该函数的同一私钥来调用 mint 函数。如果你使用任何其他的私钥则会失败,因为这是mint函数中的逻辑。

代码语言:javascript复制
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 很强大,而且我们还只是从表面上看到了代工厂的兔子洞有多深。我希望这篇文章对你的编程和智能合约之旅有所帮助,并一如既往地做好黑客工作

0 人点赞