引言
2026年的智能合约开发圈,Foundry已经从一个新兴工具成长为事实标准。越来越多的头部DeFi项目——Uniswap、Aave、OpenZeppelin——都在用Foundry重构他们的开发流程。为什么?因为它真的快。快到让很多老开发者感慨:原来测试可以这么高效。
但Foundry不仅仅是一个”快一点的测试框架”。它的设计理念、工具链完整度、对现代开发流程的支持,都代表了智能合约工具链的未来方向。这篇文章会带你系统掌握Foundry,让你在实际项目中用起来。

一、Foundry是什么
1.1 工具链组成
Foundry由四个核心工具组成:
Forge – 智能合约测试框架
- 类比Hardhat/Truffle的测试 runner
- 内置Solidity脚本执行器
- 性能远超传统JavaScript测试
Cast – 命令行交互工具
- 调用合约函数
- 发送交易
- ABI类型转换
- 签名和验证
Anvil – 本地以太坊节点
- 快速启动本地测试网络
- 支持Fork主网/测试网
- 可配置的区块参数
Chisel – Solidity REPL
- 实时执行Solidity代码
- 调试和学习Solidity
bash
# 快速验证安装
forge --version
cast --version
anvil --version
chisel --version
1.2 为什么选择Foundry
速度对比:
bash
# Hardhat + TypeScript (典型项目)
npm test # 3-5分钟
# Foundry Forge
forge test # 15-30秒
差距来自于几个方面:
- 编译缓存:Foundry的Solang编译器有增量编译,只重编译变更文件
- 执行环境:Rust实现的测试执行器比Node.js快
- 并行测试:多线程并行运行测试用例
- 无启动开销:不需要启动完整的JavaScript运行时
功能对比:
| 功能 | Hardhat | Foundry |
|---|---|---|
| 测试框架 | Mocha/Chai | Forge (内置) |
| Fuzzing | 需要solidity-coverage | 内置 |
| Invariant Testing | 第三方插件 | 内置 |
| Gas快照 | hardhat-gas-reporter | --gas-snapshot |
| 脚本执行 | .js文件 | .t.sol 或 .s.sol |
| Fork测试 | 支持 | 支持 (更快) |
| console.log | hardhat console.log | 内置 |
二、环境配置
2.1 安装Foundry
Linux/macOS一键安装:
bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
Windows安装:
powershell
# 使用scoop
scoop install foundry
手动安装(从源码):
bash
git clone https://github.com/foundry-rs/foundry
cd foundry
cargo install --locked --path forge --bins --root ~/.cargo/bin
cargo install --locked --path cast --bins --root ~/.cargo/bin
cargo install --locked --path anvil --bins --root ~/.cargo/bin
验证安装:
bash
forge --version
# forge 0.3.0 (f81c0e3 2026-01-20)
cast --version
# cast 0.3.0
anvil --version
# anvil 0.3.0 (f81c0e3 2026-01-20)
2.2 项目初始化
从零创建项目:
bash
forge init my-project
cd my-project
目录结构:
plaintext
my-project/
├── lib/ # 依赖库(通过git submodule管理)
│ └── forge-std/
├── src/ # 合约源码
│ └── Contract.sol
├── test/ # 测试文件
│ └── Contract.t.sol # 注意:必须是.t.sol后缀
├── script/ # 部署脚本
│ └── Contract.s.sol # 注意:必须是.s.sol后缀
├── foundry.toml # Foundry配置
├── .gitmodules # 依赖管理
└── remappings.txt # 导入路径映射
从Hardhat迁移:
bash
# 如果有现有的Hardhat项目
npm install --save-dev @foundry-rs/forge-tests
# 迁移测试文件(需要改写)
2.3 依赖管理
Foundry使用git submodule管理依赖(取代npm)。
添加OpenZeppelin:
bash
# 作为git submodule添加
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit
安装后会在.gitmodules中记录:
ini
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
branch = v5.0.0
配置导入别名:
在remappings.txt中设置:
plaintext
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@forge-std/=lib/forge-std/src/
然后在合约中使用:
solidity
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@forge-std/Test.sol";
三、Forge测试框架
3.1 基础测试结构
Forge测试使用原生Solidity编写,不需要JavaScript。
solidity
// test/MyToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "@forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public token;
address public owner;
address public user;
function setUp() public {
owner = address(0x1);
user = address(0x2);
// 部署合约
token = new MyToken(owner, 1000 ether);
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000 ether);
}
function testBalanceOfOwner() public {
assertEq(token.balanceOf(owner), 1000 ether);
}
function testTransfer() public {
vm.prank(user);
vm.expectRevert();
token.transfer(owner, 100 ether);
}
function testSuccessfulTransfer() public {
vm.prank(owner);
token.transfer(user, 100 ether);
assertEq(token.balanceOf(user), 100 ether);
assertEq(token.balanceOf(owner), 900 ether);
}
}
运行测试:
bash
# 运行所有测试
forge test
# 运行指定测试
forge test --match-test testTransfer
# 显示日志输出
forge test -vv
# 更详细的日志
forge test -vvvv
3.2 Cheatcodes详解
Cheatcodes是Foundry最强大的功能,允许测试代码操控EVM行为。
vm.prank – 模拟调用者
solidity
// 单次调用 prank
vm.prank(msgSender);
myContract.method();
// 多次调用 prank(整个调用栈)
vm.startPrank(msgSender);
myContract.method1();
myContract.method2();
vm.stopPrank();
vm.deal – 设置余额
solidity
// 设置地址的ETH余额
vm.deal(user, 100 ether);
// 检查余额
assertEq(user.balance, 100 ether);
vm.expectRevert – 预期 revert
solidity
// 预期 revert 但不检查消息
vm.expectRevert();
token.transfer(address(0), 1);
// 检查具体错误消息
vm.expectRevert("Insufficient balance");
token.withdraw(100 ether);
// 检查自定义错误
vm.expectRevert(Token.InsufficientBalance.selector);
token.withdraw(100 ether);
vm.expectEmit – 预期事件
solidity
function testTransferEvent() public {
vm.prank(owner);
// 声明要检查的事件参数
vm.expectEmit(true, true, true, true);
emit Transfer(owner, user, 100 ether);
token.transfer(user, 100 ether);
}
vm.warp / vm.roll – 时间操控
solidity
function testTimeLock() public {
// 设置当前区块时间
vm.warp(block.timestamp + 1 days);
// 设置区块号
vm.roll(block.number + 100);
}
vm.mockCall – 模拟外部调用
solidity
function testWithMock() public {
address mockAddr = address(0x123);
// 模拟 call 返回值
vm.mockCall(
mockAddr,
abi.encodeWithSelector(ILinkToken.getBalance.selector, address(this)),
abi.encode(100 ether)
);
// 使用 mock
uint256 balance = ILinkToken(mockAddr).getBalance(address(this));
assertEq(balance, 100 ether);
}
3.3 Fuzz Testing(模糊测试)
Fuzz测试自动生成随机输入,测试边界条件。
solidity
contract FuzzTest is Test {
Vault public vault;
function setUp() public {
vault = new Vault();
vm.deal(address(this), 100 ether);
vault.deposit{value: 100 ether}();
}
// fuzz 测试:自动生成 uint256 输入
function testFuzzWithdraw(uint256 amount) public {
// 限定有效范围
amount = bound(amount, 0, 100 ether);
uint256 balanceBefore = address(this).balance;
vault.withdraw(amount);
uint256 balanceAfter = address(this).balance;
assertEq(balanceAfter - balanceBefore, amount);
}
// bound 辅助函数:限制随机数范围
function testFuzzWithdrawBounded(uint256 amount) public pure {
// 自动限制在 1-1000 范围
amount = bound(amount, 1, 1000);
assertTrue(amount >= 1 && amount <= 1000);
}
}
运行fuzz测试:
bash
# 默认运行,会使用合理的测试次数
forge test --match-test testFuzz
# 增加测试次数
forge test --match-test testFuzz --fuzz-run 10000
3.4 Invariant Testing(不变量测试)
不变量测试验证合约在各种操作序列下始终保持某些属性。
solidity
contract InvariantTest is Test {
TargetContract public target;
Handler public handler;
function setUp() public {
target = new TargetContract();
handler = new Handler(target);
// 设置 handler
target.setHandler(handler);
// 配置不变式测试
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.withdraw.selector;
target.setSelectors(selectors);
}
// 不变量:totalDeposits >= 合约余额
function invariantConservation() public {
assertGe(target.totalDeposits(), address(target).balance);
}
}
contract Handler {
TargetContract public target;
function deposit() public payable {
target.deposit{value: msg.value}();
}
function withdraw(uint256 amount) public {
target.withdraw(amount);
}
}
四、Cast命令行工具
4.1 基础交互
调用只读函数:
bash
# 查询代币余额
cast call 0xTokenAddress "balanceOf(address)(uint256)" 0xYourAddress
# 读取合约存储槽
cast storage 0xContractAddress 0
# 获取区块信息
cast block latest --json
发送交易:
bash
# 发送ETH
cast send 0xRecipient --value 1ether --private-key $PRIVATE_KEY
# 调用合约方法
cast send 0xContractAddress "transfer(address,uint256)" \
0xRecipient \
1000000 \
--private-key $PRIVATE_KEY
4.2 ABI和类型转换
bash
# 编码函数调用
cast sig "transfer(address,uint256)"
# 输出: 0xa9059cbb
# 编码参数
cast abi-encode "f(uint256,address)" 100 0xRecipient
# 解码事件日志
cast decode --event "Transfer(address,address,uint256)" 0xLogData
# 解析交易
cast receipt 0xTxHash --json
4.3 签名操作
bash
# 签名消息
cast wallet sign "Hello World" --private-key $PK
# 验证签名
cast wallet verify "Hello World" 0xSig 0xSigner
# 计算CREATE2地址
cast create2 0x0000000000000000000000000000000000000000 \
$(cast keccak "token()") \
--init-code 0x608060...
五、Anvil本地节点
5.1 启动本地链
bash
# 默认配置启动
anvil
# 自定义配置
anvil --port 8545 \
--chain-id 1337 \
--block-time 2 \
--accounts 10 \
--balance 10000
5.2 Fork模式
Fork主网进行测试,不需要真实资金:
bash
# Fork mainnet
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# Fork特定区块
anvil --fork-url $RPC_URL --fork-block-number 19000000
# Fork并保留状态修改
anvil --fork-url $RPC_URL --no-reset
5.3 状态操作
bash
# 快照当前状态
cast rpc evm_snapshot
# 恢复快照
cast rpc evm_revert <snapshot-id>
# 调整区块时间
cast rpc evm_increaseTime 86400
cast rpc evm_mine
六、从Hardhat迁移
6.1 迁移步骤
1. 初始化Foundry项目
bash
mkdir new-project && cd new-project
forge init --no-commit
2. 复制合约代码
bash
cp old-project/contracts/* src/
3. 配置依赖
bash
# 安装 OpenZeppelin
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit
4. 迁移测试
Hardhat测试 → Forge测试对照:
javascript
// Hardhat: JavaScript测试
const { expect } = require("chai");
describe("Token", function() {
it("should transfer", async function() {
await token.transfer(recipient, amount);
expect(await token.balanceOf(recipient)).to.equal(amount);
});
});
solidity
// Foundry: Solidity测试
contract TokenTest is Test {
function testTransfer() public {
token.transfer(recipient, amount);
assertEq(token.balanceOf(recipient), amount);
}
}
6.2 配置文件对照
Hardhat配置 → Foundry配置:
toml
# foundry.toml
[profile.default]
src = "src" # 源码目录
out = "out" # 编译输出
libs = ["lib"] # 库目录
test = "test" # 测试目录
script = "script" # 脚本目录
# RPC配置
rpc_url = "https://eth-mainnet.g.alchemy.com/v2/KEY"
# Etherscan API(用于自动验证)
etherscan_api_key = "YOUR_KEY"
# 编译器设置
solc_version = "0.8.24"
optimizer = true
optimizer_runs = 200
# Gas报告
gas_reports = ["*"]
# 铸造账户(测试用)
accounts = { mnemonic = "test test test test test test test test test test test junk" }
6.3 混合使用策略
如果暂时不想完全迁移,可以混合使用:
javascript
// hardhat.config.js
module.exports = {
networks: {
hardhat: {
forking: {
url: process.env.MAINNET_RPC,
}
}
}
};
bash
# 用Anvil fork主网
anvil --fork-url $MAINNET_RPC
# 用Hardhat连接到Anvil
# hardhat.config.js
networks: {
anvil: {
url: "http://localhost:8545"
}
}
七、高级用法
7.1 Gas优化分析
bash
# 生成Gas快照
forge snapshot
# 对比Gas消耗
forge snapshot --diff
# 写入文件
forge snapshot --contract "TestContract" GasReport.txt
7.2 代码覆盖
bash
# 安装覆盖率插件
forge install --no-commit foundry-rs/foundry --branch master
# 运行覆盖率测试
forge coverage
# 生成HTML报告
forge coverage --report lcov
7.3 形式化验证
Foundry集成SMT solver进行形式化验证:
solidity
function testWithdrawCannotDrain() public {
// 尝试找出一个会失败的输入
uint256 balance = address(vault).balance;
assertGe(vault.totalSupply(), balance);
}
结语
Foundry正在改变智能合约开发的游戏规则。它的速度快、功能完整、学习曲线平缓,这些特质让它成为值得投入时间学习的工具。如果你还在用Hardhat,不妨花一个周末试试Foundry——你可能会发现,测试不再是一件需要”等待完成”的事情,而是开发流程中自然流畅的一环。
当然,工具的选择最终还是要看团队需求和项目特点。但无论如何,Foundry值得每个Solidity开发者了解。
相关阅读

发表回复