Foundry初始化、编译、测试、部署智能合约全流程介绍

0. Foundry简介

Foundry 是一套用 Rust 编写的以太坊(EVM)智能合约开发工具链,由 Paradigm 团队开源维护。它以"极速、可移植、模块化"为设计目标,将编译、测试、部署、交互等流程整合在同一 CLI 中,是目前性能最突出的 Solidity 开发框架之一 。

核心组件

  1. Forge

    • 全生命周期管理:初始化项目、编译、单元测试、模糊测试、Gas 快照、部署脚本
    • 测试用原生 Solidity 编写,无需切换语言;支持模糊测试(fuzz)与不变量(invariant)测试
    • 增量编译+缓存,测试速度相比传统框架可提升数十乃至上百倍
  2. Cast

    • 瑞士军刀式链上交互 CLI:查询区块、发送交易、调用合约、编码/解码数据、ENS 解析等
    • 一条命令即可在主网或任意 EVM 链上完成读写操作

cast wallet -h # 查看所有的命令选项

cast wallet new [DIR] <ACCOUNT_NAME> # Create a new random keypair

cast wallet new-mnemonic # mnemonic phrase

cast wallet address [PRIVATE_KEY] # private key to an address

cast wallet import -i -k <KEYSTORE_DIR> <ACCOUNT_NAME> #导入一个keystore文件到foundry的keystore目录中,并给这个账户一个别名

cast wallet import --mnemonic "test test test test test test test test test test

test junk" -k <KEYSTORE_DIR> <ACCOUNT_NAME> #基于助记词导入一个keystore文件到foundry的keystore目录中,并给这个账户一个别名

  1. Anvil

    • 本地以太坊节点,秒级启动,内置 10 个预注资测试账户
    • 支持主网分叉、链状态回放、手动调整区块号与时间,方便集成测试
  2. Chisel

    • Solidity REPL,即时编写并执行 Solidity 代码片段,用于快速验证语法或计算哈希、选择器等

主要优势

  • 性能:Rust 实现 + 并行编译,测试速度比 Hardhat/Truffle 快 5×--140×
  • 原生 Solidity 测试:测试与业务合约同语言,减少上下文切换,贴近真实运行环境
  • 内置模糊测试:只需在测试函数加参数即可自动生成数百组随机输入,帮助发现边界漏洞
  • 主网分叉&作弊码:可针对主网状态编写集成测试,并通过"作弊码"模拟时间、余额、存储等
  • 配置灵活:通过 foundry.toml 支持多网络、多环境参数;与 Hardhat 项目结构兼容,迁移成本低

典型工作流(示例)

bash 复制代码
# 1. 初始化项目
forge init my-project && cd my-project

# 2. 编译
forge build

# 3. 本地节点
anvil

# 4. 运行测试(含 fuzz)
forge test

# 5. 部署到主网
forge create src/MyToken.sol:MyToken \
  --rpc-url $RPC_URL --private-key $PRIVATE_KEY --verify \
  --etherscan-api-key $ETHERSCAN_KEY

借助以上命令,开发者可以在几分钟内完成从编写到链上验证的完整流程 。

适用场景

  • 对编译/测试速度要求高的中大型 DeFi、NFT、DAO 项目
  • 需要大量模糊测试、不变量测试或主网分叉集成的协议
  • 偏好 CLI 驱动、脚本化、CI/CD 自动化部署的团队

综上,Foundry 凭借极致性能、全 Rust 工具链与原生 Solidity 测试体验,已成为新一代 EVM 合约开发的首选框架之一。

下面以Counter合约为例,介绍一下如何用 Foundry 做一站式开发:初始化 → 编译 → 测试 → 部署。


1. 环境准备

1.1 启用WSL2并安装Linux发行版

在 Windows 上安装 WSL并使用linux命令处理文本文件

该文已提及,不再敖述。

1.2 安装并配置VSCode

  1. 安装VSCode :从官网下载并安装Visual Studio Code。
  2. 安装Remote-WSL扩展 :在VSCode的扩展市场中搜索并安装 "Remote - WSL" 扩展。这允许VSCode无缝地在WSL环境中操作。
  3. 连接WSL :在VSCode中按下 Ctrl+Shift+P (或 F1),输入 "WSL: Connect to WSL" 即可连接到你的WSL发行版。

1.3 在WSL中安装Foundry

连接到你的WSL发行版终端后,执行以下命令安装Foundry:

bash 复制代码
# 安装Foundryup
curl -L https://foundry.paradigm.xyz | bash

# 重启终端后运行foundryup
foundryup

安装完成后,可以通过运行 forge --versioncast --version 来验证安装是否成功。

🔔 注意:如果你的网络环境访问GitHub或Rust官方源较慢,可能需要进行终端代理配置或寻找国内镜像源。

1.4 解决一个在 WSL 中使用代理时非常常见的警告

wsl: 检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。

  1. 配置 Windows 防火墙

Windows 防火墙默认会阻止来自 WSL 的连接。

* 打开 Windows 的 Windows Defender 防火墙

* 点击 "高级设置"

* 在 入站规则 中,新建一条规则

* 选择 "端口" -> "TCP" -> "特定本地端口" (填写你的代理端口,如 7890)-> "允许连接" -> 全选(域、专用、公用)-> 给规则起个名字(如 "WSL Proxy")。

* 完成后,WSL 就可以畅通地访问 Windows 上的代理端口了。

  1. 在代理软件中开启 "允许局域网连接"

  2. 将以下代码块添加到你的 ~/.bashrc~/.zshrc 中,可以智能地设置代理(自动获取 IP 并设置环境变量):

    bash 复制代码
    # 获取 Windows 主机 IP
    host_ip=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}')
    # 你的代理端口
    proxy_port=7890
    
    # 设置代理环境变量
    export http_proxy="http://$host_ip:$proxy_port"
    export HTTPS_PROXY="http://$host_ip:$proxy_port"
    export ALL_PROXY="http://$host_ip:$proxy_port"
    
    # 可选:添加一些不走代理的地址(如局域网、国内网络)
    export no_proxy="localhost, 127.0.0.0/8, ::1, 192.168.0.0/16, 10.0.0.0/8"
  3. 使用 curl -v www.google.comwget -O- www.google.com 来测试代理是否正常工作。

按照以上步骤操作后,那个警告信息就不会再出现,并且你的 WSL 也能正常使用 Windows 上的代理了。如果上一步Foundry没有安装成功,那么就可以在WSL中重新安装Foundry了。


2. 项目初始化

最好就在home目录下进行, 不然可能遇到权限问题

bash 复制代码
# 在home目录下新建projects目录并进入
mkdir projects && cd projects

# 生成标准骨架
forge init hello_foundry

目录结构(已去掉了 .git 等无关文件):

复制代码
hello_foundry
├── foundry.toml  # 配置文件
├── script/       # 【合约脚本】,可用于部署合约、广播交易
├── src/          # 【合约源代码】
├── test/         # 【测试合约代码】
└── lib/          # 存放依赖库(默认安装 forge-std)
└── cache/        # 缓存信息,在 forge build 后出现
└── out/          # 存放编译输出文件

把事先写好的 Counter.sol 放进 src/,其实项目自带的有Counter合约的相关代码。


3. 安装依赖

OpenZeppelin 是常用的合约库,尽管在本次示例中的Counter合约用不到。

复制代码
# 引入 OpenZeppelin 合约库
forge install OpenZeppelin/openzeppelin-contracts --no-commit

--no-commit 防止把子模块提交到主仓库;安装后 lib/openzeppelin-contracts 会出现完整源码,同时会在.gitmodules文件中出现下文:

复制代码
[submodule "lib/openzeppelin-contracts"]
	path = lib/openzeppelin-contracts
	url = https://github.com/openzeppelin/openzeppelin-contracts

# 更新依赖(很少用)
forge update openzeppelin-contracts

# 删除依赖
forge remove openzeppelin-contracts -f

删除依赖之所以加上-f参数,是为了应对以下报错:

复制代码
Error: git rm exited with code 1:
error: the following file has staged content different from both the
file and the HEAD:
    lib/openzeppelin-contracts
(use -f to force removal)

依赖库的重映射

复制代码
forge remappings > remappings.txt

文件中的@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/

相当于对长路径起了别名起到import时能够简写的效果,写法如下:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";


4. 编译

bash 复制代码
forge build

编译成功会看到:

复制代码
[⠔] Compiling...
[⠒] Solc 0.8.20 finished in 2.15s
Compiler run successful!

产物在 out/ 目录,包含 abi、bytecode、deployedBytecode,后续部署脚本会引用。

forge inspect 以用来查看合约编译产物和元信息,示例如下:


5. 单元测试

测试文件默认用 t.sol 结尾, 非必须

导入Test合约: 提供了基本的日志和断言功能

Setup函数(可选):每个测试用例运行前都调用

前缀为 test 的函数将作为测试用例运行

testFuzz表示 模糊测试,测试用例的参数值,由 foundry 随机抽样

test/Counter.t.sol 写用例:

solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
        counter.setNumber(0);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function testFuzz_SetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
    }
}

跑测试:

bash 复制代码
forge test --mc CounterTest

输出示例:

复制代码
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 29211, ~: 29289)
[PASS] test_Increment() (gas: 28784)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.84ms (3.79ms CPU time)

Ran 1 test suite in 74.17ms (5.84ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
bash 复制代码
forge test - 运行所有测试
forge test --match-contract ContractName - 运行指定合约的测试
forge test --match-test testFunctionName - 运行单个测试函数

其中的test可以简写为t
match-contract可以简写为mc
match-test可以简写为mt

Foundry中 forge test 命令的 -v 选项用于控制测试输出的详细程度,每增加一个 v 就会显示更多调试信息。

具体级别如下:

-v:显示‌基本的详细信息‌,包括测试通过/失败的状态

-vv:显示‌更多详细信息‌,会输出每个测试的日志内容(console.log 信息)

-vvv:显示‌更详细的信息‌,包含内部调试信息,常用于深度调试

-vvvv:显示‌最详细的信息‌,通常用于复杂的调试场景

一般, -vv就可以满足需求


6. 脚本部署

脚本有两种部署方式:forge createforge script

方式1️⃣: forge create
bash 复制代码
forge create Counter --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast

# 最好指定合约的路径和名称
forge create src/Counter.sol:Counter --private-key 0xac0974bec39a17e36ba4a6b4d
238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast
方式2️⃣: forge script
  1. 本地启动: anvil

    (也可以基于指定网络的状态启动一个本地模拟环境: anvil --fork-url <RPC_RUL>)

    默认在127.0.0.1:8545 启动服务, 并会配置 10 账号

  2. script/Counter.s.sol 写部署脚本:

solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";

contract CounterScript is Script {
    Counter public counter;

    function setUp() public {}

    function run() public {
        vm.startBroadcast();
        counter = new Counter();
        console.log("Counter deployed to:", address(counter));
        vm.stopBroadcast();
    }
}
  1. 另起终端执行forge script命令
bash 复制代码
forge script script/Counter.s.sol --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545 --broadcast

成功会看到:

复制代码
== Logs ==
  Counter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
以下是两者的对比:
特性 forge create forge script
基本用途 快速部署单个合约 执行复杂部署脚本
命令复杂度 简单,一行命令 需要编写部署脚本
部署单个合约 ✅ 非常适合 ✅ 可以,但稍复杂
部署多个合约 ❌ 不支持 ✅ 非常适合
事务序列 ❌ 单次交易 ✅ 支持多个有序交易
条件逻辑 ❌ 不支持 ✅ 支持 if/else、循环等
依赖管理 ❌ 手动处理 ✅ 自动处理合约依赖
模拟运行 ❌ 不支持 ✅ 支持 --dry-run
交易重放 ❌ 不支持 ✅ 支持 --resume
错误处理 ❌ 简单失败 ✅ 复杂错误处理逻辑
Gas 优化 ❌ 基础 ✅ 可优化交易顺序
验证集成 ✅ 支持 --verify ✅ 支持 --verify
多网络部署 ❌ 手动切换 ✅ 脚本内自动切换
代码复用 ❌ 无 ✅ 可复用部署逻辑
测试集成 ❌ 分离 ✅ 可与测试结合

对于生产环境和复杂部署,强烈推荐使用 forge script,它提供了更好的灵活性、安全性和可维护性。

脚本部署的技巧1:用 keystore 账号部署

为了更直观地理解区块链钱包私钥的三种主要形式,我们可以做一个简单的类比:

形式 类比 核心要点
私钥 一把原始的、没有任何保护的钥匙 最高权限,但极其脆弱,暴露即丢失。
助记词 一把能配出所有钥匙(私钥)的"母钥匙"模具 备份一次,恢复整个钱包。是私钥的友好形式。
Keystore 一个需要密码才能打开的保险箱,而私钥就在这个保险箱里。 安全性=文件+密码。适合数字存储,但怕密码弱和两者一同丢失。

最佳实践:

  1. 优先备份助记词:这是最可靠、最通用的恢复方式。确保物理保管,且多个副本放在不同的安全地点。
  2. 永远不要泄露三者中的任何一项
  3. 使用Keystore时,务必使用强密码,并确保文件和密码分离保管。
  4. 明白一个核心:无论形式如何,其最终目的都是为了安全地控制和使用那个最终的私钥

那么具体如何使用keystore 账号来部署合约呢?

复制代码
# 导入keystore文件
cast wallet import --mnemonic "guitar resource canvas wish ghost swarm female glad inquiry fish laugh web" -k ~/.foundry/keystores anvil_test_account

# 向地址转账(部署合约需要gas)
cast send --rpc-url http://localhost:8545 --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 0x585528A0cd7C43F2E5480B2650e81d63f762a34A --value 1000ether


# 使用--account指定keystore账号进行部署(需要账户在~/.foundry/keystores目录下)
forge script script/Counter.s.sol --account anvil_test_account --rpc-url http://localhost:8545 --broadcast
forge create Counter --account anvil_test_account --rpc-url http://localhost:8545 --broadcast

# 或者使用--keystore指定文件位置进行部署
forge script script/Counter.s.sol --keystore ~/.foundry/keystores/anvil_test_account --rpc-url http://localhost:8545 --broadcast
forge create Counter --keystore ~/.foundry/keystores/anvil_test_account --rpc-url http://localhost:8545 --broadcast
脚本部署的技巧2:使用环境变量

在.env文件中配置环境变量,如LOCAL_URL = "http://127.0.0.1:8545"

然后source .env应用环境变量,这样就可以把上述部署命令简化了:

复制代码
forge script script/Counter.s.sol --account anvil_test_account --rpc-url $LOCAL_URL --broadcast
脚本部署的技巧3:使用foundry.toml配置节点

rpc_endpoints

sepolia = "${SEPOLIA_RPC_URL}"

复制代码
forge script script/Counter.s.sol --account anvil_test_account --rpc-url sepolia --broadcast
脚本部署的技巧4:使用抽象合约简化部署脚本的编写

定义抽象合约

复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";

abstract contract BaseScript is Script {
    address internal deployer;
    address internal user;
    string internal mnemonic;
    uint256 internal deployerPrivateKey;

    function setUp() public virtual {
        deployerPrivateKey = vm.envUint("PRIVATE_KEY");
    }


    function saveContract(string memory name, address addr) public {
        string memory chainId = vm.toString(block.chainid);
        
        string memory json1 = "key";
        string memory finalJson =  vm.serializeAddress(json1, "address", addr);
        string memory dirPath = string.concat(string.concat("deployments/", name), "_");
        vm.writeJson(finalJson, string.concat(dirPath, string.concat(chainId, ".json"))); 
    }

    modifier broadcaster() {
        vm.startBroadcast(deployerPrivateKey);
        _;
        vm.stopBroadcast();
    }
}

让各个部署脚本都继承BaseScript,以便复用setUp和broadcaster的逻辑

复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";

import "./BaseScript.s.sol";
import {Counter} from "../src/Counter.sol";

contract CounterScript is BaseScript {
    Counter public counter;

    function run() public broadcaster {
        counter = new Counter();
        console.log("Counter deployed on %s", address(counter));

        saveContract("Counter", address(counter));
        
        counter.setNumber(99);
        counter.increment();
    }
}

如果存在权限问题,可以在foundry.toml中增加配置来解决

复制代码
[profile.default]
fs_permissions = [
    { access = "write", path = "./deployments" }
]
脚本部署的技巧5:开源合约代码

在foundry.toml中配置Etherscan API密钥和网络:

复制代码
[etherscan]
# 需要确保将ETHERSCAN_API_KEY环境变量设置为你的Etherscan API密钥
sepolia = { key = "${ETHERSCAN_API_KEY}" }

部署,并验证合约,关键参数是--verify

复制代码
forge script scripts/Counter.s.sol:CounterScript --rpc-url sepolia --broadcast --verify -vvvv

--verify 会自动把源码提交到 Etherscan,几分钟内能看到绿色对勾

示例: sepolia测试网的Counter合约


7. 正式网部署(以 Sepolia 为例)

  1. 准备 RPC 与私钥
    把密钥放进 keystore,避免历史记录泄露:
bash 复制代码
cast wallet import sepolia-key --keystore-dir ~/.foundry/keystores
# 交互式输入私钥与密码
  1. 领水 & 执行

领水地址推荐: google faucet

登录谷歌帐号, 每天可以领取0.5ETH

bash 复制代码
forge script script/Counter.s.sol \
    --rpc-url $SEPOLIA_RPC_URL \
    --account sepolia-key \
    --keystore-dir ~/.foundry/keystores \
    --broadcast --verify -vvvv

至此,用 Foundry 完成 Counter 的全流程就闭环了:

初始化 → 编译 → 测试 → 本地/远程部署。

相关推荐
矶鹬笛手12 分钟前
(2.2) 新一代信息技术及应用
大数据·云计算·区块链·时序数据库
天涯学馆3 小时前
Solidity合约升级:让你的区块链代码永葆青春的硬核攻略
智能合约·solidity
u***096414 小时前
Web3去中心化身份
web3·去中心化·区块链
1***Q7841 天前
Web3去中心化存储
web3·去中心化·区块链
p***43481 天前
Web3在社交网络中的去中心化
web3·去中心化·区块链
OpenBuild.xyz2 天前
加密三重演进:Balaji 眼中的 “去中心化 - 可编程 - 隐私化” 完整链路
去中心化·区块链
掘金安东尼2 天前
Solana 链条:为什么这个“最快的区块链”
区块链
Web3VentureView2 天前
从“信息不对称”到“链上博弈”:解析去中心化投融资协议的机制设计
去中心化·区块链
1***s6323 天前
React区块链开发
前端·react.js·区块链