返回 SC 笔记
SC Day 79

复习 — Week 13 总结 (Mini Lending 进度 + Solana 项目回顾)

### 1. Week 13 知识图谱

2026-06-28
第四阶段:综合实战 (73-80)
reviewmini-lendingsolanatestingsecurityweek13

日期: 2026-06-28 方向: Solidity / Solana / 多链 阶段: 第四阶段:综合实战 (73-80) 标签: #review #mini-lending #solana #testing #security #week13


今日目标

类型内容
学习回顾 Week 13 所有知识点,建立知识关联
实操Mini Lending 进度检查 + 测试策略制定 + 安全审查
产出Week 13 知识图谱 + Mini Lending 测试计划 + 安全检查清单

核心概念

1. Week 13 知识图谱

Week 13 覆盖范围
├── Day 71: Move 安全
│   ├── 线性类型系统 → 防重入
│   ├── Capability 模式 → 编译时权限
│   ├── Hot Potato → 闪电贷安全
│   └── 剩余攻击面 → 逻辑/经济/权限
│
├── Day 72: DVDF #5 & #8
│   ├── 闪电贷作为攻击放大器
│   ├── 奖励分配操纵 → 时间加权防护
│   ├── 预言机操纵 → Chainlink 替代 spot price
│   └── 五种闪电贷攻击模式
│
├── Day 73: Mini Lending 设计
│   ├── 协议规格文档
│   ├── Aave/Compound 架构对比
│   ├── 核心数据结构
│   └── 接口定义
│
├── Day 74: Solana 安全
│   ├── 6 大漏洞类型
│   ├── Anchor 自动防护
│   ├── 整数溢出 (release 模式!)
│   └── 安全 Vault 示例
│
├── Day 75: Mini Lending 存款池
│   ├── 份额制会计 (ERC4626)
│   ├── 通胀攻击防护
│   ├── 利率模型实现
│   └── 利息累计机制
│
├── Day 76: Solana 性能
│   ├── Zero-Copy 反序列化
│   ├── Compute Units 优化
│   ├── Sealevel 并行模型
│   └── 数据架构设计影响吞吐量
│
├── Day 77: Mini Lending 借款
│   ├── 借款索引机制
│   ├── 健康因子计算
│   ├── Chainlink 集成
│   └── 清算级联风险
│
└── Day 78: Anchor 高级
    ├── remaining_accounts
    ├── 自定义错误码
    ├── 事件日志
    └── 增强 Escrow 程序

2. Mini Lending 进度检查

已完成功能

功能状态Day核心合约/函数
协议设计与架构已完成73数据结构定义、接口设计
利率模型已完成75InterestRateModel.sol
存款 (supply)已完成75MiniLendingPool.supply()
取款 (withdraw)已完成75MiniLendingPool.withdraw()
利息累计已完成75_accrueInterest()
份额转换已完成75_convertToShares/Assets()
借款 (borrow)已完成77MiniLendingPool.borrow()
还款 (repay)已完成77MiniLendingPool.repay()
健康因子已完成77_calculateHealthFactor()
Chainlink 集成已完成77_getAssetPriceUSD()
清算 (liquidate)待实现80Day 80 实现

待实现功能清单

Day 80 计划:
├── liquidate() 函数
│   ├── 健康因子检查 (HF < 1)
│   ├── 部分清算逻辑
│   ├── 清算奖励计算
│   └── 坏账处理
├── 完整测试套件
│   ├── 单元测试
│   ├── 集成测试
│   └── 边界测试
└── Gas 优化(可选)

3. Mini Lending 测试策略

测试金字塔

                    ┌────────┐
                    │  E2E   │ Forked Mainnet 测试
                   ┌┴────────┴┐
                   │ 集成测试  │ 多合约交互
                  ┌┴──────────┴┐
                  │   单元测试  │ 单个函数
                 ┌┴────────────┴┐
                 │   静态分析   │ Slither/Mythril
                 └──────────────┘

单元测试清单

// ============ InterestRateModel 测试 ============
contract InterestRateModelTest is Test {

    // 基础利率测试
    function test_BorrowRate_ZeroUtilization() public {
        // U = 0% → rate = baseRate
        uint256 rate = model.getBorrowRatePerYear(0);
        assertEq(rate, 2e16); // 2%
    }

    function test_BorrowRate_OptimalUtilization() public {
        // U = 80% → rate = baseRate + slope1 = 2% + 4% = 6%
        uint256 rate = model.getBorrowRatePerYear(80e16);
        assertEq(rate, 6e16);
    }

    function test_BorrowRate_MaxUtilization() public {
        // U = 100% → rate = baseRate + slope1 + slope2 = 2% + 4% + 300% = 306%
        uint256 rate = model.getBorrowRatePerYear(100e16);
        assertEq(rate, 306e16);
    }

    // 边界测试
    function test_BorrowRate_JustAboveOptimal() public {
        uint256 rate = model.getBorrowRatePerYear(80e16 + 1);
        assertGt(rate, 6e16); // 应该略高于 6%
    }

    // Fuzz 测试
    function testFuzz_BorrowRate_AlwaysPositive(uint256 utilization) public {
        utilization = bound(utilization, 0, 100e16);
        uint256 rate = model.getBorrowRatePerYear(utilization);
        assertGe(rate, 2e16); // 至少是 baseRate
    }

    function testFuzz_BorrowRate_Monotonic(uint256 u1, uint256 u2) public {
        u1 = bound(u1, 0, 100e16);
        u2 = bound(u2, u1, 100e16);
        // 利率应该随利用率单调递增
        assertGe(
            model.getBorrowRatePerYear(u2),
            model.getBorrowRatePerYear(u1)
        );
    }
}

// ============ Supply/Withdraw 测试 ============
contract SupplyWithdrawTest is Test {

    // 基础功能
    function test_Supply_FirstDepositor() public {}
    function test_Supply_SecondDepositor() public {}
    function test_Supply_AfterInterestAccrual() public {}
    function test_Withdraw_Full() public {}
    function test_Withdraw_Partial() public {}
    function test_Withdraw_MaxUint256() public {}

    // 边界条件
    function test_Supply_MinimumAmount() public {}
    function test_Supply_ZeroAmount_Reverts() public {}
    function test_Withdraw_MoreThanBalance_Reverts() public {}
    function test_Withdraw_InsufficientLiquidity_Reverts() public {}

    // 通胀攻击防护
    function test_InflationAttack_Mitigated() public {
        // 步骤 1:攻击者存入 1 wei
        vm.prank(attacker);
        pool.supply(address(usdc), 1);

        // 步骤 2:攻击者直接转入大量代币
        vm.prank(attacker);
        usdc.transfer(address(pool), 10_000e6);

        // 步骤 3:受害者存入
        vm.prank(victim);
        uint256 shares = pool.supply(address(usdc), 10_000e6);

        // 验证:受害者获得了合理数量的份额
        assertGt(shares, 0, "Victim should get shares");

        // 验证:受害者取出时不会损失过多
        vm.prank(victim);
        uint256 withdrawAmount = pool.withdraw(address(usdc), shares);
        assertGt(withdrawAmount, 9_999e6, "Victim should not lose significant value");
    }

    // Fuzz 测试
    function testFuzz_SupplyWithdraw_NoProfit(uint256 amount) public {
        amount = bound(amount, 1, 1e30);
        // 存入再立即取出,不应该获利
        vm.startPrank(alice);
        usdc.mint(alice, amount);
        usdc.approve(address(pool), amount);
        uint256 shares = pool.supply(address(usdc), amount);
        uint256 withdrawn = pool.withdraw(address(usdc), shares);
        vm.stopPrank();

        assertLe(withdrawn, amount, "Should not profit from immediate withdraw");
    }
}

// ============ Borrow/Repay 测试 ============
contract BorrowRepayTest is Test {

    function test_Borrow_BasicFlow() public {}
    function test_Borrow_ExceedsLTV_Reverts() public {}
    function test_Borrow_MultipleAssets() public {}
    function test_Repay_Full() public {}
    function test_Repay_Partial() public {}
    function test_Repay_WithAccruedInterest() public {}

    // 健康因子测试
    function test_HealthFactor_NoDebt_MaxUint() public {}
    function test_HealthFactor_SafePosition() public {}
    function test_HealthFactor_AtRisk() public {}
    function test_HealthFactor_Underwater() public {}
    function test_HealthFactor_PriceChange() public {}

    // 利息累计测试
    function test_Interest_IncreasesDebt() public {
        // 设置
        setupBorrowPosition(); // Alice 存 ETH,借 USDC

        uint256 debtBefore = pool.getUserDebt(alice, address(usdc));

        // 推进时间
        vm.warp(block.timestamp + 365 days);
        pool.accrueInterest(address(usdc)); // 触发更新

        uint256 debtAfter = pool.getUserDebt(alice, address(usdc));
        assertGt(debtAfter, debtBefore, "Debt should increase");

        // 验证利息金额合理
        uint256 interest = debtAfter - debtBefore;
        uint256 expectedInterest = debtBefore * 5 / 100; // 大约 5% 年化
        assertApproxEqRel(interest, expectedInterest, 5e16); // 5% 误差容忍
    }
}

// ============ 清算测试(Day 80 实现) ============
contract LiquidationTest is Test {

    function test_Liquidate_BasicFlow() public {}
    function test_Liquidate_HealthFactorOk_Reverts() public {}
    function test_Liquidate_PartialLiquidation() public {}
    function test_Liquidate_BonusCalculation() public {}
    function test_Liquidate_BadDebt() public {}
}

4. 安全审查检查清单

基于 Week 13 学到的所有安全知识,对 Mini Lending 进行审查:

## Mini Lending 安全审查清单

### 重入防护
- [x] 使用 ReentrancyGuard (nonReentrant)
- [x] 状态先更新,再进行外部调用 (CEI 模式)
- [ ] 检查所有外部调用点

### 整数溢出
- [x] Solidity 0.8+ 自动检查
- [ ] 检查 unchecked 块使用是否安全
- [ ] 中间计算是否可能超过 uint256

### 预言机安全
- [x] Chainlink 价格正值检查
- [x] 新鲜度检查 (MAX_PRICE_AGE)
- [x] 轮次完整性检查
- [ ] 价格精度标准化是否正确
- [ ] L2 Sequencer 检查(如果部署在 L2)
- [ ] 极端价格的处理(价格为 0、极大值)

### 份额计算
- [x] 虚拟偏移防通胀攻击
- [x] 存款向下取整(保护协议)
- [x] 取款向下取整(保护协议)
- [ ] 检查除零情况
- [ ] 精度损失是否在可接受范围内

### 借贷逻辑
- [x] 借款前检查健康因子
- [x] 取款前应检查健康因子(有借款时)
- [ ] 利息累计的精度验证
- [ ] borrowIndex 增长是否可能溢出
- [ ] 全额还款时的精度处理

### 清算机制(Day 80 实现时检查)
- [ ] 只在 HF < 1 时允许清算
- [ ] 清算后借款人 HF 应该改善
- [ ] 清算奖励不应超过抵押品价值
- [ ] 坏账情况的处理

### 权限控制
- [x] 管理函数有 onlyOwner
- [ ] 暂停机制(紧急情况)
- [ ] 市场配置参数的合理范围检查

### 通用
- [ ] 所有 external 函数的入参验证
- [ ] 事件是否正确记录所有关键操作
- [ ] 升级机制(如果需要)

5. Solana 技能回顾

Solana 开发技能树 (截至 Day 78)
├── 基础
│   ├── 账户模型理解 ✅
│   ├── PDA 推导和使用 ✅
│   ├── CPI (跨程序调用) ✅
│   └── SPL Token 操作 ✅
│
├── Anchor 框架
│   ├── 账户约束 (seeds, has_one, constraint) ✅
│   ├── 自定义错误 (#[error_code]) ✅
│   ├── 事件 (emit!) ✅
│   ├── Zero-Copy (#[zero_copy]) ✅
│   ├── remaining_accounts ✅
│   └── init_if_needed ✅
│
├── 安全
│   ├── Signer 验证 ✅
│   ├── Owner 验证 ✅
│   ├── PDA 种子碰撞 ✅
│   ├── 整数溢出 (checked_*) ✅
│   ├── 账户类型混淆 ✅
│   └── 安全关闭账户 ✅
│
├── 性能
│   ├── Zero-Copy 优化 ✅
│   ├── CU 预算管理 ✅
│   ├── 并行设计 ✅
│   └── Address Lookup Tables ✅
│
└── 项目实战
    ├── 基础 Vault ✅
    ├── 增强 Escrow ✅
    └── Token Swap (未实现)

6. 跨链安全对比总结

漏洞类型SoliditySolana/AnchorMove
重入高危(需 ReentrancyGuard)低(CPI 不重入)无(语言层面消除)
整数溢出0.8+ 自动检查Release 不检查!VM 层面检查
权限控制modifier(运行时)Signer/has_one(编译+运行)Capability(编译时)
预言机操纵常见常见常见
逻辑错误常见常见常见
闪电贷攻击常见Solana 原生不支持闪电贷Hot Potato 约束
未初始化存储偶发账户未验证
合约升级风险Proxy 模式程序升级Package 升级

代码实战

Mini Lending 集成测试模板

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import "../src/MiniLendingPool.sol";
import "../src/InterestRateModel.sol";

/// @title Mini Lending 集成测试
/// @notice 测试多步骤操作流程
contract IntegrationTest is Test {
    MiniLendingPool pool;
    MockERC20 weth;
    MockERC20 usdc;
    MockChainlink ethPriceFeed;
    MockChainlink usdcPriceFeed;

    address alice = makeAddr("alice");
    address bob = makeAddr("bob");
    address liquidator = makeAddr("liquidator");

    function setUp() public {
        // 部署合约
        pool = new MiniLendingPool();
        weth = new MockERC20("Wrapped ETH", "WETH", 18);
        usdc = new MockERC20("USD Coin", "USDC", 6);

        ethPriceFeed = new MockChainlink(8);
        ethPriceFeed.setPrice(2000e8); // $2000

        usdcPriceFeed = new MockChainlink(8);
        usdcPriceFeed.setPrice(1e8); // $1

        // 配置市场...
        // 分配代币...
    }

    /// @notice 完整借贷-清算流程测试
    function test_FullLendingCycle() public {
        // === 阶段 1:存款 ===
        // Bob 存入 1,000,000 USDC 作为流动性
        vm.prank(bob);
        pool.supply(address(usdc), 1_000_000e6);

        // Alice 存入 10 ETH 作为抵押品
        vm.prank(alice);
        pool.supply(address(weth), 10 ether);

        // === 阶段 2:借款 ===
        // Alice 借出 12,000 USDC
        // LTV 80%: 10 * 2000 * 0.8 = 16,000 可借
        vm.prank(alice);
        pool.borrow(address(usdc), 12_000e6);

        uint256 hf = pool.getHealthFactor(alice);
        assertGt(hf, 1e18, "Should be healthy after borrow");

        // === 阶段 3:利息累计 ===
        vm.warp(block.timestamp + 30 days);

        uint256 debt = pool.getUserDebt(alice, address(usdc));
        assertGt(debt, 12_000e6, "Debt should accrue interest");

        // === 阶段 4:价格下跌 → 清算 ===
        ethPriceFeed.setPrice(1100e8); // ETH 跌到 $1100

        hf = pool.getHealthFactor(alice);
        assertLt(hf, 1e18, "Should be underwater");

        // Liquidator 清算 Alice 的仓位
        vm.prank(liquidator);
        pool.liquidate(alice, address(usdc), address(weth), 6000e6);

        // 验证清算后状态
        hf = pool.getHealthFactor(alice);
        assertGt(hf, 1e18, "Should be healthy after liquidation");

        // === 阶段 5:Alice 还款 ===
        uint256 remainingDebt = pool.getUserDebt(alice, address(usdc));
        vm.prank(alice);
        pool.repay(address(usdc), type(uint256).max);

        assertEq(pool.getUserDebt(alice, address(usdc)), 0);

        // === 阶段 6:Bob 取回存款 + 利息 ===
        vm.prank(bob);
        uint256 withdrawn = pool.withdraw(address(usdc), type(uint256).max);
        assertGt(withdrawn, 1_000_000e6, "Bob should earn interest");
    }

    /// @notice 多用户并发场景
    function test_MultiUserScenario() public {
        address[] memory users = new address[](5);
        for (uint i = 0; i < 5; i++) {
            users[i] = makeAddr(string(abi.encodePacked("user", i)));
            weth.mint(users[i], 100 ether);
            usdc.mint(users[i], 1_000_000e6);

            vm.startPrank(users[i]);
            weth.approve(address(pool), type(uint256).max);
            usdc.approve(address(pool), type(uint256).max);

            // 每个用户存入不同金额
            pool.supply(address(weth), (i + 1) * 10 ether);
            pool.supply(address(usdc), (i + 1) * 100_000e6);
            vm.stopPrank();
        }

        // 部分用户借款
        for (uint i = 0; i < 3; i++) {
            vm.prank(users[i]);
            pool.borrow(address(usdc), (i + 1) * 50_000e6);
        }

        // 时间推进
        vm.warp(block.timestamp + 90 days);

        // 验证所有用户的健康因子
        for (uint i = 0; i < 5; i++) {
            uint256 hf = pool.getHealthFactor(users[i]);
            if (pool.getUserDebt(users[i], address(usdc)) > 0) {
                assertGt(hf, 0, "HF should be positive");
            } else {
                assertEq(hf, type(uint256).max, "No debt = max HF");
            }
        }
    }
}

/// @title Mock Chainlink 预言机
contract MockChainlink {
    int256 public price;
    uint8 public decimals_;
    uint256 public updatedAt;

    constructor(uint8 _decimals) {
        decimals_ = _decimals;
        updatedAt = block.timestamp;
    }

    function setPrice(int256 _price) external {
        price = _price;
        updatedAt = block.timestamp;
    }

    function decimals() external view returns (uint8) {
        return decimals_;
    }

    function latestRoundData() external view returns (
        uint80, int256, uint256, uint256, uint80
    ) {
        return (1, price, 0, updatedAt, 1);
    }
}

关键要点总结

维度本周收获
安全审计Move 安全模型 + Solidity 闪电贷攻击 + Solana 6 大漏洞
协议开发Mini Lending 存款/借款/利息/预言机 全部实现
Solana 进阶Zero-Copy/CU 优化/并行模型/增强 Escrow
测试策略单元/集成/Fuzz/边界 全面覆盖
跨链视野EVM vs Solana vs Move 安全对比

常见误区

  1. "测试通过 = 代码安全" — 测试覆盖的只是已知场景,未知的攻击向量需要专业审计
  2. "复习等于浪费时间" — 错误!系统性回顾能发现知识盲区和关联关系
  3. "Mini Lending 只是练习没有实际价值" — 这是理解 DeFi 协议架构的最佳方式,也是面试中的强力展示
  4. "Solidity 和 Solana 选一个就够了" — 多链能力在求职市场极具竞争力
  5. "安全检查做一次就够了" — 每次代码变更都应该重新检查

面试关联

面试题:你在实现借贷协议中遇到了哪些挑战?

30 秒回答: 三个核心挑战:一是份额制会计模型中的精度和通胀攻击防护;二是利息累计的正确性(borrowIndex 机制);三是Chainlink 预言机集成中的异常处理(价格过期、精度转换、L2 Sequencer 等)。

2 分钟回答: 实现 Mini Lending 协议过程中有几个关键挑战。首先是份额制会计——使用类似 ERC4626 的模型,需要处理首个存款人通胀攻击(通过虚拟偏移解决),还需要确保每次份额和资产之间的转换方向正确(存款向下取整保护协议,取款也向下取整避免多取)。第二是利息模型的实现——分段线性利率曲线在拐点处需要正确处理,borrowIndex 的增长必须是累积性的而非覆盖性的。第三是预言机安全——不仅要获取价格,还要做新鲜度检查、轮次完整性验证、精度标准化。第四是清算机制设计——需要在保护协议(坏账防护)和保护借款人(不过度清算)之间取得平衡。整个过程最大的收获是理解了 DeFi 协议各组件之间的紧密耦合关系。


参考资源

资源说明
Foundry BookFoundry 测试框架文档
Aave V3 源码生产级借贷协议参考
Morpho Blue极简借贷协议实现
Solana Test Validator本地测试验证器
Trail of Bits 测试指南高级 Fuzz 测试