返回 SC 笔记
SC Day 82

Solidity: Mini Lending - 完整测试套件 + Gas优化

### 一、DeFi 测试方法论

2026-06-29
第四阶段:综合实战
MiniLending测试FoundryGas优化DeFiforge-test

日期: 2026-06-29 方向: Solidity 阶段: 第四阶段:综合实战 标签: #MiniLending #测试 #Foundry #Gas优化 #DeFi #forge-test


今日目标

Mini Lending 协议的核心合约已经完成(存款/借贷/还款/清算),今天聚焦两个关键环节:

  1. 完整测试套件: 覆盖所有业务路径、边界条件和攻击场景的 20+ 测试用例
  2. Gas 优化: 在测试验证的保障下,对合约进行有针对性的 Gas 优化

DeFi 协议的测试不同于普通 DApp——你需要模拟多个参与者、时间推移、价格变动、极端市场条件。一个不充分的测试套件等于在主网上"裸奔"。


核心概念

一、DeFi 测试方法论

1.1 测试金字塔(DeFi 版)

                    ┌──────────────┐
                    │  E2E / Fork  │ ← 主网 Fork 测试(最真实)
                   ─┤  Tests       ├─
                  / └──────────────┘ \
                 /                    \
                ┌──────────────────────┐
                │  Integration Tests    │ ← 多合约交互、预言机模拟
               ─┤  (多合约协作)         ├─
              / └──────────────────────┘ \
             /                            \
            ┌──────────────────────────────┐
            │  Unit Tests (单函数)          │ ← 每个函数的正确性
            │  Happy Path + Edge Cases     │
            └──────────────────────────────┘

1.2 DeFi 特有的测试维度

测试维度说明Mini Lending 示例
Happy Path正常业务流程存款→借款→还款
边界条件极端参数存0、借最大额、刚好触发清算
权限控制未授权操作非 owner 调用管理函数
重入攻击回调攻击withdraw 中的重入
价格操纵预言机攻击价格骤降触发级联清算
时间依赖利息累积1年后的利息计算精度
多用户交互博弈场景多人同时清算同一仓位
数学精度溢出/下溢极大数/极小数的利息计算

二、测试基础设施搭建

// test/MiniLending.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/MiniLending.sol";
import "../src/mocks/MockERC20.sol";
import "../src/mocks/MockPriceOracle.sol";

contract MiniLendingTest is Test {
    MiniLending public lending;
    MockERC20 public collateralToken;   // WETH
    MockERC20 public borrowToken;        // USDC
    MockPriceOracle public oracle;

    address public alice = makeAddr("alice");
    address public bob = makeAddr("bob");
    address public carol = makeAddr("carol");  // 清算者
    address public owner = makeAddr("owner");

    uint256 constant INITIAL_BALANCE = 100_000e18;
    uint256 constant COLLATERAL_FACTOR = 7500;   // 75%
    uint256 constant LIQUIDATION_BONUS = 500;    // 5%
    uint256 constant ETH_PRICE = 2000e8;         // $2000

    function setUp() public {
        vm.startPrank(owner);

        // 部署 Mock 合约
        collateralToken = new MockERC20("Wrapped ETH", "WETH", 18);
        borrowToken = new MockERC20("USD Coin", "USDC", 6);
        oracle = new MockPriceOracle();

        // 设置价格
        oracle.setPrice(address(collateralToken), ETH_PRICE);
        oracle.setPrice(address(borrowToken), 1e8); // $1

        // 部署 Lending 合约
        lending = new MiniLending(address(oracle));
        lending.addMarket(
            address(collateralToken),
            COLLATERAL_FACTOR,
            LIQUIDATION_BONUS
        );
        lending.addMarket(
            address(borrowToken),
            8000,   // 80% collateral factor
            500     // 5% liquidation bonus
        );

        vm.stopPrank();

        // 分发代币
        collateralToken.mint(alice, INITIAL_BALANCE);
        collateralToken.mint(bob, INITIAL_BALANCE);
        borrowToken.mint(alice, INITIAL_BALANCE);
        borrowToken.mint(bob, INITIAL_BALANCE);
        borrowToken.mint(carol, INITIAL_BALANCE);

        // 为 lending 合约提供流动性
        borrowToken.mint(address(lending), 1_000_000e6);

        // 用户 approve
        vm.prank(alice);
        collateralToken.approve(address(lending), type(uint256).max);
        vm.prank(alice);
        borrowToken.approve(address(lending), type(uint256).max);
        vm.prank(bob);
        collateralToken.approve(address(lending), type(uint256).max);
        vm.prank(bob);
        borrowToken.approve(address(lending), type(uint256).max);
    }

    // ==================== DEPOSIT 测试 ====================

    /// @notice 测试 #1: 正常存款
    function test_deposit_happyPath() public {
        uint256 depositAmount = 10e18;  // 10 WETH

        vm.prank(alice);
        lending.deposit(address(collateralToken), depositAmount);

        assertEq(
            lending.getDepositBalance(alice, address(collateralToken)),
            depositAmount
        );
        assertEq(
            collateralToken.balanceOf(address(lending)),
            depositAmount
        );
    }

    /// @notice 测试 #2: 存款金额为0应该 revert
    function test_deposit_zeroAmount_reverts() public {
        vm.prank(alice);
        vm.expectRevert("Amount must be > 0");
        lending.deposit(address(collateralToken), 0);
    }

    /// @notice 测试 #3: 多次存款累积
    function test_deposit_multipleDeposits_accumulate() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 5e18);
        lending.deposit(address(collateralToken), 3e18);
        vm.stopPrank();

        assertEq(
            lending.getDepositBalance(alice, address(collateralToken)),
            8e18
        );
    }

    /// @notice 测试 #4: 未支持的资产存款
    function test_deposit_unsupportedAsset_reverts() public {
        MockERC20 randomToken = new MockERC20("Random", "RND", 18);
        randomToken.mint(alice, 100e18);

        vm.startPrank(alice);
        randomToken.approve(address(lending), type(uint256).max);
        vm.expectRevert("Market not supported");
        lending.deposit(address(randomToken), 10e18);
        vm.stopPrank();
    }

    // ==================== WITHDRAW 测试 ====================

    /// @notice 测试 #5: 正常取款
    function test_withdraw_happyPath() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.withdraw(address(collateralToken), 5e18);
        vm.stopPrank();

        assertEq(
            lending.getDepositBalance(alice, address(collateralToken)),
            5e18
        );
    }

    /// @notice 测试 #6: 取款超过余额
    function test_withdraw_exceedsBalance_reverts() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        vm.expectRevert("Insufficient balance");
        lending.withdraw(address(collateralToken), 11e18);
        vm.stopPrank();
    }

    /// @notice 测试 #7: 有未还借款时取款导致健康因子不足
    function test_withdraw_wouldBreachHealthFactor_reverts() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        // 借出75%的额度 (最大)
        lending.borrow(address(borrowToken), 15000e6); // 10 ETH * $2000 * 75% = $15000
        // 尝试取走抵押品
        vm.expectRevert("Health factor too low");
        lending.withdraw(address(collateralToken), 1e18);
        vm.stopPrank();
    }

    // ==================== BORROW 测试 ====================

    /// @notice 测试 #8: 正常借款
    function test_borrow_happyPath() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        // 10 ETH * $2000 * 75% = $15000 最大借款
        lending.borrow(address(borrowToken), 10000e6); // 借$10000
        vm.stopPrank();

        assertEq(
            lending.getBorrowBalance(alice, address(borrowToken)),
            10000e6
        );
    }

    /// @notice 测试 #9: 超额借款
    function test_borrow_exceedsCollateral_reverts() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        vm.expectRevert("Insufficient collateral");
        lending.borrow(address(borrowToken), 16000e6); // 超过$15000限额
        vm.stopPrank();
    }

    /// @notice 测试 #10: 无抵押品借款
    function test_borrow_noCollateral_reverts() public {
        vm.prank(alice);
        vm.expectRevert("Insufficient collateral");
        lending.borrow(address(borrowToken), 1000e6);
    }

    /// @notice 测试 #11: 刚好借到最大额度
    function test_borrow_exactMaxAmount() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        // 精确借到75%限额
        lending.borrow(address(borrowToken), 15000e6);
        vm.stopPrank();

        // 健康因子应该刚好等于1
        uint256 healthFactor = lending.getHealthFactor(alice);
        assertApproxEqAbs(healthFactor, 1e18, 1e15); // 允许0.1%误差
    }

    // ==================== REPAY 测试 ====================

    /// @notice 测试 #12: 全额还款
    function test_repay_fullRepayment() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 5000e6);

        // 时间推移累积利息
        vm.warp(block.timestamp + 365 days);

        uint256 totalOwed = lending.getBorrowBalance(alice, address(borrowToken));
        borrowToken.approve(address(lending), totalOwed);
        lending.repay(address(borrowToken), totalOwed);
        vm.stopPrank();

        assertEq(lending.getBorrowBalance(alice, address(borrowToken)), 0);
    }

    /// @notice 测试 #13: 部分还款
    function test_repay_partialRepayment() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 10000e6);
        lending.repay(address(borrowToken), 3000e6);
        vm.stopPrank();

        assertEq(
            lending.getBorrowBalance(alice, address(borrowToken)),
            7000e6
        );
    }

    /// @notice 测试 #14: 还款金额为0
    function test_repay_zeroAmount_reverts() public {
        vm.prank(alice);
        vm.expectRevert("Amount must be > 0");
        lending.repay(address(borrowToken), 0);
    }

    // ==================== LIQUIDATION 测试 ====================

    /// @notice 测试 #15: 正常清算流程
    function test_liquidate_happyPath() public {
        // Alice 存入 10 ETH,借出 $14000 USDC (接近最大额度)
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 14000e6);
        vm.stopPrank();

        // ETH 价格下跌 30%: $2000 → $1400
        oracle.setPrice(address(collateralToken), 1400e8);

        // Carol 清算 Alice
        uint256 liquidateAmount = 7000e6; // 清算一半
        vm.startPrank(carol);
        borrowToken.approve(address(lending), liquidateAmount);
        lending.liquidate(alice, address(borrowToken), liquidateAmount);
        vm.stopPrank();

        // Carol 应该获得了折扣的抵押品
        uint256 carolCollateral = collateralToken.balanceOf(carol);
        assertTrue(carolCollateral > 0, "Liquidator should receive collateral");

        // Alice 的借款减少
        assertTrue(
            lending.getBorrowBalance(alice, address(borrowToken)) < 14000e6,
            "Borrower debt should decrease"
        );
    }

    /// @notice 测试 #16: 健康仓位不能被清算
    function test_liquidate_healthyPosition_reverts() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 5000e6); // 保守借款
        vm.stopPrank();

        vm.startPrank(carol);
        borrowToken.approve(address(lending), 5000e6);
        vm.expectRevert("Position is healthy");
        lending.liquidate(alice, address(borrowToken), 5000e6);
        vm.stopPrank();
    }

    /// @notice 测试 #17: 清算奖励计算正确
    function test_liquidate_bonusCalculation() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 14000e6);
        vm.stopPrank();

        // 价格下跌
        oracle.setPrice(address(collateralToken), 1400e8);

        uint256 liquidateAmount = 1400e6; // 清算 $1400

        uint256 carolCollateralBefore = collateralToken.balanceOf(carol);

        vm.startPrank(carol);
        borrowToken.approve(address(lending), liquidateAmount);
        lending.liquidate(alice, address(borrowToken), liquidateAmount);
        vm.stopPrank();

        uint256 carolCollateralAfter = collateralToken.balanceOf(carol);
        uint256 received = carolCollateralAfter - carolCollateralBefore;

        // 预期: $1400 / $1400(新ETH价格) * 1.05(清算奖励) = 1.05 ETH
        uint256 expectedCollateral = 1.05e18;
        assertApproxEqRel(received, expectedCollateral, 0.01e18); // 1%误差
    }

    // ==================== INTEREST 测试 ====================

    /// @notice 测试 #18: 利息累积正确
    function test_interest_accruesOverTime() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 10000e6);
        vm.stopPrank();

        uint256 borrowBefore = lending.getBorrowBalance(alice, address(borrowToken));

        // 推进1年
        vm.warp(block.timestamp + 365 days);

        uint256 borrowAfter = lending.getBorrowBalance(alice, address(borrowToken));

        // 借款应该增加(利息)
        assertTrue(borrowAfter > borrowBefore, "Interest should accrue");

        // 假设年利率5%,检查大致范围
        uint256 expectedInterest = (borrowBefore * 500) / 10000; // 5%
        uint256 actualInterest = borrowAfter - borrowBefore;
        assertApproxEqRel(actualInterest, expectedInterest, 0.05e18); // 5%误差
    }

    // ==================== ATTACK 场景测试 ====================

    /// @notice 测试 #19: 重入攻击防护
    function test_attack_reentrancy_reverts() public {
        // 部署恶意合约尝试重入
        ReentrancyAttacker attacker = new ReentrancyAttacker(address(lending));
        collateralToken.mint(address(attacker), 100e18);

        vm.prank(address(attacker));
        collateralToken.approve(address(lending), type(uint256).max);

        // 攻击应该失败
        vm.expectRevert(); // ReentrancyGuard 或其他保护
        attacker.attack(address(collateralToken), 10e18);
    }

    /// @notice 测试 #20: 价格操纵 — 闪电贷场景模拟
    function test_attack_priceManipulation() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 10000e6);
        vm.stopPrank();

        // 模拟: 攻击者在同一区块内操纵价格
        // 好的预言机设计应该使用 TWAP 而非即时价格
        oracle.setPrice(address(collateralToken), 100000e8); // 价格暴涨

        // 即使价格暴涨,已有借款不应该允许借更多(除非有刷新机制)
        // 这里测试的是预言机更新后的行为是否合理
        vm.startPrank(alice);
        // 价格涨了应该可以借更多 — 但需要有TWAP保护
        lending.borrow(address(borrowToken), 100000e6);
        vm.stopPrank();

        // 价格恢复正常
        oracle.setPrice(address(collateralToken), ETH_PRICE);

        // Alice 现在应该是可清算状态
        uint256 healthFactor = lending.getHealthFactor(alice);
        assertTrue(healthFactor < 1e18, "Should be liquidatable after price reverts");
    }

    /// @notice 测试 #21: 自我清算
    function test_selfLiquidation_reverts() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);
        lending.borrow(address(borrowToken), 14000e6);
        vm.stopPrank();

        oracle.setPrice(address(collateralToken), 1400e8);

        // Alice 尝试自我清算
        vm.startPrank(alice);
        borrowToken.approve(address(lending), 7000e6);
        vm.expectRevert("Cannot self-liquidate");
        lending.liquidate(alice, address(borrowToken), 7000e6);
        vm.stopPrank();
    }

    // ==================== FUZZ 测试 ====================

    /// @notice 测试 #22: Fuzz 存款金额
    function testFuzz_deposit(uint256 amount) public {
        amount = bound(amount, 1, INITIAL_BALANCE); // 限制范围

        vm.prank(alice);
        lending.deposit(address(collateralToken), amount);

        assertEq(
            lending.getDepositBalance(alice, address(collateralToken)),
            amount
        );
    }

    /// @notice 测试 #23: Fuzz 借款不超过抵押限额
    function testFuzz_borrow_withinLimit(uint256 borrowAmount) public {
        uint256 depositAmount = 10e18;
        uint256 maxBorrow = (depositAmount * uint256(ETH_PRICE) * COLLATERAL_FACTOR)
            / (1e8 * 10000 * 1e12); // 调整 decimals

        borrowAmount = bound(borrowAmount, 1, maxBorrow);

        vm.startPrank(alice);
        lending.deposit(address(collateralToken), depositAmount);
        lending.borrow(address(borrowToken), borrowAmount);
        vm.stopPrank();

        assertEq(
            lending.getBorrowBalance(alice, address(borrowToken)),
            borrowAmount
        );
    }

    // ==================== GAS SNAPSHOT 测试 ====================

    /// @notice 测试 #24: Gas 基准测试
    function test_gas_deposit() public {
        vm.prank(alice);
        uint256 gasBefore = gasleft();
        lending.deposit(address(collateralToken), 10e18);
        uint256 gasUsed = gasBefore - gasleft();

        emit log_named_uint("Gas used for deposit", gasUsed);
        assertTrue(gasUsed < 100_000, "Deposit should cost less than 100k gas");
    }

    function test_gas_borrow() public {
        vm.startPrank(alice);
        lending.deposit(address(collateralToken), 10e18);

        uint256 gasBefore = gasleft();
        lending.borrow(address(borrowToken), 5000e6);
        uint256 gasUsed = gasBefore - gasleft();
        vm.stopPrank();

        emit log_named_uint("Gas used for borrow", gasUsed);
        assertTrue(gasUsed < 150_000, "Borrow should cost less than 150k gas");
    }
}

// ==================== 辅助合约 ====================

contract ReentrancyAttacker {
    MiniLending public lending;
    bool public attacking;

    constructor(address _lending) {
        lending = MiniLending(_lending);
    }

    function attack(address token, uint256 amount) external {
        attacking = true;
        lending.deposit(token, amount);
        lending.withdraw(token, amount);
    }

    // ERC20 转账回调中尝试重入
    fallback() external {
        if (attacking) {
            attacking = false;
            // 尝试再次取款
            // lending.withdraw(...);
        }
    }
}

三、Gas 优化策略应用

3.1 存储优化: Slot Packing

// 优化前: 3个 storage slot (每个 slot 32 bytes)
struct UserPositionBefore {
    uint256 depositAmount;    // slot 0: 32 bytes
    uint256 borrowAmount;     // slot 1: 32 bytes
    uint256 lastUpdateTime;   // slot 2: 32 bytes
}
// 读取成本: 3 * 2100 = 6300 gas (冷读取)

// 优化后: 2个 storage slot
struct UserPositionAfter {
    uint128 depositAmount;    // slot 0: 16 bytes ┐
    uint128 borrowAmount;     // slot 0: 16 bytes ┘ (同一个 slot)
    uint64 lastUpdateTime;    // slot 1: 8 bytes
    uint64 interestIndex;     // slot 1: 8 bytes
    uint128 reserved;         // slot 1: 16 bytes (预留)
}
// 读取成本: 2 * 2100 = 4200 gas (冷读取), 节省 33%

3.2 用 immutable 替代 storage 读取

// 优化前: 每次调用都读 storage
contract LendingBefore {
    address public oracle;
    uint256 public liquidationBonus;
    uint256 public collateralFactor;

    function getHealthFactor(address user) public view returns (uint256) {
        // 3次 SLOAD: 3 * 2100 = 6300 gas
        uint256 price = IPriceOracle(oracle).getPrice(token);
        uint256 maxBorrow = collateral * collateralFactor / 10000;
        // ...
    }
}

// 优化后: 使用 immutable
contract LendingAfter {
    address public immutable oracle;            // 编译进 bytecode
    uint256 public immutable liquidationBonus;  // 读取只需 3 gas
    uint256 public immutable collateralFactor;  // vs SLOAD 的 2100 gas

    constructor(address _oracle, uint256 _bonus, uint256 _factor) {
        oracle = _oracle;
        liquidationBonus = _bonus;
        collateralFactor = _factor;
    }
}

3.3 使用 unchecked 优化安全的算术

function _calculateInterest(
    uint256 principal,
    uint256 ratePerSecond,
    uint256 timeElapsed
) internal pure returns (uint256) {
    // 安全的场景下使用 unchecked 节省 gas
    // 前提: 我们已经验证不会溢出
    unchecked {
        // 这些值在合理范围内,不会溢出 uint256
        uint256 interest = (principal * ratePerSecond * timeElapsed) / 1e18;
        return interest;
    }
}

// 循环中的优化
function _updateMultiplePositions(address[] calldata users) external {
    uint256 len = users.length;
    for (uint256 i; i < len;) {
        _updatePosition(users[i]);
        unchecked { ++i; }  // 节省约 60 gas/iteration
    }
}

3.4 使用 calldata 替代 memory

// 优化前: memory 会复制数据
function batchLiquidate(address[] memory users) external {
    // 数组被复制到内存,浪费 gas
}

// 优化后: calldata 直接读取 calldata
function batchLiquidate(address[] calldata users) external {
    // 直接从 calldata 读取,不复制
    // 节省: 每个元素 ~60 gas
}

3.5 自定义错误替代字符串

// 优化前: 字符串消耗更多 gas (部署+运行时)
require(amount > 0, "Amount must be greater than zero");
// 错误字符串存储在 bytecode 中

// 优化后: 自定义错误
error AmountZero();
error InsufficientCollateral(uint256 required, uint256 available);
error PositionHealthy(uint256 healthFactor);

function deposit(address token, uint256 amount) external {
    if (amount == 0) revert AmountZero();
    // 节省: ~200 gas (运行时) + 部署时更省 bytecode
}

function liquidate(address user, address token, uint256 amount) external {
    uint256 hf = getHealthFactor(user);
    if (hf >= 1e18) revert PositionHealthy(hf);
    // 自定义错误还能携带参数,更好 debug
}

四、Gas 优化效果测量

# 使用 forge snapshot 记录 Gas 基准
forge snapshot --snap .gas-snapshot-before

# 执行优化后再次快照
forge snapshot --snap .gas-snapshot-after

# 对比差异
forge snapshot --diff .gas-snapshot-before
# 示例输出:
test_deposit_happyPath()        (gas: -3200, -4.2%)
test_borrow_happyPath()         (gas: -5100, -5.8%)
test_liquidate_happyPath()      (gas: -8700, -6.1%)
test_repay_fullRepayment()      (gas: -4500, -5.0%)

关键要点总结

  1. DeFi 测试的核心原则: 不仅测试"能不能工作",更要测试"攻击者能不能利用"。每个外部函数至少需要: happy path + 权限检查 + 边界条件 + 攻击场景。

  2. Fuzz 测试是 DeFi 的必需品: testFuzz_ 前缀让 Foundry 自动生成随机输入,覆盖手写测试无法想到的边界。用 bound() 限制输入范围。

  3. Gas 优化的黄金法则: 先写正确的代码和完整的测试,再优化。使用 forge snapshot --diff 量化每次优化的效果。

  4. 最有效的 Gas 优化手段排序: (1) 减少 SSTORE/SLOAD → (2) Slot Packing → (3) immutable/constant → (4) calldata 替代 memory → (5) unchecked → (6) 自定义错误。

  5. 测试覆盖率不等于安全性: 100% 行覆盖率不意味着合约安全。需要专门的攻击场景测试和后续的审计。


常见误区

  1. 误区: 先优化再测试 — 错!必须先有完整测试再优化。没有测试的优化等于盲目修改,可能引入 bug。

  2. 误区: unchecked 可以随处使用 — unchecked 块内溢出不会 revert。只在你确定不会溢出的地方使用(如循环计数器递增)。

  3. 误区: Gas 优化越多越好 — 过度优化会降低代码可读性和可维护性。只有高频调用的函数(deposit/borrow/swap)值得深度优化。

  4. 误区: 测试只需 happy path — DeFi 协议的绝大多数安全事件来自边界条件和攻击场景,而非正常流程中的 bug。


面试关联

Q: "你如何测试一个 DeFi 借贷协议?"

回答框架:

我会构建四层测试体系:

  1. 单元测试: 每个函数的正确性(deposit/withdraw/borrow/repay/liquidate)
  2. 边界测试: 零值、最大值、刚好触发阈值的场景
  3. 攻击测试: 重入、价格操纵、闪电贷、自我清算等已知攻击模式
  4. Fuzz 测试: 随机输入发现未预期的边界

此外,关键指标是"清算机制在极端市场条件下是否仍然正确"——这需要模拟价格暴跌、级联清算等场景。在上线前,还需要 Fork 主网进行集成测试。

Q: "你会用哪些 Gas 优化技术?"

按影响从大到小: (1) 减少存储操作(SSTORE 20000 gas)——合并多次写入、使用 events 替代存储;(2) Storage Slot Packing——将多个小变量打包到一个 32 bytes slot;(3) 使用 immutable/constant 避免运行时读取;(4) 使用 calldata 替代 memory 参数;(5) unchecked 块用于安全的算术。所有优化都必须有测试保障和量化数据。


参考资源

  1. Foundry Book - Testing — Foundry 测试官方文档
  2. Foundry Book - Gas Snapshots — Gas 快照对比
  3. Trail of Bits - Building Secure Smart Contracts — 安全测试实践
  4. Solidity Gas Optimizations — RareSkills Gas 优化指南
  5. Aave V3 Test Suite — 生产级 DeFi 测试参考