試基石:深入 Forge Standard Library(forge-std)核心組件與實(shí)戰(zhàn)用法)
WTF-Solidity 測(cè)試基石深入 Forge Standard Libraryforge-std核心組件與實(shí)戰(zhàn)用法【免費(fèi)下載鏈接】WTF-SolidityWTF Solidity 極簡(jiǎn)入門教程供小白們使用。Now supports English! 官網(wǎng): https://wtf.academy項(xiàng)目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity導(dǎo)讀Forge Standard Library簡(jiǎn)稱forge-std是 Foundry 生態(tài)中面向forge test的官方標(biāo)準(zhǔn)庫(kù)它通過封裝vm作弊碼cheatcodes大幅降低 Solidity 測(cè)試的編寫成本。本篇文章以倉(cāng)庫(kù) lib/openzeppelin-contracts/lib/forge-std/README.md 為骨架結(jié)合倉(cāng)庫(kù)內(nèi)源碼StdError.sol、Test.sol、StdCheats.sol與真實(shí)測(cè)試用例AaveV3Flashloan.t.sol系統(tǒng)講解其安裝方式、stdError錯(cuò)誤斷言、stdStorage存儲(chǔ)槽定位、stdCheats賬戶模擬以及日志輸出讀完即可在 WTF-Solidity 倉(cāng)庫(kù)中編寫、運(yùn)行并理解基于 forge-std 的測(cè)試。一、forge-std 是什么Forge Standard Library 是一組用于Forge / Foundry的輔助合約與庫(kù)。它的核心價(jià)值在于在 Forge 的 cheatcodes作弊碼之上再做一層封裝讓測(cè)試寫起來(lái)更簡(jiǎn)單、更快同時(shí)改善 cheatcodes 的使用體驗(yàn)即原文所述It leverages Forges cheatcodes to make writing tests easier and faster, while improving the UX of cheatcodes。在 WTF-Solidity 倉(cāng)庫(kù)中forge-std 被 OpenZeppelin Contracts 作為子依賴引入路徑 lib/openzeppelin-contracts/lib/forge-std同時(shí)倉(cāng)庫(kù)根目錄 foundry.toml 通過 remapping 將其暴露給所有教程章節(jié)remappings [ forge-std/lib/forge-std/src/, openzeppelin/contracts/lib/openzeppelin-contracts/contracts/, openzeppelin-contracts/lib/openzeppelin-contracts/contracts/ ]因此倉(cāng)庫(kù)內(nèi)任何測(cè)試文件都可以通過import forge-std/Test.sol;直接使用標(biāo)準(zhǔn)庫(kù)例如 AaveV3Flashloan.t.sol 的第一行就是這樣引入的。二、安裝與運(yùn)行2.1 安裝在一個(gè)全新的 Foundry 項(xiàng)目中安裝 forge-std 只需一條命令forge install foundry-rs/forge-std安裝后默認(rèn)會(huì)出現(xiàn)在項(xiàng)目的lib/forge-std/目錄下并在foundry.toml中建立對(duì)應(yīng)的 remapping。本倉(cāng)庫(kù)中 forge-std 實(shí)際位于lib/openzeppelin-contracts/lib/forge-std這正是子項(xiàng)目嵌套依賴的典型形態(tài)。2.2 在 WTF-Solidity 倉(cāng)庫(kù)中運(yùn)行測(cè)試倉(cāng)庫(kù)根目錄的 foundry.toml 已配置solc 0.8.34以及上文所示的 remappings測(cè)試入口為根目錄test/。倉(cāng)庫(kù)還提供了批量執(zhí)行腳本 scripts/run-forge-tests.sh其執(zhí)行邏輯是在根目錄執(zhí)行forge test --match-contract RootTest遍歷 60 多個(gè)教程章節(jié)目錄如57_Flashloan、S01_ReentrancyAttack等對(duì)各自含有foundry.toml的子項(xiàng)目執(zhí)行forge test --match-path test/Counter.t.sol匯總通過/失敗數(shù)量并給出退出碼。運(yùn)行方式./scripts/run-forge-tests.sh注意腳本刻意排除了依賴 URL 或 Chainlink 預(yù)言機(jī)的章節(jié)如 18_Import、39_Random因?yàn)檫@些用例需要 fork 環(huán)境或外部 RPC 才能跑通。三、stdError編譯器內(nèi)置錯(cuò)誤的統(tǒng)一封裝stdError是為錯(cuò)誤與回滾reverts斷言設(shè)計(jì)的輔助庫(kù)在 Forge 中尤其配合vm.expectRevert作弊碼使用——它把 Solidity 編譯器內(nèi)置的 Panic 錯(cuò)誤碼全部枚舉出來(lái)省去手寫abi.encodeWithSignature的麻煩。3.1 源碼Panic 錯(cuò)誤碼一覽打開 StdError.sol可以看到它定義的完整錯(cuò)誤常量常量對(duì)應(yīng) Panic 碼觸發(fā)場(chǎng)景stdError.assertionError0x01assert斷言失敗stdError.arithmeticError0x11算術(shù)溢出/除零stdError.divisionError0x12除法或取模除零stdError.enumConversionError0x21枚舉非法轉(zhuǎn)換stdError.encodeStorageError0x22編碼存儲(chǔ)錯(cuò)誤stdError.popError0x31對(duì)空數(shù)組popstdError.indexOOBError0x32數(shù)組下標(biāo)越界stdError.memOverflowError0x41內(nèi)存溢出stdError.zeroVarError0x51零值變量初始化調(diào)用3.2 實(shí)戰(zhàn)示例斷言算術(shù)錯(cuò)誤原文給出的標(biāo)準(zhǔn)寫法是先用vm.expectRevert(stdError.arithmeticError)聲明下一次調(diào)用必須回滾且錯(cuò)誤碼匹配再觸發(fā)會(huì)溢出的調(diào)用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; // 無(wú)符號(hào)減法下溢觸發(fā) Panic(0x11) } }WTF-Solidity 倉(cāng)庫(kù)中 AaveV3Flashloan.t.sol 也使用了同類手法——先用vm.expectRevert()斷言手續(xù)費(fèi)不足時(shí)閃電貸必然回滾再發(fā)起調(diào)用function testFlashloanFail() public { weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 4e16); // 手續(xù)費(fèi)不足 uint amountToBorrow 100 * 1e18; vm.expectRevert(); flashloan.flashloan(amountToBorrow); }四、stdStorage無(wú)需知曉存儲(chǔ)布局即可讀寫任意槽位stdStorage是對(duì)record與accesses兩個(gè) cheatcode 的封裝。它的核心能力是在不提前知曉存儲(chǔ)布局的情況下自動(dòng)定位并改寫某個(gè)狀態(tài)變量對(duì)應(yīng)的存儲(chǔ)槽位。4.1 工作原理與限制原理在目標(biāo)函數(shù)調(diào)用期間記錄所有的SLOAD和SSTORE。若函數(shù)只讀寫了一個(gè)槽位則直接返回該槽位否則逐一遍歷檢查每個(gè)槽位。關(guān)鍵限制對(duì)于打包packed存儲(chǔ)變量可以找到槽位但不能安全寫入。若嘗試寫入一個(gè)已初始化的 packed 槽位會(huì)拋錯(cuò)唯一例外是該槽位尚未初始化值為bytes32(0)。深度參數(shù)當(dāng)目標(biāo)變量是結(jié)構(gòu)體struct時(shí)可通過depth參數(shù)指定字段層級(jí)0 表示第 0 個(gè)字段1 表示第 1 個(gè)字段依此類推。4.2 核心 API 鏈?zhǔn)秸{(diào)用stdstore支持一條流暢的鏈?zhǔn)?API常用組合如下方法作用.target(address)指定被探測(cè)的合約地址.sig(exists())或.sig(selector)用函數(shù)選擇器字符串或 bytes4指明目標(biāo)變量.with_key(key)當(dāng)目標(biāo)是 mapping 時(shí)傳入定位所需的鍵.depth(n)當(dāng)目標(biāo)是 struct 時(shí)指定字段深度.find()返回計(jì)算出的槽位號(hào).checked_write(value)安全寫入該槽位packed 槽位會(huì)拋錯(cuò)4.3 完整示例原文給出了一個(gè)覆蓋 4 種典型場(chǎng)景的完整用例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) 支持任意存儲(chǔ)布局例如 assembly 手寫的隱藏槽位 function testFindHidden() public { uint256 slot stdstore.target(address(test)).sig(test.hidden.selector).find(); assertEq(slot, uint256(keccak256(my.random.var))); } // 4) 定位 mapping 槽位時(shí)必須傳入鍵 function testFindMapping() public { uint256 slot stdstore .target(address(test)) .sig(test.map_addr.selector) .with_key(address(this)) .find(); // Storage 構(gòu)造函數(shù)把 msg.sender 的值寫為 1 assertEq(uint(vm.load(address(test), bytes32(slot))), 1); } // 5) 定位 struct 字段時(shí)使用 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); } } // 一個(gè)存儲(chǔ)布局復(fù)雜的合約 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) { // 一個(gè)極其隱蔽的存儲(chǔ)槽位 bytes32 slot keccak256(my.random.var); assembly { t : sload(slot) } } }幾點(diǎn)值得注意的細(xì)節(jié)testFindHidden說明即使槽位通過keccak256隨機(jī)散列、常規(guī)槽位遍歷永遠(yuǎn)找不到stdstorage的機(jī)制依然能定位sig()既接受exists()這樣的字符串也接受test.hidden.selector這樣的 bytes4 選擇器最后用vm.load(address(test), bytes32(slot))直接加載槽位值來(lái)驗(yàn)證結(jié)果體現(xiàn)了 forge-std 與原生 cheatcode 的無(wú)縫配合。五、stdCheats更開發(fā)者友好的prank封裝stdCheats是對(duì)零散 cheatcode 的封裝目前主要圍繞prank系列展開核心函數(shù)是hoax與startHoax。5.1 為什么需要 hoax在 Forge 中用戶直覺上以為對(duì)某個(gè)地址prank之后該地址會(huì)自帶 ETH但出于安全考慮事實(shí)并非如此。因此官方給出了明確的使用準(zhǔn)則若目標(biāo)地址本就有預(yù)期的余額 → 直接用prank若只想顯式改變某個(gè)地址的余額 → 用deal若既要改余額、又要模擬該地址發(fā)起調(diào)用兩者都要 → 用hoax。注意hoax會(huì)覆蓋目標(biāo)地址的余額因此只應(yīng)用于那些余額本身就可預(yù)期的地址。5.2 源碼實(shí)現(xiàn)deal prank 的組合查看 StdCheats.solhoax與startHoax的實(shí)現(xiàn)本質(zhì)就是vm.deal發(fā) ETH與vm.prank/vm.startPrank模擬 msg.sender的組合并提供多重重載// 默認(rèn)給 2^128 wei約 3.4e20 wei并執(zhí)行一次性 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); } // 可同時(shí)指定 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); } // 以上四個(gè)重載均存在對(duì)應(yīng)的 startHoax 版本持續(xù) prank直到 stopPrankstartHoax對(duì)應(yīng)vm.startPrank模擬會(huì)一直持續(xù)到調(diào)用vm.stopPrank()為止適合一次交易內(nèi)連續(xù)多次以同一身份調(diào)用。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) 發(fā) ETH 并執(zhí)行一次 prank hoax(address(1337)); test.bar{value: 100}(address(1337)); // 重載顯式指定初始化 ETH 數(shù)量 hoax(address(1337), 1); test.bar{value: 1}(address(1337)); } function testStartHoax() public { // 持續(xù) prank可連續(xù)多次以同一身份調(diào)用 startHoax(address(1337)); test.bar{value: 100}(address(1337)); test.bar{value: 100}(address(1337)); vm.stopPrank(); test.bar(address(this)); // 恢復(fù)為測(cè)試合約自身身份 } } 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合約的組成部分。全部斷言實(shí)現(xiàn)位于 StdAssertions.sol覆蓋數(shù)值、地址、bytes、string 等類型測(cè)試失敗時(shí)會(huì)輸出帶 ABI 解碼信息的友好日志。6.2 console.log 與 console2.log 的選擇日志打印的使用格式與 Hardhat 一致。官方推薦優(yōu)先使用console2.sol因?yàn)槠淙罩究梢栽?Forge 的 traces 中解碼顯示而標(biāo)準(zhǔn)console.sol存在一個(gè)已知 bug使用uint256/int256類型的日志在 Forge traces 中無(wú)法被正確解碼。// 推薦經(jīng) Test.sol 間接引入或直接引入 console2.sol import forge-std/Test.sol; // 或 import forge-std/console2.sol; console2.log(someValue);// 需要與 Hardhat 兼容時(shí)才使用標(biāo)準(zhǔn) console.sol import forge-std/Test.sol; // 或 import forge-std/console.sol; console.log(someValue);七、Test.sol一切的統(tǒng)一入口以上所有能力都被匯總進(jìn) Test.sol 中。從源碼可以看到Test是一個(gè)聚合了全部標(biāo)準(zhǔn)庫(kù)模塊的抽象合約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; }也就是說測(cè)試合約只要is Test就自動(dòng)獲得vm作弊碼來(lái)自TestBase/Vm、assertXxx斷言、stdError、stdstore、hoax/startHoax、console2.log、JSON/TOML 解析stdJson/stdToml、數(shù)學(xué)輔助stdMath與工具函數(shù)stdUtils等一整套能力。這正是 WTF-Solidity 倉(cāng)庫(kù)中大量測(cè)試用例只需import forge-std/Test.sol;一行即可開工的原因。八、其他標(biāo)準(zhǔn)庫(kù)模塊速覽除上述核心組件外forge-std 的src/目錄lib/openzeppelin-contracts/lib/forge-std/src還包含Base.solTestBase與ScriptBase提供vm等基礎(chǔ)設(shè)施StdChains.sol鏈別名與 RPC URL 管理rpcUrl、createSelectFork輔助StdJson.sol / StdToml.sol在測(cè)試中解析 JSON / TOML 文件StdMath.soldelta、percentDelta等差值/百分比計(jì)算StdUtils.solbound約束模糊測(cè)試輸入范圍、computeCreateAddress計(jì)算 CREATE 地址等工具StdInvariant.sol不變量測(cè)試輔助Vm.solForge cheatcode 接口的完整聲明console.sol / console2.sol / safeconsole.sol日志打印三件套。這些模塊共同構(gòu)成了寫測(cè)試幾乎不用離開Test.sol的開發(fā)體驗(yàn)。九、許可證與生態(tài)位置Forge Standard Library 采用MIT 或 Apache-2.0雙許可詳見 LICENSE-MIT 與 LICENSE-APACHE開發(fā)者可以自由選擇其一使用這也是它能被 OpenZeppelin Contracts 等主流項(xiàng)目?jī)?nèi)嵌依賴的原因之一。在 WTF-Solidity 倉(cāng)庫(kù)中只要遵循根目錄 foundry.toml 的 remapping 約定即可在任意教程章節(jié)的測(cè)試中復(fù)用這套標(biāo)準(zhǔn)庫(kù)。【免費(fèi)下載鏈接】WTF-SolidityWTF Solidity 極簡(jiǎn)入門教程供小白們使用。Now supports English! 官網(wǎng): https://wtf.academy項(xiàng)目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考