Foundry 工程化
Remix 适合玩,但严肃项目要工程化:版本管理、依赖、单元测试、fuzzing、可复现的部署脚本。Foundry 是当下的主流工具链,而且它有个对你很友好的特点——测试也用 Solidity 写,不用在 JS 和合约之间来回切。这一章把它对应到你熟悉的 Gradle + JUnit 心智模型。
Foundry 的四件套
| 工具 | 作用 | Android 类比 |
|---|---|---|
forge | 编译、测试、部署的主命令 | gradle |
cast | 命令行发 RPC、查链、调合约 | adb / curl |
anvil | 本地一键起一条测试链(chainId 31337) | 本地模拟器 / 内存数据库 |
forge fmt | 代码格式化 | ktlint / spotless |
项目结构
curl -L https://foundry.paradigm.xyz | bash # 安装 foundryup foundryup # 装/更新工具链 forge init my-dapp # 建项目脚手架 my-dapp/ ├─ src/ # 合约源码(≈ src/main) ├─ test/ # 测试合约(≈ src/test) ├─ script/ # 部署脚本 ├─ lib/ # 依赖(git submodule) └─ foundry.toml # 配置(≈ build.gradle)
装依赖也很直接,比如引入 OpenZeppelin:
forge install OpenZeppelin/openzeppelin-contracts
用 Solidity 写测试
这是 Foundry 最舒服的地方。测试就是一个继承 Test 的合约,以 test 开头的函数会被自动跑,断言用 assertEq 等。对写过 JUnit 的你,几乎零学习成本:
import "forge-std/Test.sol"; import "../src/Counter.sol"; contract CounterTest is Test { Counter counter; function setUp() public { // ≈ @Before counter = new Counter(); } function test_Increment() public { // ≈ @Test counter.increment(); assertEq(counter.count(), 1); } function test_RevertWhen_NotOwner() public { vm.prank(address(0xBEEF)); // 伪装成别的地址来调 vm.expectRevert(); // 断言下一句会 revert counter.onlyOwnerThing(); } }
forge test # 跑全部,≈ gradle test forge test -vvv # 加详细 trace(失败时看每步调用) forge test --gas-report # 顺便输出每个函数的 gas 消耗
作弊码 vm:测试的超能力
那个 vm.* 是 Foundry 的"作弊码(cheatcodes)",能在测试里操纵链的状态——这是 JUnit 给不了你的:
| cheatcode | 作用 |
|---|---|
vm.prank(addr) | 让下一次调用的 msg.sender 变成 addr(测试权限超好用) |
vm.deal(addr, amt) | 凭空给某地址一笔 ETH |
vm.warp(ts) / vm.roll(n) | 把时间 / 区块号快进(测锁仓、利息) |
vm.expectRevert() | 断言接下来会回退 |
vm.expectEmit() | 断言会发出某个事件 |
Fuzzing:让机器帮你找边界 bug
给测试函数加个参数,Foundry 会自动用成百上千组随机输入去跑它,专找让断言失败的输入——这就是属性测试 / fuzzing,合约安全的一大利器:
// amount 会被自动灌入大量随机值 function testFuzz_Deposit(uint256 amount) public { amount = bound(amount, 1, 1e24); // 限定合理范围 vault.deposit(amount); assertEq(vault.balanceOf(address(this)), amount); }
Fuzzing 就像把 Monkey 测试和 property-based testing(你可能用过 kotest 的 property test)搬到合约里,但目标更明确:证明"无论输入什么,某个不变量始终成立"(比如"总供应量永远等于所有余额之和")。在钱直接躺在合约里的世界,这种自动找反例的能力价值千金。
可复现的部署脚本
部署不再靠手点,而是写成脚本、纳入版本管理:
forge script script/Deploy.s.sol \ --rpc-url $SEPOLIA_RPC \ --private-key $PK \ --broadcast \ --verify # 顺带在 Etherscan 上验证源码
私钥绝不写进代码或提交进 git。用环境变量($PK)或 Foundry 的加密 keystore(cast wallet)。无数惨案是把带资金的私钥推到公开仓库,几分钟内就被扫描机器人清空。把 .env 加进 .gitignore,这跟你绝不把 signing key 提交进 Android 项目是同一条铁律。
Foundry 让合约开发有了工程纪律:forge test 跑用 Solidity 写的单测、vm.* 作弊码操纵状态、fuzzing 自动找反例、forge script 做可复现部署。心智模型直接套你的 Gradle + JUnit 经验即可。在这个改一次就永久生效、且钱就在合约里的世界,测试不是可选项,是生死线。下一章我们用这套工具,从零写并测一个真正的 ERC-20 代币。
本章小结
- Foundry = forge(构建测试部署)+ cast(命令行交互)+ anvil(本地链);对应 Gradle/adb/模拟器。
- 测试用 Solidity 写,setUp≈@Before、test_*≈@Test;vm.* 作弊码能操纵 sender、时间、余额。
- Fuzzing 用随机输入验证不变量,是合约安全的关键手段。
- 部署写成可复现脚本;私钥走环境变量/keystore,永不入库。