尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Foundry 实战指南:打造高速 Solidity 智能合约开发与测试链路

发布时间:2026/9/24 19:35:20

资讯中心
01
ARTICLE

Foundry 实战指南:打造高速 Solidity 智能合约开发与测试链路

Foundry 实战指南:打造高速 Solidity 智能合约开发与测试链路
如果你最近在关注 Solidity 开发那大概率绕不开 Foundry 这个名字。作为一套用 Rust 写成的以太坊智能合约开发工具链Foundry 从一问世就打出了“快”这张牌而且它不是单纯地快一点从我自己的实际体验来看跑同一套测试之前用 JavaScript 系工具可能要等四五秒才能出结果Foundry 基本上秒出体感差别非常明显。这篇文章我不会照着官方文档给你念一遍而是按我自己从零上手到真正在几个项目里把 Foundry 用起来的完整路径把那些文档里没写透、或者容易踩坑的地方都翻出来说说。我默认看这篇文章的朋友对 Solidity 本身有一定基础知道构造函数、事件、修饰器这些概念但未必熟悉 Foundry 的工作方式。如果你完全没写过合约建议先找一份 Solidity 入门教程把语言基础过一遍再回来读会更顺畅。另外Foundry 这个名字本身就带一点“铸造厂”的意思用来比喻写代码到链上部署这条流水线还挺贴切。1. Foundry 是什么一套用 Rust 打造的智能合约开发工具箱1.1 先理解 Foundry 在解决什么问题智能合约开发最大的痛点之一是“改代码—跑测试—看结果”这个循环太慢。传统工具链大多基于 Node.js启动要加载一堆依赖编译要经过 JavaScript 层转译测试跑起来还经常因为异步调用或者类型转换出一些莫名其妙的怪问题。Foundry 的设计目标很直接要用更快的编译、更快地测试、更直接的链上交互把开发者的核心迭代周期压缩下来。它并不是一个新的智能合约语言也不是新的底层虚拟机而是一套围绕 Solidity 开发的“外围工具集”。你可以把合约本身理解成一辆需要试跑的赛车Foundry 就是那个试车场、维修区和仪器仪表一体的工具箱。代码跑在哪个链上不重要重要的是你能不能高效地验证它、调试它然后把它送上线。我刚切到 Foundry 的时候最直观的感受就是“等待变少了”。过去写一个小的权限控制函数写完测试跑一下盯着终端转圈半分钟是家常便饭现在同样的逻辑基本刚回车结果就出来了这些节省下来的碎片时间积累起来非常可观。1.2 四个核心组件forge、cast、anvil、chiselFoundry 不是一个单一的程序它是一个工具集。日常接触最多的四个成员分别是 forge、cast、anvil、chisel每个负责一条完整链路里的不同环节forge 是核心中的核心处理项目初始化、依赖管理、编译、测试和部署这些主流程。cast 是一个命令行交互工具专门用来对链发起调用比如查余额、发送交易、解析合约数据。anvil 是本地开发节点相当于在你电脑上跑一条迷你区块链专供开发和测试使用。chisel 是一个 Solidity 交互式 REPL 环境你可以像在 Python 终端里敲命令一样快速验证某一段 Solidity 表达式的输出结果。如果你之前用过 Hardhat可以把 forge 理解为 Hardhat 本体cast 对应 ethers.js 里那些链上交互脚本anvil 对应 hardhat nodechisel 则像是一个专门为 Solidity 准备的 console。它不是要取代 Solidity而是让整个外围工具链都围绕 Solidity 重新构建一次。1.3 和 Hardhat/Truffle 这类工具的主要差异我用了很长时间 Hardhat所以切换时格外关注差异点。最大的区别在于测试语言。Hardhat 默认用 JavaScript 或 TypeScript 配合 ethers.js / waffle 写测试而 Foundry 允许你直接用 Solidity 写测试。不要小看这个差异它直接把心智负担砍掉了一半。另一个差异是底层实现。Foundry 是 Rust 写的原生程序启动速度快增量编译做得好。Hardhat 跑在 Node.js 环境里性能和启动速度天然吃亏。尤其是在合约多、依赖多的大型项目里全量编译一次两者耗时可能差一个数量级。还有一个值得注意的点Foundry 是三明治结构的“集成式”思路编译、测试、部署、链上交互用一套工具链贯穿而 Hardhat 生态更多是插拔式需要装配各种插件来实现不同功能。集成的好处是开箱即用不用为了一个功能去选择和调试插件坏处是插件生态没有 JavaScript 世界那么丰富某些特殊需求可能需要等官方更新或自己写代码绕过。2. 我为什么从 Hardhat 换到 Foundry2.1 速度快背后的工程原因很多人问 Foundry 为什么快其实答案没有多玄妙底层用 Rust 实现编译出来的原生程序启动开销小执行效率高。但在工程层面它还把一件事做得很细就是增量编译。传统工具在编译时会全量扫描所有源文件哪怕你只改了一个常量也要把整个项目的合约重编一遍。Foundry 则会对文件做依赖分析只重新编译受影响的部分。在一个中型项目里改了顶层库文件后Hardhat 可能耗时十几秒Foundry 经常两秒内就完成了。如果你想验证这个结论可以在项目里跑一下forge build第一次会慢一些因为要全量编译第二次再跑你会发现输出里明确写着“compiling 0 files”基本是秒过。这种体验在长时间开发工作里帮助极其明显。2.2 用 Solidity 写测试让心智负担骤减传统模式下合约开发和测试要维护两套语言。Solidity 里的 uint256 和 JavaScript 里的 BigNumber 不是一回事测试脚本经常要在两种类型系统之间来回转换。遇到嵌套调用或者事件断言还得小心翼翼地处理异步和上下文出错时排查起来相当割裂。Foundry 把测试写成一个 Solidity 合约断言直接使用 assertEq、assertGt 这类内置方法数据格式、类型系统、错误处理都和合约内部保持一致。我在实际项目里最大的感受是写测试不再像“用另一种语言复述业务逻辑”而是直接“用合约语言检查合约行为”整个推理过程顺畅很多。更重要的是这降低了一些隐性 bug 的发生概率。类型转换错误、精度误差、异步时序问题在 Solidity 测试里基本不会出现。测试代码和合约代码放一起维护变量名、函数签名的对应关系也一目了然。2.3 内置 fuzz 测试和 gas 报告的加成Foundry 内置的模糊测试是另一个让我下定决定换工具的理由。只要测试函数名以 testFuzz 开头并且接收参数Foundry 就会自动为你生成大量随机输入来执行这个函数用来挖那些边界条件下才会暴露的 bug。这个能力以前在 JavaScript 测试生态里要么需要额外安装 fuzz 库要么需要自己写随机数据生成器。Foundry 把它变成了内置能力一行配置都不用改。配合--gas-report参数你还能在跑测试的同时拿到每个函数的 gas 消耗估算这对合约优化极有价值。另外因为 fuzz 测试本质是不断用随机数去跑你的函数它对纯函数或可以形式化验证的逻辑特别有效。你只需要把不变量写清楚剩下的交给工具暴力探测就行。3. 从零搭建一个 Foundry 项目3.1 安装 foundryup 并处理环境变量安装 Foundry 的基本路径是通过 foundryup 这个安装器。在 macOS 或 Linux 环境下打开终端执行curl -L https://foundry.paradigm.xyz | bash执行完成后安装器会提示你把一些环境变量写入你的 shell 配置文件比如~/.zshrc或~/.bashrc。这一步千万不要跳过我见过有人不 source 就直接跑下面命令结果一直提示命令找不到。然后执行foundryup这个命令会自动拉取并安装最新版本的工具链把forge、cast、anvil、chisel四个命令都装好。之后用forge --version确认版本信息。这里有一个容易踩的坑如果你以前装过旧版 Foundry或者系统里有其他工具占用了forge这个名字foundryup 之后可能还是旧版。这时候可以检查一下which forge确认是否指向正确的安装路径。升级工具链本身最简单的方式就是重新跑一遍foundryup它会自动覆盖旧版本。3.2 初始化项目、认识目录结构新建项目可以直接用forge init。我在一个空目录里执行mkdir my-foundry-demo cd my-foundry-demo forge init .初始化完成后目录下会自动生成几个重要的文件和文件夹。如果你之前用过 Hardhat可以把src理解为contractstest还是testscript对应scriptslib则类似node_modules。src/存放合约源码。test/存放测试合约文件名惯例是.t.sol后缀。script/存放部署或辅助脚本后缀常为.s.sol。lib/存放依赖库默认自带forge-std。foundry.toml项目核心配置文件。remappings.txt依赖库别名映射。我个人的习惯是在src里按功能再分子目录比如mocks、interfaces、libraries这样项目一大了也不会乱。另外建议从一开始就用 Git 管理项目因为lib目录里有很多依赖文件如果不加忽略规则提交一次会带进大量第三方代码。3.3 读懂 foundry.toml 和 remappings.txtfoundry.toml默认内容不多但几个关键字段值得搞清楚[profile.default] src src out out libs [lib]src和out分别代表源码目录和编译输出目录libs是依赖库的根目录。编译器版本也可以在这里锁定比如加一行solc 0.8.23避免不同机器上因为编译器版本不同而产生不确定的编译结果。还有一个字段optimizer_runs控制优化器重复优化的次数会影响部署后的合约 gas 消耗。部署到正式环境之前我会把它调到 200 或更高。remappings.txt则解决 import 路径问题。比如库里某个包的实际目录是lib/forge-std/src你想在代码里直接写import forge-std/Test.sol就需要在remappings.txt里写清楚别名关系。默认初始化项目时已经配好了forge-std但如果你手动添加了新依赖这块常常是编译报错的源头。4. 实操写合约、写测试、跑测试4.1 准备一个带状态和权限控制的示例合约为了不走形式我写一个简单但有代表性的合约里面有状态变量、事件、状态变更和权限控制。把这些基础语法都覆盖到用来演示测试非常合适。// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Counter { uint256 public count; address public owner; event Incremented(uint256 newCount); constructor() { owner msg.sender; } function increment() external { count 1; emit Incremented(count); } function reset() external { if (msg.sender ! owner) { revert(caller is not owner); } count 0; } }把它保存到src/Counter.sol。这个合约里有一个全局状态count一个只有 owner 能调用的reset以及一个会触发事件的increment。这三样东西基本覆盖了日常合约最常见的几种测试场景查询状态、改变状态、验证事件、验证权限抛出异常。4.2 编写基于 forge-std 的测试合约在test目录下创建Counter.t.sol写入测试代码// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test, console2} from forge-std/Test.sol; import {Counter} from ../src/Counter.sol; contract CounterTest is Test { Counter counter; function setUp() public { counter new Counter(); } function test_InitialCountIsZero() public view { assertEq(counter.count(), 0); } function test_IncrementUpdatesCount() public { counter.increment(); assertEq(counter.count(), 1); counter.increment(); assertEq(counter.count(), 2); } function test_IncrementEmitsEvent() public { vm.expectEmit(true, true, true, true); emit Counter.Incremented(1); counter.increment(); } function test_ResetRevertsWhenNotOwner() public { vm.prank(address(0x123)); vm.expectRevert(bytes(caller is not owner)); counter.reset(); } }这里有两个概念很关键setUp和vm。setUp类似其他测试框架里的beforeEach每条测试用例执行前都会先执行一次。在上面的例子里每个测试函数都会先创建一份全新的Counter实例。vm是 forge-std 集成的作弊码工具用它可以在测试中模拟各种 EVM 行为。vm.prank表示下一次调用的msg.sender被模拟成指定地址vm.expectRevert断言接下来这一次调用必须抛出指定异常否则测试失败vm.expectEmit用来断言事件是否按预期发出。跑测试forge test输出大致是Running 4 tests for test/Counter.t.sol:CounterTest [PASS] test_InitialCountIsZero() [PASS] test_IncrementEmitsEvent() [PASS] test_IncrementUpdatesCount() [PASS] test_ResetRevertsWhenNotOwner() Suite result: ok. 4 tests passed; 0 failed; 0 skipped; finished in 12.62ms第一次看到这个毫秒级的输出时我是有点惊讶的。以前同样规模的测试在旧工具里至少要等好几秒这里真的就是一瞬间。4.3 模糊测试和测试发散Foundry 的 fuzz 测试用起来非常方便。只要测试函数名以testFuzz开头并且带参数Foundry 就会自动用随机输入反复执行这个函数。比如给 Counter 合约加一个加法函数function add(uint256 a, uint256 b) external pure returns (uint256) { return a b; }然后在测试合约里写function testFuzz_AddNeverOverflows(uint256 a, uint256 b) public { vm.assume(a type(uint128).max); vm.assume(b type(uint128).max); uint256 result counter.add(a, b); assertEq(result, a b); }vm.assume用来过滤输入避免某些极端输入导致测试失去意义。这里的逻辑是当a和b都小于2^128时两个数相加一定不会超过uint256的最大值所以期望add永远返回正常加法结果。Foundry 会默认跑大量随机样例极大提高边界 bug 被发现的概率。模糊测试对纯函数尤其有效但在有状态函数上也能用只是需要提前设计好不变量。比如你可以写“无论怎么调用 incrementcount 永远等于调用次数”这类全局不变式让 fuzz 去验证。4.4 常用 vm cheatcode 速查与注意点vm是 forge-std 里最强大的工具下面列几个我高频使用的作弊码vm.prank(address)把下一次调用的msg.sender临时改成指定地址。vm.startPrank(address)从当前开始之后所有调用的msg.sender都变成指定地址直到vm.stopPrank()。vm.expectRevert(bytes)断言下一次调用必须 revert且错误信息匹配。vm.expectEmit(...)断言下一个事件必须匹配当前 emit 的事件。vm.warp(uint256)修改当前区块时间戳。vm.roll(uint256)修改当前区块高度。vm.deal(address, uint256)给指定地址塞余额。vm.store(address, bytes32, bytes32)直接修改合约指定存储位置的原始数据用来模拟一些极端状态。一个非常容易踩的坑是顺序问题。vm.expectRevert必须写在触发 revert 的调用之前它只能“预期下一次调用”。很多人先调用合约再写 expectRevert结果测试报错说“test not expected to revert”但实际问题只是顺序反了。另外vm.prank只影响下一次调用如果你需要连续在某个地址身份下执行多次操作用startPrank更合适。我自己曾在这种“只影响下一次”和“持续影响”的行为之间吃过亏排查半天才发现是 prank 作用域的问题。5. 部署与链上交互的完整流程5.1 用 anvil 在本地启动一条开发链部署之前先在本地把链起来。一条命令就行anvil默认情况下anvil 会在http://localhost:8545提供一个节点同时会打印一组带测试 ETH 的账户和私钥。这些账户在本地链上有大量余额专门用于测试。它还支持--port指定端口--chain-id指定链 ID比如anvil --port 8546 --chain-id 31338本地链的好处是交易即时确认基本没有出块等待时间。智能合约开发和普通后端开发不一样后端本地跑个 service 就能调智能合约如果不部署到一条链上很多逻辑是完全无法验证的。anvil 就是为这个环节量身定制的开发节点。5.2 用 forge create 完成合约部署环境就绪后部署合约最直接的方式是forge createforge create src/Counter.sol:Counter \ --rpc-url http://localhost:8545 \ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80其中--rpc-url指向链的 RPC 地址--private-key是你的部署账户私钥。执行成功后终端会打印合约地址、部署交易哈希、gas 费用等关键信息。如果构造函数有参数用--constructor-args传参。比如某个合约构造函数需要uint256初始值forge create src/Counter.sol:Counter \ --constructor-args 42 \ --rpc-url http://localhost:8545 \ --private-key 你的私钥这里我想特别强调一点本地开发用的默认私钥可以随便暴露但真实测试网或主网环境下不要把私钥直接写在命令行里。后面第 7 节我会专门讲安全相关的注意事项。5.3 用 cast 读链上信息、发交易合约部署完之后日常交互就可以交给cast了。读函数用cast call比如查一下刚部署合约的countcast call 0x合约地址 count() --rpc-url http://localhost:8545返回的是一串十六进制数据可以用cast --to-dec把它转换成可读的十进制数字或者直接利用cast call配合--abi等参数一步到位。写函数用cast sendcast send 0x合约地址 increment() \ --rpc-url http://localhost:8545 \ --private-key 你的私钥cast send会实际广播交易并等待上链返回交易哈希、区块高度和 gas 消耗。遇到需要调用带参数函数的情况可以写成cast send 0x合约地址 transfer(address,uint256) 0x目标地址 100 \ --rpc-url http://localhost:8545 \ --private-key 你的私钥cast还有个很实用的功能cast abi-encode当你需要准备一段 calldata 时它可以直接帮你编码好省去手工拼字节的麻烦。5.4 更规范的部署forge script 脚本系统项目一旦复杂起来部署就不只是一条命令的事了可能需要先部署依赖合约再初始化参数再部署业务合约。forge script这个脚本系统就是为复杂部署流程准备的。在script/目录下建一个部署脚本// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Script} from forge-std/Script.sol; import {Counter} from ../src/Counter.sol; contract DeployScript is Script { function run() external { uint256 deployerPrivateKey vm.envUint(PRIVATE_KEY); vm.startBroadcast(deployerPrivateKey); Counter counter new Counter(); vm.stopBroadcast(); console2.log(Counter deployed at:, address(counter)); } }然后执行forge script script/Deploy.s.sol:DeployScript \ --rpc-url http://localhost:8545 \ --broadcastvm.startBroadcast之后的所有交易会被真实广播到链上。脚本的好处是它可以按顺序调用多个合约并且在脚本里直接写日志输出部署结果。以后要在测试网重放一遍部署只需要把 RPC 地址切换成测试网地址即可。6. 项目落地在 CI 和团队协作中用上 Foundry6.1 在 GitHub Actions 里跑测试个人开发时“顺手”的工具团队协作里一样能跑得很顺。把 Foundry 测试集成进 CI最直接的方式是在 workflow 文件里安装 Foundry 后执行forge testname: CI on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: foundry-rs/foundry-toolchainv1 with: version: nightly - name: Install dependencies run: forge install - name: Run tests run: forge test --gas-report这样每次 push 都会自动触发全量测试。我在一个项目里把这条链路搭起来之后明显感觉到团队合并代码时的安全感提高了因为在合并之前就能通过 CI 跑完所有测试不需要信任某个人本地环境“应该是好的”。6.2 保持依赖一致性Foundry 的依赖管理用forge install和forge update完成它会直接拉 GitHub 仓库到lib目录。问题是如果团队里有人拉了一个新版本依赖而另一个人的lib没有同步本地测试结果就可能对不上。解决办法是把lib提交到 Git 仓库或者至少提交一份锁文件。临时项目可以不加但长期项目我强烈建议把lib纳入版本管理。因为 Solidity 依赖本身变动频繁一些第三方库的小版本更新可能在你看不到的地方破坏 API 兼容性。另外上文提到的remappings.txt也要随项目一起提交避免不同机器上依赖路径不一致导致编译失败。6.3 团队切换与渐进式采用很多团队现在还是 Hardhat 和 Foundry 共存。这种情况并不冲突因为 Foundry 是独立的工具链你完全可以在一个项目里同时保留hardhat.config.ts和foundry.toml。一些依赖 Hardhat 插件的功能继续走 Hardhat普通测试和部署用 Foundry。不过我个人建议如果你决定在新项目里全面采用 Foundry就尽量在项目初期把测试全部写成 Solidity 测试。因为一旦老测试越来越多新旧工具之间的心智切换成本会重新出现反而失去了工具链统一带来的效率。7. 高频问题与避坑记录7.1 测试里的地址冲突与 vm 使用顺序Foundry 会给测试合约和作弊码预留一些内部地址范围。如果你在测试里用vm.prank模拟了一个地址而这个地址恰好落在预留范围内就可能出现诡异的权限或余额问题。我的做法是固定使用一个有辨识度的地址比如address(0xABCD)并且把测试中常用的模拟地址统一维护在一个常量文件里避免不同测试之间因为地址重合产生干扰。vm.expectRevert和vm.prank的顺序也要注意。vm.prank只影响下一次调用如果你希望某个地址身份连续执行多个操作必须使用vm.startPrank并以vm.stopPrank收尾。顺序错了轻则测试失败重则测出了一个错误的通过结果反而更危险。7.2 依赖和 remappings 导致编译失败import forge-std/Test.sol这类路径之所以能生效是因为remappings.txt里做了别名映射。如果你手动安装了一个新库却忘了更新 remappings编译时就会汇报找不到文件。解决方式有两种。一种是用forge remappings自动生成当前依赖的映射关系然后写进remappings.txt另一种是直接让 Foundry 自动检测在foundry.toml里设置auto_detect_remappings true但我个人的习惯是手动维护 remappings尤其是在依赖较复杂的项目里。自动检测有时候会选一个与你预期不同的路径导致“为什么他那边能编译我这边不行”的尴尬局面。7.3 私钥安全与使用环境变量forge create和cast send都支持--private-key参数但这条命令会完整出现在 shell 历史记录里存在被窃取的风险。本地开发可以用但在测试网或主网环境更安全的做法是使用环境变量export RPC_URLhttps://your.rpc.endpoint export PRIVATE_KEYyour-private-key forge script script/Deploy.s.sol:DeployScript \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --broadcast也要警惕自己的代码仓库掉 token。很多人习惯把.env文件加入.gitignore但如果某个文件里硬编码了私钥并提交上去后果可能很严重。安全习惯越早养成越好这不是危言耸听。7.4 版本升级带来的兼容性变化Foundry 迭代非常快新版本经常调整函数签名或默认行为。遇到版本升级后突然冒出来一批编译错误第一件事不是改代码而是检查当前工具链版本forge --version然后跑一次foundryup把工具链升到最新。很多兼容性问题在最新版本中已经修复。另外在查看教程或开源项目代码时也要注意对方用的 Foundry 版本。半年前的项目和现在可能在某些 API 上已经有差异直接照抄代码不一定能跑。如果你发现一份看起来没问题的代码在本地编译不通过先看看它是基于哪个版本写的。8. 我个人的一些实操体会如果让我给 Foundry 一个整体的定位我想说它最大的价值不在于某一个单独功能而在于把开发、测试、部署、链上交互这件事的“摩擦”降到了最低。以前写合约测试是一件有点痛苦的事需要额外维护一套 JavaScript 测试代码现在测试就是 Solidity写起来自然跑起来快反馈也足够清晰。我在几个项目实践里还发现一个额外好处因为 fuzz 测试和 gas 报告都是内置的代码评审时可以把“你跑过 fuzzified 测试吗”和“你清理过 gas 热点吗”当成默认要求这比靠人肉 review 找问题要可靠得多。最后再分享一个小习惯现在我用 Foundry 建新项目时第一步永远是先写一个“最简可运行”的合约和测试确保forge test能秒过再开始加复杂逻辑。这样后面所有改动都可以有一个干净的参照系任何一次失败的测试都能快速定位到新加的代码上。这个小技巧帮我省下的调试时间远比我当初花几分钟搭建基础项目所付出的成本要高得多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。