Foundry 实战手册

如果你还在用 Hardhat 写 JS 测试,或者被 Truffle 的慢速编译折磨过,Foundry 会是你的解药。

它用 Rust 编写,底层集成 revm(一个极快的 Rust EVM 实现)。最大的卖点就一个:快,而且你可以直接用 Solidity 写测试。

不用在 JS 和 Solidity 之间来回切换心智,测试跑起来像飞一样。


四大组件:各司其职

Foundry 不是单一工具,而是一套命令行全家桶。

🔨 Forge:编译、测试、部署

日常干活的主力。forge test 跑 Solidity 测试,forge script 写部署脚本,forge build 编译合约。
📖 官方文档

常用命令速查

1
2
3
4
5
6
7
8
9
forge init my-project              # 初始化项目
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 # 装依赖
forge build --sizes # 编译并显示合约体积排名
forge test --match-test testFork -vvv # 跑特定测试,带详细日志
forge test --gas-report # 跑完直接出 Gas 报告
forge snapshot --diff # 对比上次 Gas 快照,看优化效果
forge fmt --check # 检查代码格式是否符合规范
forge create src/Token.sol:MyToken --rpc-url $RPC --private-key $PK --constructor-args 1000000
forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify

[!tip] 部署参数小技巧
构造函数参数直接跟在命令末尾。如果参数带空格或特殊字符,用引号包起来。
验证合约时加上 --verify,Foundry 会自动把源码推给 Etherscan。

⚡ Cast:链上交互的瑞士军刀

不想开浏览器、不想写脚本?cast 让你在终端里直接跟链上合约对话。
📖 官方文档

1
2
3
4
5
6
7
8
9
10
cast call 0xContract "balanceOf(address)(uint256)" 0xUser --rpc-url $RPC
cast send 0xToken "transfer(address,uint256)" 0xReceiver 1000 --private-key $PK
cast balance 0xUser --rpc-url $RPC
cast to-wei 1.5 ether
cast 4byte 0xa9059cbb # 查这个 selector 是哪个函数
cast tx <txhash> # 查看交易详情
cast receipt <txhash> # 查看交易回执(状态、Gas使用等)
cast block latest # 查看最新区块信息
cast wallet new # 生成新钱包(私钥+地址)
cast wallet address --private-key <KEY> # 通过私钥查地址

🏠 Anvil:本地节点,秒级启动

比 Ganache 快得多。一键启动本地链,自带 10 个有钱的测试账户。
📖 官方文档

1
2
3
4
5
6
7
anvil                           # 默认启动
anvil --fork-url $MAINNET_RPC # 分叉主网,本地模拟主网状态
anvil --fork-block-number 18000000 # 锁定在历史某个区块
anvil --host 0.0.0.0 --port 8545 # 指定监听地址和端口
anvil --gas-limit 10000000 # 自定义区块 Gas 上限
anvil --dump-state state.json # 退出时保存当前状态到文件
anvil --load-state state.json # 启动时加载历史状态

[!example] 分叉测试的杀手级场景
你想测试协议在 Uniswap V3 池子里的交互,但不想花真金白银。anvil --fork-url 把主网状态拉到本地,你的测试合约可以直接跟主网上的 Uniswap 交互(只读),随便折腾不花钱。

💻 Chisel:Solidity 的 REPL

类似 Python 的交互式终端。写两行代码立刻看结果,不用建文件、不用编译。
📖 官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chisel
>> uint256 x = 100 + 200;
>> x
Type: uint256
├ Hex: 0x000000000000000000000000000000000000000000000000000000000000012c
└ Dec: 300

# 常用内部指令
!help # 查看所有可用命令
!exec <cmd> # 执行系统命令
!save <session> # 保存当前会话到文件
!load <session> # 加载历史会话
!clear # 清空当前环境
!source # 显示当前会话生成的完整 Solidity 源码

作弊码(Cheatcodes):测试环境的神

测试时,你需要控制时间、余额、区块高度,甚至伪装成别人调用合约。vm 对象就是干这个的。

1. 伪装身份 & 发钱

1
2
3
vm.prank(alice);           // 下一次调用伪装成 alice
vm.startPrank(alice); // 持续伪装,直到 vm.stopPrank()
vm.deal(alice, 100 ether); // 给 alice 塞 100 ETH(测试网随便印钞)

2. 快进时间 & 跳区块

1
2
vm.warp(block.timestamp + 1 days); // 时间快进 1 天
vm.roll(block.number + 100); // 区块高度 +100

3. 预期报错 & 事件

1
2
3
vm.expectRevert("Not owner");      // 下一行调用必须 revert,且错误信息匹配
vm.expectEmit(true, true, false, true); // 预期下一个事件发射
emit MyContract.Transfer(alice, bob, 100);

4. 直接读写 Storage(上帝模式)

1
2
vm.store(targetContract, bytes32(uint256(0)), bytes32(uint256(999)));
uint256 val = uint256(vm.load(targetContract, bytes32(uint256(0))));

[!warning] 慎用 store/load
直接操作存储槽会绕过合约逻辑。通常用于测试极端边界情况,或者给没有 setter 的私有变量塞数据。

5. 快照与回滚

1
2
3
uint256 snap = vm.snapshot();
// ... 做一堆破坏性操作 ...
vm.revertTo(snap); // 状态瞬间恢复

6. 主网分叉测试

1
2
3
uint256 mainnetFork = vm.createFork(vm.rpcUrl("mainnet"));
vm.selectFork(mainnetFork);
vm.rollFork(18000000); // 切到指定高度

常用参数与调试技巧

参数 作用
-v 打印 console.log 输出
-vv 失败测试显示断言对比
-vvv 显示所有测试的堆栈跟踪
-vvvv 核弹级:显示完整执行轨迹和内存变化(调试神器)
--ffi 允许测试调用外部系统命令(慎用,有安全风险)
--isolate 每个测试用例在独立 EVM 实例跑,互不干扰

打印调试:console2

Foundry 内置了类似 JS console.log 的功能:

1
2
3
4
5
6
7
import "forge-std/console2.sol";

function testDebug() public {
console2.log("Current balance:", address(alice).balance);
console2.logUint(someNumber);
console2.logAddress(target);
}

forge test -vv 就能看到输出。


写在最后

Foundry 的学习曲线比 Hardhat 陡一点,因为你要习惯命令行和 Solidity 测试的写法。但一旦跑通,你会发现:

  • 测试速度从分钟级降到秒级
  • 不用在 JS 和 Solidity 之间翻译逻辑
  • castanvil 让链上调试变得极其顺手

forge test 跑成肌肉记忆,你的合约质量会肉眼可见地提升。