返回 SC 笔记
SC Day 31

复习 - Week 5 总结 (Foundry + Rust CLI)

### 一、Foundry 工具链全景回顾

2026-05-01
第二阶段:框架实战
FoundryRustCLI复习Week5

日期: 2026-05-01 方向: Solidity + Rust 阶段: 第二阶段:框架实战 标签: #Foundry #Rust #CLI #复习 #Week5


今日目标

  1. 系统回顾 Foundry 工具链的完整工作流(build/test/fuzz/fork/deploy)
  2. 复盘 Rust CLI 项目中使用的核心 crate(reqwest/clap/serde/toml/tracing)
  3. 限时挑战:30 分钟内从零搭建一个完整的 Foundry 项目并通过测试
  4. 总结 Week 5 中两条学习路径的交叉点与协同效应

核心概念

一、Foundry 工具链全景回顾

Foundry 是目前 Solidity 开发中最主流的工具链,由四个核心组件构成:

工具功能对标常用命令
forge编译/测试/部署Hardhatforge build, forge test, forge create
cast链上交互/数据查询ethers.js CLIcast call, cast send, cast block
anvil本地测试节点Hardhat Node/Ganacheanvil, anvil --fork-url
chiselSolidity 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 数据处理
tomlTOML 配置解析应用配置管理
tracing结构化日志+追踪生产级日志和调试

两条路径的交叉点

Solidity (Foundry)              Rust CLI
      ↓                            ↓
  合约开发                      工具开发
      ↓                            ↓
  测试验证 ←――― fuzz理念共通 ―――→ 属性测试
      ↓                            ↓
  链上交互 ←――― RPC调用共通 ―――→ API交互
      ↓                            ↓
  部署脚本 ←――― 自动化共通 ―――→ 部署工具

常见误区

  1. 误区:Foundry 只适合简单项目

    • 事实:Uniswap V4、Aave V3 等顶级项目都用 Foundry
    • Foundry 支持复杂的脚本系统和多链部署
  2. 误区:Fuzz Testing 只是随机测试,不可靠

    • 事实:Fuzz Testing 能发现人工写测试时遗漏的边界条件
    • 配合 vm.assume 约束条件可以精确控制输入范围
  3. 误区:Rust 的 async 在 CLI 中没必要

    • 事实:与区块链 RPC 交互天然是 I/O 密集型
    • 并发请求多个 API 可以显著提升性能
  4. 误区: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()断言会发出事件测试事件

参考资源

  1. Foundry Book — Foundry 官方文档
  2. Foundry GitHub — 源代码和 Issue
  3. reqwest 文档 — Rust HTTP 客户端
  4. clap 文档 — 命令行解析
  5. serde 文档 — 序列化框架
  6. tracing 文档 — 日志和追踪
  7. Solidity by Example — Solidity 代码示例
  8. Rust by Example — Rust 代码示例