估不能只看表面結(jié)果)
合約安全評(píng)估不能只看表面結(jié)果“主要流程跑通”和“人工看過(guò)一遍”只說(shuō)明少量樣本沒(méi)有立即失敗不能說(shuō)明資金賬本、權(quán)限和外部依賴(lài)在異常順序下仍然正確。合約測(cè)試要保存環(huán)境、輸入和斷言代碼或依賴(lài)變化后才能重放同一結(jié)論。自動(dòng)化測(cè)試、靜態(tài)分析和人工審計(jì)各自覆蓋一部分風(fēng)險(xiǎn)。單函數(shù)測(cè)試檢查邊界與權(quán)限不變性測(cè)試探索調(diào)用序列fork 測(cè)試驗(yàn)證某個(gè)鏈上快照下的集成行為經(jīng)濟(jì)模型和治理風(fēng)險(xiǎn)仍要單獨(dú)討論。覆蓋率是觀察測(cè)試范圍的信號(hào)不能換算成籠統(tǒng)的“安全分?jǐn)?shù)”。1. 合約安全評(píng)估的客觀度量體系建立評(píng)估材料時(shí)可以從覆蓋信息、業(yè)務(wù)不變性和外部依賴(lài)三個(gè)角度組織證據(jù)。1.1 分支覆蓋率與 opcode 指令覆蓋率行覆蓋率容易掩蓋條件組合。例如require(a || b)所在行被執(zhí)行過(guò)不表示a為假、b為真等路徑都經(jīng)過(guò)斷言。因此應(yīng)關(guān)注分支和失敗路徑。opcode 覆蓋可以補(bǔ)充觀察字節(jié)碼執(zhí)行范圍但覆蓋率高不代表斷言正確也不代表跨合約經(jīng)濟(jì)行為已經(jīng)驗(yàn)證。1.2 屬性不變性Invariant Check的隨機(jī)游走次數(shù)不變性描述在允許狀態(tài)下始終應(yīng)成立的約束例如賬面總額等于用戶(hù)份額之和。模糊測(cè)試會(huì)探索許多輸入與調(diào)用順序但次數(shù)增加不等于覆蓋完整狀態(tài)空間。運(yùn)行次數(shù)、序列深度和輸入分布應(yīng)按合約復(fù)雜度、CI 時(shí)間和歷史缺陷調(diào)整并保存失敗種子方便復(fù)現(xiàn)。1.3 Fork 主網(wǎng)狀態(tài)下的組合攻擊抗性涉及外部池、預(yù)言機(jī)或治理合約時(shí)本地 mock 容易遺漏真實(shí)接口和狀態(tài)組合。主網(wǎng) fork 能固定到某個(gè)區(qū)塊重放調(diào)用適合檢查集成假設(shè)但它不會(huì)自動(dòng)模擬未來(lái)流動(dòng)性、區(qū)塊構(gòu)建和跨鏈條件。測(cè)試還要主動(dòng)構(gòu)造價(jià)格變化、更新延遲和權(quán)限變更。2. Solidity 合約與 Foundry Fuzzing 套件實(shí)現(xiàn)下面用一個(gè)簡(jiǎn)化 Vault 展示 Foundry Handler 和 ghost 變量。它沒(méi)有利息、份額價(jià)格、管理員或升級(jí)邏輯不應(yīng)被當(dāng)成生產(chǎn)金庫(kù)實(shí)現(xiàn)。2.1 待測(cè) Vault 目標(biāo)合約// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import openzeppelin/contracts/token/ERC20/IERC20.sol; import openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol; import openzeppelin/contracts/utils/ReentrancyGuard.sol; contract YieldVault is ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable stakingToken; uint256 public totalSupply; mapping(address uint256) private _balances; event Deposited(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); constructor(address _stakingToken) { require(_stakingToken ! address(0), Invalid token address); stakingToken IERC20(_stakingToken); } function balanceOf(address account) external view returns (uint256) { return _balances[account]; } function deposit(uint256 amount) external nonReentrant { require(amount 0, Cannot deposit 0); uint256 balanceBefore stakingToken.balanceOf(address(this)); stakingToken.safeTransferFrom(msg.sender, address(this), amount); uint256 balanceAfter stakingToken.balanceOf(address(this)); // 抵御支持轉(zhuǎn)賬扣稅/通縮型代幣的實(shí)際到賬校驗(yàn) uint256 actualDeposited balanceAfter - balanceBefore; require(actualDeposited 0, Zero deposit amount); _balances[msg.sender] actualDeposited; totalSupply actualDeposited; emit Deposited(msg.sender, actualDeposited); } function withdraw(uint256 amount) external nonReentrant { require(amount 0, Cannot withdraw 0); require(_balances[msg.sender] amount, Insufficient balance); _balances[msg.sender] - amount; totalSupply - amount; stakingToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } }通過(guò)前后余額計(jì)算實(shí)際到賬量可以避免入賬金額高于金庫(kù)收到的金額。但對(duì)扣費(fèi)型代幣取款者最終收到多少仍取決于代幣的轉(zhuǎn)賬語(yǔ)義若產(chǎn)品不支持這類(lèi)資產(chǎn)更清晰的做法是在資產(chǎn)準(zhǔn)入時(shí)拒絕而不是只在存款端兼容。2.2 Foundry Invariant 模糊測(cè)試與屬性斷言// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import forge-std/Test.sol; import ./YieldVault.sol; import openzeppelin/contracts/token/ERC20/ERC20.sol; // 基礎(chǔ) Mock扣費(fèi)、回調(diào)和返回值異常應(yīng)使用單獨(dú)的惡意 Token 測(cè)試 contract MockERC20 is ERC20 { constructor() ERC20(Mock Token, MTK) { _mint(msg.sender, 1_000_000_000 * 10**18); } function mint(address to, uint256 amount) external { _mint(to, amount); } } // Handler 模式限制隨機(jī)調(diào)用的邊界 contract VaultHandler is Test { YieldVault public vault; MockERC20 public token; uint256 public ghost_sumDeposits; address[] public actors; address internal currentActor; constructor(YieldVault _vault, MockERC20 _token) { vault _vault; token _token; actors.push(address(0x1111)); actors.push(address(0x2222)); actors.push(address(0x3333)); for (uint i 0; i actors.length; i) { token.mint(actors[i], 1_000_000 * 10**18); vm.prank(actors[i]); token.approve(address(vault), type(uint256).max); } } function deposit(uint256 actorIndex, uint256 amount) public { currentActor actors[bound(actorIndex, 0, actors.length - 1)]; amount bound(amount, 1, 100_000 * 10**18); vm.prank(currentActor); vault.deposit(amount); ghost_sumDeposits amount; } function withdraw(uint256 actorIndex, uint256 amount) public { currentActor actors[bound(actorIndex, 0, actors.length - 1)]; uint256 actorBalance vault.balanceOf(currentActor); if (actorBalance 0) return; amount bound(amount, 1, actorBalance); vm.prank(currentActor); vault.withdraw(amount); ghost_sumDeposits - amount; } } // 主 Invariant 測(cè)試合約 contract YieldVaultInvariantTest is Test { YieldVault public vault; MockERC20 public token; VaultHandler public handler; function setUp() public { token new MockERC20(); vault new YieldVault(address(token)); handler new VaultHandler(vault, token); // 目標(biāo) Target 僅指向 Handler targetContract(address(handler)); } /// 核心不變性 1: Vault 內(nèi)代幣余額必須始終大于等于總記賬 supply function invariant_solvency() public view { assertGe( token.balanceOf(address(vault)), vault.totalSupply(), Vault is insolvent! Contract balance lower than totalSupply. ); } /// 核心不變性 2: 總 totalSupply 必須精確等于 Ghost 變量記錄的用戶(hù)存款和 function invariant_supplyEqualsGhostSum() public view { assertEq( vault.totalSupply(), handler.ghost_sumDeposits(), TotalSupply desynchronized with actual user deposits. ); } }3. 端到端 Mainnet Fork 集成測(cè)試規(guī)范對(duì)于依賴(lài)真實(shí)流動(dòng)性池或預(yù)言機(jī)接口的合約可以增加主網(wǎng) Fork 集成測(cè)試。區(qū)塊高度必須固定否則同一用例會(huì)隨鏈上狀態(tài)變化RPC 地址通過(guò)測(cè)試環(huán)境注入不應(yīng)寫(xiě)入倉(cāng)庫(kù)或日志。下面代碼只展示固定區(qū)塊和賬戶(hù)模擬。地址、區(qū)塊與余額都屬于該快照的前提使用前應(yīng)核對(duì)末尾沒(méi)有實(shí)現(xiàn)價(jià)格沖擊和協(xié)議斷言因此不能把用例名稱(chēng)當(dāng)成已經(jīng)驗(yàn)證的安全結(jié)論。import { expect } from chai; import { ethers, network } from hardhat; describe(Mainnet Fork Integration Security Suite, function () { const UNISWAP_V3_ROUTER 0xE592427A0AEce92De3Edee1F18E0157C05861564; const WETH_ADDRESS 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; const USDC_ADDRESS 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // 示例賬戶(hù)必須在固定區(qū)塊上核對(duì)資產(chǎn)余額 const WHALE_ADDRESS 0x47ac0Fb3F2D84898e4D9E7b4DaB3C24507a6D503; before(async function () { // 強(qiáng)制 Fork 指定高度的主網(wǎng)快照 await network.provider.request({ method: hardhat_reset, params: [ { forking: { jsonRpcUrl: process.env.MAINNET_RPC_URL || , blockNumber: 18500000, }, }, ], }); }); it(為價(jià)格沖擊場(chǎng)景準(zhǔn)備固定的 fork 狀態(tài), async function () { const whaleSigner await ethers.getImpersonatedSigner(WHALE_ADDRESS); // 給鯨魚(yú)賬戶(hù)補(bǔ)充 ETH 支付 Gas await network.provider.send(hardhat_setBalance, [ WHALE_ADDRESS, 0x1000000000000000000, ]); const USDC await ethers.getContractAt(IERC20, USDC_ADDRESS, whaleSigner); const initialBalance await USDC.balanceOf(WHALE_ADDRESS); expect(initialBalance).to.be.gt(0); // 后續(xù)必須執(zhí)行真實(shí)池交易并斷言被測(cè)協(xié)議使用的價(jià)格與清算結(jié)果。 // 如果缺少這些步驟此用例只能驗(yàn)證測(cè)試夾具不驗(yàn)證抗操縱能力。 }); });4. 落地測(cè)試策略的建立路線測(cè)試策略應(yīng)當(dāng)和資產(chǎn)、權(quán)限及依賴(lài)一起演進(jìn)流水線負(fù)責(zé)穩(wěn)定重放人工評(píng)審負(fù)責(zé)判斷斷言是否覆蓋業(yè)務(wù)風(fēng)險(xiǎn)。第一步是在 CI 中運(yùn)行編譯、單元測(cè)試和靜態(tài)分析。靜態(tài)工具的告警要分類(lèi)處理確認(rèn)的問(wèn)題修復(fù)誤報(bào)記錄理由和適用范圍。簡(jiǎn)單要求“零 Warning”容易誘導(dǎo)屏蔽規(guī)則也無(wú)法替代人工判斷。第二步為關(guān)鍵資產(chǎn)操作寫(xiě)不變性并讓 Handler 只調(diào)用業(yè)務(wù)允許的入口。快速配置可在每次提交運(yùn)行更深的序列放到定時(shí)任務(wù)次數(shù)與深度記錄在配置中失敗時(shí)保存隨機(jī)種子和最小化后的調(diào)用序列。第三步針對(duì)外部協(xié)議做固定區(qū)塊的 Fork 測(cè)試覆蓋正常交換、價(jià)格源過(guò)期、流動(dòng)性變化和權(quán)限變更。另設(shè)更新區(qū)塊的兼容測(cè)試可以發(fā)現(xiàn)依賴(lài)升級(jí)但它與可重復(fù)的固定快照用例應(yīng)分開(kāi)。最后把測(cè)試未覆蓋項(xiàng)寫(xiě)進(jìn)評(píng)審報(bào)告例如治理密鑰、跨鏈消息、經(jīng)濟(jì)攻擊成本和部署參數(shù)。分層測(cè)試能提供更可靠的證據(jù)卻不能證明“沒(méi)有漏洞”真正有價(jià)值的是每個(gè)結(jié)論都能回到具體斷言、輸入和環(huán)境。