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
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:
- 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
| Công cụ | Tốt nhất cho | Cheatcodes | Runner không thay đổi | Fuzzing | Kênh học tập |
|---|---|---|---|---|---|
| Hàn gốm | Tốc độ, DeFi-native, fork/invariant/fuzz tất cả trong một | ✅ Tốt | ✅ Được tích hợp | ✅ Đã tích hợp | Thấp (Solidity) |
| Mũ bảo hiểm | Các nhóm TypeScript, hệ sinh thái plugin | ❌ thủ công | ❌ External | ❌ Ngoại bộ | Trung bình (JS/TS) |
| Echidna | Fuzzing dựa trên thuộc tính, có trạng thái | N/A | N/A | ✅ Đặc biệt | Tốt (giống với Haskell) |
| Medusa | Fuzzing với hiệu suất cao, CI/CD | N/A | N/A | ✅ Nhanh nhất | Cao |
| Slither | Phân tích tĩnh, phát hiện lỗ hổng | N/A | N/A | N/A | Thấ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
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
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
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ức | Cá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ọc | totalShares * 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ị:
- Dòng gọi dẫn đến vi phạm
- Tình trạng trước/ sau mỗi cuộc gọi
- 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
- Replay — Thêm chuỗi như một thử nghiệm đơn để debug
`bash
[FAIL. Reason: invariant_reserveProductNonDecreasing() failed] Counterexample:
- swap(token0, 100 ether) → reserve0=1100, reserve1=909
- swap(token1, 500 ether) → reserve0=1000, reserve1=1000 // k decreased!
- 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
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ạnh | Fuzzing | Kiểm tra không thay đổi |
|---|---|---|
| Goal | Find edge cases, crashes | Hãy chứng minh các thuộc tính đúng |
| Nhập | Ngẫ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 ra | Nhập liệu bị lỗi | Dãy thí dụ phản bác |
| Tốt nhất cho | Toá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), Echidna | Foundry (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
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ẫu | Tối ưu hóa | Gas Saved |
|---|---|---|
for (uint i=0; i | Độ dài bộ nhớ đệm: uint len = arr.length | ~3,000/ vòng |
storage trong vòng lặp | Tự 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 string | Lỗi tùy chỉnh với tham số | ~100/khôi phục |
Nhiều SSTORE cho cùng một slot | Bộ 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
- 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
requirecầ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?
reserve0 * reserve1 ≥ kLast(với phí)reserve0 > 0 && reserve1 > 0(không có dự trữ bằng 0)getAmountOutphù hợp với công thức sản phẩm không đổitotalSupplybằ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)
