SC Day 25
Solidity/Foundry 环境搭建 + forge init/build/test + 项目结构
### 一、Foundry 是什么?
2026-04-25
第二阶段:框架实战 (Day 25-30)Foundryforge测试开发环境Solidity
日期: 2026-04-25 方向: Solidity / Foundry 阶段: 第二阶段:框架实战 (Day 25-30) 标签: #Foundry #forge #测试 #开发环境 #Solidity
今日目标
- 安装和配置 Foundry 工具链
- 理解 Foundry 项目结构(src/test/script/lib)
- 掌握 foundry.toml 核心配置
- 将 ERC20 项目迁移到 Foundry
- 编写第一个 Foundry 测试(setUp + test 函数)
- 对比 Foundry 和 Hardhat 的差异
核心概念
一、Foundry 是什么?
Foundry 是用 Rust 编写的 Solidity 开发框架,2022 年由 Paradigm 推出,已成为 DeFi 开发的事实标准。
1.1 Foundry 工具链
| 工具 | 功能 | 等价于 |
|---|---|---|
forge | 编译、测试、部署 | Hardhat compile/test |
cast | 链上交互 CLI | ethers.js 脚本 |
anvil | 本地测试节点 | Hardhat Network / Ganache |
chisel | Solidity REPL | - |
1.2 为什么从 Hardhat 迁移到 Foundry?
| 维度 | Hardhat | Foundry |
|---|---|---|
| 语言 | JavaScript/TypeScript | Solidity (测试也用 Solidity!) |
| 速度 | 慢 (Node.js) | 极快 (Rust) |
| 测试 | JS 测试 + ethers.js | Solidity 测试 + cheatcodes |
| Fuzz 测试 | 需要额外工具 | 内置 |
| Fork 测试 | 支持但较慢 | 支持且极快 |
| Gas 报告 | 插件 | 内置 forge snapshot |
| 依赖管理 | npm | git submodules |
| 学习曲线 | JS 开发者友好 | 需要熟悉 Solidity |
| 社区趋势 | 老牌,插件生态丰富 | DeFi 团队首选 |
| 适用场景 | 前端集成、脚本 | 合约开发、审计 |
总结: 大多数 DeFi 协议(Uniswap V4, Aave V3, OpenSea)已迁移到 Foundry。但 Hardhat 在前端集成场景仍有优势。
二、安装 Foundry
2.1 安装步骤
# Linux / macOS / WSL
curl -L https://foundry.paradigm.xyz | bash
# 安装/更新到最新版本
foundryup
# 验证安装
forge --version
cast --version
anvil --version
chisel --version
2.2 Windows 注意事项
Windows 用户推荐使用 WSL2 或 Git Bash。Foundry 原生支持 Windows,但部分功能在 WSL 下更稳定。
# Windows (PowerShell)
# 安装 foundryup
curl -L https://win.foundry.paradigm.xyz | bash
# 或使用 cargo 安装
cargo install --git https://github.com/foundry-rs/foundry --profile release forge cast anvil chisel
三、项目结构
3.1 forge init
# 创建新项目
forge init my-project
cd my-project
# 项目结构:
my-project/
├── foundry.toml # 配置文件 (类似 hardhat.config.ts)
├── src/ # 合约源代码
│ └── Counter.sol # 示例合约
├── test/ # 测试文件
│ └── Counter.t.sol # 示例测试 (约定: .t.sol)
├── script/ # 部署/交互脚本
│ └── Counter.s.sol # 示例脚本 (约定: .s.sol)
├── lib/ # 依赖 (git submodules)
│ └── forge-std/ # 标准库 (自动安装)
├── .github/ # CI 配置
├── .gitmodules # git 子模块配置
└── remappings.txt # import 路径映射
文件命名约定:
*.sol— 合约源代码(放在src/)*.t.sol— 测试文件(放在test/)*.s.sol— 脚本文件(放在script/)
3.2 foundry.toml 配置
[profile.default]
# 编译配置
src = "src" # 源代码目录
out = "out" # 编译输出目录
libs = ["lib"] # 依赖目录
test = "test" # 测试目录
script = "script" # 脚本目录
# Solidity 编译器配置
solc_version = "0.8.20" # 指定 solc 版本
optimizer = true # 启用优化器
optimizer_runs = 200 # 优化轮数 (越大越优化运行时Gas,越小越优化部署Gas)
via_ir = false # 是否使用 IR pipeline
# EVM 配置
evm_version = "paris" # EVM 版本
# 测试配置
fuzz = { runs = 256 } # fuzz 测试轮数
verbosity = 2 # 日志详细度 (0-5)
# 格式化
[fmt]
line_length = 100 # 行宽
tab_width = 4 # 缩进
bracket_spacing = true # 花括号空格
# 不同环境的配置
[profile.ci]
fuzz = { runs = 10000 } # CI 环境跑更多 fuzz 轮数
[profile.lite]
optimizer = false # 开发时不优化,编译更快
# RPC 端点 (用于 fork 测试)
[rpc_endpoints]
mainnet = "${ETH_RPC_URL}"
sepolia = "https://rpc.sepolia.org"
arbitrum = "https://arb1.arbitrum.io/rpc"
3.3 依赖管理
# 安装 OpenZeppelin
forge install OpenZeppelin/openzeppelin-contracts
# 安装指定版本
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0
# 安装其他库
forge install transmissions11/solmate
forge install Uniswap/v3-core
# 更新依赖
forge update
# 删除依赖
forge remove openzeppelin-contracts
安装后需要配置 remappings,让 Solidity import 能正确解析:
# 生成 remappings
forge remappings > remappings.txt
remappings.txt 内容:
@openzeppelin/=lib/openzeppelin-contracts/
@solmate/=lib/solmate/src/
forge-std/=lib/forge-std/src/
四、核心命令
# 编译
forge build # 编译所有合约
forge build --sizes # 显示合约大小
forge build --force # 强制重新编译
# 测试
forge test # 运行所有测试
forge test -vvvv # 详细输出 (显示每个操作码)
forge test --match-test testXxx # 只运行匹配的测试
forge test --match-contract Xxx # 只运行匹配的合约
forge test --gas-report # 显示 Gas 报告
forge test --fork-url $RPC # Fork 主网测试
# Gas 快照
forge snapshot # 生成 gas 快照
forge snapshot --diff # 与上次快照对比
# 本地节点
anvil # 启动本地节点 (默认 8545 端口)
anvil --fork-url $RPC # Fork 主网
# 链上交互
cast call $ADDR "balanceOf(address)" $USER # 只读调用
cast send $ADDR "transfer(address,uint256)" $TO $AMT # 发送交易
cast balance $ADDR # 查看 ETH 余额
cast to-dec 0x1a # 十六进制转十进制
cast abi-encode "foo(uint256,bool)" 123 true # ABI 编码
# 格式化
forge fmt # 格式化代码
forge fmt --check # 检查格式
代码实战
将 ERC20 迁移到 Foundry 项目
Step 1: 项目初始化
forge init momo-token
cd momo-token
forge install OpenZeppelin/openzeppelin-contracts
Step 2: 合约 (src/MomoToken.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MomoToken
* @notice 学习用 ERC20 代币,包含基础功能 + Permit + 铸造上限
*/
contract MomoToken is ERC20, ERC20Burnable, ERC20Permit, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 1e18; // 10亿枚
event Minted(address indexed to, uint256 amount);
constructor(
address initialOwner,
uint256 initialSupply
)
ERC20("Momo Token", "MOMO")
ERC20Permit("Momo Token")
Ownable(initialOwner)
{
require(initialSupply <= MAX_SUPPLY, "Exceeds max supply");
_mint(initialOwner, initialSupply);
}
/// @notice Owner 可以铸造新代币(不超过 MAX_SUPPLY)
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
emit Minted(to, amount);
}
}
Step 3: 测试 (test/MomoToken.t.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MomoToken.sol";
/**
* @title MomoTokenTest
* @notice MomoToken 的完整测试套件
* @dev 演示 Foundry 测试的核心模式
*/
contract MomoTokenTest is Test {
// ============ 状态变量 ============
MomoToken public token;
// 测试地址 (Foundry 的 makeAddr 创建标签化地址)
address public owner;
address public alice;
address public bob;
uint256 public constant INITIAL_SUPPLY = 100_000_000 * 1e18; // 1亿
// ============ setUp: 每个测试函数执行前都会调用 ============
function setUp() public {
// 创建有标签的地址 (在 trace 中更易读)
owner = makeAddr("owner");
alice = makeAddr("alice");
bob = makeAddr("bob");
// 以 owner 身份部署
vm.prank(owner);
token = new MomoToken(owner, INITIAL_SUPPLY);
// 给测试账户一些 ETH
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
}
// ============ 部署测试 ============
function test_InitialState() public view {
assertEq(token.name(), "Momo Token");
assertEq(token.symbol(), "MOMO");
assertEq(token.decimals(), 18);
assertEq(token.totalSupply(), INITIAL_SUPPLY);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY);
assertEq(token.owner(), owner);
}
function test_MaxSupply() public view {
assertEq(token.MAX_SUPPLY(), 1_000_000_000 * 1e18);
}
// ============ Transfer 测试 ============
function test_Transfer() public {
uint256 amount = 1000 * 1e18;
// owner 转给 alice
vm.prank(owner);
bool success = token.transfer(alice, amount);
assertTrue(success);
assertEq(token.balanceOf(alice), amount);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY - amount);
}
function test_TransferEmitsEvent() public {
uint256 amount = 500 * 1e18;
// 期望发出 Transfer 事件
// 参数: checkTopic1, checkTopic2, checkTopic3, checkData
vm.expectEmit(true, true, false, true);
emit IERC20.Transfer(owner, alice, amount);
vm.prank(owner);
token.transfer(alice, amount);
}
function test_RevertTransferInsufficientBalance() public {
uint256 amount = INITIAL_SUPPLY + 1;
// 期望 revert
vm.prank(owner);
vm.expectRevert();
token.transfer(alice, amount);
}
function test_RevertTransferToZeroAddress() public {
vm.prank(owner);
vm.expectRevert();
token.transfer(address(0), 100);
}
// ============ Approve + TransferFrom 测试 ============
function test_Approve() public {
uint256 amount = 1000 * 1e18;
vm.prank(owner);
bool success = token.approve(alice, amount);
assertTrue(success);
assertEq(token.allowance(owner, alice), amount);
}
function test_TransferFrom() public {
uint256 amount = 1000 * 1e18;
// owner approve alice
vm.prank(owner);
token.approve(alice, amount);
// alice 从 owner 转给 bob
vm.prank(alice);
token.transferFrom(owner, bob, amount);
assertEq(token.balanceOf(bob), amount);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY - amount);
assertEq(token.allowance(owner, alice), 0); // allowance 用完
}
function test_RevertTransferFromInsufficientAllowance() public {
vm.prank(owner);
token.approve(alice, 100);
vm.prank(alice);
vm.expectRevert();
token.transferFrom(owner, bob, 200); // 超过 allowance
}
// ============ Mint 测试 ============
function test_MintByOwner() public {
uint256 mintAmount = 50_000_000 * 1e18;
vm.prank(owner);
token.mint(alice, mintAmount);
assertEq(token.balanceOf(alice), mintAmount);
assertEq(token.totalSupply(), INITIAL_SUPPLY + mintAmount);
}
function test_RevertMintExceedsMaxSupply() public {
uint256 tooMuch = token.MAX_SUPPLY() - INITIAL_SUPPLY + 1;
vm.prank(owner);
vm.expectRevert("Exceeds max supply");
token.mint(alice, tooMuch);
}
function test_RevertMintByNonOwner() public {
vm.prank(alice);
vm.expectRevert();
token.mint(alice, 100);
}
// ============ Burn 测试 ============
function test_Burn() public {
uint256 burnAmount = 1000 * 1e18;
vm.prank(owner);
token.burn(burnAmount);
assertEq(token.balanceOf(owner), INITIAL_SUPPLY - burnAmount);
assertEq(token.totalSupply(), INITIAL_SUPPLY - burnAmount);
}
// ============ 时间操纵测试 ============
function test_TimestampManipulation() public view {
// vm.warp 设置 block.timestamp
// vm.roll 设置 block.number
uint256 currentTime = block.timestamp;
uint256 currentBlock = block.number;
// 验证初始状态
assertGt(currentTime, 0);
assertGt(currentBlock, 0);
}
}
Step 4: 部署脚本 (script/Deploy.s.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/MomoToken.sol";
contract DeployScript is Script {
function run() external {
// 从环境变量获取私钥
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
console.log("Deployer:", deployer);
console.log("Balance:", deployer.balance);
vm.startBroadcast(deployerPrivateKey);
MomoToken token = new MomoToken(
deployer,
100_000_000 * 1e18 // 1亿初始供应
);
console.log("MomoToken deployed at:", address(token));
vm.stopBroadcast();
}
}
Step 5: 运行
# 编译
forge build
# 运行测试
forge test
# 详细输出
forge test -vvv
# 单个测试
forge test --match-test test_Transfer -vvvv
# Gas 报告
forge test --gas-report
# 部署到本地 (先启动 anvil)
# 终端1: anvil
# 终端2:
forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast
# 部署到 Sepolia
forge script script/Deploy.s.sol \
--rpc-url $SEPOLIA_RPC \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_KEY
关键要点总结
Foundry 测试核心模式
| 模式 | 代码 | 说明 |
|---|---|---|
| setUp | function setUp() public | 每个测试前执行 |
| test_ | function test_Xxx() | 测试函数前缀 |
| testFail_ | function testFail_Xxx() | 期望 revert 的测试 |
| vm.prank | vm.prank(addr) | 下一次调用以 addr 身份执行 |
| vm.deal | vm.deal(addr, amount) | 设置 ETH 余额 |
| vm.expectRevert | vm.expectRevert("msg") | 期望下一次调用 revert |
| vm.expectEmit | vm.expectEmit(t1,t2,t3,d) | 期望发出事件 |
| makeAddr | makeAddr("label") | 创建有标签的地址 |
| console.log | console.log("msg", val) | 调试输出 |
项目结构最佳实践
project/
├── src/
│ ├── Token.sol # 主合约
│ ├── interfaces/ # 接口定义
│ │ └── IToken.sol
│ └── libraries/ # 库
│ └── MathLib.sol
├── test/
│ ├── Token.t.sol # 单元测试
│ ├── integration/ # 集成测试
│ └── invariant/ # 不变量测试
├── script/
│ ├── Deploy.s.sol # 部署脚本
│ └── Interact.s.sol # 交互脚本
└── foundry.toml
常见误区
- 测试函数不以
test开头: Foundry 不会识别为测试,也不会报错(静默跳过) - 忘记在 setUp 中部署合约: 每个 test 函数都是独立的,需要 setUp 初始化
- remappings 配置错误: 安装依赖后记得运行
forge remappings > remappings.txt - 混淆
vm.prank和vm.startPrank:prank只影响下一次调用,startPrank影响后续所有调用直到stopPrank - 在测试中使用 Hardhat 的
expect(...).to.be.reverted: Foundry 测试是 Solidity,不是 JS
面试关联
| 面试题 | 本课关联 |
|---|---|
| "你用什么工具开发 Solidity?" | Foundry,因为测试速度快、fuzz 内置、DeFi 主流 |
| "如何测试智能合约?" | setUp + test_ 函数 + cheatcodes + fork 测试 |
| "Foundry 和 Hardhat 的区别?" | 语言、速度、fuzz 支持、社区趋势 |
| "如何保证合约部署的确定性?" | forge script + CREATE2 + 验证 |
| "如何自动化合约部署?" | forge script + CI/CD |
参考资源
- Foundry Book — 官方文档,必读
- forge-std Source — 标准库源码
- Foundry Cheatcodes Reference
- Paradigm CTF Solutions (Foundry)
- Foundry Template by PaulRBerg
- [Hardhat vs Foundry (a]i's comparison)](https://www.alchemy.com/overviews/hardhat-vs-foundry)