Foundry 智能合约开发教程
Foundry 智能合约开发教程
Foundry 是用 Rust 编写的高性能以太坊开发工具链,以极快的编译速度和强大的测试功能著称。与 Hardhat 相比,Foundry 用 Solidity 编写测试,无需 JavaScript,深受智能合约开发者喜爱。
Foundry 简介
Foundry 包含以下核心工具:
| 工具 | 说明 |
|---|---|
| Forge | 智能合约编译、测试和部署工具 |
| Cast | 与链交互的命令行工具 |
| Anvil | 本地以太坊节点(类似 Hardhat Node) |
| Chisel | Solidity REPL 交互环境 |
安装
# 安装 Foundryup(Foundry 安装管理器)curl -L https://foundry.paradigm.xyz | bash
# 安装最新版本的 Foundryfoundryup
# 验证安装forge --versioncast --versionanvil --version创建项目
# 创建新项目forge init my-foundry-projectcd my-foundry-project
# 项目结构ls -lamy-foundry-project/├── src/ # 智能合约源文件│ └── Counter.sol├── test/ # 测试文件(用 Solidity 编写)│ └── Counter.t.sol├── script/ # 部署脚本│ └── Counter.s.sol├── lib/ # 依赖库(git submodules)│ └── forge-std/└── foundry.toml # 配置文件配置文件
[profile.default]src = "src"out = "out"libs = ["lib"]solc = "0.8.24"
# 优化设置optimizer = trueoptimizer_runs = 200
# 测试设置verbosity = 2ffi = false
# Gas 报告gas_reports = ["*"]
[profile.default.rpc_endpoints]mainnet = "${MAINNET_RPC_URL}"sepolia = "${SEPOLIA_RPC_URL}"
[profile.default.etherscan]mainnet = { key = "${ETHERSCAN_API_KEY}" }sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.etherscan.io/api" }编写合约
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@openzeppelin/contracts/access/Ownable.sol";
contract Token is ERC20, Ownable { uint256 public constant MAX_SUPPLY = 1_000_000 ether;
error ExceedsMaxSupply(uint256 requested, uint256 available);
event TokensMinted(address indexed to, uint256 amount);
constructor(string memory name, string memory symbol, address initialOwner) ERC20(name, symbol) Ownable(initialOwner) { _mint(initialOwner, 100_000 ether); }
function mint(address to, uint256 amount) external onlyOwner { uint256 remaining = MAX_SUPPLY - totalSupply(); if (amount > remaining) { revert ExceedsMaxSupply(amount, remaining); } _mint(to, amount); emit TokensMinted(to, amount); }}安装依赖
# 安装 OpenZeppelinforge install OpenZeppelin/openzeppelin-contracts
# 或通过 npm 包映射# 在 foundry.toml 中添加:# remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]编译合约
forge build
# 输出编译结果[⠒] Compiling...[⠢] Compiling 15 files with 0.8.24[⠆] Solc 0.8.24 finished in 2.34sCompiler run successful!编写测试(Forge Test)
Foundry 的独特之处在于测试完全用 Solidity 编写:
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "forge-std/Test.sol";import "../src/Token.sol";
contract TokenTest is Test { Token public token; address public owner; address public alice; address public bob;
// 每个测试前执行 function setUp() public { owner = makeAddr("owner"); alice = makeAddr("alice"); bob = makeAddr("bob");
vm.prank(owner); // 模拟 owner 发起下一个调用 token = new Token("My Token", "MTK", owner); }
// ==================== 基础功能测试 ====================
function test_InitialState() public view { assertEq(token.name(), "My Token"); assertEq(token.symbol(), "MTK"); assertEq(token.owner(), owner); assertEq(token.totalSupply(), 100_000 ether); assertEq(token.balanceOf(owner), 100_000 ether); }
function test_Transfer() public { uint256 amount = 1000 ether;
vm.prank(owner); token.transfer(alice, amount);
assertEq(token.balanceOf(alice), amount); assertEq(token.balanceOf(owner), 100_000 ether - amount); }
// ==================== 铸造测试 ====================
function test_Mint() public { uint256 amount = 5000 ether;
vm.prank(owner); vm.expectEmit(true, false, false, true); emit Token.TokensMinted(alice, amount);
token.mint(alice, amount);
assertEq(token.balanceOf(alice), amount); assertEq(token.totalSupply(), 100_000 ether + amount); }
function test_RevertWhen_NonOwnerMints() public { vm.prank(alice); vm.expectRevert(); // 期望 revert token.mint(bob, 1000 ether); }
function test_RevertWhen_MintExceedsMaxSupply() public { uint256 overAmount = token.MAX_SUPPLY() - token.totalSupply() + 1;
vm.prank(owner); vm.expectRevert( abi.encodeWithSelector( Token.ExceedsMaxSupply.selector, overAmount, token.MAX_SUPPLY() - token.totalSupply() ) ); token.mint(alice, overAmount); }
// ==================== 模糊测试(Fuzz Testing) ====================
function testFuzz_Transfer(uint256 amount) public { // bound 限制随机值的范围 amount = bound(amount, 0, token.balanceOf(owner));
vm.prank(owner); token.transfer(alice, amount);
assertEq(token.balanceOf(alice), amount); }
function testFuzz_MintAndTransfer(address to, uint256 mintAmount) public { vm.assume(to != address(0)); vm.assume(to != owner); mintAmount = bound(mintAmount, 1, token.MAX_SUPPLY() - token.totalSupply());
vm.prank(owner); token.mint(to, mintAmount);
assertGe(token.balanceOf(to), mintAmount); }
// ==================== 不变量测试(Invariant Testing) ==================== // 在 test/invariant/ 目录中编写不变量测试
// ==================== Gas 测试 ====================
function test_GasTransfer() public { vm.prank(owner); uint256 gasBefore = gasleft(); token.transfer(alice, 1000 ether); uint256 gasAfter = gasleft(); console.log("Transfer Gas:", gasBefore - gasAfter); }}运行测试
# 运行所有测试forge test
# 详细输出forge test -vvv
# 运行特定测试forge test --match-test test_Mint
# 运行特定合约的测试forge test --match-contract TokenTest
# 运行模糊测试(增加运行次数)forge test --fuzz-runs 10000
# Gas 快照forge snapshot
# 与上次快照对比forge snapshot --diff输出示例:
Running 8 tests for test/Token.t.sol:TokenTest[PASS] testFuzz_MintAndTransfer(address,uint256) (runs: 256, μ: 95432, ~: 95432)[PASS] testFuzz_Transfer(uint256) (runs: 256, μ: 45231, ~: 45231)[PASS] test_GasTransfer() (gas: 48234)[PASS] test_InitialState() (gas: 12345)[PASS] test_Mint() (gas: 67890)[PASS] test_RevertWhen_MintExceedsMaxSupply() (gas: 23456)[PASS] test_RevertWhen_NonOwnerMints() (gas: 18901)[PASS] test_Transfer() (gas: 48234)Test result: ok. 8 passed; 0 failed; 0 skipped; finished in 2.34s部署脚本
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "forge-std/Script.sol";import "../src/Token.sol";
contract DeployToken is Script { function run() external returns (Token token) { // 从环境变量获取部署者私钥 uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey);
console.log("部署者地址:", deployer); console.log("部署者余额:", deployer.balance);
// 开始广播交易 vm.startBroadcast(deployerPrivateKey);
token = new Token("My Token", "MTK", deployer);
vm.stopBroadcast();
console.log("Token 合约地址:", address(token)); }}执行部署
# 部署到本地 Anvilforge script script/DeployToken.s.sol --rpc-url http://localhost:8545 --broadcast
# 部署到 Sepolia 测试网forge script script/DeployToken.s.sol \ --rpc-url $SEPOLIA_RPC_URL \ --broadcast \ --verify \ -vvvv
# 模拟部署(不广播)forge script script/DeployToken.s.sol --rpc-url $SEPOLIA_RPC_URLCast 命令行工具
Cast 是与区块链交互的强大命令行工具:
# 查询余额cast balance 0xAddress --rpc-url $MAINNET_RPC_URL
# 发送交易cast send 0xContractAddress "transfer(address,uint256)" 0xTo 1000ether \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_RPC_URL
# 调用只读函数cast call 0xTokenAddress "balanceOf(address)(uint256)" 0xAddress \ --rpc-url $MAINNET_RPC_URL
# 解码 ABIcast abi-decode "transfer(address,uint256)(bool)" 0x0000...0001
# 计算函数选择器cast sig "transfer(address,uint256)"# 输出: 0xa9059cbb
# 转换单位cast to-wei 1 ethercast from-wei 1000000000000000000
# 查询区块信息cast block latest --rpc-url $MAINNET_RPC_URL
# 获取交易信息cast tx 0xTxHash --rpc-url $MAINNET_RPC_URLAnvil 本地节点
# 启动本地节点anvil
# 分叉主网anvil --fork-url $MAINNET_RPC_URL
# 指定区块分叉anvil --fork-url $MAINNET_RPC_URL --fork-block-number 19000000
# 自定义账户数量和余额anvil --accounts 10 --balance 1000
# 设置区块时间anvil --block-time 12合约验证
# 验证已部署的合约forge verify-contract \ 0xDeployedAddress \ src/Token.sol:Token \ --constructor-args $(cast abi-encode "constructor(string,string,address)" "My Token" "MTK" "0xOwner") \ --chain sepolia \ --etherscan-api-key $ETHERSCAN_API_KEYChisel(Solidity REPL)
# 启动 Chiselchisel
# 在 REPL 中测试 Solidity 代码> uint256 x = 100> x * 2200> address(0x1234)0x0000000000000000000000000000000000001234Foundry vs Hardhat 对比
| 特性 | Foundry | Hardhat |
|---|---|---|
| 编写语言 | Solidity(测试)+ Rust | JavaScript/TypeScript |
| 编译速度 | 极快 | 较慢 |
| 测试速度 | 极快 | 较慢 |
| 模糊测试 | 内置支持 | 需要插件 |
| 插件生态 | 较少 | 丰富 |
| 学习曲线 | 需要熟悉 Solidity | JS 开发者友好 |
| 调试体验 | 强大 | 强大 |
总结
Foundry 是现代以太坊开发的优选工具:
- Forge:编译、测试、部署一站式解决方案
- Cast:命令行链上交互,无需编写脚本
- Anvil:高性能本地节点,支持主网分叉
- 模糊测试:内置属性测试,自动发现边界问题
- 速度:Rust 编写,比 Hardhat 快 10-100 倍
推荐从 Foundry 开始学习智能合约开发,配合 forge-std 标准库可以高效编写安全的合约和测试。