DeFi Protocolsintermediatetutorial20 min read

Hướng dẫn thử nghiệm DeFi: Hướng dẫn đầy đủ cho các nhà phát triển hợp đồng thông minh | VCC

Kiểm tra DeFi kết hợp giữa kiểm tra đơn vị, kiểm tra fork, kiểm tra invariant và fuzzing để xác nhận hành vi của hợp đồng thông minh trong các điều kiện đối kháng. Các công cụ cần thiết: Foundry (cheatcodes, runner invariant), Hardhat (TypeScript, fork mainnet) và các khung chuyên dụng như Echidna c

Vietcardano teamTeam

Đội ngũ biên tập VietCardanoCommunity — các chuyên gia blockchain và Cardano, cung cấp nội dung giáo dục và nghiên cứu chuyên sâu cho cộng đồng.

Published 14 tháng 9, 2026

Kiểm tra DeFi kết hợp giữa kiểm tra đơn vị, kiểm tra fork, kiểm tra invariant và fuzzing để xác nhận hành vi của hợp đồng thông minh trong các điều kiện đối kháng. Các công cụ cần thiết: Foundry (cheatcodes, runner invariant), Hardhat (TypeScript, fork mainnet) và các khung chuyên dụng như Echidna cho kiểm tra dựa trên thuộc tính. Kiểm tra các tác động của oracle, cho vay nhanh, khả năng tái nhập và các tính chất bất biến về kinh tế trước khi triển khai mainnet.

Các giao thức DeFi bảo mật hàng tỷ đô la trong TVL, nhưng thử nghiệm vẫn là giai đoạn được xem nhẹ nhất trong quá trình phát triển. Mặt khác với phần mềm truyền thống, lỗi của DeFi là không thể đảo ngược và có thể được khai thác ngay lập tức. Hướng dẫn này bao gồm toàn bộ bộ thử nghiệm: từ các thử nghiệm đơn vị cho các chức năng riêng lẻ đến thử nghiệm không thay đổi, chứng minh toán học các thuộc tính của giao thức trong mọi điều kiện. Chúng tôi sẽ sử dụng Foundry làm công cụ chính (ngắn gọn, có cheatcode tốt nhất) cùng với các lựa chọn thay thế của Hardhat, và trình bày các vector tấn công thực tế từ các cuộc tấn công DeFi lớn (Euler, Mango, Cream) như các trường hợp thử nghiệm.

Diagram - DeFi Testing Stack Architecture

Diagram - DeFi Testing Stack Architecture

1. Những nguyên tắc cơ bản của thử nghiệm DeFi

Tại sao thử nghiệm DeFi khác với thử nghiệm Web2 lại có ba đặc điểm: không thể thay đổi (không có hotfix), khả năng kết hợp (sự tương tác của hợp đồng của bạn với các giao thức chưa được biết), và các cuộc tấn công kinh tế (những kẻ thù hướng theo lợi nhuận). Một sai sót đơn lẻ – một lớp bảo vệ không có khả năng tái nhập, giá oracle không được kiểm tra, lỗi tròn trong việc ghi sổ – có thể cạn kiệt toàn bộ giao thức.

Pyramid kiểm tra cho DeFi có năm tầng, mỗi tầng bắt những điều mà tầng trước bỏ sót:

  1. Test đơn vị — các chức năng riêng lẻ, logic đơn thuần, các phép toán toán học
Flowchart - Unit Test Coverage for DeFi Primitives

Flowchart - Unit Test Coverage for DeFi Primitives

Công cụTốt nhất choCheatcodesRunner không thay đổiFuzzingKênh học tập
Hàn gốmTốc độ, DeFi-native, fork/invariant/fuzz tất cả trong một✅ Tốt✅ Được tích hợp✅ Đã tích hợpThấp (Solidity)
Mũ bảo hiểmCác nhóm TypeScript, hệ sinh thái plugin❌ thủ công❌ External❌ Ngoại bộTrung bình (JS/TS)
EchidnaFuzzing dựa trên thuộc tính, có trạng tháiN/AN/A✅ Đặc biệtTốt (giống với Haskell)
MedusaFuzzing với hiệu suất cao, CI/CDN/AN/A✅ Nhanh nhấtCao
SlitherPhân tích tĩnh, phát hiện lỗ hổngN/AN/AN/AThấp (CLI)

Mục tiêu của đoạn trích nổi bật: "Công cụ nào tốt nhất cho thử nghiệm DeFi?" → Foundry cho tốc độ/cheatcodes, Echidna cho thử nghiệm dựa trên đặc tính.

2. Cài đặt môi trường thử nghiệm

Cài đặt Foundry và cấu trúc dự án

`bash

curl -L https://foundry.paradigm.xyz | bash foundryup

forge init defi-testing-demo cd defi-testing-demo

`

Cài đặt thay thế cho Hardhat

`bash npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npx hardhat init `

Cấu hình kiểm tra Fork

Kiểm tra fork yêu cầu một điểm kết thúc RPC có quyền truy cập node lưu trữ. Các nhà cung cấp được khuyến nghị:

  • Alchemy - cấp miễn phí rộng rãi, mainnet Ethereum + L2s

Cấu hình trong foundry.toml:

`toml [profile.ci] fork_url = "https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY" fork_block_number = 19_500_000 # Pin to specific block for reproducibility

[fuzz] runs = 10000 max_test_rejects = 10000

[invariant] runs = 1000 depth = 50 fail_on_revert = false `

Dây chuyền CI/CD

Chuỗi xử lý GitHub Actions (.github/workflows/test.yml):

`yaml name: DeFi Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps:

  • uses: actions/checkout@v4
  • uses: foundry-rs/foundry-toolchain@v1

with: version: nightly

  • run: forge build
  • run: forge test -vvv
  • run: forge test --invariant --invariant-runs 1000
  • run: forge coverage --report lcov
  • uses: codecov/codecov-action@v3

`

Diagram - DeFi Testing Stack Architecture

Diagram - DeFi Testing Stack Architecture

3. Kiểm tra đơn vị cho các hàm cơ bản của DeFi

Kiểm tra tuân thủ ERC20/ERC721

Sử dụng các công cụ kiểm tra OpenZeppelin để tuân thủ các tiêu chuẩn:

`solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/test/helpers/ERC20TestHelpers.sol";

contract MyTokenTest is Test, ERC20TestHelpers { MyToken token; address alice = makeAddr("alice"); address bob = makeAddr("bob");

function setUp() public { token = new MyToken("Test", "TST"); }

function testTransfer() public { token.mint(alice, 1000 ether); testTransferFrom(alice, bob, 100 ether); }

function testPermit() public { token.mint(alice, 1000 ether); testPermit(alice, address(this), 100 ether, 0, 0, 0); } } `

AMM Math: Công thức sản phẩm không đổi

Invariant trung tâm: x * y = k (giữ nguyên sản phẩm, trừ phí).

`solidity contract UniswapV2MathTest is Test { function testConstantProduct() public { uint256 reserveA = 1000 ether; uint256 reserveB = 2000 ether; uint256 k = reserveA * reserveB;

// Swap 100 A for B uint256 amountIn = 100 ether; uint256 amountOut = (amountIn * reserveB) / (reserveA + amountIn);

// Verify invariant holds (with 0.3% fee) uint256 newReserveA = reserveA + amountIn; uint256 newReserveB = reserveB - amountOut; uint256 fee = (amountIn 3) / 1000; uint256 expectedK = (reserveA reserveB) + (fee * reserveB);

assertApproxEqAbs(newReserveA * newReserveB, expectedK, 1); }

function testSwapRounding() public { // Edge case: tiny swaps should not drain reserves uint256 reserveA = 1 ether; uint256 reserveB = 1 ether; uint256 amountOut = (1 wei * reserveB) / (reserveA + 1 wei); assertEq(amountOut, 0); // Too small, correctly returns 0 } } `

Protokol cho vay: yếu tố bảo đảm và thanh lý

`solidity contract LendingMathTest is Test { // Collateral factor: 75% = borrow up to 75% of collateral value // Liquidation threshold: 80% = liquidate when debt > 80% of collateral

function testMaxBorrow() public { uint256 collateralValue = 1000 ether; // $1000 worth of ETH uint256 collateralFactor = 7500; // 75% in basis points uint256 maxBorrow = (collateralValue * collateralFactor) / 10000; assertEq(maxBorrow, 750 ether); }

function testLiquidationTrigger() public { uint256 collateralValue = 1000 ether; uint256 debt = 850 ether; // 85% — above 80% threshold uint256 liquidationThreshold = 8000; // 80%

uint256 healthFactor = (collateralValue liquidationThreshold) / (debt 10000); assertLt(healthFactor, 10000); // Health factor < 1 = liquidatable } } `

Kiểm toán kho/vùng chia sẻ: Tròn số khi nạp/nhả

`solidity contract VaultAccountingTest is Test { Vault vault; ERC20Mock asset;

function setUp() public { asset = new ERC20Mock("Asset", "AST"); vault = new Vault(asset); asset.mint(address(this), 10000 ether); }

function testDepositWithdrawRoundTrip() public { uint256 depositAmount = 1000 ether; asset.approve(address(vault), depositAmount);

uint256 sharesBefore = vault.totalSupply(); uint256 assetsBefore = asset.balanceOf(address(vault));

vault.deposit(depositAmount, address(this));

uint256 sharesAfter = vault.totalSupply(); uint256 assetsAfter = asset.balanceOf(address(vault));

// Shares received should match assets deposited (first depositor gets 1:1) assertEq(sharesAfter - sharesBefore, depositAmount); assertEq(assetsAfter - assetsBefore, depositAmount); }

function testSharePriceManipulation() public { // Attack: donate assets directly to vault to inflate share price asset.mint(address(vault), 1000 ether); // Donation

// Victim deposits — gets fewer shares due to inflated price asset.mint(alice, 100 ether); asset.approve(address(vault), 100 ether, alice); vm.prank(alice); vault.deposit(100 ether, alice);

// Alice gets fewer shares than she should uint256 aliceShares = vault.balanceOf(alice); assertLt(aliceShares, 100 ether); // Rounding loss! } } `

Chạy bằng: forge test --match-contract VaultAccountingTest -vv

4. Kiểm tra Fork: Replay Mainnet và các kịch bản đối kháng

Sequence - Fork Test Execution Flow

Sequence - Fork Test Execution Flow

Test Fork cho phép bạn tái tạo trạng thái mainnet ở bất kỳ khối nào và mô phỏng các giao dịch đối với nó. Cheatcodes của Foundry làm cho điều này trở nên mạnh mẽ:

`solidity contract ForkTest is Test { function setUp() public { // Create fork at block 19_500_000 (pre-exploit) vm.createFork("https://eth-mainnet.alchemyapi.io/v2/KEY", 19_500_000); }

function testEulerFinanceExploit() public { // Euler exploit: donate → borrow → manipulate → liquidate // 1. Attacker donates eToken to protocol (inflates totalAssets) // 2. Borrows against inflated collateral // 3. Manipulates price oracle // 4. Liquidates own position at profit

address attacker = makeAddr("attacker"); address euler = 0x...; // Euler lending contract

vm.prank(attacker); // ... exploit steps replayed from tx trace

// Verify attacker profits assertGt(eth.balanceOf(attacker), initialBalance); }

function testFlashLoanAttack() public { // Single-transaction: borrow → manipulate → repay address pool = 0x...; // Aave/Uniswap pool address attacker = makeAddr("attacker");

vm.startPrank(attacker); // Execute flash loan // Manipulate oracle/price // Repay loan + profit vm.stopPrank(); }

function testOracleManipulation() public { // Chainlink stale price: don't update for hours // Pyth TWAP: manipulate over multiple slots // Custom oracle: direct price setter

// Simulate Chainlink stale price vm.warp(block.timestamp + 2 hours); // Past heartbeat // Price now stale — protocol uses wrong value }

function testMEVSandwich() public { // Frontrun: detect pending swap, buy before, sell after // Backrun: detect liquidation, repay debt, claim collateral

vm.expectRevert(); // Victim tx reverts due to slippage // ... sandwich execution } } `

Kiểm tra xử lý Oracle – Cách tiếp cận sâu

Giá Chainlink cũ: `solidity function testChainlinkStalePrice() public { AggregatorV3Interface feed = AggregatorV3Interface(0x...);

// Normal price (, int256 price,,,) = feed.latestRoundData(); assertGt(price, 0);

// Fast-forward past heartbeat (typically 1-24 hours) vm.warp(block.timestamp + 25 hours);

// Price now stale — roundId hasn't changed (uint80 roundId,,,,) = feed.latestRoundData(); // Protocol should reject or use fallback } `

Sự sai lệch của TWAP của Pyth: `solidity function testPythTWAPManipulation() public { // Pyth publishes price every slot // TWAP = average over N slots // Attacker manipulates N consecutive slots

for (uint i = 0; i < 10; i++) { vm.roll(block.number + 1); // Submit manipulated price via Pyth wormhole pythMock.updatePrice(manipulatedPrice); }

// TWAP now reflects manipulated prices uint256 twap = pythMock.getTWAP(10); assertApproxEqAbs(twap, manipulatedPrice, 1%); } `

5. Kiểm tra không thay đổi: Bảo đảm toán học

Diagram - Invariant Testing Architecture

Diagram - Invariant Testing Architecture

Các thuộc tính nào là không thay đổi? Các thuộc tính phải LUÔN LUỒN tồn tại, bất kể hành động của người dùng, điều kiện thị trường hoặc các vector tấn công. Mặt khác với các thử nghiệm đơn vị (nhập liệu cụ thể → kết quả mong đợi), các thử nghiệm không thay đổi nói: "Đối với bất kỳ chuỗi các hoạt động hợp lệ nào, các thuộc tính này vẫn đúng."

Catalog DeFi không thay đổi

Loại giao thứcCác biến số không thay đổi quan trọng
AMM (Uniswap V2)reserve0 * reserve1 ≥ k_last (với phí), reserve0 > 0 && reserve1 > 0, không có quá trình tràn trong getAmountOut
AMM (Uniswap V3)totalLiquidity = Σ position.liquidity, sqrtPriceX96 trong phạm vi tick, tăng phí theo chiều đơn vị
Cho vay (Aave/Compound)totalAssets ≥ totalBorrows, collateralRatio ≥ liquidationThreshold cho tất cả các vị trí, supplyIndex tăng dần
Vaults (ERC4626)totalAssets() ≥ convertToAssets(totalSupply()), previewDeposit(shares) ≥ assets, không có sự can thiệp vào giá cổ phiếu thông qua việc tặng cổ phiếu
Đặt cọctotalShares * giá mỗi cổ phần = tổng tài sản, tiền thưởng đã phân phối ≤ tiền thưởng có sẵn, không có trường hợp xin nhận 2 lần
Quản trịquorumReached → executionDelay → timelock → execute, proposalThreshold ≤ votingPower, không có việc bỏ phiếu hai lần

Viết các thử nghiệm không thay đổi trong Foundry

`solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

import "forge-std/Test.sol"; import "../src/UniswapV2Pair.sol";

contract UniswapV2InvariantTest is Test { UniswapV2Pair pair; ERC20Mock token0; ERC20Mock token1; address alice = makeAddr("alice"); address bob = makeAddr("bob");

function setUp() public { token0 = new ERC20Mock("Token0", "TK0"); token1 = new ERC20Mock("Token1", "TK1"); pair = new UniswapV2Pair();

// Initialize with liquidity token0.mint(alice, 10000 ether); token1.mint(alice, 10000 ether); vm.prank(alice); token0.approve(address(pair), type(uint256).max); token1.approve(address(pair), type(uint256).max); pair.initialize(alice); // Sets up reserves }

// Invariant: reserve product never decreases (accounting for fees) function invariant_reserveProductNonDecreasing() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); uint256 k = uint256(reserve0) * uint256(reserve1); // kLast tracks reserve product at last sync // After swaps with fees, k should increase assertGe(k, pair.kLast()); }

// Invariant: reserves never zero (prevents division by zero) function invariant_reservesNonZero() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); assertGt(reserve0, 0); assertGt(reserve1, 0); }

// Invariant: swap output matches formula function invariant_swapFormulaCorrect() public view { (uint112 reserve0, uint112 reserve1,) = pair.getReserves();

// Test various input amounts uint256[] memory testAmounts = [1 ether, 10 ether, 100 ether, 1000 ether]; for (uint i = 0; i < testAmounts.length; i++) { uint256 amountIn = testAmounts[i]; if (amountIn >= reserve0) continue; // Skip invalid

uint256 expectedOut = (amountIn 997 reserve1) / (reserve0 1000 + amountIn 997); uint256 actualOut = pair.getAmountOut(amountIn, reserve0, reserve1); assertEq(actualOut, expectedOut); } }

// Invariant: total supply equals sum of user balances function invariant_totalSupplyEqualsSum() public view { // This requires tracking all holders — use for simpler contracts // For pair, totalSupply = liquidity tokens minted assertEq(pair.totalSupply(), pair.balanceOf(alice) + pair.balanceOf(bob) + ...); } } `

Chạy các thử nghiệm không thay đổi

`bash

forge test --invariant --invariant-depth 50 --invariant-runs 1000

forge test --invariant --match-contract UniswapV2InvariantTest

forge test --invariant -vvvv

forge test --invariant --invariant-depth 10 --match-test invariant_reserveProductNonDecreasing -vvvv `

Tuyên giải kết quả

Khi một invariant thất bại, Foundry cung cấp một cú pháp phản đối - một chuỗi các gọi vi phạm đặc tính. Kết quả hiển thị:

  1. Dòng gọi dẫn đến vi phạm
  2. Tình trạng trước/ sau mỗi cuộc gọi
  3. Shrink — Nhà sản xuất cố gắng làm giảm tối thiểu các trường hợp phản đối
  4. Replay — Thêm chuỗi như một thử nghiệm đơn để debug

`bash

[FAIL. Reason: invariant_reserveProductNonDecreasing() failed] Counterexample:

  1. swap(token0, 100 ether) → reserve0=1100, reserve1=909
  2. swap(token1, 500 ether) → reserve0=1000, reserve1=1000 // k decreased!
  3. sync() → kLast updated incorrectly

`

6. Test lỏng & kiểm tra dựa trên thuộc tính

Diagram - Fuzzing vs Invariant Testing Comparison

Diagram - Fuzzing vs Invariant Testing Comparison

Fuzzing không có trạng thái (Foundry)

Đối với các hàm đơn thuần, việc fuzzing không trạng thái tạo ra các đầu vào ngẫu nhiên:

`solidity contract MathFuzzTest is Test { // Foundry automatically fuzzes any test function with parameters function testGetAmountOutFuzz(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public { // Filters: skip invalid inputs vm.assume(reserveIn > 0); vm.assume(reserveOut > 0); vm.assume(amountIn > 0); vm.assume(amountIn < reserveIn); // Can't swap more than reserve

uint256 expected = (amountIn 997 reserveOut) / (reserveIn 1000 + amountIn 997); uint256 actual = UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); assertEq(actual, expected); }

// Run: forge test --match-test testGetAmountOutFuzz --fuzz-runs 100000 } `

Fuzzing trạng thái (Echidna/Medusa)

Đối với các thuộc tính cấp giao thức yêu cầu trạng thái:

`yaml

config: testMode: "assertion" maxTx: 50 maxBlocks: 100 corpusDir: "./corpus"

targets:

  • "LendingProtocol"

dictionary:

  • "deposit"
  • "withdraw"
  • "borrow"
  • "repay"
  • "liquidate"

`

`solidity // Echidna test contract contract LendingProtocolFuzz is Test { LendingProtocol protocol; ERC20Mock collateral; ERC20Mock debt;

function setUp() public { collateral = new ERC20Mock("COL", "COL"); debt = new ERC20Mock("DEBT", "DEBT"); protocol = new LendingProtocol(collateral, debt); }

// Property: health factor never < 1 for non-liquidated positions function echidna_healthFactor() public view returns (bool) { // Check all positions for (uint i = 0; i < protocol.positionCount(); i++) { Position memory pos = protocol.positions(i); if (pos.collateral > 0) { uint256 hf = protocol.healthFactor(pos.user); if (hf < 1e18 && !pos.liquidated) { return false; // Violated! } } } return true; }

// Property: total assets always ≥ total borrows function echidna_solvency() public view returns (bool) { return protocol.totalAssets() >= protocol.totalBorrows(); } } `

Chạy: echidna-test LendingProtocolFuzz.sol --config echidna.yaml

Khi nào nên sử dụng Fuzzing so với kiểm tra không thay đổi

Khía cạnhFuzzingKiểm tra không thay đổi
GoalFind edge cases, crashesHãy chứng minh các thuộc tính đúng
NhậpNgẫu nhiên (dựa trên cơ sở dữ liệu)Các chuỗi gọi ngẫu nhiên
Tỉnh/Thành phốKhông có quốc tịch (đại đa số)Có trạng thái
Xuất raNhập liệu bị lỗiDãy thí dụ phản bác
Tốt nhất choToán học nguyên sơ, mã hóa/đọc mãCác biến số kinh tế, các dòng chảy phức tạp
Các công cụFoundry (builtin), EchidnaFoundry (builtin), Medusa

Sử dụng cả hai: Fuzzing tìm ra lỗi trong các chức năng riêng lẻ; kiểm tra không thay đổi chứng minh các thuộc tính ở cấp giao thức.

7. Kiểm tra giao thức cắt ngang và nâng cấp

Diagram - Cross-Chain Testing Architecture

Diagram - Cross-Chain Testing Architecture

Kiểm tra tin nhắn của cầu

`solidity contract BridgeMessageTest is Test { Bridge bridge; MockEndpoint endpoint;

function setUp() public { endpoint = new MockEndpoint(); bridge = new Bridge(endpoint); }

function testMessagePassing() public { address user = makeAddr("user"); bytes memory payload = abi.encodeWithSelector( ILendingProtocol.deposit.selector, 100 ether );

// Send message from source chain vm.prank(user); bridge.sendMessage(chainIdB, address(protocolB), payload);

// Verify message queued assertEq(endpoint.queuedMessages.length, 1);

// Simulate relay on destination chain endpoint.relayMessage(0);

// Verify execution on destination assertEq(protocolB.totalAssets(), 100 ether); }

function testReplayProtection() public { // Each message has unique nonce // Replay attempt should fail bytes memory payload = abi.encodeWithSelector(...);

bridge.sendMessage(chainIdB, address(protocolB), payload); endpoint.relayMessage(0);

vm.expectRevert("AlreadyExecuted"); endpoint.relayMessage(0); // Duplicate nonce } } `

Hợp đồng Omnichain (LayerZero, Wormhole, Axelar)

`solidity contract OmnichainTest is Test { // Mock LayerZero endpoint MockLZEndpoint lzEndpoint; OmnichainVault vaultA; OmnichainVault vaultB;

function testCrossChainDeposit() public { // User deposits on Chain A vm.prank(alice); vaultA.depositCrossChain(100 ether, chainIdB, address(vaultB));

// Message sent via LayerZero // Simulate receipt on Chain B lzEndpoint.simulateReceipt(chainIdA, address(vaultA), payload);

// Vault B should credit user assertEq(vaultB.balanceOf(alice), 100 ether); } } `

Kiểm tra proxy/có thể nâng cấp

`solidity contract UpgradeableTest is Test { ProxyAdmin admin; TransparentUpgradeableProxy proxy; VaultV1 vaultV1; VaultV2 vaultV2;

function setUp() public { admin = new ProxyAdmin(); vaultV1 = new VaultV1(); proxy = new TransparentUpgradeableProxy( address(vaultV1), admin, "" // No initializer ); }

function testStorageLayoutCollision() public { // Deploy V2 with new state variable vaultV2 = new VaultV2();

// Upgrade vm.prank(address(admin)); proxy.upgradeTo(address(vaultV2));

// Verify V1 state preserved IVault(proxy).deposit(100 ether, alice); assertEq(IVault(proxy).balanceOf(alice), 100 ether);

// New V2 functionality works IVaultV2(proxy).newFeature(); }

function testInitializerGuard() public { // V2 initializer should not be callable twice vm.expectRevert("Initializable: already initialized"); IVaultV2(proxy).initialize(); } } `

Kiểm tra Timelock quản trị

`solidity contract GovernanceTimelockTest is Test { TimelockController timelock; Governor governor; Vault vault;

function testProposalLifecycle() public { // 1. Propose bytes memory calldata = abi.encodeWithSelector( IVault.setFee.selector, 500 // 5% new fee ); uint256 proposalId = governor.propose( [address(vault)], [0], [calldata], "Update fee to 5%" );

// 2. Vote vm.warp(votingDelay + 1); vm.prank(whale); governor.castVote(proposalId, 1); // For

// 3. Queue (after voting period) vm.warp(votingPeriod + votingDelay + 1); governor.queue(proposalId);

// 4. Execute (after timelock delay) vm.warp(timelockDelay + 1); governor.execute(proposalId);

// Verify assertEq(vault.fee(), 500); } } `

8. Tính chất và kiểm tra hiệu suất của khí

`bash

forge test --gas-report

forge snapshot

forge snapshot --diff # Shows gas increases/decreases

forge loadtest --rpc-url $RPC_URL --tx-count 1000 --gas-limit 30000000 `

Danh sách kiểm tra tối ưu hóa khí

MẫuTối ưu hóaGas Saved
for (uint i=0; iĐộ dài bộ nhớ đệm: uint len = arr.length~3,000/ vòng
storage trong vòng lặpTự lưu trữ vào nhớ~20,000/ lần truy cập
require(msg.sender == owner)Lỗi tùy chỉnh: error NotOwner()~200/khôi phục
Lỗi thông báo stringLỗi tùy chỉnh với tham số~100/khôi phục
Nhiều SSTORE cho cùng một slotBộ trong bộ nhớ, ghi một lần~20,000/viết
unchecked { x + y }Từ bỏ kiểm tra quá tải (khi an toàn)~50/op

9. Những sai lầm phổ biến và các mô hình chống lại

  1. Chỉ kiểm tra các trường hợp thuận lợi — Không bao giờ kiểm tra các trường hợp thất bại (trở về trạng thái ban đầu, các trường hợp đặc biệt). Mỗi require cần có một thử nghiệm kích hoạt nó.

10. FAQ

Sự khác biệt giữa thử nghiệm fork và thử nghiệm invariant là gì? Thử nghiệm fork tái tạo trạng thái mainnet để mô phỏng các tình huống cụ thể (sự tấn công, trạng thái oracle). Thử nghiệm invariant bằng cách chứng minh toán học các thuộc tính đúng cho tất cả các chuyển đổi trạng thái có thể. Sử dụng thử nghiệm fork cho các cuộc tấn công đã được biết đến; sử dụng tính không thay đổi cho các cuộc tấn công chưa được biết đến.

Bao nhiêu lần chạy thử có đủ cho các hợp đồng DeFi? Tối thiểu 10.000 lần chạy thử cho việc thử nghiệm không trạng thái; 1.000+ lần chạy thử không thay đổi với độ sâu 50+ lần. Đối với các giao thức có giá trị cao, hãy chạy thử hàng đêm với 100.000+ lần chạy thử và 10.000+ lần chạy thử không thay đổi.

Tôi có thể thử các giao thức DeFi mà không cần truy cập RPC trên mainnet không? Có — sử dụng RPC công khai (tầng miễn phí của Alchemy), các fork anvil địa phương (anvil --fork-url) hoặc các fork testnet (Sepolia, Arbitrum Sepolia). Đối với CI, sử dụng các fork tạm thời thông qua GitHub Actions.

Các thuộc tính không thay đổi nào phải được mọi AMM đáp ứng?

  1. reserve0 * reserve1 ≥ kLast (với phí)
  2. reserve0 > 0 && reserve1 > 0 (không có dự trữ bằng 0)
  3. getAmountOut phù hợp với công thức sản phẩm không đổi
  4. totalSupply bằng tổng các vị trí LP

Làm thế nào để kiểm tra các hợp đồng DeFi có thể nâng cấp một cách an toàn?


* Bạn sẵn sàng kiểm tra quy trình DeFi của mình không? Đội ngũ an ninh của chúng tôi chuyên về kiểm tra không đổi, tái phát lại fork và xác minh chính thức. [Đặt lịch tư vấn](/security-audit)

Tags:

defi testing guidesdefi testing guides: complete tutorial for smart contract de