Foundry搭建和使用

Foundry介绍

image-20240325133227628

Foundry 是一个智能合约开发工具链。

Foundry 管理您的依赖关系、编译项目、运行测试、部署,并允许您通过命令行和 Solidity 脚本与链交互。

开始使用 Foundry

要开始使用 Foundry,请安装 Foundry 并设置第一个项目。

启动 Foundry 项目

本节将向您概述如何创建和使用现有项目。

Forge 概述

本节概述将为您提供有关如何使用“forge”来开发、测试和部署智能合约所需的所有知识。

Cast 概述

了解如何使用“cast”与智能合约交互、发送交易以及从命令行获取链上数据。

Anvil 概述

了解 anvil, Foundry 的本地节点。

Chisel 概述

了解如何使用 chisel - Foundry 集成的 Solidity REPL。

Foundry 配置

Foundry 配置指引:

教程

与 Foundry 建立智能合约的教程。

附录

参考手册, 疑难解答等等

Foundry安装

如果在安装过程中遇到任何问题,请查看 常见问题解答

预编译二进制文件

可以从 GitHub 发布页面获取预编译的二进制文件。 最好使用 Foundryup 来更好地管理这些文件。

使用 Foundryup

Foundryup 是 Foundry 工具链安装程序。您可以在 这里找到更多信息。

打开终端并运行以下命令:

1
curl -L https://foundry.paradigm.xyz | bash

这将安装 Foundryup,然后只需按照屏幕上的说明操作,即可在 CLI 中使用 foundryup 命令。

仅运行 foundryup 将安装最新的(夜间) 预编译二进制文件forgecastanvilchisel。 使用 foundryup --help 获取更多选项,例如从特定版本或提交安装。

从源代码构建

先决条件

您将需要 Rust 编译器和 Cargo,Rust 包管理器。 安装两者的最简单方法是使用 rustup.rs

Foundry 通常仅支持在最新的稳定 Rust 版本上构建。 如果您使用较旧的 Rust 版本,可以使用 rustup 进行更新:

1
rustup update stable

在 Windows 上,您还需要安装带有 “使用 C++进行桌面开发” 工作负载选项的最新版本的 Visual Studio

构建

您可以使用不同的 Foundryup 标志:

1
2
foundryup --branch master
foundryup --path path/to/foundry

或者,使用单个 Cargo 命令:

1
cargo install --git https://github.com/foundry-rs/foundry --profile local --locked forge cast chisel anvil

或者,通过手动从 Foundry 存储库的本地副本构建:

1
2
3
4
5
6
7
8
9
10
11
# clone the repository
git clone https://github.com/foundry-rs/foundry.git
cd foundry
# install Forge
cargo install --path ./crates/forge --profile local --force --locked
# install Cast
cargo install --path ./crates/cast --profile local --force --locked
# install Anvil
cargo install --path ./crates/anvil --profile local --force --locked
# install Chisel
cargo install --path ./crates/chisel --profile local --force --locked

使用 Docker 使用 Foundry

Foundry 也可以完全在 Docker 容器中使用。如果您没有安装 Docker,可以直接从 Docker 网站安装。

安装后,可以通过运行以下命令下载最新版本:

1
docker pull ghcr.io/foundry-rs/foundry:latest

也可以在本地构建 docker 镜像。从 Foundry 存储库运行:

1
docker build -t foundry .

对 Foundry 项目进行容器化

本教程向您展示如何使用 Foundry 的 Docker 镜像构建、测试和部署智能合约。 它改编自 solmate nft 教程中的代码。 如果您还没有完成该教程,并且是 solidity 的新手,您可能想先从它开始。 或者,如果您对 Docker 和 Solidity 有一定的了解,您可以使用自己现有的项目并进行相应的调整。 此处 提供了 NFT 和 Docker 内容的完整源代码。

本教程仅用于说明目的,并按原样提供。 本教程未经审核或全面测试。 不应在生产环境中使用本教程中的任何代码。

安装和设置

运行本教程所需的唯一安装是 Docker,以及您选择的 IDE(可选)。 按照 Docker 安装说明

为了使以后的命令简洁明了,让我们重新标记镜像(image): docker tag ghcr.io/foundry-rs/foundry:latest foundry:latest

在本地安装 Foundry 并不是严格要求的,但它可能有助于调试。 您可以使用 foundryup 安装它。

最后,要使用本教程的任何 castforge create 部分,您需要访问以太坊节点。 如果您没有自己的节点在运行(可能),您可以使用第三方节点服务。 我们不会在本教程中推荐特定的提供商。 开始学习节点即服务的好地方是 Ethereum 的文章 主题。

对于本教程的其余部分,假设您的以太坊节点的 RPC 端点设置如下export RPC_URL=<YOUR_RPC_URL>

Foundry docker 镜像导览

docker 镜像可以通过两种主要方式使用:

  1. 作为接口直接使用 forge 和 cast
  2. 作为构建您自己的容器化测试、构建和部署工具的基础镜像

我们将涵盖两者,但让我们先看看使用 docker 与 foundry 的接口。 这也是一个很好的测试,表明您的本地安装工作正常!

我们可以针对我们的 docker 镜像运行任何 cast 命令。 让我们获取最新的区块信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker run foundry "cast block --rpc-url $RPC_URL latest"
baseFeePerGas "0xb634241e3"
difficulty "0x2e482bdf51572b"
extraData "0x486976656f6e20686b"
gasLimit "0x1c9c380"
gasUsed "0x652993"
hash "0x181748772da2f968bcc91940c8523bb6218a7d57669ded06648c9a9fb6839db5"
logsBloom "0x406010046100001198c220108002b606400029444814008210820c04012804131847150080312500300051044208430002008029880029011520380060262400001c538d00440a885a02219d49624aa110000003094500022c003600a00258009610c410323580032000849a0408a81a0a060100022505202280c61880c80020e080244400440404520d210429a0000400010089410c8408162903609c920014028a94019088681018c909980701019201808040004100000080540610a9144d050020220c10a24c01c000002005400400022420140e18100400e10254926144c43a200cc008142080854088100128844003010020c344402386a8c011819408"
miner "0x1ad91ee08f21be3de0ba2ba6918e714da6b45836"
mixHash "0xb920857687476c1bcb21557c5f6196762a46038924c5f82dc66300347a1cfc01"
nonce "0x1ce6929033fbba90"
number "0xdd3309"
parentHash "0x39c6e1aa997d18a655c6317131589fd327ae814ef84e784f5eb1ab54b9941212"
receiptsRoot "0x4724f3b270dcc970f141e493d8dc46aeba6fffe57688210051580ac960fe0037"
sealFields []
sha3Uncles "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
size "0x1d6bb"
stateRoot "0x0d4b714990132cf0f21801e2931b78454b26aad706fc6dc16b64e04f0c14737a"
timestamp "0x6246259b"
totalDifficulty "0x9923da68627095fd2e7"
transactions [...]
uncles []

如果我们在一个包含一些 Solidity 源代码 的目录中,我们可以将该目录挂载到 docker 中,并根据需要使用 forge。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ docker run -v $PWD:/app foundry "forge test --root /app --watch"
No files changed, compilation skipped

Running 8 tests for test/OpenZeppelinNft.t.sol:OpenZeppelinNftTests
[PASS] testBalanceIncremented() (gas: 217829)
[PASS] testFailMaxSupplyReached() (gas: 134524)
[PASS] testFailMintToZeroAddress() (gas: 34577)
[PASS] testFailNoMintPricePaid() (gas: 5568)
[PASS] testFailUnSafeContractReceiver() (gas: 88276)
[PASS] testMintPricePaid() (gas: 81554)
[PASS] testNewMintOwnerRegistered() (gas: 190956)
[PASS] testSafeContractReceiver() (gas: 273132)
Test result: ok. 8 passed; 0 failed; finished in 2.68ms

Running 8 tests for test/SolmateNft.sol:SolmateNftTests
[PASS] testBalanceIncremented() (gas: 217400)
[PASS] testFailMaxSupplyReached() (gas: 134524)
[PASS] testFailMintToZeroAddress() (gas: 34521)
[PASS] testFailNoMintPricePaid() (gas: 5568)
[PASS] testFailUnSafeContractReceiver() (gas: 87807)
[PASS] testMintPricePaid() (gas: 81321)
[PASS] testNewMintOwnerRegistered() (gas: 190741)
[PASS] testSafeContractReceiver() (gas: 272636)
Test result: ok. 8 passed; 0 failed; finished in 2.79ms

您可以看到我们的代码完全在容器内编译和测试。 此外,由于我们传递了 --watch 选项,容器将在检测到更改时重新编译代码。

注意:Foundry docker 镜像基于 alpine 构建,并设计为尽可能轻量化。 因此,它目前不包括像 git 这样的开发资源。 如果您计划在容器内管理整个开发生命周期,您应该在 Foundry 的镜像之上构建自定义开发镜像。

创建一个 “构建和测试” 镜像

让我们使用 Foundry 的 docker 镜像作为使用我们自己的 Docker 镜像的基础。 我们将使用镜像来:

  1. 构建我们的 solidity 代码
  2. 运行我们的可靠性测试

一个简单的 Dockerfile 可以实现这两个目标:

1
2
3
4
5
6
7
8
9
10
# Use the latest foundry image
FROM ghcr.io/foundry-rs/foundry

# Copy our source code into the container
WORKDIR /app

# Build and test the source code
COPY . .
RUN forge build
RUN forge test

您可以构建此 docker 镜像并观察 forge 在容器内构建/运行测试:

1
$ docker build --no-cache --progress=plain 。

现在,如果我们的一个测试失败了怎么办? 随意修改 src/test/NFT.t.sol 使其中一个测试失败。 再次尝试构建镜像。

1
2
3
4
5
6
7
8
$ docker build --no-cache --progress=plain .
<...>
#9 0.522 Failed tests:
#9 0.522 [FAIL. Reason: Ownable: caller is not the owner] testWithdrawalFailsAsNotOwner() (gas: 193917)
#9 0.522
#9 0.522 Encountered a total of 1 failing tests, 9 tests succeeded
------
error: failed to solve: executor failed running [/bin/sh -c forge test]: exit code: 1

我们的镜像未能建立,因为我们的测试失败了! 这实际上是一个很好的属性,因为这意味着如果我们有一个成功构建的 Docker 镜像(因此可以使用),我们就知道镜像中的代码通过了测试。*

*当然,docker 镜像的监管链非常重要。 Docker 层哈希对于验证非常有用。 在生产环境中,考虑[签署你的 docker 镜像](https://docs.docker.com/engine/security/trust/#:%7E:text=To%20sign%20a%20Docker%20Image,the%20local%20Docker%20trust%20repository)。

创建部署镜像

现在,我们将继续讨论更高级的 Dockerfile。 让我们添加一个入口点,允许我们使用构建(和测试!)的镜像来部署我们的代码。 我们可以首先针对 Rinkeby 测试网。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Use the latest foundry image
FROM ghcr.io/foundry-rs/foundry

# Copy our source code into the container
WORKDIR /app

# Build and test the source code
COPY . .
RUN forge build
RUN forge test

# Set the entrypoint to the forge deployment command
ENTRYPOINT ["forge", "create"]

让我们构建镜像,这次给它命名:

1
$ docker build --no-cache --progress=plain -t nft-deployer .

以下是我们如何使用我们的 docker 镜像进行部署:

1
2
3
4
5
$ docker run nft-deployer --rpc-url $RPC_URL --constructor-args "ForgeNFT" "FNFT" "https://ethereum.org" --private-key $PRIVATE_KEY ./src/NFT.sol:NFT
No files changed, compilation skipped
Deployer: 0x496e09fcb240c33b8fda3b4b74d81697c03b6b3d
Deployed to: 0x23d465eaa80ad2e5cdb1a2345e4b54edd12560d3
Transaction hash: 0xf88c68c4a03a86b0e7ecb05cae8dea36f2896cd342a6af978cab11101c6224a9

我们刚刚在 docker 容器中完全构建、测试和部署了我们的合约! 本教程旨在用于测试网,但您可以针对主网运行完全相同的 Docker 镜像,并确信相同的代码正在由相同的工具部署。

为什么这有用?

Docker 是关于可移植性、可复制性和环境不变性的。 这意味着当您在环境、网络、开发人员等之间切换时,您可以减少对意外变化的关注。以下是一些基本示例,说明为什么喜欢使用 Docker 镜像进行智能合约部署:

  • 减少确保系统级依赖项在部署环境之间匹配的开销(例如,您的生产运行器是否始终具有与您的开发运行器相同版本的 forge?)
  • 增加代码在部署之前已经过测试并且没有被更改的信心(例如,如果在上面的镜像中,您的代码在部署时重新编译,这是一个主要的危险信号)。
  • 缓解职责分离的痛点:拥有主网凭证的人无需确保他们拥有最新的编译器、代码库等。很容易确保某人在测试网中运行的 docker 部署映像与针对主网的相同。
  • 冒着听起来像 web2 的风险,Docker 是几乎所有公共云提供商的公认标准。 它可以轻松安排需要与区块链交互的作业、任务等。

故障排除

如上所述,默认情况下,Foundry 镜像不包含 git。 这可能会导致某些命令在没有明确原因的情况下失败。 例如:

1
2
3
4
5
6
7
8
$ docker run foundry "forge init --no-git /test"
Initializing /test...
Installing ds-test in "/test/lib/ds-test", (url: https://github.com/dapphub/ds-test, tag: None)
Error:
0: No such file or directory (os error 2)

Location:
cli/src/cmd/forge/install.rs:107

在这种情况下,失败仍然是由于缺少 git 安装造成的。 建议的修复方法是构建现有的 Foundry 镜像并安装您需要的任何其他开发依赖项。

创建一个新项目

要使用 Foundry 启动一个新项目,请使用 forge init

1
$ forge init hello_foundry

这将从默认模板创建一个新目录 hello_foundry 。 这也会初始化一个新的 git 存储库。

如果你想使用不同的模板创建一个新项目,你可以传递 --template 指令,如下所示:

1
$ forge init --template https://github.com/foundry-rs/forge-template hello_template

现在,让我们检查一下默认模板的样子:

1
2
3
4
5
6
7
8
9
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test

4 directories

默认模板安装了一个依赖项:Forge 标准库。 这是用于 Foundry 项目的首选测试库。 此外,该模板还附带一个空的入门合约和一个简单的测试。

让我们构建项目:

1
2
3
4
$ forge build
Compiling 10 files with 0.8.16
Solc 0.8.16 finished in 3.97s
Compiler run successful

并运行测试:

1
2
3
4
5
6
7
$ forge test
No files changed, compilation skipped

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 28312)
[PASS] testSetNumber(uint256) (runs: 256, μ: 27376, ~: 28387)
Test result: ok. 2 passed; 0 failed; finished in 24.43ms

您会注意到产生了两个新目录:outcache

out 目录包含您的合约工件(artifact,例如 ABI,而 cache 目录被 forge 使用来(记录),以便仅仅去重新编译那些必要编译的内容。

💡 提示

您始终可以通过在末尾添加 --help 来打印任何子命令(或它们的子命令)的帮助。

你可以观看 这些初学者教程,如果你是一个视频学习者

在现有项目工作

如果您下载一个使用 Foundry 的现有项目,那真的很容易上手。

对于这个例子,我们将使用PaulRBergfoundry-template

首先,克隆该项目并在项目目录中运行 forge install

1
2
3
$ git clone https://github.com/PaulRBerg/foundry-template
$ cd foundry-template
$ forge install

我们运行 forge install 来安装项目中的子模块依赖项。

要构建,请使用 forge build:

1
2
3
4
$ forge build
Compiling 10 files with 0.8.15
Solc 0.8.15 finished in 4.35s
Compiler run successful

要进行测试,请使用 forge test:

1
2
3
4
5
6
$ forge test
No files changed, compilation skipped

Running 1 test for test/Greeter.t.sol:GreeterTest
[PASS] testSetGm() (gas: 107402)
Test result: ok. 1 passed; 0 failed; finished in 4.77ms

依赖

默认情况下,Forge 使用 git submodules 管理依赖项,这意味着它可以与任何包含智能合约的 GitHub 代码库一起使用。

添加依赖

要添加依赖项,请运行 forge install

1
2
3
$ forge install transmissions11/solmate
Installing solmate in "/private/var/folders/p_/xbvs4ns92wj3b9xmkc1zkw2w0000gn/T/tmp.FRH0gNvz/deps/lib/solmate" (url: Some("https://github.com/transmissions11/solmate"), tag: None)
Installed solmate

这将拉取 solmate 库,在 git 中暂存 .gitmodules 文件并使用消息“Installed solmate”进行提交。

如果我们现在检查 lib 文件夹:

1
2
3
4
5
6
7
$ tree lib -L 1
lib
├── forge-std
├── solmate
└── weird-erc20

3 directories, 0 files

我们可以看到Forge安装了solmate

默认情况下,forge install 安装最新的 master 分支版本。 如果你想安装一个特定的标签或提交,你可以这样做:

1
$ forge install transmission11/solmate@v7

重新映射依赖项

Forge 可以重新映射(remap)依赖关系,使它们更容易导入。 Forge 将自动尝试为您推断出一些重新映射:

1
2
3
4
5
$ forge remappings
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
weird-erc20/=lib/weird-erc20/src/

这些重新映射意味着:

  • 要从 forge-std 导入,我们会这样写:import "forge-std/Contract.sol";
  • 要从 ds-test 导入,我们会这样写:import "ds-test/Contract.sol";
  • 要从 solmate 导入,我们会这样写:import "solmate/Contract.sol";
  • 要从 weird-erc20 导入,我们会这样写:import "weird-erc20/Contract.sol";

您可以通过在项目的根目录中创建一个 remappings.txt 文件来自定义这些重新映射。

让我们创建一个名为 solmate-utils 的重映射,它指向 solmate repo中的 utils 文件夹!

1
solmate-utils/=lib/solmate/src/utils/

您还可以在 foundry.toml 中设置重映射。

1
2
3
remappings = [
"@solmate-utils/=lib/solmate/src/utils/",
]

现在我们可以像这样导入 solmate repo的 src/utils 中的任何合约:

1
import "solmate-utils/LibString.sol";

更新依赖

您可以使用 forge update 将特定依赖项更新为您指定版本的最新提交。 例如,如果我们想从我们之前安装的 solmate 主版本中提取最新的提交,我们将运行:

1
$ forge update lib/solmate

或者,您可以通过运行 forge update 一次对所有依赖项执行更新。

删除依赖

您可以使用 forge remove ... 删除依赖项,其中 <deps> 是依赖项的完整路径或只是名称 . 例如,要删除 solmate,这两个命令是等价的:

1
2
3
$ forge remove solmate
# ... 等同于 ...
$ forge remove lib/solmate

与 Hardhat 兼容

Forge 还支持基于 Hardhat 的项目,其中依赖项是 npm 包(存储在 node_modules 中)并且其合约存储在 contracts 中而不是 src 中。

要启用 Hardhat 兼容模式,请传递 --hh 标志。

项目布局

Forge 在构建项目的方式上是灵活的。 默认情况下,Forge 项目结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── foundry.toml
├── lib
│ └── forge-std
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.md
│ ├── foundry.toml
│ ├── lib
│ └── src
├── script
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.sol

7 directories, 8 files
  • 您可以使用 foundry.toml 配置 Foundry 的行为。
  • 重新映射在 remappings.txt 中指定。
  • 合约的默认目录是 src/
  • 测试的默认目录是test/,其中任何具有以test开头的函数的合约都被视为测试。
  • 依赖项作为 git 子模块存储在 lib/ 中。

您可以分别使用 --lib-paths--contracts 标志配置 Forge 在何处查找依赖项和合约。 或者,您可以在 foundry.toml 中配置它。

结合重新映射,这为您提供了支持其他工具链(例如 Hardhat 和 Truffle)的项目结构所需的灵活性。

对于获得自动 Hardhat 支持,您还可以传递 --hh 标志,它设置以下标志:--lib-paths node_modules --contracts contracts