0. Foundry简介
Foundry 是一套用 Rust 编写的以太坊(EVM)智能合约开发工具链,由 Paradigm 团队开源维护。它以"极速、可移植、模块化"为设计目标,将编译、测试、部署、交互等流程整合在同一 CLI 中,是目前性能最突出的 Solidity 开发框架之一 。
核心组件
-
Forge
- 全生命周期管理:初始化项目、编译、单元测试、模糊测试、Gas 快照、部署脚本
- 测试用原生 Solidity 编写,无需切换语言;支持模糊测试(fuzz)与不变量(invariant)测试
- 增量编译+缓存,测试速度相比传统框架可提升数十乃至上百倍
-
Cast
- 瑞士军刀式链上交互 CLI:查询区块、发送交易、调用合约、编码/解码数据、ENS 解析等
- 一条命令即可在主网或任意 EVM 链上完成读写操作
cast wallet -h # 查看所有的命令选项
cast wallet new [DIR] <ACCOUNT_NAME> # Create a new random keypair
cast wallet new-mnemonic # mnemonic phrase
cast wallet address [PRIVATE_KEY] # private key to an address
cast wallet import -i -k <KEYSTORE_DIR> <ACCOUNT_NAME> #导入一个keystore文件到foundry的keystore目录中,并给这个账户一个别名
cast wallet import --mnemonic "test test test test test test test test test test
test junk" -k <KEYSTORE_DIR> <ACCOUNT_NAME> #基于助记词导入一个keystore文件到foundry的keystore目录中,并给这个账户一个别名
-
Anvil
- 本地以太坊节点,秒级启动,内置 10 个预注资测试账户
- 支持主网分叉、链状态回放、手动调整区块号与时间,方便集成测试
-
Chisel
- Solidity REPL,即时编写并执行 Solidity 代码片段,用于快速验证语法或计算哈希、选择器等
主要优势
- 性能:Rust 实现 + 并行编译,测试速度比 Hardhat/Truffle 快 5×--140×
- 原生 Solidity 测试:测试与业务合约同语言,减少上下文切换,贴近真实运行环境
- 内置模糊测试:只需在测试函数加参数即可自动生成数百组随机输入,帮助发现边界漏洞
- 主网分叉&作弊码:可针对主网状态编写集成测试,并通过"作弊码"模拟时间、余额、存储等
- 配置灵活:通过
foundry.toml支持多网络、多环境参数;与 Hardhat 项目结构兼容,迁移成本低
典型工作流(示例)
bash
# 1. 初始化项目
forge init my-project && cd my-project
# 2. 编译
forge build
# 3. 本地节点
anvil
# 4. 运行测试(含 fuzz)
forge test
# 5. 部署到主网
forge create src/MyToken.sol:MyToken \
--rpc-url $RPC_URL --private-key $PRIVATE_KEY --verify \
--etherscan-api-key $ETHERSCAN_KEY
借助以上命令,开发者可以在几分钟内完成从编写到链上验证的完整流程 。
适用场景
- 对编译/测试速度要求高的中大型 DeFi、NFT、DAO 项目
- 需要大量模糊测试、不变量测试或主网分叉集成的协议
- 偏好 CLI 驱动、脚本化、CI/CD 自动化部署的团队
综上,Foundry 凭借极致性能、全 Rust 工具链与原生 Solidity 测试体验,已成为新一代 EVM 合约开发的首选框架之一。
下面以Counter合约为例,介绍一下如何用 Foundry 做一站式开发:初始化 → 编译 → 测试 → 部署。
1. 环境准备
1.1 启用WSL2并安装Linux发行版
在 Windows 上安装 WSL并使用linux命令处理文本文件
该文已提及,不再敖述。
1.2 安装并配置VSCode
- 安装VSCode :从官网下载并安装Visual Studio Code。
- 安装Remote-WSL扩展 :在VSCode的扩展市场中搜索并安装 "Remote - WSL" 扩展。这允许VSCode无缝地在WSL环境中操作。
- 连接WSL :在VSCode中按下
Ctrl+Shift+P(或F1),输入 "WSL: Connect to WSL" 即可连接到你的WSL发行版。
1.3 在WSL中安装Foundry
连接到你的WSL发行版终端后,执行以下命令安装Foundry:
bash
# 安装Foundryup
curl -L https://foundry.paradigm.xyz | bash
# 重启终端后运行foundryup
foundryup
安装完成后,可以通过运行 forge --version 和 cast --version 来验证安装是否成功。
🔔 注意:如果你的网络环境访问GitHub或Rust官方源较慢,可能需要进行终端代理配置或寻找国内镜像源。
1.4 解决一个在 WSL 中使用代理时非常常见的警告
wsl: 检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。
- 配置 Windows 防火墙
Windows 防火墙默认会阻止来自 WSL 的连接。
* 打开 Windows 的 Windows Defender 防火墙 。
* 点击 "高级设置" 。
* 在 入站规则 中,新建一条规则 。
* 选择 "端口" -> "TCP" -> "特定本地端口" (填写你的代理端口,如 7890)-> "允许连接" -> 全选(域、专用、公用)-> 给规则起个名字(如 "WSL Proxy")。
* 完成后,WSL 就可以畅通地访问 Windows 上的代理端口了。
-
在代理软件中开启 "允许局域网连接"。
-
将以下代码块添加到你的
~/.bashrc或~/.zshrc中,可以智能地设置代理(自动获取 IP 并设置环境变量):bash# 获取 Windows 主机 IP host_ip=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}') # 你的代理端口 proxy_port=7890 # 设置代理环境变量 export http_proxy="http://$host_ip:$proxy_port" export HTTPS_PROXY="http://$host_ip:$proxy_port" export ALL_PROXY="http://$host_ip:$proxy_port" # 可选:添加一些不走代理的地址(如局域网、国内网络) export no_proxy="localhost, 127.0.0.0/8, ::1, 192.168.0.0/16, 10.0.0.0/8" -
使用
curl -v www.google.com或wget -O- www.google.com来测试代理是否正常工作。
按照以上步骤操作后,那个警告信息就不会再出现,并且你的 WSL 也能正常使用 Windows 上的代理了。如果上一步Foundry没有安装成功,那么就可以在WSL中重新安装Foundry了。
2. 项目初始化
最好就在home目录下进行, 不然可能遇到权限问题
bash
# 在home目录下新建projects目录并进入
mkdir projects && cd projects
# 生成标准骨架
forge init hello_foundry
目录结构(已去掉了 .git 等无关文件):
hello_foundry
├── foundry.toml # 配置文件
├── script/ # 【合约脚本】,可用于部署合约、广播交易
├── src/ # 【合约源代码】
├── test/ # 【测试合约代码】
└── lib/ # 存放依赖库(默认安装 forge-std)
└── cache/ # 缓存信息,在 forge build 后出现
└── out/ # 存放编译输出文件
把事先写好的 Counter.sol 放进 src/,其实项目自带的有Counter合约的相关代码。
3. 安装依赖
OpenZeppelin 是常用的合约库,尽管在本次示例中的Counter合约用不到。
# 引入 OpenZeppelin 合约库
forge install OpenZeppelin/openzeppelin-contracts --no-commit
--no-commit 防止把子模块提交到主仓库;安装后 lib/openzeppelin-contracts 会出现完整源码,同时会在.gitmodules文件中出现下文:
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
# 更新依赖(很少用)
forge update openzeppelin-contracts
# 删除依赖
forge remove openzeppelin-contracts -f
删除依赖之所以加上-f参数,是为了应对以下报错:
Error: git rm exited with code 1:
error: the following file has staged content different from both the
file and the HEAD:
lib/openzeppelin-contracts
(use -f to force removal)
依赖库的重映射
forge remappings > remappings.txt
文件中的@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
相当于对长路径起了别名起到import时能够简写的效果,写法如下:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4. 编译
bash
forge build
编译成功会看到:
[⠔] Compiling...
[⠒] Solc 0.8.20 finished in 2.15s
Compiler run successful!
产物在 out/ 目录,包含 abi、bytecode、deployedBytecode,后续部署脚本会引用。
forge inspect 以用来查看合约编译产物和元信息,示例如下:

5. 单元测试
测试文件默认用 t.sol 结尾, 非必须
导入Test合约: 提供了基本的日志和断言功能
Setup函数(可选):每个测试用例运行前都调用
前缀为 test 的函数将作为测试用例运行
testFuzz表示 模糊测试,测试用例的参数值,由 foundry 随机抽样
在 test/Counter.t.sol 写用例:
solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
跑测试:
bash
forge test --mc CounterTest
输出示例:
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 29211, ~: 29289)
[PASS] test_Increment() (gas: 28784)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.84ms (3.79ms CPU time)
Ran 1 test suite in 74.17ms (5.84ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
bash
forge test - 运行所有测试
forge test --match-contract ContractName - 运行指定合约的测试
forge test --match-test testFunctionName - 运行单个测试函数
其中的test可以简写为t
match-contract可以简写为mc
match-test可以简写为mt
Foundry中 forge test 命令的 -v 选项用于控制测试输出的详细程度,每增加一个 v 就会显示更多调试信息。
具体级别如下:
-v:显示基本的详细信息,包括测试通过/失败的状态
-vv:显示更多详细信息,会输出每个测试的日志内容(console.log 信息)
-vvv:显示更详细的信息,包含内部调试信息,常用于深度调试
-vvvv:显示最详细的信息,通常用于复杂的调试场景
一般, -vv就可以满足需求
6. 脚本部署
脚本有两种部署方式:forge create 与 forge script。
方式1️⃣: forge create
bash
forge create Counter --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast
# 最好指定合约的路径和名称
forge create src/Counter.sol:Counter --private-key 0xac0974bec39a17e36ba4a6b4d
238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast
方式2️⃣: forge script
-
本地启动: anvil
(也可以基于指定网络的状态启动一个本地模拟环境: anvil --fork-url <RPC_RUL>)

默认在127.0.0.1:8545 启动服务, 并会配置 10 账号
-
在
script/Counter.s.sol写部署脚本:
solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
contract CounterScript is Script {
Counter public counter;
function setUp() public {}
function run() public {
vm.startBroadcast();
counter = new Counter();
console.log("Counter deployed to:", address(counter));
vm.stopBroadcast();
}
}
- 另起终端执行forge script命令
bash
forge script script/Counter.s.sol --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast
成功会看到:
== Logs ==
Counter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
以下是两者的对比:
| 特性 | forge create | forge script |
|---|---|---|
| 基本用途 | 快速部署单个合约 | 执行复杂部署脚本 |
| 命令复杂度 | 简单,一行命令 | 需要编写部署脚本 |
| 部署单个合约 | ✅ 非常适合 | ✅ 可以,但稍复杂 |
| 部署多个合约 | ❌ 不支持 | ✅ 非常适合 |
| 事务序列 | ❌ 单次交易 | ✅ 支持多个有序交易 |
| 条件逻辑 | ❌ 不支持 | ✅ 支持 if/else、循环等 |
| 依赖管理 | ❌ 手动处理 | ✅ 自动处理合约依赖 |
| 模拟运行 | ❌ 不支持 | ✅ 支持 --dry-run |
| 交易重放 | ❌ 不支持 | ✅ 支持 --resume |
| 错误处理 | ❌ 简单失败 | ✅ 复杂错误处理逻辑 |
| Gas 优化 | ❌ 基础 | ✅ 可优化交易顺序 |
| 验证集成 | ✅ 支持 --verify | ✅ 支持 --verify |
| 多网络部署 | ❌ 手动切换 | ✅ 脚本内自动切换 |
| 代码复用 | ❌ 无 | ✅ 可复用部署逻辑 |
| 测试集成 | ❌ 分离 | ✅ 可与测试结合 |
对于生产环境和复杂部署,强烈推荐使用 forge script,它提供了更好的灵活性、安全性和可维护性。
脚本部署的技巧1:用 keystore 账号部署
为了更直观地理解区块链钱包私钥的三种主要形式,我们可以做一个简单的类比:
| 形式 | 类比 | 核心要点 |
|---|---|---|
| 私钥 | 一把原始的、没有任何保护的钥匙 | 最高权限,但极其脆弱,暴露即丢失。 |
| 助记词 | 一把能配出所有钥匙(私钥)的"母钥匙"模具 | 备份一次,恢复整个钱包。是私钥的友好形式。 |
| Keystore | 一个需要密码才能打开的保险箱,而私钥就在这个保险箱里。 | 安全性=文件+密码。适合数字存储,但怕密码弱和两者一同丢失。 |
最佳实践:
- 优先备份助记词:这是最可靠、最通用的恢复方式。确保物理保管,且多个副本放在不同的安全地点。
- 永远不要泄露三者中的任何一项。
- 使用Keystore时,务必使用强密码,并确保文件和密码分离保管。
- 明白一个核心:无论形式如何,其最终目的都是为了安全地控制和使用那个最终的私钥。
那么具体如何使用keystore 账号来部署合约呢?
# 导入keystore文件
cast wallet import --mnemonic "guitar resource canvas wish ghost swarm female glad inquiry fish laugh web" -k ~/.foundry/keystores anvil_test_account
# 向地址转账(部署合约需要gas)
cast send --rpc-url http://localhost:8545 --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 0x585528A0cd7C43F2E5480B2650e81d63f762a34A --value 1000ether
# 使用--account指定keystore账号进行部署(需要账户在~/.foundry/keystores目录下)
forge script script/Counter.s.sol --account anvil_test_account --rpc-url http://localhost:8545 --broadcast
forge create Counter --account anvil_test_account --rpc-url http://localhost:8545 --broadcast
# 或者使用--keystore指定文件位置进行部署
forge script script/Counter.s.sol --keystore ~/.foundry/keystores/anvil_test_account --rpc-url http://localhost:8545 --broadcast
forge create Counter --keystore ~/.foundry/keystores/anvil_test_account --rpc-url http://localhost:8545 --broadcast
脚本部署的技巧2:使用环境变量
在.env文件中配置环境变量,如LOCAL_URL = "http://127.0.0.1:8545"
然后source .env应用环境变量,这样就可以把上述部署命令简化了:
forge script script/Counter.s.sol --account anvil_test_account --rpc-url $LOCAL_URL --broadcast
脚本部署的技巧3:使用foundry.toml配置节点
rpc_endpoints
sepolia = "${SEPOLIA_RPC_URL}"
forge script script/Counter.s.sol --account anvil_test_account --rpc-url sepolia --broadcast
脚本部署的技巧4:使用抽象合约简化部署脚本的编写
定义抽象合约
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
abstract contract BaseScript is Script {
address internal deployer;
address internal user;
string internal mnemonic;
uint256 internal deployerPrivateKey;
function setUp() public virtual {
deployerPrivateKey = vm.envUint("PRIVATE_KEY");
}
function saveContract(string memory name, address addr) public {
string memory chainId = vm.toString(block.chainid);
string memory json1 = "key";
string memory finalJson = vm.serializeAddress(json1, "address", addr);
string memory dirPath = string.concat(string.concat("deployments/", name), "_");
vm.writeJson(finalJson, string.concat(dirPath, string.concat(chainId, ".json")));
}
modifier broadcaster() {
vm.startBroadcast(deployerPrivateKey);
_;
vm.stopBroadcast();
}
}
让各个部署脚本都继承BaseScript,以便复用setUp和broadcaster的逻辑
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import "./BaseScript.s.sol";
import {Counter} from "../src/Counter.sol";
contract CounterScript is BaseScript {
Counter public counter;
function run() public broadcaster {
counter = new Counter();
console.log("Counter deployed on %s", address(counter));
saveContract("Counter", address(counter));
counter.setNumber(99);
counter.increment();
}
}
如果存在权限问题,可以在foundry.toml中增加配置来解决
[profile.default]
fs_permissions = [
{ access = "write", path = "./deployments" }
]
脚本部署的技巧5:开源合约代码
在foundry.toml中配置Etherscan API密钥和网络:
[etherscan]
# 需要确保将ETHERSCAN_API_KEY环境变量设置为你的Etherscan API密钥
sepolia = { key = "${ETHERSCAN_API_KEY}" }
部署,并验证合约,关键参数是--verify
forge script scripts/Counter.s.sol:CounterScript --rpc-url sepolia --broadcast --verify -vvvv
--verify 会自动把源码提交到 Etherscan,几分钟内能看到绿色对勾
7. 正式网部署(以 Sepolia 为例)
- 准备 RPC 与私钥
把密钥放进 keystore,避免历史记录泄露:
bash
cast wallet import sepolia-key --keystore-dir ~/.foundry/keystores
# 交互式输入私钥与密码
- 领水 & 执行
领水地址推荐: google faucet
登录谷歌帐号, 每天可以领取0.5ETH
bash
forge script script/Counter.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--account sepolia-key \
--keystore-dir ~/.foundry/keystores \
--broadcast --verify -vvvv
至此,用 Foundry 完成 Counter 的全流程就闭环了:
初始化 → 编译 → 测试 → 本地/远程部署。