简介与先决条件
Compound 是 DeFi 中最重要的借贷协议之一,几乎启发了多个区块链上所有借贷协议的设计。本文将解释 V3 的智能合约架构。
由于 Compound 是一个借贷协议,我们假设读者熟悉 DeFi 中的利率机制,并熟悉 DeFi 贷款背景下的清算与抵押品。
与 Compound V2 不同,Compound 的每个实例只出借一种资产。也就是说,你只能从 USDC 市场借入 USDC,只能从 ETH 市场借入 ETH——你不能借入其他任何资产。要借入这些资产,你需要根据市场规定的抵押率(该比例由治理设定)提供抵押品。
在 Compound V3 中,可借入的资产被称为 base asset。因为 USDC 是最受欢迎的借贷资产,我们将在贯穿全文的示例中使用 USDC 作为 base asset。处理借贷核心逻辑的智能合约在其文档和代码库中被称为 Comet。在撰写本文时,Compound 在 Ethereum 主网和各种 L2(如 Polygon、Base 和 Arbitrum)上都有实例。可用的 Compound V3 市场列表在此。
本文简要概述了如何使用该智能合约以及 Solidity 文件的架构。
第 1 部分:使用 Compound
在 Compound V3 中可以执行三个主要操作:
-
存出(出借) base asset
-
提供抵押品并借入 base asset
-
清算抵押不足的贷款。
存出(出借) USDC
以下视频展示了在 Polygon 网络上出借 USDC 的操作(Polygon 市场上的 USDC 链接)。
参与协议的用户会获得 COMP 代币奖励。如粉色箭头所示,上述账户目前已累计 0.0004 COMP 奖励。这些奖励尚未被领取。此外,该账户目前已累计超过 6 美分的利息(4,602.0649 - 4,602.00 = 0.0649)。

绿色框和黄色框中价格不匹配的原因在于 USDC 并不总是恰好等于 1.00 美元。下图展示了最近 Chainlink 在 Polygon 上 USDC / USD 的预言机价格,读者可以看到它并不总是精确的 1 美元。

借入 USDC
以下视频展示了借款过程。这次该账户提供了 0.06 ETH(110美元)作为抵押品,并借入了 100 美元的 USDC。此操作是在 BASE L2 借贷市场 上完成的。
检查浏览器钱包,该账户中现在有 100 个 Base USDC。USDCbC 是桥接到 Base 的 USDC。

以下视频展示了用户偿还 USDC(在利息大量累积之前)并提取 ETH 抵押品的过程。
了解净借款利率与净出借利率

在上面的截图中,请注意 Net Supply APR(出借利率)高于 Net Borrow APR(黄色圆圈)。这通常是不可能的,因为出借人赚取的利息不可能多于借款人支付的利息。Compound 在计算中计入了 COMP 奖励的价值。具体来说,借款人目前支付 6.71% 的利息(上方红色圆圈),但获得了 2.58% 的 COMP 奖励收益(上方蓝色圆圈)。6.71 - 2.58 = 4.13%,这就是 Net Borrow APR。
同样地,出借人目前从出借 USDC 中获得 6.47% 的收益(下方红色圆圈),但也从 COMP 奖励的价值中获得了额外的 4.63% 收益(下方蓝色圆圈)。两者的总和为 11.10%。
由于借款人支付 6.71% 的 USDC 利息,而出借人收取 6.47% 的利息,因此 0.24% 的 USDC 利息流入了协议。
COMP 奖励率的变动受治理投票决定。
第 2 部分:代码库导航
不算注释或空格,Compound V3 有 4,304 行 Solidity 代码。

万米高空俯瞰 Compound V3 架构
下面我们截取了 Compound V3 的 GitHub 仓库图片。
- 所有用绿色高亮显示的文件都包含核心借贷功能。它们之间的继承关系将在后面展示。Comet 是 Compound V3 的主要借贷智能合约。
- 所有用蓝色高亮显示的文件构成了在升级期间部署新 Comet 实例的智能合约。同样,它们之间的继承关系也会在后面展示。
- 粉色高亮显示的文件是分发奖励的合约。

下图总结了构成 Compound V3 的已部署智能合约。这只是一个高层次的概述,后面会给出更详细的说明。请注意,颜色编码与上方的高亮一致。具体来说,主要借贷合约(Comet)为绿色,与部署新 Comet 实例相关的合约为蓝色,而奖励合约为粉色。

大多数用户将通过 comet proxy(未在 GitHub 中显示)或与奖励合约交互来参与 Compound V3,作为出借人或借款人赚取 COMP 奖励。所有面向用户的逻辑都在comet合约中,comet proxy 将其功能委托给它。
配置和工厂合约用于在治理通过升级投票时部署新的 comet 实例。
带星号 (*) 的智能合约有几个祖先合约。在下一节中,我们将展示 Comet 的继承链。
Compound V3 采用了一种不同寻常的基于治理更新参数的方法
如果加密经济状况发生变化,利率模型可以由治理进行更改,清算系数也是如此。Compound 将所有这些信息存储在 immutable 变量中,而不是 storage 变量中。为了改变这些值,必须部署一个新的 Comet 实例,并将 proxy 更新到新的实现合约。
这似乎是一个奇怪的设计选择,但它有几个优点:
- immutable 变量比 storage 变量具有更高的 Gas 效率
- 核心合约不需要被大量的 setter 函数弄得凌乱
我们将在后面考察参数更新的生命周期。
Comet 的继承关系
Comet 的祖先合约如下图所示。在本节中,我们将对每个祖先合约进行高层次的概述。

CometMath.sol(左上角灰色椭圆)
Comet math 简单地包含了一系列用于将无符号整数强制向下转换至更低位数无符号整数的函数,如果会导致溢出则进行 revert。例如,如果我们将 uint256 转换为 uint104,但 uint256 的值大于 uint104 所能存储的最大值,操作就会 revert。你会看到像 safe64 这样的函数散布在代码库中。希望这些内容不需进一步解释也能轻易看懂。该文件很小,因此我们在此展示它的全貌:

CometStorage.sol(红色椭圆)
Comet 所使用的每一个 storage 变量都定义在 CometStorage.sol 中,其他地方没有。也就是说,除了 CometStorage,Comet 继承链上的任何其他合约都没有包含任何 storage 变量。
CometCore.sol(第二排灰色椭圆)
CometCore.sol 定义了 Compound 如何追踪和解释出借人与借款人的利息累积。它还定义了一些全局常量。当我们讨论 Principal Value 和 Present Value 时,我们将深入探究这个文件。
CometMainInterface.sol(左侧蓝色椭圆)
顾名思义,CometMainInterface.sol 仅仅是一个接口文件,唯一奇怪的是它被定义为一个 abstract contract。由于接口是不言自明的,我们在此省略讨论。
Comet.sol(绿色椭圆)
Comet.sol 是全场的主角。它提供了供用户出借、借款、还款和清算贷款的所有 public 函数。
CometExt 是通过 delegatecall 对 Comet 的扩展
为了避免触发 24 KB 的合约部署体积限制,Comet 使用了 fallback-extension 模式 将一些额外的函数卸载到了 CometExt 中。例如,name() 函数不在 Comet.sol 中,因此 无法在 Etherscan 上看到。
然而,如果我们通过 Foundry 的 cast 命令调用该函数,可以看到合约表现得就像该函数存在一样。

实际发生的情况是,函数调用触发了 fallback 函数,并将调用 delegate 给了 CometExt,后者里面包含一个 name() 函数。
重要的是,CometExt 必须遵循 Comet 的 storage 布局并对其进行扩展,而不产生冲突。 这是通过让 CometExt 模仿 Comet 的继承结构来实现的。
请记住,继承只是语义上的描述——当合约被部署时,整个继承链变成了同一个合约。已部署的合约是不可能继承自另一个已部署的合约的。
下面有一张更详细的图解展示了 Comet 和 CometExt 之间的关系:

奖励发放
粉色框中显示了唯一一个用于处理非利息奖励发放的合约。

出借人和借款人因参与生态系统而获得 COMP 代币奖励。Comet 合约跟踪用户的参与情况,但它不处理奖励发放。Reward 合约读取 Comet 的状态并发行 COMP 代币。发放奖励的速率是 Reward 合约内部的参数,由治理设定。
参数更新的生命周期
如前所述,Comet 是通过 immutable 变量进行参数化的。要更改这些参数,需要更新 proxy 使其指向由 Configuration 合约部署的新实现。
在实践中,更改利率曲线的频率非常低。主网合约仅经历过三次利率曲线更新:
https://compound.finance/governance/proposals/162 (2023年5月30日)
https://compound.finance/governance/proposals/168 (2023年7月17日)
https://compound.finance/governance/proposals/201 (2023年12月11日)
利率并不是唯一可以更改的参数——其他值得注意的参数包括改变清算比例,甚至是允许的抵押资产。好奇的读者可以阅读其治理提案以了解详情。
下面的 GIF 展示了 Configuration 智能合约的结构,以及治理机构如何部署带有更新参数的新 Comet 实例。

CometConfiguration.sol 与 ConfiguratorStorage.sol
CometConfiguration.sol 定义了对 Comet 的完整行为进行参数化设置的 struct。CometStorage 只是存储了这些 struct。
如果您阅读过我们关于《DeFi 利率如何运作》以及《DeFi 抵押与清算》的文章,那么希望 struct 中的变量名在您看来大部分都是不言自明的。ConfiguratorStorage 继承了这些定义,并将 struct 存储在 mapping 中。
这些 storage 变量不是 Comet 的一部分。Configurator 合约使用这些存储的配置来部署 Comet 实例。
下面我们展示了 CometConfiguration 和 ConfiguratorStorage 的代码快照。

这比在 calldata 中提供一个极其庞大的 struct 来部署新 Comet 实例的方式要好得多。
当带有更新的新 Comet 实例被部署时,我们只需更新一个特定的 storage 变量,其余保持不变。例如,如果我们更改抵押品 wBTC 的清算阈值,那么仅需在 Configurator 中修改受影响的那个 storage 变量。
Configurator.sol
Configurator.sol 合约继承了 ConfigurationStorage,并提供了仅供治理使用的 setter 函数。Configurator.sol 文件 相当大,因此我们不展示其全部内容。然而,仅仅通过查看其中定义的 event,我们就可以很好地了解该合约的作用——更新用于参数化部署新 Comet 实例的 struct 中的各个字段。

Configurator.sol 还包含用于部署新 Comet 实例的 deploy() 函数。

上述代码中的 CometFactory 定义在 CometFactory.sol 中,并且非常简短,所以我们展示了该文件的全部内容。这里的 clone 函数有点误导人,因为它并不创建 proxy clones——它创建的是一个新实例。

我们将回顾之前相同的动画,把所有前面的讨论联系在一起。

参数更改的真实案例
让我们以治理 Proposal 162 为例。我们可以看到它调用了 Configurator 上的一些 setter 函数来更新参数,然后在最后一步(步骤 8)中部署了一个新的 Comet 实例。

总结全貌
下面展示了所有文件之间的关系以及它们如何互相交互。接口文件被省略。

通过 RareSkills 了解更多
请查看我们的 区块链训练营 以了解更多。
原文首发于 2024 年 1 月 3 日