学习目标: 使用 Foundry 完成开发、测试、调试与部署。
上一课
本课目录
- 1. Foundry简介
- 2. 为什么选择Foundry
- 3. Foundry核心组件
- 4. 安装与配置
- 5. 开发实战:NFT项目
- 6. 测试与调试
- 7. 合约部署与验证
- 8. 链上交互
- 9. 安全研究应用
- 10. 常见问题与最佳实践
- 结语
1. Foundry简介
Foundry官网
Foundry是一个基于Rust编写的智能合约开发工具包,专为以太坊和EVM兼容链设计。它提供了一套完整的开发、测试、部署工具链。
主要特点
- 高性能:基于Rust实现,执行速度远超传统工具
- 原生Solidity测试:直接用Solidity编写测试代码
- 丰富的调试功能:提供详细的执行跟踪和状态信息
- 模块化设计:包含多个专用工具
2. 为什么选择Foundry
2.1 五大核心优势
1. 用Solidity编写测试
传统工具如Truffle使用JavaScript,Brownie使用Python编写测试,存在以下问题:
- 语言抽象层增加复杂性
- JavaScript需要BigNumber库处理大数运算
- 学习成本高,需要掌握多种语言
Foundry直接用Solidity编写测试,保持语言一致性。
2. 模糊测试(Fuzzing)功能
自动生成随机输入参数,发现开发者未考虑到的边界情况:
function testFuzz_AddNumbers(uint256 x) public {
// Foundry会自动生成各种x值进行测试
uint256 result = myContract.addNumber(x);
// 测试逻辑
}
3. VM状态覆盖
可以修改区块链状态进行测试:
- 模拟成为合约owner
- 直接修改代币余额
- 改变区块时间和高度
4. 实时网络状态测试
支持fork主网状态进行测试:
vm.createSelectFork("mainnet", blockNumber);
5. 详细调试信息
提供丰富的日志和执行跟踪,帮助快速定位问题。
3. Foundry核心组件
3.1 Forge
主要的构建和测试工具,功能包括:
- 项目创建和管理
- 合约编译
- 测试执行
- 合约部署
- 代码覆盖率分析
3.2 Cast
命令行RPC交互工具,支持:
- 发送交易
- 调用合约函数
- 查询链上数据
- ABI编解码
- 单位转换
3.3 Anvil
本地测试网节点,类似Ganache但性能更优。
3.4 Chisel
Solidity REPL环境,可以快速测试代码片段。
4. 安装与配置
4.1 安装Foundry
安装Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup
4.2 验证安装
forge --version
cast --version
anvil --version
chisel --version
4.3 更新Foundry
foundryup
4.4 项目初始化
forge init my-project
cd my-project
4.5 项目结构
my-project/
├── lib/ # 依赖库
├── src/ # 合约源码
├── test/ # 测试文件
├── script/ # 部署脚本
├── foundry.toml # 配置文件
└── remappings.txt # 路径映射
4.6 配置文件详解
foundry.toml配置示例:
https://www.getfoundry.sh/config
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"
[rpc_endpoints]
mainnet = "https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY"
goerli = "https://eth-goerli.alchemyapi.io/v2/YOUR-API-KEY"
[fuzz]
runs = 1000
[invariant]
runs = 256
depth = 15
5. 开发实战:NFT项目
5.1 创建项目
forge init nft-project
cd nft-project
5.2 安装依赖
forge install OpenZeppelin/openzeppelin-contracts
forge install transmissions11/solmate
5.3 编写NFT合约
创建src/NFT.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
contract NFT is ERC721, Ownable {
using Strings for uint256;
uint256 public constant MINT_PRICE = 0.08 ether;
uint256 public constant MAX_SUPPLY = 1000;
uint256 public totalSupply;
string private _baseTokenURI;
constructor(
string memory name,
string memory symbol,
string memory baseURI,
address initialOwner
) ERC721(name, symbol) Ownable(initialOwner) {
_baseTokenURI = baseURI;
}
function mintTo(address recipient) public payable returns (uint256) {
require(msg.value == MINT_PRICE, "Insufficient payment");
require(totalSupply < MAX_SUPPLY, "Max supply reached");
uint256 tokenId = totalSupply;
totalSupply++;
_safeMint(recipient, tokenId);
return tokenId;
}
function withdrawPayments() public onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Transfer failed");
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
}
5.4 编译合约
forge build
6. 测试与调试
6.1 编写测试合约
创建test/NFT.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 导入 Foundry 测试框架
import "forge-std/Test.sol";
// 导入被测试的 NFT 合约
import "../src/NFT.sol";
/**
* @title NFTTest
* @dev NFT 合约的测试套件
* @notice 继承 Foundry 的 Test 合约以获得测试功能和 cheat codes
*/
contract NFTTest is Test {
// 被测试的 NFT 合约实例
NFT public nft;
// 测试用地址:合约所有者
address public owner = address(0x1);
// 测试用地址:普通用户
address public user = address(0x2);
/**
* @dev 测试设置函数,在每个测试函数执行前自动调用
* @notice 初始化测试环境,部署 NFT 合约
*/
function setUp() public {
// 使用 cheat code 模拟 owner 地址发起交易
vm.prank(owner);
// 部署 NFT 合约,设置名称、符号和基础 URI
nft = new NFT("Test NFT", "TNFT", "https://example.com/");
}
/**
* @dev 测试成功铸造 NFT 的场景
* @notice 验证用户支付正确金额后能成功铸造 NFT
*/
function testMintSuccess() public {
// 模拟用户地址发起交易
vm.prank(user);
// 给用户地址分配 1 ether 余额
vm.deal(user, 1 ether);
// 用户支付 0.08 ether 铸造 NFT 给自己
uint256 tokenId = nft.mintTo{value: 0.08 ether}(user);
// 断言:验证 NFT 的所有者是用户
assertEq(nft.ownerOf(tokenId), user);
// 断言:验证总供应量为 1
assertEq(nft.totalSupply(), 1);
}
/**
* @dev 测试支付不足时铸造失败的场景
* @notice testFail 前缀表示期望这个测试会失败(revert)
* @notice 验证支付金额不足时交易会回滚
*/
function test_RevertWhen_MintInsufficientPayment() public {
// 模拟用户地址发起交易
vm.deal(user, 1 ether);
vm.prank(user);
vm.expectRevert("Insufficient payment");
// 尝试用 0.01 ether(不足)铸造 NFT,期望失败
nft.mintTo{value: 0.01 ether}(user);
}
/**
* @dev 测试只有所有者可以提取资金的场景
* @notice 验证访问控制和资金提取功能
*/
function testWithdrawOnlyOwner() public {
// === 第一步:生成收入 ===
// 模拟用户铸造 NFT 以产生合约收入
vm.prank(user);
vm.deal(user, 1 ether);
nft.mintTo{value: 0.08 ether}(user);
// === 第二步:提取资金 ===
// 只有 owner 可以提取合约中的资金
vm.prank(owner);
nft.withdrawPayments();
// === 验证结果 ===
// 断言:合约余额应该为 0(资金已被提取)
assertEq(address(nft).balance, 0);
}
/**
* @dev 模糊测试:测试非正确价格的所有情况
* @param price 随机生成的价格参数
* @notice 使用模糊测试验证只有正确价格才能铸造成功
*/
function testFuzz_MintPrice(uint256 price) public {
// 假设条件:排除正确的铸造价格 0.08 ether
vm.assume(price != 0.08 ether);
// 模拟用户交易
vm.prank(user);
// 给用户分配对应金额的余额
vm.deal(user, price);
// 期望交易回滚,并显示特定错误消息
vm.expectRevert("Insufficient payment");
// 尝试用错误的价格铸造 NFT
nft.mintTo{value: price}(user);
}
}
6.2 运行测试
# 基本测试
forge test
# 详细输出
forge test -vvv
# 运行特定测试
forge test --match-test testMintSuccess
6.3 模糊测试示例
添加一个容易出错的函数到NFT合约:
function addNumbers(uint256 x) public pure returns (uint256) {
return x + 1000;
}
测试函数:
function testFuzz_AddNumbers(uint256 x) public {
uint256 result = nft.addNumbers(x);
assertGt(result, x); // 这会在溢出时失败
}
6.4 调试功能
使用forge debug调试失败的交易:
forge debug --target-contract NFT --sig "mintTo(address)" 0x123...
6.5 Gas报告
forge test --gas-report
7. 合约部署与验证
7.1 部署到测试网
首先设置环境变量:
export PRIVATE_KEY=0x...
export GOERLI_RPC_URL=https://eth-goerli.alchemyapi.io/v2/YOUR-API-KEY
部署合约:
forge create --rpc-url $GOERLI_RPC_URL \
--private-key $PRIVATE_KEY \
--constructor-args "My NFT" "MNFT" "https://api.example.com/metadata/" \
src/NFT.sol:NFT
7.2 验证合约代码
forge verify-contract \
--chain-id 5 \
--num-of-optimizations 200 \
--watch \
--constructor-args $(cast abi-encode "constructor(string,string,string)" "My NFT" "MNFT" "https://api.example.com/metadata/") \
--etherscan-api-key $ETHERSCAN_API_KEY \
0x... \
src/NFT.sol:NFT
7.3 使用脚本部署
创建script/Deploy.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/NFT.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
NFT nft = new NFT(
"Scripted NFT",
"SNFT",
"https://api.example.com/metadata/"
);
console.log("NFT deployed to:", address(nft));
vm.stopBroadcast();
}
}
运行脚本:
forge script script/Deploy.s.sol:DeployScript --rpc-url $GOERLI_RPC_URL --broadcast
8. 链上交互
8.1 使用Cast进行链上交互
查询合约状态
# 查询总供应量
cast call $CONTRACT_ADDRESS "totalSupply()" --rpc-url $GOERLI_RPC_URL
# 查询NFT所有者
cast call $CONTRACT_ADDRESS "ownerOf(uint256)" 0 --rpc-url $GOERLI_RPC_URL
发送交易
# Mint NFT
cast send $CONTRACT_ADDRESS \
"mintTo(address)" $USER_ADDRESS \
--value 0.08ether \
--private-key $PRIVATE_KEY \
--rpc-url $GOERLI_RPC_URL
8.2 Fork主网进行测试
在测试中fork主网:
function testForkMainnet() public {
// Fork主网到特定区块
vm.createSelectFork("mainnet", 17_000_000);
// 现在可以与主网合约交互
IERC20 usdc = IERC20(0xA0b86a33E6441e5f3e8b8e5e5e5e5e5e5e5e5e5);
uint256 balance = usdc.balanceOf(0x...);
console.log("USDC Balance:", balance);
}
8.3 使用Anvil本地节点
启动本地节点:
anvil --fork-url $MAINNET_RPC_URL --fork-block-number 17000000
9. 安全研究应用
9.1 事件复现示例
以下是一个安全事件复现的示例结构:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
interface IVulnerableContract {
function vulnerableFunction(bytes32 root) external returns (bool);
}
contract ExploitTest is Test {
IVulnerableContract target;
function setUp() public {
// Fork到攻击发生前的区块
vm.createSelectFork("mainnet", 16_800_000);
target = IVulnerableContract(0x...);
}
function testExploit() public {
console.log("Balance before:", address(this).balance);
// 复现攻击逻辑
bool result = target.vulnerableFunction(bytes32(0));
assertTrue(result);
console.log("Balance after:", address(this).balance);
}
}
9.2 使用Cheat Codes
常用的cheat codes:
// 改变调用者
vm.prank(address);
// 改变时间
vm.warp(timestamp);
// 改变区块高度
vm.roll(blockNumber);
// 设置余额
vm.deal(address, amount);
// 修改存储
vm.store(address, slot, value);
// 期望回滚
vm.expectRevert("Error message");
9.3 高级调试技巧
使用vm.getCode部署旧合约:
function deployOldContract() internal {
bytes memory bytecode = vm.getCode("OldContract.sol:OldContract");
bytes memory initcode = abi.encodePacked(bytecode, abi.encode(arg1, arg2));
address deployed;
assembly {
deployed := create2(0, add(initcode, 0x20), mload(initcode), salt)
}
// 或者直接设置到特定地址
vm.etch(targetAddress, bytecode);
}
10. 常见问题与最佳实践
10.1 常见问题
编译错误
- 检查Solidity版本兼容性
- 确认依赖库正确安装
- 查看remappings.txt配置
测试失败
- 使用-vvv参数获取详细日志
- 检查gas限制设置
- 验证测试逻辑和断言
部署问题
- 确认私钥和RPC URL正确
- 检查账户余额是否足够
- 验证构造函数参数
10.2 最佳实践
项目结构
project/
├── src/
│ ├── interfaces/
│ ├── libraries/
│ └── contracts/
├── test/
│ ├── unit/
│ ├── integration/
│ └── fuzz/
├── script/
└── lib/
测试策略
- 单元测试:测试单个函数
- 集成测试:测试合约交互
- 模糊测试:发现边界情况
- 不变量测试:验证系统属性
安全考虑
- 使用环境变量存储敏感信息
- 在测试网充分测试后再部署主网
- 定期更新依赖库
- 进行代码审计
10.3 性能优化
编译优化
[profile.default]
optimizer = true
optimizer_runs = 200
测试优化
[fuzz]
runs = 10000
max_test_rejects = 65536
10.4 与其他工具集成
与Hardhat集成
Foundry可以与现有Hardhat项目集成,保持兼容性。
CI/CD集成
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run tests
run: forge test
结语
Foundry是一个功能强大的智能合约开发工具,特别适合:
- 需要高性能测试的项目
- 安全研究和漏洞分析
- 复杂的DeFi协议开发
- 需要精确控制测试环境的场景
通过本教程,你应该已经掌握了Foundry的核心功能和使用方法。建议在实际项目中多加练习,逐步熟悉各种高级功能。
推荐资源
🛠️
