Foundry简介与核心理念
Foundry 是一个用 Rust 编写的现代化、快速的以太坊开发工具包。它与Hardhat的主要区别在于:完全用Solidity编写测试,无需JavaScript/TypeScript,为Solidity开发者提供了原生开发体验。
核心优势
-
极速测试:Rust编写的测试运行器比JavaScript测试快10-100倍
-
内置Fuzzing测试:自动生成随机输入测试你的合约
-
强大的作弊码(Cheatcodes) :提供
vm对象模拟各种区块链状态 -
命令行工具集成:Cast(合约交互)、Anvil(本地节点)、Forge(测试/构建)
安装Foundry
1. 系统要求
-
Rust环境(Foundry基于Rust,但安装时会自动处理)
-
Git(用于克隆模板和安装)
-
命令行终端
2. 一键安装(推荐)
打开终端,执行以下命令:
bash
# 使用foundryup一键安装(macOS/Linux)
curl -L https://foundry.paradigm.xyz | bash
# 然后重启终端,或运行:
source ~/.bashrc # 或 source ~/.zshrc
# 最后运行安装
foundryup
# Windows用户使用PowerShell
powershell -c "irm https://foundry.paradigm.xyz | iex"
3. 验证安装
bash
forge --version
cast --version
anvil --version
看到版本号输出即表示安装成功。
Foundry项目结构详解
让我们创建一个完整的Foundry项目并理解其结构:
bash
# 1. 初始化新项目
forge init hello-foundry
cd hello-foundry
# 2. 查看生成的项目结构
tree -L 2
典型的Foundry项目结构如下:
bash
hello-foundry/
├── src/ # 智能合约源代码
│ └── Counter.sol # 示例合约
├── test/ # 测试文件(也用Solidity写!)
│ └── Counter.t.sol # 对应测试合约
├── script/ # 部署脚本
│ └── Counter.s.sol # 部署脚本示例
├── lib/ # 依赖库(git submodules)
├── foundry.toml # 项目配置文件
└── .gitmodules # Git子模块配置
核心工具实战演示
1. Forge - 构建与测试工具
bash
# 编译合约
forge build
# 运行所有测试
forge test
# 详细模式运行测试(查看Gas消耗、追踪)
forge test -vvv
# 运行特定测试合约
forge test --match-contract CounterTest
# 运行特定测试函数
forge test --match-test testIncrement
2. Anvil - 本地开发节点
bash
# 启动本地测试节点(默认端口8545)
anvil
# 或自定义端口
anvil --port 8546
# 启动时预分配测试账户和ETH
anvil --mnemonic "test test test test test test test test test test test junk"
3. Cast - 合约交互工具
bash
# 调用只读函数
cast call <合约地址> "functionName()"
# 发送交易
cast send <合约地址> "functionName(uint256)" 123 --private-key <私钥>
# 编码/解码数据
cast calldata "increment()"
cast --from-utf8 "Hello Foundry"
完整开发流程DEMO:计数器合约
步骤1:编写智能合约
创建 src/Counter.sol:
javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title 一个简单的计数器合约
/// @notice 演示Foundry开发流程
contract Counter {
uint256 private count;
address public owner;
// 事件
event Incremented(address indexed sender, uint256 newCount);
event Reset(address indexed sender);
// 错误定义
error NotOwner();
constructor(uint256 _initialCount) {
count = _initialCount;
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
/// @notice 获取当前计数值
function get() public view returns (uint256) {
return count;
}
/// @notice 计数器加1
function inc() public {
count += 1;
emit Incremented(msg.sender, count);
}
/// @notice 计数器加指定值
function incBy(uint256 x) public {
count += x;
emit Incremented(msg.sender, count);
}
/// @notice 重置计数器(仅所有者)
function reset() public onlyOwner {
count = 0;
emit Reset(msg.sender);
}
}
步骤2:编写Solidity测试
创建 test/Counter.t.sol(Foundry测试约定使用.t.sol后缀):
javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol"; // 引入Foundry测试库
import "../src/Counter.sol";
contract CounterTest is Test {
Counter counter;
address owner = address(0x123);
address user = address(0x456);
// 在每个测试用例前运行
function setUp() public {
vm.prank(owner); // 作弊码:模拟下一个调用者
counter = new Counter(10); // 初始值10
}
// 测试1:验证初始状态
function testInitialCount() public {
assertEq(counter.get(), 10, "初始值应为10");
assertEq(counter.owner(), owner, "所有者应正确设置");
}
// 测试2:测试inc()函数
function testIncrement() public {
vm.prank(user);
counter.inc();
assertEq(counter.get(), 11, "inc()后应变为11");
}
// 测试3:测试incBy()函数
function testIncrementBy() public {
vm.prank(user);
counter.incBy(5);
assertEq(counter.get(), 15, "incBy(5)后应变为15");
}
// 测试4:测试权限(只有所有者能reset)
function testResetPermission() public {
// 非所有者调用应失败
vm.prank(user);
vm.expectRevert(Counter.NotOwner.selector);
counter.reset();
// 所有者调用应成功
vm.prank(owner);
counter.reset();
assertEq(counter.get(), 0, "reset后应变为0");
}
// 测试5:Fuzzing测试 - 自动生成随机输入
function testFuzzIncrementBy(uint256 x) public {
vm.assume(x <= type(uint256).max - counter.get()); // 避免溢出
uint256 initial = counter.get();
vm.prank(user);
counter.incBy(x);
assertEq(counter.get(), initial + x, "incBy(x)应正确增加");
}
// 测试6:测试事件发射
function testEventEmission() public {
vm.prank(user);
// 预期会发出Incremented事件
vm.expectEmit(true, false, false, false);
emit Incremented(user, 11);
counter.inc();
}
// 测试7:Gas消耗测试
function testGasCost() public {
vm.prank(user);
uint256 gasStart = gasleft();
counter.inc();
uint256 gasUsed = gasStart - gasleft();
console.log("Gas used for inc():", gasUsed);
assertLt(gasUsed, 50000, "inc()的Gas消耗应小于50000");
}
}
步骤3:运行测试
bash
# 运行所有测试
forge test
# 带颜色和详细输出
forge test -vv
# 只运行特定测试
forge test --match-test testIncrement
# 带Gas报告
forge test --gas-report
步骤4:编写部署脚本
javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/Counter.sol";
contract CounterScript is Script {
function run() public {
// 获取部署者私钥
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// 开始广播交易(模拟真实部署)
vm.startBroadcast(deployerPrivateKey);
// 部署合约,初始值为100
Counter counter = new Counter(100);
// 记录部署地址
console.log("Counter deployed at:", address(counter));
vm.stopBroadcast();
}
}
步骤5:部署到测试网
bash
# 1. 首先设置环境变量(私钥和RPC URL)
export PRIVATE_KEY=0x你的私钥
export RPC_URL=https://sepolia.infura.io/v3/你的项目ID
# 2. 部署到Sepolia测试网
forge script script/Counter.s.sol:CounterScript \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
-vvvv
# 3. 或者使用交互模式(更安全)
forge script script/Counter.s.sol:CounterScript \
--rpc-url $RPC_URL \
--interactive
步骤6:与已部署合约交互
bash
# 假设合约地址为 0x742d35Cc6634C0532925a3b844Bc9e...
export CONTRACT_ADDRESS=0x742d35Cc6634C0532925a3b844Bc9e...
# 1. 调用只读函数get()
cast call $CONTRACT_ADDRESS "get()" --rpc-url $RPC_URL
# 2. 发送交易调用inc()
cast send $CONTRACT_ADDRESS "inc()" \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
# 3. 编码调用数据
cast calldata "incBy(uint256)" 25
oundry配置文件解析
创建 foundry.toml 进行个性化配置:
XML
[profile.default]
# 编译器设置
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.19"
optimizer = true
optimizer_runs = 200
via_ir = false # 是否启用IR优化器
# 测试设置
fuzz_runs = 256 # Fuzzing测试运行次数
invariant_runs = 256 # 不变性测试运行次数
# 格式器设置
auto_detect_solhint = true
auto_detect_remappings = true
# 网络设置(在脚本和测试中使用)
[rpc_endpoints]
sepolia = "https://sepolia.infura.io/v3/${INFURA_API_KEY}"
mainnet = "https://mainnet.infura.io/v3/${INFURA_API_KEY}"
localhost = "http://localhost:8545"
[fmt]
line_length = 120
Foundry vs Hardhat 对比
| 特性 | Foundry | Hardhat |
|---|---|---|
| 测试语言 | Solidity | JavaScript/TypeScript |
| 测试速度 | ⚡ 极快(Rust) | 中等(Node.js) |
| Fuzzing测试 | ✅ 内置支持 | ❌ 需要插件 |
| 调试体验 | 优秀的堆栈追踪 | 良好的堆栈追踪 |
| 学习曲线 | 较陡(需懂Solidity测试) | 平缓(对前端友好) |
| 社区生态 | 快速增长 | 成熟完善 |
| 部署脚本 | Solidity | JavaScript |
最佳实践建议
-
1、充分利用作弊码:
javascript
// 常用作弊码示例
vm.startPrank(user); // 模拟用户地址
vm.deal(user, 10 ether); // 给用户10 ETH
vm.warp(block.timestamp + 1 days); // 时间旅行
vm.roll(block.number + 100); // 增加区块号
2、组织测试结构
3.
javascript
contract ComplexTest is Test {
using stdStorage for StdStorage;
function setUp() public {
// 初始化复杂状态
}
function testFunction1() public { /* ... */ }
function testFunction2() public { /* ... */ }
// 使用内联汇编进行低级测试
function testAssembly() public {
bytes memory code = hex"600a600052";
address addr;
assembly {
addr := create(0, add(code, 0x20), mload(code))
}
}
}
3、性能优化技巧
4.
bash
# 并行运行测试(利用多核CPU)
forge test --mt testIncrement --fork-url $RPC_URL -j 4
# 缓存编译结果
forge build --force --via-ir
# 生成覆盖率报告
forge coverage --report lcov
常见问题排查
bash
# 1. 如果遇到"error: no such subcommand: `foundryup`"
# 手动安装:
curl -L https://foundry.paradigm.xyz | bash
foundryup
# 2. 解决依赖问题
forge install openzeppelin/openzeppelin-contracts
# 3. 更新所有依赖
forge update
# 4. 清理构建缓存
forge clean
# 5. 查看测试追踪(调试失败测试)
forge test -vvv --match-test testFailing
Foundry是一个强大且发展迅速的工具链。它最大的优势是让Solidity开发者能够用同一种语言完成开发、测试和部署的全流程。从简单的计数器开始,逐步尝试更复杂的合约和测试模式,你会发现它的效率远超传统工具。
接下来,你可以尝试用Foundry实现一个ERC20代币、一个多签钱包或一个简单的DEX,这些都是很好的练习项目。