完整实战指南)
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 测试框架的官方标准库它把 Forge 内置 cheatcode 封装成更易用的函数、断言与工具集让 Solidity 测试的编写更快、更稳、体验更佳。在 WTF-Solidity 仓库中foundry.toml 通过 remapping 将forge-std/指向本仓库 lib/forge-std/src57_Flashloan 等章节的测试正是基于forge-std/Test.sol编写。读完本文你将掌握 forge-std 的安装方式、核心组件stdError、stdStorage、stdCheats、StdAssertions、StdConfig、console.log的用法与底层实现原理并能在 WTF-Solidity 仓库中直接运行示例验证。安装与集成在任意 Foundry 项目根目录执行forge install foundry-rs/forge-std该命令会把 forge-std 克隆到lib/目录。在 WTF-Solidity 仓库中它已经作为 git 子模块submodule存在于 lib/forge-std并包含src/源码、test/自测、foundry.toml、package.json、CONTRIBUTING.md与双许可文件。要让合约正确找到库需要在foundry.toml中配置 remapping。WTF-Solidity 根目录的 foundry.toml 是这样声明的[profile.default] src src out out test test libs [lib, node_modules] solc 0.8.34 remappings [ forge-std/lib/forge-std/src/, openzeppelin/contracts/lib/openzeppelin-contracts/contracts/, openzeppelin-contracts/lib/openzeppelin-contracts/contracts/ ]因此测试代码中直接写import forge-std/Test.sol;即可。仓库提供了批量跑测试的脚本 scripts/run-forge-tests.sh它会遍历全部章节目录执行forge test并汇总通过/失败/跳过数量你可以在仓库根目录用./scripts/run-forge-tests.sh复现。架构概览Test.sol 聚合了全部模块forge-std 的核心入口是 src/Test.sol。它通过 import 把各工具模块组装成一个统一的抽象合约abstract contract Test is TestBase, StdAssertions, StdChains, StdCheats, StdInvariant, StdUtils { bool public IS_TEST true; }从源码可以看到Test合约一次性继承了断言StdAssertions、链配置StdChains、cheatcode 封装StdCheats、不变量测试StdInvariant、通用工具StdUtils并同时导入console、console2、safeconsole、stdError、stdJson、stdMath、stdStorage、stdToml等库。测试合约只要is Test就能零配置使用全部能力这也是 WTF-Solidity 各章节测试统一import forge-std/Test.sol的原因。stdError内置错误码常量库stdError是一个专门配合vm.expectRevert使用的辅助库它把 Solidity 0.8 编译器内置的 Panic 错误统一编码好测试时不必手写abi.encodeWithSignature(Panic(uint256), ...)。其完整定义见 src/StdError.sol常量Panic 码含义assertionError0x01断言失败arithmeticError0x11算术溢出/下溢divisionError0x12除零或取模为零enumConversionError0x21enum 转换越界encodeStorageError0x22非法存储编码popError0x31对空数组 popindexOOBError0x32数组越界访问memOverflowError0x41内存溢出zeroVarError0x51调用零初始化内部函数注意 StdError.sol 将 pragma 放宽为0.8.13 0.9.0以保证与旧版 Test 合约兼容。典型用法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; } }在 WTF-Solidity 中同样能看到该模式57_Flashloan/test/AaveV3Flashloan.t.sol 在断言“手续费不足导致失败”时使用了vm.expectRevert()其思路与expectRevert(stdError.xxx)一脉相承——先用 cheatcode 声明期望的错误再调用目标函数。stdStorage无需知晓存储布局即可读写插槽stdStorage是 forge-std 中较大的组件见 src/StdStorage.sol约 475 行主要围绕 cheatcode 的record与accesses封装。它能不依赖存储布局知识地定位并读写某个变量对应的存储槽位底层机制是用vm.record()记录目标合约一次调用过程中的所有SLOAD/SSTORE用vm.accesses()取回被访问的槽位列表逐个“试探性改写”槽位值观察返回值是否随之改变checkSlotMutatesCall命中即锁定槽位。基本用法查找与写入import forge-std/Test.sol; contract TestContract is Test { using stdStorage for StdStorage; Storage test; function setUp() public { test new Storage(); } function testFindExists() public { // 通过函数选择器查找 public 变量 exists 的槽位 uint256 slot stdstore.target(address(test)).sig(exists()).find(); assertEq(slot, 0); } function testWriteExists() public { // 直接向该槽位写入 100 stdstore.target(address(test)).sig(exists()).checked_write(100); assertEq(test.exists(), 100); } }支持任意存储布局它甚至能定位用汇编assembly手工声明的“隐藏槽位”因为机制不依赖布局推导而是靠读取行为反推function testFindHidden() public { // hidden 是随机哈希槽位逐槽遍历找不到本机制可以 uint256 slot stdstore.target(address(test)).sig(test.hidden.selector).find(); assertEq(slot, uint256(keccak256(my.random.var))); }映射与结构体with_key 与 depth目标为 mapping 时必须用with_key传入完整的键底层按keccak256(abi.encode(key, slot))规则计算槽位function testFindMapping() public { uint256 slot stdstore .target(address(test)) .sig(test.map_addr.selector) .with_key(address(this)) .find(); // 构造函数中已把 msg.sender 对应的值写为 1 assertEq(uint(vm.load(address(test), bytes32(slot))), 1); }目标为结构体字段时用depth指定字段深度0 表示第 0 个字段1 表示第 1 个字段源码注释中给出了清晰示意struct T { uint256 a; // depth 0 uint256 b; // depth 1 }function testFindStruct() public { uint256 slot_for_a_field stdstore .target(address(test)) .sig(test.basicStruct.selector) .depth(0) .find(); assertEq(uint(vm.load(address(test), bytes32(slot_for_a_field))), 1); }打包存储Packed Slots支持默认情况下对打包多个小类型共享一个槽位的存储变量不支持直接写入会报错。如需启用先调用enable_packed_slots()再执行find()或checked_write()。源码中 findOffset 会逐个位试探、从左右两侧确定变量在槽内的偏移offsetLeft/offsetRight从而在不破坏同槽其他变量的前提下精确读写。stdCheats更友好的 cheatcode 封装stdCheatssrc/StdCheats.sol把散落的 cheatcode 包装成开发友好的函数覆盖冒充身份prank、ETH/代币余额操作deal、合约部署、测试地址生成、时间操纵、fuzzing 辅助等。hoax给地址打钱并冒充设计上有意区分几个概念prank仅冒充msg.sender不会给地址塞 ETH出于安全考虑deal仅显式修改某地址余额hoax同时完成“给地址初始化余额 prank 冒充”适合用于余额可预期的地址——因为它会覆盖目标地址的既有余额若目标地址已有 ETH应改用prank要改余额就用deal。import forge-std/Test.sol; contract StdCheatsTest is Test { Bar test; function setUp() public { test new Bar(); } function testHoax() public { // hoax 给 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 { // startHoax 打钱 startPrank可持续冒充多次 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); } }从 StdCheats.sol 源码可见其同样通过keccak256(hevm cheat code)的固定地址拿到 cheatcode 入口再在hoax/deal内部组合vm.deal与vm.startPrank等原语。StdAssertions全类型断言函数StdAssertionssrc/StdAssertions.sol提供系统化的断言体系相等性assertEq、assertNotEq比较assertLt、assertGt、assertLe、assertGe近似相等assertApproxEqAbs绝对误差、assertApproxEqRel相对误差布尔assertTrue、assertFalse。所有断言均支持多种数据类型并可选携带自定义错误信息。值得注意的实现细节合约内部维护了一个“failed”标记bytes32 private constant FAILED_SLOT bytes32(failed)StdAssertions.solfail()会把该标记写入 cheatcode 地址并置位_failedfailed()再据此判断测试是否失败——这也是 Forge 捕获断言失败并汇总报告的底层依据。StdConfig从 TOML 配置加载变量StdConfigsrc/StdConfig.sol是一个解析 TOML 配置文件的合约部署时把文件内容解析进存储并自动完成类型转换。它假定 TOML 的顶层键是链 ID 或链别名每个链键下按类型分表组织变量类型只允许bool、address、bytes32、uint、int、string、bytes共 7 种对应源码中的NUM_TYPES 7见 StdConfig.sol。支持的 TOML 格式[mainnet] endpoint_url ${MAINNET_RPC} [mainnet.bool] is_live true [mainnet.address] weth 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 whitelisted_admins [ ${MAINNET_ADMIN}, 0x00000000000000000000000000000000deadbeef, 0x000000000000000000000000000000c0ffeebabe ] [mainnet.uint] important_number 123字符串值支持${ENV_VAR}环境变量占位——构造函数中通过vm.resolveEnv(vm.readFile(configFilePath))先解析环境变量再解析 TOMLStdConfig.sol。在脚本中使用import forge-std/Script.sol; import forge-std/StdConfig.sol; contract MyScript is Script { StdConfig config; function run() public { // 加载配置writeToFile 仅脚本中可设为 true用于持久化修改 config new StdConfig(config.toml, false); // 读取当前链的配置 uint256 myNumber config.get(important_number).toUint256(); address weth config.get(weth).toAddress(); address[] memory admins config.get(whitelisted_admins).toAddressArray(); // 读取指定链链 ID 1的配置 bool isLive config.get(1, is_live).toBool(); // 判断键是否存在 if (config.exists(optional_param)) { // ... } // 获取当前链或指定链的 RPC URL string memory rpc config.getRpcUrl(); string memory mainnetRpc config.getRpcUrl(1); // 获取所有已配置的链 ID uint256[] memory chainIds config.getChainIds(); } }关键机制链键解析resolveChainId先尝试把字符串按数字解析失败则用vm.getChain按别名解析如mainnet→ 1两者都失败时抛InvalidChainKeyStdConfig.sol类型自动判定每个变量先按单值解析、失败再按数组解析两种都失败则revert UnableToParseVariable(key)StdConfig.sol写回文件保护writeToFiletrue时会在构造时校验vm.isContext(ScriptGroup)仅脚本上下文允许防止测试并发 I/O 损坏配置文件StdConfig.solset系列方法在启用写回时会用vm.writeToml把 JSON 格式的值写回 TOML 对应键位。console.log 与 console2日志调试日志用法与 Hardhat 的console.log保持一致。推荐优先使用console2.sol因为它能在 Forge trace 中直接展示解码后的日志若必须与 Hardhat 兼容则用标准console.sol——但要注意 README.md 明确指出的缺陷console.sol存在 buguint256/int256类型日志在 Forge trace 中无法被正确解码。// 通过 Test.sol 间接引入 import forge-std/Test.sol; // 或直接引入 import forge-std/console2.sol; // ... console2.log(someValue);// 通过 Test.sol 间接引入 import forge-std/Test.sol; // 或直接引入 import forge-std/console.sol; // ... console.log(someValue);此外 forge-std 还提供safeconsolegas 更安全、避免编译期栈过深的日志库同样已在 src/Test.sol 中一并导入。在 WTF-Solidity 仓库中的实际落地forge-std 并非孤立存在WTF-Solidity 全仓库的测试体系都建立在它之上根目录 foundry.toml 统一配置forge-std/remapping使任何章节的测试都能import forge-std/Test.sol57_Flashloan/test/AaveV3Flashloan.t.sol 继承Test后直接使用vm.expectRevert、setUp、weth.deposit{value: ...}()等能力验证闪电贷成功与手续费不足两种场景scripts/run-forge-tests.sh 对全仓库章节统一执行forge test并将“依赖 URL/Chainlink 的章节”单独跳过如18_Import、39_Random保证 CI 与本地回归的可复现性。贡献与许可如需参与 forge-std 开发请阅读 lib/forge-std/CONTRIBUTING.md。Forge Standard Library 采用双许可发布可在 MIT 或 Apache-2.0 中任选其一使用。建议先查阅 Foundry Book 的 Forge-Std 指南确认 API 用法再结合本仓库 lib/forge-std/test 下的测试用例如StdStorage.t.sol、StdConfig.t.sol、StdError.t.sol等验证行为是否符合预期。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考