本文深入探讨了Compound V3的多个关键主题,包括抵押品估值、清算机制、抵押品出售、储备金的作用及其对清算的影响。文章通过详细的代码示例和图表,解释了用户抵押品的存储结构、资产信息的管理,以及清算过程的具体实现。
在本章中,我们将探讨以下关于 Compound V3 的主题:
这些主题在一篇文章中覆盖的内容较多,但它们彼此密切相关,所以最好在一篇文章中讨论。
读者应对 Compound V3 如何定义本金和现值 以及 DeFi 清算和抵押品 已经有一定了解。
让我们回顾一下 CometStorage.sol 中的 UserBasic 结构。
如果 principal
(蓝框)为负,则表示用户是借款人,负值将是他们债务的 加粗 本金值。
assetsIn
(红框)是一个位图,用于指示他们是否存入了一定的抵押资产。在撰写时,该位图布局如下:
变量 baseTrackingIndex
和 baseTrackingAccrued
用于记录奖励的分配,将在另文中讨论。变量 _reserved
未使用。
请注意,此结构未告诉我们用户持有多少抵押品。抵押品的数量存储在 UserCollateral 结构 的 balance
变量中,该结构存储在 userCollateral
嵌套映射中。变量 _reserved
未使用。
要列出用户提供的抵押品,我们遍历 0…numAssets
Compound 存储,并检查该用户的位是否设置为 1。如果是,我们获取与该位关联的代币地址,并检查 userCollateral[user][collateralAsset]
中用户持有的该抵押品数量。
通过将 balance
与预言机价格相乘,我们知道用户抵押品的美元价值。以下表格给出了计算用户抵押品总价值的一个示例。
Compound 从中获取抵押品价格的预言机地址存储在 AssetInfo 结构中(蓝框)。
观察上面的 AssetInfo
结构大小为 432 位 —— 它占用 2 个槽来存储。我们将在后面的部分回顾这一点。
让我们将上面显示的 AssetInfo
结构的内容与 Compound Finance 市场 UI 进行比较。这里我们可以看到大多数显示的信息。
文档和代码未说明变量 scale
的用途,但它持有数字 1e18,因此推测是为了让使用者知道如何缩放百分比。
这些变量的含义在 清算和抵押品 的文章中有解释。
当我们查询 UNI 代币的当前值(assetId 3)时,可以将其值与市场上显示的值进行比较。两者之间的关系应该很明确。特别需注意:清算罚金为 1 - liquidation factor
。liquidateCollateralFactor
是清算贷款的贷款价值比(LTV)。liquidationFactor
编码了清算罚金。结构中的 liquidationFactor
与 UI 中的 liquidationFactor
名称不一是令人困惑的。
顶部图像是截图,下面的值是从 Etherscan 查询 getAssetInfo()
针对 UNI 代币获得的。
以下是 Compound UI 与查询 getAssetInfo()
针对 UNI 代币参数之间的关系。
显而易见的下一个问题是,“Compound 将 AssetInfo
结构存储在哪里?”
每个资产的信息被打包到不可变变量中 —— 它不存储在内存中以提高气体效率。由于 AssetInfo
结构需要两个 32 字节的字来存储,Comet 通过 assetXX_a, assetXX_b
为 uint256 字分配编号。这里的 XX
表示资产索引。因此 asset00_a
和 asset00_b
共同持有资产 0 的 AssetInfo
结构。请记住,存储 AssetInfo 需要两个 256 位的变量。
我们现在可以展示 getAssetInfo()
从 Comet.sol:280-356 的实现。它只是将不可变变量解包到 AccountInfo
结构中并返回。所使用的位移和打包是直接的,我们在这里不进行解释。
由于这些变量是不可变的,治理 必须部署新的实现并更新代理,才想增加另一个抵押资产或更改某个资产的参数。必须小心,仅追加资产,而不干扰之前的定义。
Comet.sol 的 isLiquidatable() 函数 将用户持有的抵押资产乘以其 liquidationFactor 进行求和。如果这个总和小于他们债务的现值(这是一个负数),那么用户就是可以被清算的。
这意味着借款人可能只有一个资产低于清算阈值,但如果其他抵押资产抵消了这个缺口,那么用户就不会被清算。
抵押品的完整价值不会“计入”用户的抵押品余额 — 它会根据清算因子减少。
这里是之前示例的同一图片,显示用户抵押资产的真实价值。
在上面的例子中,假设借款人在其贷款余额超过 $8,360 时将被清算。
如果 isLiquidatable()
函数返回 true,则借款人的抵押品可以被吸收到协议中。某些协议称之为“清算”,而 Compound V3 称之为“吸收”。吸收是全有或全无 — 在 Compound V3 中没有选择清算部分抵押品的选项。借款人所有的资产余额将被设为零。
假设 Bob 在 Compound V3 中存入了 $1000 的 ETH,并借入 $800 USDC。这样满足了 80% 的抵押率。ETH 的价值降至 $880,导致贷款价值比(LTV)达到 90.9%,触发了 90% 的清算阈值。
清算者在 Bob 的账户上调用 absorb()
,则 $880 的 ETH 抵押品被吸收进协议中。假设清算罚金为 5%。由于抵押品当前价值为 $880 的 ETH,因此 5% 的罚金为 $44的 ETH。
协议会从 Bob 的抵押品中扣除 $44 作为罚金,剩余 $836。由于 Bob 借入了 $800 USDC,剩余 $36。因此,$800 将被协议用来偿还债务,留下 $36。这个 $36 被记入 Bob 的账户,他现在成为了拥有 $36 USDC 存款的借款人。
Bob 在借款时已提取 $800 USDC,因此他目前的总资产为 $836。
请注意,absorb()
交互中没有任何内容直接奖励清算者。
请注意 借款人被清算时,如果抵押品足以覆盖债务,他们将成为贷款人。
如果抵押品不足以覆盖债务,则协议将从其 准备金 中隐含地遭受损失,我们将在下一节讨论。
借款人支付的超过贷方所得的利息被称为 Compound V3 中的“准备金”。
Alice 向协议借出 100 USDC,并获得 5% 的利息。Bob 从协议中借入 100 USDC,并支付 10% 的利息。为简单起见,假设 Alice 和 Bob 是系统中唯一的参与者。Bob 支付的多出的 5% 利息是 Alice 没有赚到的。这多出的部分就是准备金。
无论 Bob 是否已经偿还了贷款(即,他是否已经向协议转移了 110 USDC),都无所谓。他欠协议 110 USDC,协议欠 Alice 105 USDC。因此,准备金为 5 USDC。
假设 Bob 偿还了贷款。现在协议的余额为 110 USDC,其中 105 已逾 Alice。仍然有 5 USDC 的准备金 — 没有变化。
getReserves()
函数返回此值。协议“拥有”的 USDC 是以下两者的总和:
1) 协议持有的 USDC 余额,即 ERC20(baseToken).balanceOf(address(this))
和
2) totalBorrow
的现值,
3) 减去协议欠贷方的金额,即 totalSupply
的现值。
换句话说,它是 usdc_balance + totalBorrow - totalSupply
。
正如你在下面的函数中所见,totalSupply
被赋予负号,因为这是 Compound 欠贷方的金额。正值部分 — 持有的 USDC 数量和借款人欠 Compound 的 USDC 净额 — 是 Compound “拥有”的 USDC 数量。
如果我们查看 Etherscan上的 getReserves() 函数,将会看到在撰写时的准备金为 347 万美元(6 位小数)。
当我们查看 USDC / 主网 Compound 市场 时,我们也可以看到前端显示当前准备金为 347 万。
如果我们在 absorb()
之前和之后调用 getReserves()
,我们会注意到准备金减少。这发生的原因有两个:
1) 协议偿还了贷款(因此欠的金额减少)。这来自准备金,因此自然准备金减少。
2) 借款人成为具有小额存款的贷方。此存款是由 Compound 欠贷方的,从而进一步减少准备金。
多余的准备金可供治理使用,并可以使用以下函数提取。
Compound 在 Comet.sol 中定义了一个公共不可变变量 targetReserves。
当我们查看 Etherscan 上的 targetReserves 时,会看到其为 500 万 USDC。
目标准备金在协议中的唯一用途是确定协议是否有足够的“安全边际”以不出售吸收的抵押品。请注意,通过部署新的 Comet 实例,治理可以更改此值。
也就是说,如果 Compound V3 有足够的“多余现金”,他们宁愿继续持有抵押品,以期望其增值。
让我们检查使用此变量的唯一函数。
抵押品在吸收后仍然在协议内部。发生的仅仅是用户的抵押余额被设置为零 — 然而抵押品并没有转移到任何地方。这些抵押品仍然“在” Compound V3 中。
为了激励清算者,Compound 持有的抵押品通过 buyCollateral() 函数以折扣价出售。
此函数中有两条关键的业务逻辑:
如果准备金数量大于目标准备金(500 万美元),此函数将回滚,无法让清算者购买抵押品(如下代码中的黄色框)。如上所述,Compound 希望对抵押品进行投机。因为它已经处于现金充足的位置,所以不希望再积累更多现金。
协议以 quoteCollateral()
函数中确定的汇率出售抵押品(如下代码中的红框)。
其余代码应该是不言而喻的。
要清算借款人,清算者在同一交易中调用 absorb()
,以借款人的账户作为参数,随后调用 buyCollateral()
。清算者应检查准备金是否不超过目标准备金,以及账户是否通过 isLiquidateable()
检查可被清算。 Compound V3 提供了一个 参考清算机器人。请记住,这只是一个参考实现 — 在其他人之前抢先进行清算是高度竞争的,因此你的代码需要高度 气体优化,才能在其他人之前成功清算一个位置。
Compound V3 所接受的抵押品列表 — 以及诸如抵押比例、清算比例、预言机地址等参数,均存储在不可变变量中。要更改这些,必须对 Compound V3 进行代理升级。
用户的抵押品余额是通过组合指示他们是否为某项资产拥有非零余额的位图,和借款人⇒资产⇒资产余额的嵌套映射来追踪。用户的总抵押品余额是每项资产乘以预言机价格的总和。
清算是全有或全无。当用户被清算时,他们将失去 1 - liquidationRatio
的抵押品,剩余部分将用于偿还债务并将用户分配一个正余额。
该协议现在持有多余抵押品,并根据 quoteCollateral()
函数以折扣价出售。这不会发生,如果准备金超过 targetReserves
。
准备金仅仅是协议欠款加上协议的 USDC 余额,减去协议欠贷方的金额。这笔钱由治理提取。
查看 区块链训练营 以获取高级技术 web3 课程。
最初发布于 2024 年 1 月 8 日
- 原文链接: rareskills.io/post/compo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!