返回 SC 笔记
SC Day 73

Mini Lending 协议设计 — 规格文档 + 数据结构

### 1. DeFi 借贷协议核心机制

2026-06-22
第四阶段:综合实战 (73-80)
lendingprotocol-designarchitectureaavecompounddata-structures

日期: 2026-06-22 方向: Solidity 阶段: 第四阶段:综合实战 (73-80) 标签: #lending #protocol-design #architecture #aave #compound #data-structures


今日目标

类型内容
学习借贷协议核心设计原理,对比 Aave/Compound 架构
实操编写 Mini Lending 协议规格文档、定义核心数据结构和接口
产出完整的协议规格文档 + 数据结构定义 + 接口设计

核心概念

1. DeFi 借贷协议核心机制

借贷协议是 DeFi 中最重要的基础设施之一。核心参与者和流程:

存款人 (Lender)              借款人 (Borrower)
    │                            │
    │ 存入 USDC                   │ 存入 ETH 作为抵押品
    │ 获得利息                     │ 借出 USDC
    ▼                            ▼
┌─────────────────────────────────────┐
│          Lending Protocol           │
│                                     │
│  存款池 (Supply Pool)               │
│  ├── USDC Pool: $10M               │
│  ├── ETH Pool: 5000 ETH            │
│  └── ...                           │
│                                     │
│  核心参数                           │
│  ├── 利率模型 (Interest Rate Model) │
│  ├── 抵押因子 (Collateral Factor)   │
│  ├── 清算阈值 (Liquidation Threshold)│
│  └── 清算奖励 (Liquidation Bonus)   │
│                                     │
│  安全机制                           │
│  ├── 健康因子 (Health Factor)       │
│  └── 清算 (Liquidation)            │
└─────────────────────────────────────┘
    │                            │
    │ 清算人 (Liquidator)         │
    │ 当 Health Factor < 1 时     │
    │ 偿还借款人的部分债务        │
    │ 获得折价抵押品作为奖励      │

2. 核心公式

利用率 (Utilization Rate)

U = Total Borrows / Total Deposits

示例:
  总存款: $10,000,000
  总借款: $6,000,000
  利用率: 60%

利率模型 (分段线性)

if U <= U_optimal:
    borrowRate = baseRate + (U / U_optimal) * slope1

if U > U_optimal:
    borrowRate = baseRate + slope1 + ((U - U_optimal) / (1 - U_optimal)) * slope2

其中 slope2 >> slope1,形成"拐点"效应
利率
 ^
 |                          /
 |                         / (slope2 = 300%)
 |                        /
 |                       /
 |          ............/ ← U_optimal (80%)
 |         /
 |        / (slope1 = 4%)
 |       /
 |      /
 |     /
 |    /
 |───/──────────────────────→ 利用率
   baseRate(2%)          100%

健康因子 (Health Factor)

HF = Σ(collateral_i * price_i * LTV_i) / Σ(debt_j * price_j)

HF > 1: 安全
HF = 1: 即将可被清算
HF < 1: 可被清算

3. Aave vs Compound 架构对比

维度Compound V2Aave V2/V3我们的 Mini Lending
Token 模型cToken(份额代币)aToken(1:1 映射)+ debtToken份额代币(类 ERC4626)
利率模型分段线性分段线性 + 可切换模型分段线性(简化版)
抵押管理每个市场独立统一账户模型统一账户模型
清算清算 50% 债务可配置清算因子清算 50% 债务
预言机Chainlink + 自建ChainlinkChainlink
闪电贷无(V3 有)原生支持不实现
治理COMP TokenAAVE Token不实现
隔离模式V3 支持不实现

4. Mini Lending 协议规格文档

4.1 项目范围

实现的功能

  • 多资产存款/取款
  • 超额抵押借款/还款
  • 基于利用率的利率模型
  • Chainlink 预言机集成
  • 健康因子计算
  • 清算机制(含清算奖励)

不实现的功能(简化):

  • 闪电贷
  • 治理代币
  • 隔离模式
  • 效率模式(eMode)
  • 可变/稳定利率切换

4.2 系统角色

角色操作说明
Lender(存款人)supply, withdraw存入资产赚取利息
Borrower(借款人)supply(作为抵押), borrow, repay超额抵押借款
Liquidator(清算人)liquidate清算不健康的仓位
Admin(管理员)addMarket, updateParams管理协议参数

4.3 协议参数

参数说明典型值
LTV (Loan-to-Value)最大借款比例ETH: 80%, USDC: 85%
Liquidation Threshold清算触发阈值ETH: 85%, USDC: 90%
Liquidation Bonus清算人奖励5%
Base Rate基础利率2%
Slope 1拐点前斜率4%
Slope 2拐点后斜率300%
Optimal Utilization最优利用率80%
Reserve Factor协议留存比例10%

代码实战

核心数据结构定义

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

/// @title Mini Lending Protocol - 数据结构定义
/// @author MomoWeb3
/// @notice 教学用途的借贷协议,核心数据结构

// ============ 常量 ============

/// @dev 精度常量
uint256 constant PRECISION = 1e18;
uint256 constant PERCENTAGE_FACTOR = 10000; // 基点 (100% = 10000)
uint256 constant SECONDS_PER_YEAR = 365 days;

// ============ 错误定义 ============

error MarketNotListed();
error MarketAlreadyListed();
error InsufficientCollateral();
error HealthFactorOk();           // 清算时 HF >= 1
error HealthFactorBelowOne();     // 借款/取款后 HF < 1
error InsufficientLiquidity();
error ZeroAmount();
error InvalidPrice();
error StalePrice();

// ============ 事件定义 ============

event MarketAdded(address indexed asset, address indexed priceFeed);
event Supply(address indexed user, address indexed asset, uint256 amount, uint256 shares);
event Withdraw(address indexed user, address indexed asset, uint256 amount, uint256 shares);
event Borrow(address indexed user, address indexed asset, uint256 amount);
event Repay(address indexed user, address indexed asset, uint256 amount);
event Liquidation(
    address indexed liquidator,
    address indexed borrower,
    address indexed debtAsset,
    address collateralAsset,
    uint256 debtRepaid,
    uint256 collateralSeized
);
event InterestAccrued(address indexed asset, uint256 interestAccumulated, uint256 newIndex);

// ============ 核心结构体 ============

/// @notice 市场配置参数
struct MarketConfig {
    bool isListed;                    // 是否已上线
    bool canBeCollateral;             // 是否可作为抵押品
    bool canBeBorrowed;               // 是否可被借出
    uint16 ltvBps;                    // Loan-to-Value (基点, e.g., 8000 = 80%)
    uint16 liquidationThresholdBps;   // 清算阈值 (基点, e.g., 8500 = 85%)
    uint16 liquidationBonusBps;       // 清算奖励 (基点, e.g., 500 = 5%)
    uint16 reserveFactorBps;          // 协议留存 (基点, e.g., 1000 = 10%)
    uint8 decimals;                   // 资产精度
}

/// @notice 市场状态数据
struct MarketState {
    uint256 totalSupplyShares;        // 总存款份额
    uint256 totalSupplyAssets;        // 总存款资产(含利息)
    uint256 totalBorrows;             // 总借款
    uint256 borrowIndex;              // 借款利息索引(累计乘数)
    uint256 supplyIndex;              // 存款利息索引
    uint40 lastUpdateTimestamp;       // 最后更新时间
    uint256 reserveBalance;           // 协议留存金额
}

/// @notice 用户在某个市场的仓位
struct UserPosition {
    uint256 supplyShares;             // 存款份额
    uint256 borrowAmount;             // 借款本金(按 borrowIndex 标准化)
    uint256 borrowIndex;              // 用户借款时的 borrowIndex 快照
}

/// @notice 利率模型参数
struct InterestRateParams {
    uint256 baseRatePerYear;          // 基础年利率 (e.g., 2e16 = 2%)
    uint256 slope1PerYear;            // 拐点前斜率 (e.g., 4e16 = 4%)
    uint256 slope2PerYear;            // 拐点后斜率 (e.g., 300e16 = 300%)
    uint256 optimalUtilization;       // 最优利用率 (e.g., 80e16 = 80%)
}

协议核心合约骨架

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @title MiniLendingPool - 核心借贷池合约
/// @notice 教学用途,实现存款、借款、还款、清算
contract MiniLendingPool is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ============ 状态变量 ============

    /// @notice 已上线的资产列表
    address[] public marketList;

    /// @notice 市场配置 asset => MarketConfig
    mapping(address => MarketConfig) public marketConfigs;

    /// @notice 市场状态 asset => MarketState
    mapping(address => MarketState) public marketStates;

    /// @notice 利率参数 asset => InterestRateParams
    mapping(address => InterestRateParams) public interestRateParams;

    /// @notice 价格预言机 asset => Chainlink AggregatorV3
    mapping(address => AggregatorV3Interface) public priceFeeds;

    /// @notice 用户仓位 user => asset => UserPosition
    mapping(address => mapping(address => UserPosition)) public userPositions;

    /// @notice 用户作为抵押品的资产列表 user => asset[]
    mapping(address => address[]) public userCollaterals;

    /// @notice 用户借入的资产列表 user => asset[]
    mapping(address => address[]) public userBorrows;

    // ============ 管理函数 ============

    /// @notice 添加新市场
    function addMarket(
        address asset,
        address priceFeed,
        MarketConfig calldata config,
        InterestRateParams calldata rateParams
    ) external onlyOwner {
        if (marketConfigs[asset].isListed) revert MarketAlreadyListed();

        marketConfigs[asset] = config;
        marketConfigs[asset].isListed = true;
        marketConfigs[asset].decimals = IERC20Metadata(asset).decimals();

        priceFeeds[asset] = AggregatorV3Interface(priceFeed);
        interestRateParams[asset] = rateParams;

        // 初始化市场状态
        marketStates[asset] = MarketState({
            totalSupplyShares: 0,
            totalSupplyAssets: 0,
            totalBorrows: 0,
            borrowIndex: PRECISION,      // 初始索引 = 1e18
            supplyIndex: PRECISION,
            lastUpdateTimestamp: uint40(block.timestamp),
            reserveBalance: 0
        });

        marketList.push(asset);
        emit MarketAdded(asset, priceFeed);
    }

    // ============ 核心操作(后续实现) ============

    /// @notice 存款
    function supply(address asset, uint256 amount) external nonReentrant {
        // Day 75 实现
    }

    /// @notice 取款
    function withdraw(address asset, uint256 shares) external nonReentrant {
        // Day 75 实现
    }

    /// @notice 借款
    function borrow(address asset, uint256 amount) external nonReentrant {
        // Day 77 实现
    }

    /// @notice 还款
    function repay(address asset, uint256 amount) external nonReentrant {
        // Day 77 实现
    }

    /// @notice 清算
    function liquidate(
        address borrower,
        address debtAsset,
        address collateralAsset,
        uint256 debtAmount
    ) external nonReentrant {
        // Day 80 实现
    }

    // ============ 视图函数 ============

    /// @notice 获取用户健康因子
    function getHealthFactor(address user) public view returns (uint256) {
        // Day 77 实现
    }

    /// @notice 获取资产价格(USD,18位精度)
    function getAssetPrice(address asset) public view returns (uint256) {
        AggregatorV3Interface feed = priceFeeds[asset];
        (
            uint80 roundId,
            int256 price,
            ,
            uint256 updatedAt,
            uint80 answeredInRound
        ) = feed.latestRoundData();

        if (price <= 0) revert InvalidPrice();
        if (block.timestamp - updatedAt > 1 hours) revert StalePrice();
        if (answeredInRound < roundId) revert StalePrice();

        // Chainlink 价格通常是 8 位精度,标准化到 18 位
        uint8 feedDecimals = feed.decimals();
        if (feedDecimals < 18) {
            return uint256(price) * 10 ** (18 - feedDecimals);
        } else {
            return uint256(price) / 10 ** (feedDecimals - 18);
        }
    }

    /// @notice 获取当前利用率
    function getUtilization(address asset) public view returns (uint256) {
        MarketState storage state = marketStates[asset];
        uint256 totalDeposits = state.totalSupplyAssets;
        if (totalDeposits == 0) return 0;
        return state.totalBorrows * PRECISION / totalDeposits;
    }

    /// @notice 获取当前借款利率(年化)
    function getBorrowRate(address asset) public view returns (uint256) {
        uint256 utilization = getUtilization(asset);
        InterestRateParams storage params = interestRateParams[asset];

        if (utilization <= params.optimalUtilization) {
            // 拐点前:baseRate + utilization/optimal * slope1
            return params.baseRatePerYear +
                utilization * params.slope1PerYear / params.optimalUtilization;
        } else {
            // 拐点后:baseRate + slope1 + excessUtil/(1-optimal) * slope2
            uint256 excessUtilization = utilization - params.optimalUtilization;
            uint256 maxExcess = PRECISION - params.optimalUtilization;
            return params.baseRatePerYear +
                params.slope1PerYear +
                excessUtilization * params.slope2PerYear / maxExcess;
        }
    }

    /// @notice 获取当前存款利率(年化)
    function getSupplyRate(address asset) public view returns (uint256) {
        uint256 borrowRate = getBorrowRate(asset);
        uint256 utilization = getUtilization(asset);
        uint16 reserveFactor = marketConfigs[asset].reserveFactorBps;

        // supplyRate = borrowRate * utilization * (1 - reserveFactor)
        return borrowRate * utilization / PRECISION
            * (PERCENTAGE_FACTOR - reserveFactor) / PERCENTAGE_FACTOR;
    }

    // ============ 内部函数 ============

    /// @notice 更新市场利息累计
    function _accrueInterest(address asset) internal {
        MarketState storage state = marketStates[asset];
        uint256 timeElapsed = block.timestamp - state.lastUpdateTimestamp;
        if (timeElapsed == 0) return;

        uint256 borrowRate = getBorrowRate(asset);

        // 计算这段时间内累计的利息
        // simpleInterest = borrowRate * timeElapsed / SECONDS_PER_YEAR
        uint256 interestFactor = borrowRate * timeElapsed / SECONDS_PER_YEAR;

        // 更新总借款(加上利息)
        uint256 interestAccumulated = state.totalBorrows * interestFactor / PRECISION;
        state.totalBorrows += interestAccumulated;

        // 更新借款索引
        state.borrowIndex += state.borrowIndex * interestFactor / PRECISION;

        // 协议留存
        uint256 reserveShare = interestAccumulated
            * marketConfigs[asset].reserveFactorBps / PERCENTAGE_FACTOR;
        state.reserveBalance += reserveShare;

        // 总存款资产增加(利息 - 协议留存)
        state.totalSupplyAssets += (interestAccumulated - reserveShare);

        // 更新存款索引
        if (state.totalSupplyShares > 0) {
            state.supplyIndex = state.totalSupplyAssets * PRECISION / state.totalSupplyShares;
        }

        state.lastUpdateTimestamp = uint40(block.timestamp);

        emit InterestAccrued(asset, interestAccumulated, state.borrowIndex);
    }

    /// @notice 份额 → 资产数量
    function _sharesToAssets(
        uint256 shares,
        uint256 totalShares,
        uint256 totalAssets
    ) internal pure returns (uint256) {
        if (totalShares == 0) return shares; // 1:1 映射
        return shares * totalAssets / totalShares;
    }

    /// @notice 资产数量 → 份额
    function _assetsToShares(
        uint256 assets,
        uint256 totalShares,
        uint256 totalAssets
    ) internal pure returns (uint256) {
        if (totalAssets == 0) return assets; // 1:1 映射
        return assets * totalShares / totalAssets;
    }
}

接口定义

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

/// @title IMiniLendingPool - 借贷池接口
interface IMiniLendingPool {
    // ============ 核心操作 ============

    /// @notice 存入资产
    /// @param asset 资产地址
    /// @param amount 存入数量
    function supply(address asset, uint256 amount) external;

    /// @notice 取出资产
    /// @param asset 资产地址
    /// @param shares 取出的份额数量
    function withdraw(address asset, uint256 shares) external;

    /// @notice 借出资产
    /// @param asset 资产地址
    /// @param amount 借出数量
    function borrow(address asset, uint256 amount) external;

    /// @notice 偿还借款
    /// @param asset 资产地址
    /// @param amount 偿还数量(type(uint256).max 表示全部偿还)
    function repay(address asset, uint256 amount) external;

    /// @notice 清算不健康的仓位
    /// @param borrower 被清算用户
    /// @param debtAsset 偿还的债务资产
    /// @param collateralAsset 获取的抵押品资产
    /// @param debtAmount 偿还的债务数量
    function liquidate(
        address borrower,
        address debtAsset,
        address collateralAsset,
        uint256 debtAmount
    ) external;

    // ============ 视图函数 ============

    function getHealthFactor(address user) external view returns (uint256);
    function getAssetPrice(address asset) external view returns (uint256);
    function getUtilization(address asset) external view returns (uint256);
    function getBorrowRate(address asset) external view returns (uint256);
    function getSupplyRate(address asset) external view returns (uint256);

    // ============ 用户数据 ============

    /// @notice 获取用户总抵押品价值(USD)
    function getUserTotalCollateralUSD(address user) external view returns (uint256);

    /// @notice 获取用户总债务价值(USD)
    function getUserTotalDebtUSD(address user) external view returns (uint256);

    /// @notice 获取用户可借额度(USD)
    function getUserBorrowCapacityUSD(address user) external view returns (uint256);
}

架构图

┌─────────────────────────────────────────────────────┐
│                    MiniLendingPool                    │
│                                                       │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ Market Config │  │ Market State │  │  User Pos  │ │
│  │              │  │              │  │            │ │
│  │ LTV         │  │ totalSupply  │  │ shares     │ │
│  │ liqThreshold│  │ totalBorrow  │  │ borrowAmt  │ │
│  │ liqBonus    │  │ borrowIndex  │  │ borrowIdx  │ │
│  │ reserveFactor│  │ supplyIndex  │  │            │ │
│  └──────────────┘  └──────────────┘  └────────────┘ │
│                                                       │
│  ┌──────────────────────────────────────────────────┐│
│  │              Interest Rate Model                  ││
│  │  borrowRate = f(utilization, base, slope1, slope2)││
│  │  supplyRate = borrowRate * U * (1 - reserveFactor)││
│  └──────────────────────────────────────────────────┘│
│                                                       │
│  ┌──────────────────────────────────────────────────┐│
│  │              Price Oracle (Chainlink)             ││
│  │  getAssetPrice() → staleness check → normalize   ││
│  └──────────────────────────────────────────────────┘│
│                                                       │
│  核心流程:                                            │
│  supply() → _accrueInterest() → mint shares          │
│  borrow() → _accrueInterest() → check HF → transfer  │
│  liquidate() → check HF < 1 → repay debt → seize col │
└─────────────────────────────────────────────────────┘

与 Compound V2 的 cToken 模型对比

// Compound V2 方式:每个资产部署一个 cToken 合约
// cToken 本身就是 ERC20,代表用户的存款份额
contract cToken is ERC20 {
    IERC20 public underlying;

    // 汇率 = (totalCash + totalBorrows - totalReserves) / totalSupply
    function exchangeRateCurrent() public returns (uint256) {
        accrueInterest();
        return (getCash() + totalBorrows - totalReserves) * 1e18 / totalSupply();
    }

    // 存款:用户存入 underlying,获得 cToken
    function mint(uint256 mintAmount) external returns (uint256) {
        accrueInterest();
        uint256 exchangeRate = exchangeRateStored();
        uint256 cTokenAmount = mintAmount * 1e18 / exchangeRate;
        _mint(msg.sender, cTokenAmount);
        underlying.transferFrom(msg.sender, address(this), mintAmount);
        return 0;
    }
}

// 我们的方式:单一合约管理所有市场(类似 Aave V3)
// 优势:节省 gas(无跨合约调用)
// 劣势:单点故障风险更高
contract MiniLendingPool {
    // 内部用 mapping 管理份额,不发行单独的 ERC20
    // 简化设计,适合教学
    mapping(address => mapping(address => uint256)) public supplyShares;
}

关键要点总结

要点说明
份额 vs 资产存款使用份额制(类 ERC4626),借款使用索引制
利率模型是核心分段线性模型通过拐点机制调节利用率
利息持续累计每次操作前先 _accrueInterest()
价格预言机必须有新鲜度检查,防止使用过期价格
健康因子连接抵押品价值和债务价值的安全指标
清算是安全网当 HF < 1 时允许第三方清算,保护协议

常见误区

  1. "利率是管理员手动设置的" — 错误!利率是根据利用率自动计算的,无需人工干预
  2. "存款和借款金额永远相等" — 错误!借款金额 ≤ 存款金额,差值就是闲置流动性
  3. "利息是每天计算一次" — 不精确。利息在每次用户操作时更新(按时间精确计算),而不是定时任务
  4. "LTV 和清算阈值是同一个东西" — 错误!LTV(如 80%)限制最大借款比例,清算阈值(如 85%)是触发清算的临界点,两者之间有安全缓冲
  5. "份额和资产数量始终 1:1" — 只在初始状态如此。随着利息累计,1 份额 > 1 资产

面试关联

面试题:设计一个借贷协议需要考虑哪些关键因素?

30 秒回答: 核心是五个要素:利率模型(自动调节资金利用率)、抵押管理(LTV/清算阈值保证偿付能力)、清算机制(保护协议不产生坏账)、预言机集成(获取可靠的资产价格)、以及利息累计方式(compound vs simple interest)。

2 分钟回答: 设计借贷协议需要从经济模型和技术实现两个维度考虑。经济模型方面:利率模型决定了资金效率——分段线性模型通过拐点机制在高利用率时大幅提高借款成本,激励还款或存款;抵押参数(LTV、清算阈值、清算奖励)需要在资本效率和安全性之间权衡。技术实现方面:利息累计需要在 gas 效率和精度之间取舍——Compound 的 cToken 模型通过汇率变化隐式表达利息,Aave 的 aToken 模型通过 rebase 实时反映;价格预言机必须使用 Chainlink 等外部源并加上新鲜度检查;清算机制需要给清算人足够激励同时保护借款人利益。此外还要考虑坏账处理、储备金机制、以及多资产场景下的风险隔离。

追问准备

  • Q: 为什么不直接用 spot price? A: spot price 可被闪电贷操纵,一次交易就能触发大量错误清算。
  • Q: 如何处理坏账? A: 设立安全模块(Safety Module),用协议收入或治理代币拍卖来覆盖。

参考资源

资源说明
Aave V3 技术文档最全面的借贷协议文档
Compound V2 白皮书借贷协议经典设计
ERC4626 标准代币化金库标准
Chainlink 价格 Feed预言机集成指南
DeFiLlama Lending借贷协议 TVL 排名