在本章中,我们将探讨关于 Compound V3 的以下主题:
- 抵押品估值
- 吸收抵押不足的贷款(清算)
- 出售吸收的抵押品
- 什么是储备金
- 以及储备金如何影响清算。
这些是在一篇文章中要讨论的很多主题,但它们都紧密交织在一起,因此最好在一篇文章中进行探讨。
先决条件
读者应该已经熟悉 Compound V3 如何定义本金和现值 以及 DeFi 清算和抵押品。
UserBasic 存储结构体
让我们回顾一下 CometStorage.sol 中的 UserBasic 结构体。

如果 principal(蓝框)为负数,表示用户是借款人,该负值将是其债务的**本金**价值。
assetsIn(红框)是一个位图,用于指示他们是否存入了特定的抵押资产。在撰写本文时,位图的布局如下:

变量 baseTrackingIndex 和 baseTrackingAccrued 用于奖励分配的记账,将在单独的文章中讨论。变量 _reserved 未被使用。
请注意,此结构体并未告诉我们用户持有多少抵押品。该信息保存在 userCollateral 嵌套映射中存储的 UserCollateral 结构体 的 balance 变量中。_reserved 变量未被使用。

为了列出用户已提供的抵押品,我们循环遍历 Compound 存储的 0…numAssets,并检查该用户的对应位是否设置为 1。如果是,我们将获取与该位关联的代币地址,并检查 userCollateral[user][collateralAsset] 中的用户余额,以查看用户持有多少该抵押品。
通过将 balance 乘以预言机价格,我们就能知道用户抵押品的美元价值。下表给出了计算用户抵押品总价值的示例。

AssetInfo
Compound 获取抵押品价格的预言机地址存储在 AssetInfo 结构体(蓝框)中。

观察上面的 AssetInfo 结构体,其大小为 432 位——需要占用 2 个插槽来存储。我们将在后面的部分再次讨论这个问题。
在 Compound Finance 市场上显示 AssetInfo
让我们将上面显示的 AssetInfo 结构体的内容与 Compound Finance 市场 UI 进行比较。在这里我们可以看到显示的大部分信息。
文档和代码没有告诉我们变量 scale 是用来做什么的,但它包含数字 1e18,因此大概是为了让使用者知道如何缩放百分比。
这些变量的含义已在关于清算和抵押品的文章中进行了解释。

当我们查询代币 UNI(assetId 3)的当前值时(https://etherscan.io/address/0xc3d688B66703497DAA19211EEdff47f25384cdc3#readProxyContract#F16),我们可以将这些值与市场上显示的值进行比较。它们之间的关系应该很清楚。值得注意的是:清算罚金等于 1 - liquidation factor。liquidateCollateralFactor 是触发贷款清算的 LTV。liquidationFactor 对清算罚金进行了编码。结构体中的 liquidationFactor 与 UI 中的 liquidationFactor 含义不同,这一事实容易令人感到困惑。
第一张图片是屏幕截图,下方的值是通过 Etherscan 查询代币 UNI 的 getAssetInfo() 得到的截图。
下面我们展示了 Compound UI 上的 UNI 抵押参数与 Etherscan 上查询 UNI 的 getAssetInfo() 之间的关系。

下一个显而易见的问题是:“Compound 在哪里存储 AssetInfo 结构体?”
AssetInfo “结构体”保存在不可变变量中
每种资产的信息都被打包到不可变变量中——出于节省 Gas 的目的,它并不保存在存储中。由于存储 AssetInfo 结构体需要两个 32 字节的字,Comet 使用 assetXX_a, assetXX_b 对 uint256 字进行编号。这里的 XX 表示资产索引。因此 asset00_a 和 asset00_b 共同保存资产 0 的 AssetInfo 结构体。请记住,保存大小为 432 位的 AssetInfo 需要两个 256 位变量。

现在我们可以展示 getAssetInfo() 在 Comet.sol:280-356 中的实现。它只是将不可变变量解包到 AccountInfo 结构体并返回它。它所使用的位移和打包操作非常简单明了,因此我们在这里不再赘述。

因为这些变量是不可变的,如果治理想要添加另一个抵押资产或更改其中一个资产的参数,则必须部署一个新的实现并更新代理。必须小心谨慎地仅追加资产,而不要干扰先前的定义。
检查借款人是否可被清算
Comet.sol 中的 isLiquidatable() 函数 将用户持有的抵押资产总和乘以它们的 liquidationFactor。如果该总和小于其债务的现值(这是一个负数),则该用户是可以被清算的。

这意味着借款人可能有一项资产低于清算阈值,但如果其他抵押资产弥补了这一赤字,那么该用户就不会被清算。
抵押品的全部价值并不直接“计入”用户的抵押品余额中——它会根据清算因子进行缩减。
这是前面的同一个示例,显示了用户抵押资产的真实价值。

在上述示例中,假设借款人的贷款余额超过 8,360 美元,他们就会被清算。
清算借款人(吸收)
如果 isLiquidatable() 函数返回 true,那么借款人的抵押品就可以被协议吸收。有些协议称之为“清算”,而 Compound V3 称之为“吸收 (absorb)”。
吸收是全有或全无的——在 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 美元。这笔盈余记在 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 是以下几项的总和:
-
Compound 持有的 USDC 余额,即
ERC20(baseToken).balanceOf(address(this))和 -
totalBorrow的现值, -
减去协议欠存款人的金额,即
totalSupply的现值。
换句话说,它是 usdc_balance + totalBorrow - totalSupply。
正如你在下面的函数中看到的,totalSupply 被赋予了一个负号,因为这是 Compound 欠存款人的钱。正数因子——持有的 USDC 数量和借款人欠 Compound 的 USDC 净额——是 Compound “拥有”的 USDC 数量。

如果我们在 Etherscan 上查看 getReserves() 函数,我们将看到在撰写本文时储备金为 347 万美元(6 个小数位)。

当我们查看 USDC / Mainnet Compound 市场 时,我们也会看到前端显示的当前储备金为 347 万美元。

如果我们在 absorb() 调用前后调用 getReserves(),我们会注意到储备金减少了。发生这种情况有两个原因:
-
协议偿还了贷款(因此欠它的钱减少了)。这是从储备金中扣除的,所以储备金自然就变少了。
-
借款人成为有一笔小额存款的存款人。这笔存款是 Compound 欠存款人的,进一步减少了储备金。
withdrawReserves()
多余的储备金供治理层使用,并且可以使用下面的函数提取。

目标储备金
Compound 有一个定义在 Comet.sol 中的公共不可变变量 targetReserves

当我们在 Etherscan 上查看 targetReserves 时,我们可以看到它是 500 万 USDC。

目标储备金在协议中只有一个用途:用于决定协议是否有足够的“安全边际”从而不卖出吸收的抵押品。请注意,治理层可以通过部署一个新的 Comet 实例来更改此值。
也就是说,如果 Compound V3 有足够的“多余现金”,他们宁愿持有抵押品以投机其价值会上涨。
让我们检查唯一使用此变量的函数。
buyCollateral()
在吸收之后,抵押品仍然在协议内部。所发生的一切仅仅是用户的抵押品余额被设置为零——但抵押品并没有转移到任何地方。这笔抵押品仍然在 Compound V3 的“内部”。
为了激励清算人,Compound 持有的抵押品通过 buyCollateral() 函数 折价出售。
此函数中有两个关键的业务逻辑:
-
如果
reserves金额大于目标储备金(500 万美元),此函数将被回退,不允许清算人购买抵押品。(如下面代码中的黄框所示)。如上所述,Compound 希望用抵押品进行投机。由于其已经处于现金充裕的头寸中,他们不希望积累更多的现金。 -
协议出售抵押品的汇率由
quoteCollateral()函数决定(如下面代码中的红框所示)。
其余代码应该是一目了然的。

清算机器人
要清算借款人,清算人使用借款人的账户作为参数调用 absorb(),然后在同一笔交易中调用 buyCollateral()。清算人应该检查储备金是否没有超过目标储备金,并通过 isLiquidateable() 检查该账户是否可以清算。Compound V3 提供了一个参考清算机器人。请记住,这是一个参考实现——比其他人更快地执行清算竞争非常激烈,所以你的代码将需要进行极度的 Gas 优化 以便能够比其他人抢先有利可图地清算头寸。
总结
Compound V3 接受作为抵押品的资产列表——以及它们的参数(如抵押率、清算率、预言机地址等),都被“打包”在不可变变量中。要改变这些,需要对 Compound V3 进行代理升级。
用户的抵押品余额是通过组合跟踪的:首先是一个位图,指示他们在某种资产上是否具有非零余额,然后是 borrower ⇒ asset ⇒ assetBalance 的嵌套映射。他们的总抵押品余额是每项资产的数量乘以预言机价格的总和。
清算是全有或全无的。当用户被清算时,他们将损失 1 - liquidationRatio 的抵押品,剩余部分将用于偿还债务,并赋予用户正余额。
协议此时持有超额抵押品,并根据 quoteCollateral() 函数折价出售。但是如果储备金高于 targetReserves,它将不会出售。
储备金简单来说就是欠协议的钱加上协议的 USDC 余额,再减去协议欠存款人的钱。这笔资金可由治理层提取。
通过 RareSkills 了解更多
查看我们的区块链训练营以获取高级技术 Web3 课程。
最初发表于 2024 年 1 月 8 日