Skip to content

visionfire/lender

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

LendingPool — 完整设计文档


目录

  1. 概览
  2. 总体架构与关键设计决策
  3. 合约清单与职责说明
  4. 关键数据结构说明(ReserveData、WithdrawalRequest、UserVault)
  5. 主要流程(supply / borrow / repay / withdraw / liquidation)与时序图
  6. 合约API参考
  7. 利率算法和流程
  8. 事件与日志
  9. 附录: 单位/定点数约定(WAD / RAY / 18-dec)
  10. 附录: 抵押模拟案例
  11. 附录: 借贷模拟案例

概览

本项目实现了一个 Aave-like 的借贷协议原型,并在其基础上引入了用户托管合约(UserVault),满足如下目标:

  • 抵押:允许用户操作 EOA地址 (即用户自己掌握私钥的地址)或 合约地址 (多签合约地址或其他智能合约地址)转入资产抵押
    • EOA地址: 授权EOA地址对应资产权限给到LendingPool,调用LendingPool里面的supply方法进入存款抵押
    • 合约地址: 授权合约地址(操作者需要合约管理或签名权限)对应资产权限给到LendingPool,调用LendingPool里面的supply方法进入存款抵押
  • 超额借贷支持:允许在风控约束下实现高杠杆(例如上限 10x),但通过托管合约与额外约束防止资金被直接套现;
  • 借款资金托管:所有借贷出的底层资产统一转入 UserVault,不会直接发送到用户 EOA;
  • Vault 内受限操作:用户可通过 UserVault.executeAdapter 调用受信任的 adapter(或白名单合约)与 DEX 交互,但不能直接将 Vault 资金转走到任意外部地址;
  • 还款 / 清算优先从 Vault 操作repayliquidation 会首先尝试使用 Vault 中的余额来偿还/覆盖,若不足再从外部地址补足;
  • 清算资产可回收:支持把 Vault 中的 token sweep 回 Pool 并通过 liquidation adapter 变现以补偿债务。

总体架构与关键设计决策

架构图(概念)

+-----------------+        +-----------------+        +----------------+
|     User EOA    | <----> |    LendingPool  | <----> |    Reserves    |
| (owner of vault)|        |  (core logic)   |        | (ERC20 assets) |
+-----------------+        +-----------------+        +----------------+
          |                         ^  ^
          |                         |  |
          v                         |  +--> InterestRateStrategy
+-----------------+                 |
|    UserVault    | <---------------+
| (per-user proxy) |
+-----------------+
          |
          v
    Allowed adapters (DEX/router)

关键决策(简述)

  • Vault-first 资金流:借出的资产必须先到 Vault,用户不能直接拿到借出的资产,Vault 里的流动性只能通过受控 adapter 交互,或由 Pool 在审核下释放。这样能阻止用户直接套现,同时仍允许 Vault 在受控范围内进行 on-chain 交易。
  • Adapter 模式:Vault 只调用审计过的 adapter(或 pool 管理的白名单合约),并强制 adapter 把 swap/交易输出留在 Vault 或 Pool,防止 adapter 向任意 recipient 发钱。
  • Debt 仍计到用户 EOA:VariableDebtToken 的债务持有人为 user(EOA),因此清算逻辑按用户身份触发,但资金回收优先来自对应的 Vault。
  • 清算优先级:先从 Vault 扣取债务资产 / sweep token;不足时再 burn aToken(collateral)并支付给 liquidator。

合约清单与职责说明

  • LendingPool(核心)

    • 管理 reserves、利率、用户借贷生命周期;与 UserVault 协同执行 borrow/repay/withdraw/ liquidation。
    • 维护 allowedTargets(adapter 白名单)、userVaults 映射。
  • AToken(抵押凭证)

    • 执行 scaled accounting(scaled balances + liquidityIndex),代表用户抵押份额;但在本实现中 aToken 可被转移(转移会转移抵押权利),在生产中需注意业务规则。
  • VariableDebtToken

    • 记录用户可变利率债务(scaled accounting),禁用 transfer/approve,防止债务转移。
  • InterestRateStrategy(利率模型)

    • 根据利用率与保留因子计算 liquidity rate / variable borrow rate。
  • UserVault(每用户单独合约)

    • 存放借出资产与用户通过借款买入的资产;提供 executeAdapter(或 execute)以调用白名单 adapter;Pool 可调用 transferByPool / sweepTokens 扣押资产以便清算或补偿。
  • IAggregatorV3(Chainlink-like price oracle)

    • 提供资产价格(需在部署时配置),用于借款校验、清算计算等。

关键数据结构说明

ReserveData(简要字段回顾)

字段与单位(重要)

  • asset (address): ERC20 地址
  • aToken (address): aToken 合约地址
  • variableDebtToken (address):债务 token
  • decimals (uint8):资产原始 decimals
  • liquidityIndex (uint256, RAY):流动性索引(RAY=1e27),scaled -> real
  • variableBorrowIndex (uint256, RAY):债务索引
  • currentLiquidityRate (RAY)
  • currentVariableBorrowRate (RAY)
  • lastUpdateTimestamp (uint256)
  • totalScaledVariableDebt (scaled units)
  • totalLiquidity (18-dec units, WAD-based internal)
  • ltv (RAY):loan-to-value
  • liquidationThreshold (RAY)
  • liquidationBonus (RAY)
  • reserveFactor (RAY)
  • interestStrategy (address)
  • priceOracle (IAggregatorV3)
  • minLiquidityRatio (WAD)

备注:务必在阅读代码时保持单位意识(RAY / WAD / 18-dec / raw token decimals),错用单位会导致严重计算错误。

WithdrawalRequest(保留提现延迟逻辑)

  • amount18:WAD 单位记录请求金额
  • unlockTimestamp:可领取时间戳
  • exists:是否存在请求

UserVault 状态

  • owner:vault 的所有者(user EOA)
  • pool:pool 地址(只有 pool 可调用 seize/pull)
  • Vault 中的 token 由标准 ERC20 balanceOf(vault) 表示

主要流程与时序图

1) Supply(用户抵押)

sequenceDiagram
  participant User as User(EOA)
  participant Pool as LendingPool
  participant AToken as AToken
  participant ERC20 as ERC20(asset)

  User->>Pool: supply(asset, amount)
  Note right of Pool: Pool.transferFrom(User, Pool, amount)
  Pool->>ERC20: transferFrom(User, Pool, amount)
  Pool->>Pool: _updateReserveState(asset)
  Pool->>AToken: mintScaled(vault, scaledAmount)
  Pool->>ERC20: hold funds in pool liquidity
  Pool->>Pool: update totalLiquidity
  Pool-->>User: event Supply
Loading

2) Borrow(借款 -> 资金到 Vault)

sequenceDiagram
  participant User
  participant Pool
  participant Vault
  participant DebtToken
  participant ERC20

  User->>Pool: borrow(asset, amount)
  Pool->>Pool: _updateReserveState(asset)
  Pool->>Pool: _getUserAccountData(user) (HF check)
  Pool->>DebtToken: mintScaled(user, scaledDebt)
  Pool->>Vault: createVaultIfNeeded(user)
  Pool->>ERC20: transfer(asset, vault, amount)
  Pool->>Pool: update totalScaledVariableDebt/totalLiquidity
  Pool-->>User: event Borrow(user, asset, amount, vault)
Loading

3) Repay(优先从 Vault)

sequenceDiagram
  participant User
  participant Pool
  participant Vault
  participant ERC20
  participant DebtToken

  User->>Pool: repay(asset, amount)
  Pool->>Pool: _updateReserveState(asset)
  Pool->>Vault: check vault balance
  opt VaultHasFunds
    Vault->>Pool: transferByPool(asset, pool, take)
  end
  alt remaining > 0
    User->>Pool: transferFrom(user, pool, remaining)
  end
  Pool->>DebtToken: burnScaled(user, scaled)
  Pool->>Pool: update totalLiquidity
  Pool-->>User: emit Repay
Loading

4) Withdraw(提现只能从 Vault 发放)

sequenceDiagram
  participant User
  participant Pool
  participant Vault
  participant AToken
  participant ERC20

  User->>Pool: withdraw(asset, amount)
  Pool->>Pool: _updateReserveState(asset)
  Pool->>AToken: check balanceOf(user)
  Pool->>AToken: burnScaled(user, scaled)
  Pool->>Pool: check trigger conditions
  alt immediate
    Pool->>Vault: vaultBalance = UserVault.balanceOf(asset)
    alt vaultBalance >= amount
      Vault->>User: transferByPool(asset, user, amount)
    else vaultBalance < amount
      Pool->>ERC20: transfer(vault, remain)
      Vault->>User: transferByPool(asset, user, remain)
    end
  else delayed
    Pool->>Pool: record WithdrawalRequest
  end
  Pool-->>User: Withdraw event
Loading

5) Liquidation(清算优先从 Vault)

sequenceDiagram
  participant Liquidator
  participant Pool
  participant Vault
  participant DebtToken
  participant AToken
  participant ERC20

  Liquidator->>Pool: liquidationCall(collAsset, debtAsset, user, debtToCover, tokensToSeize)
  Pool->>Pool: _getUserAccountData(user) (hf check)
  Pool->>Vault: if(vault) UserVault.balanceOf(debtAsset)
  alt vault has funds
    Vault->>Pool: transferByPool(debtAsset, pool, take)
  end
  alt remaining > 0
    Liquidator->>Pool: transferFrom(liquidator, pool, remaining)
  end
  Pool->>DebtToken: burnScaled(user, scaledDebt)
  Pool->>Pool: compute coll amount to seize (with bonus)
  Pool->>AToken: burnScaled(user, scaledToBurn)
  Pool->>ERC20: transfer(collAsset, liquidator, collRaw)
  opt vaultTokensToSeize
    Vault->>Pool: sweepTokens([...], pool)
    Pool->>LiquidationAdapter: convert swept tokens to debtAsset (optional)
  end
  Pool-->>Liquidator: LiquidationCall event
Loading

合约API参考

下面逐 contract 列出关键方法、输入输出、行为与注意点。为了可读性,省略了私有/内部只在文件中用到的 helper(详见源码)。

LendingPool(核心)

  • initReserve(address asset, uint8 decimals, address priceOracle, address interestStrategy, uint256 ltv, uint256 liquidationThreshold, uint256 liquidationBonus, uint256 reserveFactor)

    • 作用:初始化一个储备;部署 aToken 与 VariableDebtToken;设置初始 index。
    • 注意:owner-only;ltv / liquidationThreshold / liquidationBonus / reserveFactor 单位均为 RAY(1e27)。
  • supply(address asset, uint256 amountRaw)

    • 作用:用户抵押资产到 pool;pool 扣取用户 token 并给 userVault(或 user)mint aToken(此实现 mint 给 user)。
    • 注意:你可调整为 mint 给 userVault 以更强保证抵押不可转出。
  • borrow(address asset, uint256 amountRaw)

    • 作用:用户借款;先检查 health factor;mint debt 给 user;并把借款资产转入用户 Vault。
    • 注意:借款的实际 token 转账是 IERC20(asset).transfer(vault, amountRaw)
  • repay(address asset, uint256 amountRaw)

    • 作用:优先从 userVault 拉取还款,若不足则从 msg.sender 拉取。
    • 注意:使用 before/after balance 差值能提高对 fee-on-transfer 的兼容性。
  • withdraw(address asset, uint256 amountRaw)claimWithdrawal

    • 作用:用户提取存款。必须从 Vault 提取(本实现会创建 Vault 并从 Vault 发放)。
    • 注意:提现逻辑需保证 atomicity;若触发延时,记录请求。
  • liquidationCall(address collateralAsset, address debtAsset, address user, uint256 debtToCoverRaw, address[] calldata vaultTokensToSeize)

    • 作用:清算函数;首先尝试使用 Vault 中的债务资产还款,若不足则使用清算者提供的资产;随后按 liquidationBonus 计算需没收的 collateral quantity 并发给清算者;最后可 sweepTokens 把 vault 里的指定 token 转回 pool。
    • 注意:现实中需把 sweepTokens 里拿回的资产通过 liquidationAdapter 变现以补偿池子或回收损失。
  • setAllowedTarget(address target, bool allowed)

    • 作用:由 owner 管理 Vault 可调用的 adapter/目标合约白名单(强烈建议只允许 protocol-provided adapter)。
  • _createVaultIfNeeded(address user)

    • 内部方法:当用户第一次借款或发生需要 Vault 的操作时创建 UserVault
    • 注意:创建 Vault 的成本高,建议使用 EIP-1167 minimal proxy 优化(Clones)。

UserVault(托管)

  • constructor(address _owner, address _pool)

    • 初始化 vault(owner/user 与 pool 链接)。
  • execute(address target, bytes calldata data)

    • 作用:owner(user)发起对 target 的低级调用。但必须先通过 pool 的 isAllowedTarget(target) 才能调用。
    • 安全性千万不要target 设为任意 Router,除非你在 adapter 内强制 recipient = vault,自行审计并保证不能把资金送到 arbitrary recipient。
  • transferByPool(address token, address to, uint256 amount)

    • 作用:只有 pool 可以调用,用于清算或 protocol 操作时把 vault 内资金转出。
  • sweepTokens(address[] calldata tokens, address to)

    • 作用:pool 可以一次性把 vault 中一组 token 转到 to(通常 to=pool,用于后续变现)。

利率算法和流程

在此 LendingPool 合约中,采用基于 InterestRateStrategy 合约的可变利率机制。利率的计算涉及两个主要部分:抵押利率和债务利率。

抵押利率计算

抵押利率是指存入资金(资产)的回报率。它主要与池中的总流动性(即池中的资产总额)以及借款池中资产的需求有关。根据池中资产的借贷需求情况,利率会动态变化。具体计算公式如下:

  1. 总借款和总流动性:
    • utilization = totalBorrows / (availableLiquidity + totalBorrows) 计算借贷利用率。
  2. 基础利率(base rate):
  • 当利用率较低时,抵押利率将比较低。
  • 利率计算如下:
    • variableBorrowRate = baseVariableBorrowRate + (utilization / optimalUtilizationRate) * slope1
  • 当利用率较高时,利率将开始增加,超过一定利用率后进入坡度2阶段:
    • variableBorrowRate = baseVariableBorrowRate + slope1 + ((utilization - optimalUtilizationRate) / (1 - optimalUtilizationRate)) * slope2
  1. 抵押利率(liquidityRate):
  • 计算流动性回报:
    • liquidityRate = rayMul(rayMul(variableBorrowRate, utilization), oneMinusReserve)
  • 这里的 oneMinusReserve 是为了考虑部分资金池资金用于担保等其他用途。该部分被称为“储备金(reserveFactor)”。

债务利率计算

债务利率是借款人借用资金时需要支付的利率。这个利率基于池中的借款总额和池中可用流动性的变化而变化。与抵押利率相似,债务利率也会随着借款池的需求和可用流动性的变化而变化。

  1. 借款利率计算:
  • 借款利率与 variableBorrowIndex 和 availableLiquidity、totalBorrows 的利用率挂钩,借款的成本随着池子流动性紧张而增高。
  1. 变量借款率(variableBorrowRate):
  • InterestRateStrategy 合约中,债务利率的计算基于相同的 utilization(借款使用率)值。
  • 当借款需求增加时,借款利率将上升。
  • 计算方式与上述的 抵押利率 类似,利用 optimalUtilizationRateslope1slope2 来调整借款利率。

事件与日志

  • ReserveInitialized(asset, aToken, variableDebtToken)
  • Supply(user, asset, amountRaw)
  • Borrow(user, asset, amountRaw, vault)
  • Repay(payer, asset, amountRaw)
  • LiquidationCall(collateralAsset, debtAsset, user, liquidator, debtRepaidRaw, collateralSeizedRaw)
  • Withdraw(user, asset, amountRaw, delayed, unlockTimestamp)
  • ClaimWithdrawal(user, asset, amountRaw)
  • VaultCreated(user, vault)
  • AllowedTargetSet(target, allowed)
  • UserVault.Executed(adapter, data, result) (在 Vault 合约内)
  • UserVault.TransferredByPool(token, to, amount)

附录-单位定点数约定

  • WAD = 1e18:用于以 18-dec 为基准的金额(USD 估值、内部 18-dec 归一化)。
  • RAY = 1e27:用于索引与利率(更高精度)。
  • raw token decimals:ERC20 的原始 decimals(例如 USDC=6);使用 PoolUtils.to18() / from18() 在 raw <-> 18-dec 间转换。

附录-抵押模拟案例

下面用一个具体、逐步、带公式与调用示例的模拟,完整说明“抵押资金”从用户发起到在系统内的流转:包括 ERC20 原始金额、Pool 内部的 18-dec 归一化、AToken 的 scaled 记账、利息如何通过 index 增长、以及后续提现/清算时各个合约之间的交互。

假设与前提

  • 资产:USDT(示例假设 decimals = 6)
  • 用户:UserA(EOA)
  • 初始 Pool 中该资产的 liquidityIndex = RAY(1e27)variableBorrowIndex = RAY
  • WAD = 1e18,RAY = 1e27。
  • 我们使用 PoolUtils.to18(amountRaw, decimals) 将 raw -> 18-dec internal units。

举例数值:UserA 抵押 100 USDT(即 raw = 100 * 10^6)。

Step 0 — 用户准备(Approve)

UserA 在钱包地址EOA中对 Poolsupply 授权: IERC20(USDT).approve(address(pool), 100 * 10**6); 说明:必须先 approvePoolsupplytransferFrom 用户。

Step 1 — 用户调用 supply(抵押进入 Pool)

调用: LendingPool.supply(USDT, 100 * 10**6); 发生的链上动作(顺序解释):

  1. Pool 调用 IERC20(USDT).transferFrom(UserA, address(pool), 100 * 10**6) —— 将 100 USDT 转移到 Pool 合约地址。
  2. Pool 调用 _updateReserveState(asset) 更新 index
  3. 内部将 raw 转为 18-dec(统一内部单位):
  • amount18 = PoolUtils.to18(100 * 10**6, 6) = 100 * 10**18
  1. 计算 scaled(用于 scaled accounting):
  • scaled = WadRayMath.rayDiv(amount18, liquidityIndex)
  • 因为初始 liquidityIndex = RAY,所以 scaled = amount18(按公式 a * RAY / RAY = a)。
  1. 重要:PoolmintScaled(vault, scaled) 给对应的抵押持有人。

结果:

  • Pool 的 totalLiquidity 增加 100 * 1e18(内部 18-dec 单位)。
  • 用户地址EOA现在持有相当于 100 USDT 的 aToken 份额(aToken.balanceOf(vault) = 100 USDT raw)。

Step 2 — aToken 与 scaled 会如何反映利息(index 变化)

核心概念:aToken 的 balanceOf(account) 是基于该账户的 _scaledBalances[account] 与 reserve 的 liquidityIndex 计算得出:

amount18 = rayMul(_scaledBalances[account], liquidityIndex)
rawAmount = PoolUtils.from18(amount18, decimals)

因此,当 liquidityIndex 随时间增长(因为池中有人借款并产生利息,或系统调整利率),aToken 表示的 raw 余额会自动增加,体现利息收益。

例如:

  • 初始 scaled = 100 * 1e18。若下一年后 liquidityIndex 增长 5%(liquidityIndex_new = liquidityIndex * 1.05),则:
  • amount18 = scaled * liquidityIndex_new / RAY = 100 * 1e18 * 1.05 = 105 * 1e18
  • raw = PoolUtils.from18(105 * 1e18, 6) = 105 USDT

所以持有 aToken 的 用户地址EOA 在链上会看到 aToken.balanceOf(addr) 从 100 USDT -> 105 USDT(反映利息)。注意:底层真实的 ERC20 资产仍在 Pool 的合约地址或策略里,aToken 是对这些资产份额的索引/凭证。


附录-借贷模拟案例

假设用户 UserA 抵押了 100 USDT,并决定借贷 500 USDT。以下是详细的资金流转步骤:

1. 用户抵押资产

UserA100 USDT 抵押到 LendingPool。

// 用户转账抵押资产
IERC20(USDT).transferFrom(userA, address(pool), 100e18);

该资产将被转换为 aToken(抵押凭证),并存入池子中。

2. 计算抵押利率

接下来,池子计算存入的 100 USDT 的流动性回报率(即抵押利率)。假设当前池子的流动性率为 5%。 计算公式: liquidityRate = 5% * 100 USDT = 5 USDT;

3. 借款申请

UserA 向 LendingPool 申请借款 500 USDT。此时,借款的利率由池子中的债务利率控制。

4. 借款资金的流转

借款资金将直接转入 UserA 的托管账户 UserVault,并锁定在合约内,防止用户直接转走。

// 将借款金额发送到用户的托管账户(UserVault)
IERC20(USDT).transfer(userVault, 500e18);

5. 更新借款和抵押的状态

池子中的抵押资产和借款总额将被更新,计算借款的利率、总的流动性等。池子内部将通过 variableBorrowIndexliquidityIndex 更新相应的计算参数。

6. 资金流转情况

流程步骤 金额 说明
1. 用户抵押资产 100 USDT UserA 抵押资产到池子
2. 计算抵押利率 5% 计算得流动性回报 5%
3. 用户借款申请 500 USDT UserA 借款 500 USDT
4. 借款资金流转 500 USDT 借款资金转入 UserVault
5. 更新状态 - 更新池子的利率、总流动性

7. 清算条件

假设在 UserA 借款后,市场发生波动,导致用户的健康因子(HF)低于 1(即出现爆仓)。此时,清算机制将会触发。

清算触发条件:

  • 健康因子 (HF) 小于 1
  • 清算的金额由池子自动计算并通知清算者。

8. 清算流程

  1. 清算者 将会接管 UserA 的部分资产,并偿还其借款。
  2. 清算资金会通过 UserVault 进行转移。
  3. 清算奖励将分配给清算者。

通过这一过程,整个资产借贷和清算机制得以完整流转,用户借款后,所有资产都被锁定在托管合约中,且能通过池子管理、清算等操作来保障资金安全。

9. 资金流转时序图

以下是资金流转的时序图,展示了用户如何从池子中借款,并且资金如何通过托管账户流转:

sequenceDiagram
    participant UserA as UserA
    participant Pool as LendingPool
    participant UserVault as UserVault

    UserA->>Pool: 抵押 100 USDT
    Pool->>Pool: 计算抵押利率
    Pool->>UserA: 发放 500 USDT 借款
    Pool->>UserVault: 将 500 USDT 转入托管账户
    UserVault->>UserA: 用户可通过托管账户进行交易
    Pool->>Pool: 计算更新利率
    UserA->>Pool: 还款 500 USDT
    UserVault->>Pool: 从托管账户还款
    Pool->>Pool: 计算还款和清算
    Pool->>UserVault: 清算资金通过托管账户流转
Loading

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published