SC Day 31
复习 - Week 5 总结 (Foundry + Rust CLI)
### 一、Foundry 工具链全景回顾
2026-05-01
第二阶段:框架实战FoundryRustCLI复习Week5
日期: 2026-05-01 方向: Solidity + Rust 阶段: 第二阶段:框架实战 标签: #Foundry #Rust #CLI #复习 #Week5
今日目标
- 系统回顾 Foundry 工具链的完整工作流(build/test/fuzz/fork/deploy)
- 复盘 Rust CLI 项目中使用的核心 crate(reqwest/clap/serde/toml/tracing)
- 限时挑战:30 分钟内从零搭建一个完整的 Foundry 项目并通过测试
- 总结 Week 5 中两条学习路径的交叉点与协同效应
核心概念
一、Foundry 工具链全景回顾
Foundry 是目前 Solidity 开发中最主流的工具链,由四个核心组件构成:
| 工具 | 功能 | 对标 | 常用命令 |
|---|---|---|---|
| forge | 编译/测试/部署 | Hardhat | forge build, forge test, forge create |
| cast | 链上交互/数据查询 | ethers.js CLI | cast call, cast send, cast block |
| anvil | 本地测试节点 | Hardhat Node/Ganache | anvil, anvil --fork-url |
| chisel | Solidity REPL | 无直接对标 | chisel |
1.1 forge build — 编译流程
# 初始化项目
forge init my-project
cd my-project
# 编译
forge build
# 指定优化器运行次数
forge build --optimizer-runs 200
# 查看合约大小(部署 Gas 估算)
forge build --sizes
编译产物在 out/ 目录下,包含 ABI、字节码、源代码映射等。
1.2 forge test — 测试体系
Foundry 的测试是用 Solidity 写的,这是与 Hardhat(JavaScript/TypeScript 测试)最大的区别。
// test/Counter.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
// setUp 在每个测试函数前执行(类似 beforeEach)
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
// test 前缀的函数自动被识别为测试
function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}
// testFail 前缀期望 revert
function testFail_Decrement() public {
counter.decrement(); // 如果没有 decrement 函数,会 revert
}
// 使用 vm cheatcode 模拟不同场景
function test_SetNumber() public {
vm.prank(address(0x1234)); // 模拟调用者
counter.setNumber(42);
assertEq(counter.number(), 42);
}
}
测试命令汇总:
# 运行所有测试
forge test
# 运行特定测试文件
forge test --match-path test/Counter.t.sol
# 运行特定测试函数
forge test --match-test test_Increment
# 详细输出(-v 到 -vvvvv,v 越多信息越详细)
forge test -vvvv
# Gas 报告
forge test --gas-report
1.3 Fuzz Testing — 模糊测试
这是 Foundry 最强大的特性之一。测试函数接收参数时自动变为 fuzz test:
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
// 带约束条件的 fuzz test
function testFuzz_Transfer(uint256 amount) public {
vm.assume(amount > 0 && amount <= 1000 ether);
// ...
}
# 设置 fuzz 运行次数
forge test --fuzz-runs 10000
# 在 foundry.toml 中配置
# [fuzz]
# runs = 256
# max_test_rejects = 65536
1.4 Fork Testing — 分叉测试
可以在真实链状态的快照上运行测试,无需自己部署依赖的合约:
function test_SwapOnUniswap() public {
// 使用 fork 环境,真实的 Uniswap 合约都可用
IUniswapV2Router02 router = IUniswapV2Router02(
0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
);
// ... 直接调用主网合约
}
# 命令行指定 fork
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# foundry.toml 配置
# [profile.default]
# eth_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
1.5 Deploy — 部署
# 使用 forge create 部署
forge create src/Counter.sol:Counter \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY
# 使用 forge script 部署(推荐,更灵活)
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $RPC_URL \
--broadcast \
--verify
部署脚本示例:
// script/Deploy.s.sol
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/Counter.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
Counter counter = new Counter();
counter.setNumber(42);
vm.stopBroadcast();
}
}
二、Rust CLI 核心 Crate 回顾
2.1 reqwest — HTTP 客户端
use reqwest;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct EthPrice {
ethereum: PriceData,
}
#[derive(Deserialize, Debug)]
struct PriceData {
usd: f64,
}
// 异步 GET 请求
async fn fetch_eth_price() -> Result<f64, reqwest::Error> {
let url = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd";
let resp: EthPrice = reqwest::get(url).await?.json().await?;
Ok(resp.ethereum.usd)
}
// 带超时和自定义 header 的请求
async fn fetch_with_config() -> Result<String, reqwest::Error> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let resp = client.get("https://api.example.com/data")
.header("Authorization", "Bearer TOKEN")
.send()
.await?
.text()
.await?;
Ok(resp)
}
2.2 clap — 命令行参数解析
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "web3-cli")]
#[command(about = "Web3 command line tools")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 查询 ETH 价格
Price {
/// 代币符号
#[arg(short, long, default_value = "ethereum")]
token: String,
},
/// 查询地址余额
Balance {
/// 钱包地址
#[arg(short, long)]
address: String,
/// 链名称
#[arg(short, long, default_value = "ethereum")]
chain: String,
},
}
2.3 serde — 序列化/反序列化
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct WalletConfig {
name: String,
address: String,
#[serde(default)]
networks: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
api_key: Option<String>,
}
// JSON
let json_str = serde_json::to_string_pretty(&config)?;
let config: WalletConfig = serde_json::from_str(&json_str)?;
2.4 toml — 配置文件
use serde::Deserialize;
#[derive(Deserialize)]
struct AppConfig {
rpc_url: String,
api_keys: ApiKeys,
}
#[derive(Deserialize)]
struct ApiKeys {
alchemy: Option<String>,
etherscan: Option<String>,
}
fn load_config() -> Result<AppConfig, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("config.toml")?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
config.toml 示例:
rpc_url = "https://eth-mainnet.g.alchemy.com/v2/xxx"
[api_keys]
alchemy = "your-alchemy-key"
etherscan = "your-etherscan-key"
2.5 tracing — 结构化日志
use tracing::{info, warn, error, instrument};
use tracing_subscriber;
fn setup_logging() {
tracing_subscriber::fmt()
.with_env_filter("web3_cli=debug,reqwest=warn")
.with_target(false)
.init();
}
#[instrument(skip(client))]
async fn fetch_balance(client: &reqwest::Client, address: &str) -> Result<f64, Error> {
info!(address = %address, "Fetching balance");
// ...
warn!(balance = %balance, "Low balance detected");
Ok(balance)
}
代码实战
限时挑战:30 分钟从零搭建 Foundry 项目
目标:构建一个 TokenVault 合约,可以存取 ERC20 代币,并包含完整测试。
步骤 1:项目初始化(2 分钟)
forge init token-vault-challenge
cd token-vault-challenge
forge install OpenZeppelin/openzeppelin-contracts --no-commit
配置 foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]
步骤 2:编写合约(12 分钟)
// src/TokenVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract TokenVault is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable token;
mapping(address => uint256) public balances;
uint256 public totalDeposited;
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
error InsufficientBalance(uint256 requested, uint256 available);
error ZeroAmount();
constructor(address _token) {
token = IERC20(_token);
}
function deposit(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
token.safeTransferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
totalDeposited += amount;
emit Deposited(msg.sender, amount);
}
function withdraw(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
if (balances[msg.sender] < amount) {
revert InsufficientBalance(amount, balances[msg.sender]);
}
balances[msg.sender] -= amount;
totalDeposited -= amount;
token.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function balanceOf(address user) external view returns (uint256) {
return balances[user];
}
}
步骤 3:编写测试(12 分钟)
// test/TokenVault.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/TokenVault.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// 模拟 ERC20 代币
contract MockToken is ERC20 {
constructor() ERC20("Mock", "MCK") {
_mint(msg.sender, 1_000_000 ether);
}
}
contract TokenVaultTest is Test {
TokenVault public vault;
MockToken public token;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
token = new MockToken();
vault = new TokenVault(address(token));
// 给 alice 和 bob 分配代币
token.transfer(alice, 10_000 ether);
token.transfer(bob, 10_000 ether);
}
function test_Deposit() public {
vm.startPrank(alice);
token.approve(address(vault), 1000 ether);
vault.deposit(1000 ether);
vm.stopPrank();
assertEq(vault.balanceOf(alice), 1000 ether);
assertEq(vault.totalDeposited(), 1000 ether);
}
function test_Withdraw() public {
// 先存入
vm.startPrank(alice);
token.approve(address(vault), 1000 ether);
vault.deposit(1000 ether);
// 再取出
vault.withdraw(500 ether);
vm.stopPrank();
assertEq(vault.balanceOf(alice), 500 ether);
assertEq(token.balanceOf(alice), 9500 ether);
}
function test_RevertWhen_WithdrawExceedsBalance() public {
vm.startPrank(alice);
token.approve(address(vault), 100 ether);
vault.deposit(100 ether);
vm.expectRevert(
abi.encodeWithSelector(
TokenVault.InsufficientBalance.selector,
200 ether,
100 ether
)
);
vault.withdraw(200 ether);
vm.stopPrank();
}
function test_RevertWhen_DepositZero() public {
vm.prank(alice);
vm.expectRevert(TokenVault.ZeroAmount.selector);
vault.deposit(0);
}
// Fuzz test
function testFuzz_DepositAndWithdraw(uint256 amount) public {
vm.assume(amount > 0 && amount <= 10_000 ether);
vm.startPrank(alice);
token.approve(address(vault), amount);
vault.deposit(amount);
vault.withdraw(amount);
vm.stopPrank();
assertEq(vault.balanceOf(alice), 0);
}
}
步骤 4:运行和验证(4 分钟)
forge build
forge test -vvv
forge test --gas-report
关键要点总结
Foundry 核心要点
| 要点 | 说明 |
|---|---|
| Solidity-native 测试 | 测试代码本身是 Solidity,消除语言切换成本 |
| Cheatcodes (vm) | vm.prank/vm.deal/vm.warp 等模拟链上环境 |
| Fuzz Testing | 函数参数自动随机化,发现边界情况 |
| Fork Testing | 在真实链状态上测试,确保与线上合约兼容 |
| Gas Report | 内置 Gas 消耗报告,优化部署和调用成本 |
Rust CLI 核心要点
| Crate | 核心价值 | 使用场景 |
|---|---|---|
| reqwest | 异步 HTTP 客户端 | 调用 API(价格、余额) |
| clap | 类型安全的参数解析 | 命令行工具界面 |
| serde | 零成本序列化框架 | JSON/TOML/YAML 数据处理 |
| toml | TOML 配置解析 | 应用配置管理 |
| tracing | 结构化日志+追踪 | 生产级日志和调试 |
两条路径的交叉点
Solidity (Foundry) Rust CLI
↓ ↓
合约开发 工具开发
↓ ↓
测试验证 ←――― fuzz理念共通 ―――→ 属性测试
↓ ↓
链上交互 ←――― RPC调用共通 ―――→ API交互
↓ ↓
部署脚本 ←――― 自动化共通 ―――→ 部署工具
常见误区
-
误区:Foundry 只适合简单项目
- 事实:Uniswap V4、Aave V3 等顶级项目都用 Foundry
- Foundry 支持复杂的脚本系统和多链部署
-
误区:Fuzz Testing 只是随机测试,不可靠
- 事实:Fuzz Testing 能发现人工写测试时遗漏的边界条件
- 配合
vm.assume约束条件可以精确控制输入范围
-
误区:Rust 的 async 在 CLI 中没必要
- 事实:与区块链 RPC 交互天然是 I/O 密集型
- 并发请求多个 API 可以显著提升性能
-
误区:Fork Testing 不需要本地测试
- 事实:Fork Testing 依赖网络,速度慢,且 RPC 有限
- 建议:本地单元测试为主,Fork 集成测试为辅
面试关联
Q1: 为什么选择 Foundry 而不是 Hardhat?
简短回答:Foundry 用 Solidity 写测试,避免了语言切换;内置 fuzz testing 和 fork testing;编译和测试速度比 Hardhat 快 10-100 倍。
详细回答:
- 语言统一:Hardhat 用 JS/TS 写测试,需要在 Solidity 和 JS 之间切换思维。Foundry 测试是纯 Solidity,开发者可以用同一种语言思考。
- 性能:Foundry 基于 Rust 实现的 EVM (revm),比 Hardhat 的 JS-based EVM 快得多。大型项目测试套件从几分钟缩短到几秒。
- 内置 Fuzz:不需要额外安装 Echidna 等工具,
forge test原生支持。 - 链上交互:
cast工具让命令行链上操作非常方便。
Q2: 如何在 Rust 项目中处理错误?
答案要点:
- 使用
Result<T, E>类型和?运算符 - 用
thiserror定义自定义错误类型 - 用
anyhow简化错误传播 - CLI 中用
clap的验证功能做输入校验
Q3: Foundry 的 cheatcode 有哪些常用的?
| Cheatcode | 用途 | 示例 |
|---|---|---|
vm.prank(addr) | 模拟调用者 | 测试权限控制 |
vm.deal(addr, val) | 设置 ETH 余额 | 准备测试状态 |
vm.warp(ts) | 设置时间戳 | 测试时间锁 |
vm.roll(num) | 设置区块号 | 测试区块依赖逻辑 |
vm.expectRevert() | 断言会 revert | 测试错误处理 |
vm.expectEmit() | 断言会发出事件 | 测试事件 |
参考资源
- Foundry Book — Foundry 官方文档
- Foundry GitHub — 源代码和 Issue
- reqwest 文档 — Rust HTTP 客户端
- clap 文档 — 命令行解析
- serde 文档 — 序列化框架
- tracing 文档 — 日志和追踪
- Solidity by Example — Solidity 代码示例
- Rust by Example — Rust 代码示例