FEATURED · 精选文章

WTF-Solidity 测试基石:深入 Forge Standard Library(forge-std)核心组件与实战用法

发布时间 / 2026/9/16 13:05:16
来源 / 创域科博编辑部
栏目 / 资讯中心
WTF-Solidity 测试基石:深入 Forge Standard Library(forge-std)核心组件与实战用法 WTF-Solidity 测试基石深入 Forge Standard Libraryforge-std核心组件与实战用法【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity导读Forge Standard Library简称forge-std是 Foundry 生态中面向forge test的官方标准库它通过封装vm作弊码cheatcodes大幅降低 Solidity 测试的编写成本。本篇文章以仓库 lib/openzeppelin-contracts/lib/forge-std/README.md 为骨架结合仓库内源码StdError.sol、Test.sol、StdCheats.sol与真实测试用例AaveV3Flashloan.t.sol系统讲解其安装方式、stdError错误断言、stdStorage存储槽定位、stdCheats账户模拟以及日志输出读完即可在 WTF-Solidity 仓库中编写、运行并理解基于 forge-std 的测试。一、forge-std 是什么Forge Standard Library 是一组用于Forge / Foundry的辅助合约与库。它的核心价值在于在 Forge 的 cheatcodes作弊码之上再做一层封装让测试写起来更简单、更快同时改善 cheatcodes 的使用体验即原文所述It leverages Forges cheatcodes to make writing tests easier and faster, while improving the UX of cheatcodes。在 WTF-Solidity 仓库中forge-std 被 OpenZeppelin Contracts 作为子依赖引入路径 lib/openzeppelin-contracts/lib/forge-std同时仓库根目录 foundry.toml 通过 remapping 将其暴露给所有教程章节remappings [ forge-std/lib/forge-std/src/, openzeppelin/contracts/lib/openzeppelin-contracts/contracts/, openzeppelin-contracts/lib/openzeppelin-contracts/contracts/ ]因此仓库内任何测试文件都可以通过import forge-std/Test.sol;直接使用标准库例如 AaveV3Flashloan.t.sol 的第一行就是这样引入的。二、安装与运行2.1 安装在一个全新的 Foundry 项目中安装 forge-std 只需一条命令forge install foundry-rs/forge-std安装后默认会出现在项目的lib/forge-std/目录下并在foundry.toml中建立对应的 remapping。本仓库中 forge-std 实际位于lib/openzeppelin-contracts/lib/forge-std这正是子项目嵌套依赖的典型形态。2.2 在 WTF-Solidity 仓库中运行测试仓库根目录的 foundry.toml 已配置solc 0.8.34以及上文所示的 remappings测试入口为根目录test/。仓库还提供了批量执行脚本 scripts/run-forge-tests.sh其执行逻辑是在根目录执行forge test --match-contract RootTest遍历 60 多个教程章节目录如57_Flashloan、S01_ReentrancyAttack等对各自含有foundry.toml的子项目执行forge test --match-path test/Counter.t.sol汇总通过/失败数量并给出退出码。运行方式./scripts/run-forge-tests.sh注意脚本刻意排除了依赖 URL 或 Chainlink 预言机的章节如 18_Import、39_Random因为这些用例需要 fork 环境或外部 RPC 才能跑通。三、stdError编译器内置错误的统一封装stdError是为错误与回滚reverts断言设计的辅助库在 Forge 中尤其配合vm.expectRevert作弊码使用——它把 Solidity 编译器内置的 Panic 错误码全部枚举出来省去手写abi.encodeWithSignature的麻烦。3.1 源码Panic 错误码一览打开 StdError.sol可以看到它定义的完整错误常量常量对应 Panic 码触发场景stdError.assertionError0x01assert断言失败stdError.arithmeticError0x11算术溢出/除零stdError.divisionError0x12除法或取模除零stdError.enumConversionError0x21枚举非法转换stdError.encodeStorageError0x22编码存储错误stdError.popError0x31对空数组popstdError.indexOOBError0x32数组下标越界stdError.memOverflowError0x41内存溢出stdError.zeroVarError0x51零值变量初始化调用3.2 实战示例断言算术错误原文给出的标准写法是先用vm.expectRevert(stdError.arithmeticError)声明下一次调用必须回滚且错误码匹配再触发会溢出的调用import forge-std/Test.sol; contract TestContract is Test { ErrorsTest test; function setUp() public { test new ErrorsTest(); } function testExpectArithmetic() public { vm.expectRevert(stdError.arithmeticError); test.arithmeticError(10); } } contract ErrorsTest { function arithmeticError(uint256 a) public { a a - 100; // 无符号减法下溢触发 Panic(0x11) } }WTF-Solidity 仓库中 AaveV3Flashloan.t.sol 也使用了同类手法——先用vm.expectRevert()断言手续费不足时闪电贷必然回滚再发起调用function testFlashloanFail() public { weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 4e16); // 手续费不足 uint amountToBorrow 100 * 1e18; vm.expectRevert(); flashloan.flashloan(amountToBorrow); }四、stdStorage无需知晓存储布局即可读写任意槽位stdStorage是对record与accesses两个 cheatcode 的封装。它的核心能力是在不提前知晓存储布局的情况下自动定位并改写某个状态变量对应的存储槽位。4.1 工作原理与限制原理在目标函数调用期间记录所有的SLOAD和SSTORE。若函数只读写了一个槽位则直接返回该槽位否则逐一遍历检查每个槽位。关键限制对于打包packed存储变量可以找到槽位但不能安全写入。若尝试写入一个已初始化的 packed 槽位会抛错唯一例外是该槽位尚未初始化值为bytes32(0)。深度参数当目标变量是结构体struct时可通过depth参数指定字段层级0 表示第 0 个字段1 表示第 1 个字段依此类推。4.2 核心 API 链式调用stdstore支持一条流畅的链式 API常用组合如下方法作用.target(address)指定被探测的合约地址.sig(exists())或.sig(selector)用函数选择器字符串或 bytes4指明目标变量.with_key(key)当目标是 mapping 时传入定位所需的键.depth(n)当目标是 struct 时指定字段深度.find()返回计算出的槽位号.checked_write(value)安全写入该槽位packed 槽位会抛错4.3 完整示例原文给出了一个覆盖 4 种典型场景的完整用例import forge-std/Test.sol; contract TestContract is Test { using stdStorage for StdStorage; Storage test; function setUp() public { test new Storage(); } // 1) 查找普通 public 变量的槽位 function testFindExists() public { uint256 slot stdstore.target(address(test)).sig(exists()).find(); assertEq(slot, 0); } // 2) 直接改写普通变量的值 function testWriteExists() public { stdstore.target(address(test)).sig(exists()).checked_write(100); assertEq(test.exists(), 100); } // 3) 支持任意存储布局例如 assembly 手写的隐藏槽位 function testFindHidden() public { uint256 slot stdstore.target(address(test)).sig(test.hidden.selector).find(); assertEq(slot, uint256(keccak256(my.random.var))); } // 4) 定位 mapping 槽位时必须传入键 function testFindMapping() public { uint256 slot stdstore .target(address(test)) .sig(test.map_addr.selector) .with_key(address(this)) .find(); // Storage 构造函数把 msg.sender 的值写为 1 assertEq(uint(vm.load(address(test), bytes32(slot))), 1); } // 5) 定位 struct 字段时使用 depth 指定字段深度 function testFindStruct() public { uint256 slot_for_a_field stdstore .target(address(test)) .sig(test.basicStruct.selector) .depth(0) .find(); uint256 slot_for_b_field stdstore .target(address(test)) .sig(test.basicStruct.selector) .depth(1) .find(); assertEq(uint(vm.load(address(test), bytes32(slot_for_a_field))), 1); assertEq(uint(vm.load(address(test), bytes32(slot_for_b_field))), 2); } } // 一个存储布局复杂的合约 contract Storage { struct UnpackedStruct { uint256 a; uint256 b; } constructor() { map_addr[msg.sender] 1; } uint256 public exists 1; mapping(address uint256) public map_addr; mapping(address UnpackedStruct) public map_struct; mapping(address mapping(address uint256)) public deep_map; mapping(address mapping(address UnpackedStruct)) public deep_map_struct; UnpackedStruct public basicStruct UnpackedStruct({a: 1, b: 2}); function hidden() public view returns (bytes32 t) { // 一个极其隐蔽的存储槽位 bytes32 slot keccak256(my.random.var); assembly { t : sload(slot) } } }几点值得注意的细节testFindHidden说明即使槽位通过keccak256随机散列、常规槽位遍历永远找不到stdstorage的机制依然能定位sig()既接受exists()这样的字符串也接受test.hidden.selector这样的 bytes4 选择器最后用vm.load(address(test), bytes32(slot))直接加载槽位值来验证结果体现了 forge-std 与原生 cheatcode 的无缝配合。五、stdCheats更开发者友好的prank封装stdCheats是对零散 cheatcode 的封装目前主要围绕prank系列展开核心函数是hoax与startHoax。5.1 为什么需要 hoax在 Forge 中用户直觉上以为对某个地址prank之后该地址会自带 ETH但出于安全考虑事实并非如此。因此官方给出了明确的使用准则若目标地址本就有预期的余额 → 直接用prank若只想显式改变某个地址的余额 → 用deal若既要改余额、又要模拟该地址发起调用两者都要 → 用hoax。注意hoax会覆盖目标地址的余额因此只应用于那些余额本身就可预期的地址。5.2 源码实现deal prank 的组合查看 StdCheats.solhoax与startHoax的实现本质就是vm.deal发 ETH与vm.prank/vm.startPrank模拟 msg.sender的组合并提供多重重载// 默认给 2^128 wei约 3.4e20 wei并执行一次性 prank function hoax(address msgSender) internal virtual { vm.deal(msgSender, 1 128); vm.prank(msgSender); } // 可指定给多少 ETH function hoax(address msgSender, uint256 give) internal virtual { vm.deal(msgSender, give); vm.prank(msgSender); } // 可同时指定 tx.origin function hoax(address msgSender, address origin) internal virtual { vm.deal(msgSender, 1 128); vm.prank(msgSender, origin); } // 指定 ETH 与 tx.origin function hoax(address msgSender, address origin, uint256 give) internal virtual { vm.deal(msgSender, give); vm.prank(msgSender, origin); } // 以上四个重载均存在对应的 startHoax 版本持续 prank直到 stopPrankstartHoax对应vm.startPrank模拟会一直持续到调用vm.stopPrank()为止适合一次交易内连续多次以同一身份调用。5.3 使用示例// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.0; import forge-std/Test.sol; // 继承 Test 即可获得 stdCheats contract StdCheatsTest is Test { Bar test; function setUp() public { test new Bar(); } function testHoax() public { // 给 address(1337) 发 ETH 并执行一次 prank hoax(address(1337)); test.bar{value: 100}(address(1337)); // 重载显式指定初始化 ETH 数量 hoax(address(1337), 1); test.bar{value: 1}(address(1337)); } function testStartHoax() public { // 持续 prank可连续多次以同一身份调用 startHoax(address(1337)); test.bar{value: 100}(address(1337)); test.bar{value: 100}(address(1337)); vm.stopPrank(); test.bar(address(this)); // 恢复为测试合约自身身份 } } contract Bar { function bar(address expectedSender) public payable { require(msg.sender expectedSender, !prank); } }六、StdAssertions 与 console 日志6.1 Std AssertionsStdAssertions提供了丰富的断言方法assertEq、assertTrue、assertGt、assertApproxEqAbs、fail等是Test合约的组成部分。全部断言实现位于 StdAssertions.sol覆盖数值、地址、bytes、string 等类型测试失败时会输出带 ABI 解码信息的友好日志。6.2 console.log 与 console2.log 的选择日志打印的使用格式与 Hardhat 一致。官方推荐优先使用console2.sol因为其日志可以在 Forge 的 traces 中解码显示而标准console.sol存在一个已知 bug使用uint256/int256类型的日志在 Forge traces 中无法被正确解码。// 推荐经 Test.sol 间接引入或直接引入 console2.sol import forge-std/Test.sol; // 或 import forge-std/console2.sol; console2.log(someValue);// 需要与 Hardhat 兼容时才使用标准 console.sol import forge-std/Test.sol; // 或 import forge-std/console.sol; console.log(someValue);七、Test.sol一切的统一入口以上所有能力都被汇总进 Test.sol 中。从源码可以看到Test是一个聚合了全部标准库模块的抽象合约import {console} from ./console.sol; import {console2} from ./console2.sol; import {safeconsole} from ./safeconsole.sol; import {StdAssertions} from ./StdAssertions.sol; import {StdChains} from ./StdChains.sol; import {StdCheats} from ./StdCheats.sol; import {StdConstants} from ./StdConstants.sol; import {stdError} from ./StdError.sol; import {StdInvariant} from ./StdInvariant.sol; import {stdJson} from ./StdJson.sol; import {stdMath} from ./StdMath.sol; import {StdStorage, stdStorage} from ./StdStorage.sol; import {StdStyle} from ./StdStyle.sol; import {stdToml} from ./StdToml.sol; import {StdUtils} from ./StdUtils.sol; import {Vm} from ./Vm.sol; import {TestBase} from ./Base.sol; abstract contract Test is TestBase, StdAssertions, StdChains, StdCheats, StdInvariant, StdUtils { // Note: IS_TEST() must return true. bool public IS_TEST true; }也就是说测试合约只要is Test就自动获得vm作弊码来自TestBase/Vm、assertXxx断言、stdError、stdstore、hoax/startHoax、console2.log、JSON/TOML 解析stdJson/stdToml、数学辅助stdMath与工具函数stdUtils等一整套能力。这正是 WTF-Solidity 仓库中大量测试用例只需import forge-std/Test.sol;一行即可开工的原因。八、其他标准库模块速览除上述核心组件外forge-std 的src/目录lib/openzeppelin-contracts/lib/forge-std/src还包含Base.solTestBase与ScriptBase提供vm等基础设施StdChains.sol链别名与 RPC URL 管理rpcUrl、createSelectFork辅助StdJson.sol / StdToml.sol在测试中解析 JSON / TOML 文件StdMath.soldelta、percentDelta等差值/百分比计算StdUtils.solbound约束模糊测试输入范围、computeCreateAddress计算 CREATE 地址等工具StdInvariant.sol不变量测试辅助Vm.solForge cheatcode 接口的完整声明console.sol / console2.sol / safeconsole.sol日志打印三件套。这些模块共同构成了写测试几乎不用离开Test.sol的开发体验。九、许可证与生态位置Forge Standard Library 采用MIT 或 Apache-2.0双许可详见 LICENSE-MIT 与 LICENSE-APACHE开发者可以自由选择其一使用这也是它能被 OpenZeppelin Contracts 等主流项目内嵌依赖的原因之一。在 WTF-Solidity 仓库中只要遵循根目录 foundry.toml 的 remapping 约定即可在任意教程章节的测试中复用这套标准库。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻