Compound V3 抵押品、清算与储备金详解

理解清算因子、isLiquidatable 判定、absorb 全额吸收、buyCollateral 折价购买与储备金策略

8 分钟阅读
Compound V3 抵押品、清算与储备金详解

Compound V3 抵押品、清算与储备金详解

目录


一、概览

本文讲 Compound V3 如何管理抵押品估值、清算机制、抵押品出售和储备金——这些环环相扣,是协议偿付能力的核心。

二、存储结构

2.1 UserBasic 与 UserCollateral

UserBasic 结构体追踪借款账户:

  • principal:负值=债务,正值=存款(一个有符号字段表达两种状态,上一篇讲过);
  • assetsIn:一个位图(bitmap),标识用户存了哪些抵押资产;
  • baseTrackingIndex/baseTrackingAccrued:奖励记账;
  • _reserved:未用。

抵押品数量单独存在 UserCollateral 结构体里,通过嵌套 mapping 访问:userCollateral[user][collateralAsset].balance。算总抵押价值时,遍历位图找出有非零余额的资产,逐个乘以预言机价格。

用位图的好处:快速判断用户持有哪些抵押品,避免遍历所有可能资产、减少存储读取。

2.2 AssetInfo 与 immutable 存储

AssetInfo 结构体(432 位,占两个 256 位插槽)含关键抵押参数:

字段含义
token抵押资产合约地址
priceFeed提供价格的预言机地址
scale1e18(百分比缩放因子)
borrowCollateralFactor借款时的最大 LTV 阈值
liquidateCollateralFactor触发清算资格的阈值
liquidationFactor编码清算惩罚/折价
supplyCap协议接受该资产的上限

注意两个 collateral factor 的区别:

  • borrowCollateralFactor(较低,如 0.80):决定你最多能借多少——更保守;
  • liquidateCollateralFactor(较高,如 0.90):决定何时被清算——给价格波动留缓冲。

两者之间的间隙是安全带:借到上限后价格小跌不会立刻被清算。

immutable 存储:AssetInfo 不存可变存储,而是打包进 immutable 变量(asset00_aasset00_b 是资产 0 的两个槽,依此类推),getAssetInfo() 解包返回结构体。所以增改抵押资产必须部署新实现 + 切代理(呼应架构篇)。

三、清算资格判定 isLiquidatable

当账户的有效抵押价值低于债务时变得可清算。公式:

Σ(抵押余额 × 预言机价格 × liquidationFactor) < |债务|

关键:抵押品不按全额市值计入健康度,而要打 liquidationFactor 的折扣。例如 ETH 的 liquidationFactor 是 0.80,则只有 80% 市值计入抵押充足性。

数值例子:某借款人持有:

  • $1,000 ETH(liquidationFactor 0.90)= $900 有效抵押
  • $500 USDC(liquidationFactor 0.95)= $475 有效抵押
  • $600 USDT(liquidationFactor 0.85)= $510 有效抵押

有效抵押合计 $1,885。债务一旦超过此值即可被清算。注意单个抵押品跌破阈值时,账户仍可能因其他抵押品而保持健康。

四、清算机制 absorb

4.1 全额吸收,无部分清算

对可清算账户执行 absorb() 时,协议立刻没收借款人的全部抵押头寸——所有资产余额同时清零。Compound V3 不允许部分清算

吸收流程:

  1. 应用清算惩罚(来自 liquidationFactor);
  2. 用剩余抵押品偿还未付债务;
  3. 任何盈余作为新的存款返还给借款人。

4.2 详细数值例子

Bob 的情景:

  • 存入:$1,000 ETH
  • 借出:$800 USDC
  • 初始 LTV:80%(合格)
  • ETH 跌价 → 抵押变 $880
  • 新 LTV:90.9%(超过 90% 清算阈值)

执行 absorb()

步骤数值
协议没收 ETH$880
清算惩罚 5%−$44
剩余抵押$836
偿还债务$800
返还 Bob 的盈余$36 USDC(Bob 变成出借人!)

关键洞察:清算后,如果抵押品超过债务,借款人会变成出借人(拿回盈余作为存款)。反之,若抵押不足以覆盖债务,协议从储备金承担损失。注意 absorb 不在同一次调用里直接奖励清算人——清算人的利润来自后面的 buyCollateral。

五、储备金系统

储备金(Reserves) = 借款人付的利息和出借人赚的利息之间的差额——协议留存的超额利息收入。

计算例子

  • Alice 以 5% 年息出借 100 USDC → 协议欠出借人 105
  • Bob 以 10% 年息借 100 USDC → 协议被欠 110
  • 储备 = 110 − 105 = 5 USDC(利差)

getReserves() 公式

储备 = 协议持有的 USDC 余额 + totalBorrow 现值 − totalSupply 现值
  • USDC 余额:Compound 实际持有的基础代币(正);
  • totalBorrow:借款人欠的(正贡献);
  • totalSupply:协议欠出借人的(作为负债减去)。

发布时 Compound 主网 USDC 市场储备约 347 万美元

吸收对储备的影响:执行 absorb 时储备减少(协议欠被清算人的债增加,且把抵押价值转去还债)。

六、目标储备 targetReserves

一个 immutable 参数(主网 500 万 USDC),设定理想的储备”缓冲垫”。它单一用途:决定协议是否出售积累的抵押品。

如果 Compound V3 有足够的超额现金,它倾向于持有抵押品以投机其升值

七、buyCollateral 折价购买

清算人不能立刻从被吸收的抵押品获利。抵押品留在 Compound 内,直到清算人通过 buyCollateral() 折价购买

两条关键业务规则:

  1. 储备阈值闸门:如果当前储备 超过 targetReserves,buyCollateral() revert。协议在现金充裕时拒绝再积累基础代币,优先持有抵押品投机升值;
  2. 折价定价quoteCollateral() 决定清算人购买被没收抵押品的折扣价。

清算人的利润 = 折扣差价,而非协议直接奖励。

八、完整清算工作流与储备影响

专业清算人执行:

  1. isLiquidatable() 确认目标账户够格;
  2. 验证储备未超过 targetReserves(否则 buyCollateral 失败);
  3. 执行 absorb() 没收抵押;
  4. 同一笔交易里立刻调 buyCollateral() 折价买入;
  5. 从折扣差价获利。

完整例子(含储备)

  • 协议储备 400 万 USDC,targetReserves 500 万;
  • 待清算账户:10 万 USDC 债务,8.5 万 USDC 抵押;
  • 吸收后:1.5 万 USDC 缺口由储备承担 → 新储备 398.5 万;
  • 储备仍低于目标 → 清算人可买抵押品。

若储备本是 520 万,buyCollateral() 会 revert,直到储备通过其他机制降到 500 万以下。

withdrawReserves():治理可提取超过 targetReserves 的超额储备(即提取利润)。

九、关键公式汇总

项目公式
清算资格Σ(抵押余额 × 价格 × liquidationFactor) < |债务|
储备baseToken 余额 + totalBorrow 现值 − totalSupply 现值
清算惩罚扣减抵押价值 × (1 − liquidationFactor)

十、设计原则

  • 抵押参数 immutable → 改动须部署新实现;
  • 位图追踪资产 → 减少存储读取;
  • 全额吸收 → 简化清算逻辑;
  • 储备闸门 → 现金充裕时不过度囤稳定币、转而投机抵押品升值;
  • 清算人激励来自折价,而非协议直接奖励。

十一、动手练习项目:清算引擎 LiquidationEngine

项目目标

在 MiniComet(架构篇练习)基础上实现完整清算:isLiquidatable 判定、absorb 全额吸收、buyCollateral 折价购买、储备金计算与 targetReserves 闸门。部署到 Sepolia,跑通一次清算获利。

合约要求

在 MiniComet 上扩展:

  • AssetInfo:每个抵押资产配 borrowCollateralFactorliquidateCollateralFactorliquidationFactorpriceFeed(用一个可设价的 MockOracle 便于测试);
  • isLiquidatable(address) public view returns (bool):实现第三节公式(Σ 抵押 × 价格 × liquidationFactor < |债务|);
  • isBorrowCollateralized(address) view:用 borrowCollateralFactor 判断能否借(注意与清算阈值的区别);
  • absorb(address absorber, address[] accounts):对每个可清算账户没收全部抵押、扣惩罚、还债、盈余转为存款、不足则减储备;抵押转入协议持有;
  • getReserves() public view returns (int):baseToken 余额 + totalBorrow 现值 − totalSupply 现值;
  • targetReserves immutable;
  • quoteCollateral(address asset, uint baseAmount) view:按折扣(storeFrontPriceFactor)给出能买到的抵押量;
  • buyCollateral(address asset, uint minCollateral, uint baseAmount, address recipient):若 getReserves() >= targetReserves 则 revert(闸门);否则收 base、按折扣发抵押。

测试要求(Foundry)

  1. test_TwoCollateralFactors:借款上限用 borrowCollateralFactor、清算阈值用 liquidateCollateralFactor,验证两者间的安全带(借到上限后小幅跌价不被清算);
  2. test_IsLiquidatable:复现第三节 $1,885 有效抵押的例子,债务超过即可清算;
  3. test_AbsorbBobExample:复现 4.2 的 Bob 例子,断言 Bob 抵押清零、债务清偿、盈余 $36 变成存款(principal 转正);
  4. test_AbsorbShortfallFromReserves:抵押不足覆盖债务时,缺口从储备扣除;
  5. test_GetReserves:构造 Alice 存/Bob 借的利差场景,断言储备 = 利差;
  6. test_BuyCollateralGate:储备 > targetReserves 时 buyCollateral revert;低于时成功且清算人按折扣获利;
  7. test_FullLiquidationFlow:isLiquidatable → absorb → buyCollateral 一条龙,断言清算人净赚折扣差价。

Sepolia 部署与验证步骤

  1. 部署 MiniComet + MockOracle + 测试 base/抵押代币;
  2. 建仓:账户 B 存抵押借 base,使 LTV 接近清算线;
  3. 用 MockOracle 调低抵押价格,使 isLiquidatable(B) 返回 true;
  4. 清算人账户调 absorb(B),再 buyCollateral 折价买入;
  5. 读 getReserves 观察储备变化,确认清算人获利。

进阶挑战(可选)

  • 实现 storeFrontPriceFactor 让折扣随储备水平动态变化(储备越低折扣越大,激励清算);
  • 写注释回答:为什么 absorb 和 buyCollateral 分两步而不是清算时直接奖励清算人?储备闸门体现了协议怎样的”投机抵押品升值”策略?

💬 评论