上链ON-CHAIN BLOCK 13 / 26
卷三 · 合约工程BLOCK 13

Foundry 工程化


Remix 适合玩,但严肃项目要工程化:版本管理、依赖、单元测试、fuzzing、可复现的部署脚本。Foundry 是当下的主流工具链,而且它有个对你很友好的特点——测试也用 Solidity 写,不用在 JS 和合约之间来回切。这一章把它对应到你熟悉的 Gradle + JUnit 心智模型。

Foundry 的四件套

工具作用Android 类比
forge编译、测试、部署的主命令gradle
cast命令行发 RPC、查链、调合约adb / curl
anvil本地一键起一条测试链(chainId 31337)本地模拟器 / 内存数据库
forge fmt代码格式化ktlint / spotless

项目结构

bash · 初始化
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:

bash
forge install OpenZeppelin/openzeppelin-contracts

用 Solidity 写测试

这是 Foundry 最舒服的地方。测试就是一个继承 Test 的合约,以 test 开头的函数会被自动跑,断言用 assertEq 等。对写过 JUnit 的你,几乎零学习成本:

Solidity · test/Counter.t.sol
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();
    }
}
bash · 跑测试
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,合约安全的一大利器:

Solidity · fuzz 测试
// amount 会被自动灌入大量随机值
function testFuzz_Deposit(uint256 amount) public {
    amount = bound(amount, 1, 1e24);   // 限定合理范围
    vault.deposit(amount);
    assertEq(vault.balanceOf(address(this)), amount);
}
Android 类比

Fuzzing 就像把 Monkey 测试和 property-based testing(你可能用过 kotest 的 property test)搬到合约里,但目标更明确:证明"无论输入什么,某个不变量始终成立"(比如"总供应量永远等于所有余额之和")。在钱直接躺在合约里的世界,这种自动找反例的能力价值千金。

可复现的部署脚本

部署不再靠手点,而是写成脚本、纳入版本管理:

bash · 部署到测试网
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 代币。

本章小结

◆ 返回目录