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 V2 | Aave V2/V3 | 我们的 Mini Lending |
|---|---|---|---|
| Token 模型 | cToken(份额代币) | aToken(1:1 映射)+ debtToken | 份额代币(类 ERC4626) |
| 利率模型 | 分段线性 | 分段线性 + 可切换模型 | 分段线性(简化版) |
| 抵押管理 | 每个市场独立 | 统一账户模型 | 统一账户模型 |
| 清算 | 清算 50% 债务 | 可配置清算因子 | 清算 50% 债务 |
| 预言机 | Chainlink + 自建 | Chainlink | Chainlink |
| 闪电贷 | 无(V3 有) | 原生支持 | 不实现 |
| 治理 | COMP Token | AAVE 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 时允许第三方清算,保护协议 |
常见误区
- "利率是管理员手动设置的" — 错误!利率是根据利用率自动计算的,无需人工干预
- "存款和借款金额永远相等" — 错误!借款金额 ≤ 存款金额,差值就是闲置流动性
- "利息是每天计算一次" — 不精确。利息在每次用户操作时更新(按时间精确计算),而不是定时任务
- "LTV 和清算阈值是同一个东西" — 错误!LTV(如 80%)限制最大借款比例,清算阈值(如 85%)是触发清算的临界点,两者之间有安全缓冲
- "份额和资产数量始终 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 排名 |