Compound V3 抵押品、清算与储备金详解
目录
- 一、概览
- 二、存储结构
- 三、清算资格判定 isLiquidatable
- 四、清算机制 absorb
- 五、储备金系统
- 六、目标储备 targetReserves
- 七、buyCollateral 折价购买
- 八、完整清算工作流与储备影响
- 九、关键公式汇总
- 十、设计原则
- 十一、动手练习项目:清算引擎 LiquidationEngine
一、概览
本文讲 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 | 提供价格的预言机地址 |
| scale | 1e18(百分比缩放因子) |
| borrowCollateralFactor | 借款时的最大 LTV 阈值 |
| liquidateCollateralFactor | 触发清算资格的阈值 |
| liquidationFactor | 编码清算惩罚/折价 |
| supplyCap | 协议接受该资产的上限 |
注意两个 collateral factor 的区别:
- borrowCollateralFactor(较低,如 0.80):决定你最多能借多少——更保守;
- liquidateCollateralFactor(较高,如 0.90):决定何时被清算——给价格波动留缓冲。
两者之间的间隙是安全带:借到上限后价格小跌不会立刻被清算。
immutable 存储:AssetInfo 不存可变存储,而是打包进 immutable 变量(asset00_a、asset00_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 不允许部分清算。
吸收流程:
- 应用清算惩罚(来自 liquidationFactor);
- 用剩余抵押品偿还未付债务;
- 任何盈余作为新的存款返还给借款人。
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() 折价购买。
两条关键业务规则:
- 储备阈值闸门:如果当前储备 超过 targetReserves,
buyCollateral()revert。协议在现金充裕时拒绝再积累基础代币,优先持有抵押品投机升值; - 折价定价:
quoteCollateral()决定清算人购买被没收抵押品的折扣价。
清算人的利润 = 折扣差价,而非协议直接奖励。
八、完整清算工作流与储备影响
专业清算人执行:
- 调
isLiquidatable()确认目标账户够格; - 验证储备未超过 targetReserves(否则 buyCollateral 失败);
- 执行
absorb()没收抵押; - 同一笔交易里立刻调
buyCollateral()折价买入; - 从折扣差价获利。
完整例子(含储备):
- 协议储备 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:每个抵押资产配
borrowCollateralFactor、liquidateCollateralFactor、liquidationFactor、priceFeed(用一个可设价的 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 现值;targetReservesimmutable;quoteCollateral(address asset, uint baseAmount) view:按折扣(storeFrontPriceFactor)给出能买到的抵押量;buyCollateral(address asset, uint minCollateral, uint baseAmount, address recipient):若getReserves() >= targetReserves则 revert(闸门);否则收 base、按折扣发抵押。
测试要求(Foundry)
test_TwoCollateralFactors:借款上限用 borrowCollateralFactor、清算阈值用 liquidateCollateralFactor,验证两者间的安全带(借到上限后小幅跌价不被清算);test_IsLiquidatable:复现第三节 $1,885 有效抵押的例子,债务超过即可清算;test_AbsorbBobExample:复现 4.2 的 Bob 例子,断言 Bob 抵押清零、债务清偿、盈余 $36 变成存款(principal 转正);test_AbsorbShortfallFromReserves:抵押不足覆盖债务时,缺口从储备扣除;test_GetReserves:构造 Alice 存/Bob 借的利差场景,断言储备 = 利差;test_BuyCollateralGate:储备 > targetReserves 时 buyCollateral revert;低于时成功且清算人按折扣获利;test_FullLiquidationFlow:isLiquidatable → absorb → buyCollateral 一条龙,断言清算人净赚折扣差价。
Sepolia 部署与验证步骤
- 部署 MiniComet + MockOracle + 测试 base/抵押代币;
- 建仓:账户 B 存抵押借 base,使 LTV 接近清算线;
- 用 MockOracle 调低抵押价格,使
isLiquidatable(B)返回 true; - 清算人账户调 absorb(B),再 buyCollateral 折价买入;
- 读 getReserves 观察储备变化,确认清算人获利。
进阶挑战(可选)
- 实现 storeFrontPriceFactor 让折扣随储备水平动态变化(储备越低折扣越大,激励清算);
- 写注释回答:为什么 absorb 和 buyCollateral 分两步而不是清算时直接奖励清算人?储备闸门体现了协议怎样的”投机抵押品升值”策略?