Compound V3 合约的表现类似于一个 rebasing ERC-20 token。Rebasing token 是一种供应量通过算法调整而不是固定不变的代币。这里的“代币”代表了正向 USDC 余额的现值(present value)。也就是说,贷方可以将他们本金(principal)的现值转移到其他地址,就像转移 ERC-20 token 一样。由于利息的累积,本金价值通常在不断增加,因此这个 ERC-20 token 随着时间的推移呈现向上 rebasing(变基)的趋势。
Compound V3 不使用代币金库标准(例如 ERC-4626)来跟踪借贷池的“份额(shares)”。
正如我们在讨论本金和现值时指出的那样,用户可能存入了 100 USDC,但由于利息累积,其信用额度达到了 110 USDC——这 110 就是现值。Compound V3 的 ERC-20 功能管理的正是这种记账单位。
前置知识
用户必须熟悉利率指数(interest indexes)以及 Compound V3 中 present value and principal value(现值和本金价值)的概念。在本文中,Compound V3 和 Comet 这两个词会交替使用,因为 Comet 是包含我们此处讨论功能的主要智能合约的名称。
本文结构
每个标题将讨论一个 Compound V3 实现的 ERC-20 函数,以及它是如何实现该函数的。其中一些函数并不属于 ERC-20 标准,但与本次讨论相关。
totalSupply 和 totalBorrow (Comet.sol)
顾名思义,totalBorrow 是借出的 USDC 总量。也就是说,它是债务的现值。我们可以通过合约返回的结果以及我们在 Compound 平台上看到的数据来证实这一点。这不仅仅是借款人从平台提取的 USDC 数量——它还包括借出的 USDC 所产生的利息。
下面我们展示了显示此数值的 Compound V3 UI 截图,以及 Etherscan 返回的 totalBorrow() 函数结果。

同样地,totalSupply() 并不是贷方存入 Compound 的 USDC 数量——它是总存款的现值。
下方截取的 totalSupply and totalBorrow code 应该能清楚地表明它们与现值的关系。

下面我们截取了两次对 totalSupply 的查询。请注意,在右侧的第二张截图中,totalSupply 已经增加了。

totalSupply() 函数的表现与 ERC-20 中的 totalSupply() 一致。
借款人无法转移债务,因此 totalBorrow 没有被用于任何类似代币的接口。
balanceOf (Comet.sol)
BalanceOf 在我们讨论本金和现值时已经被涵盖了,因此我们在此不再赘述。
transfer 和 transferFrom (Comet.sol)
transfer 和 transferFrom 都将转移贷方的现值。amount 参数是以现值计量的。

这两个函数在底层都调用了 transferInternal。在 Compound V3 中转移全部余额的正确方法是转移 uint256 max value(最大值)。指定全部余额可能会有些棘手,因为它每秒都在增加。

请注意,借款人没有将抵押品转移到其他地址的机制,因为 transferCollateral 仅为 internal 函数。此函数用于清算。
其余的 ERC-20 函数在 CometExt.sol 中
由于 24 KB 的部署限制,Comet 使用 fallback extension pattern 将其部分功能分离到了 CometExt.sol 中。CometExt 中的大部分函数都与 ERC-20 功能相关。
approve (CometExt.sol)
cUSDCv3 的 approve() 功能是非标准的,因为它只接受 type(uint256).max 或零。由于账户的余额由于 rebasing 正在不断变化,因此不可能给某人提供恰好等于全部余额的 allowance(授权额度),因为随着利息的累积,余额会不断增加。

approve() 在底层调用了 allowInternal,这值得单独探讨。
allow() 和 allowInternal()
allow() 函数不属于 ERC-20 的一部分,但它的表现与 approve() 相同,即它会赋予一个地址最大的 allowance。approve() 和 allow() 都在底层使用 allowInternal() 来实现赋予一个地址最大 allowance 的操作。
因为 approve 本质上是二元的,allow 的表现类似于 approve,区别在于它接受一个布尔参数来赋予全额批准,而不是一个 uint256。

“allowances” 存储变量在 CometStorage.sol 中被保存为 isAllowed。

在 Compound V3 中,Allowance 要么是全额,要么是零。这就是为什么 approve 只接受最大的 uint256 值。这里没有像传统的 ERC-20 代币那样用于将 allowance 存储为数字的存储变量。
allowance (CometExt.sol)
Allowance 是二元的,你要么拥有最大 uint256 值的批准,要么为零。任何其他值都会导致 revert。
hasPermission 只是返回一个布尔值,表示一个地址拥有无限批准或根本没有批准。
allowBySig() 是一个非标准 ERC-20 permit() 函数
CometStorage 将 mapping(address => uint256) public userNonce 暴露为一个公共变量,而不是 EIP 2612 中指定的 nonces(address owner) external returns (uint)。
name() 和 symbol() (CometExt.sol)
name() 和 symbol() 函数是返回字符串的可选 ERC-20 函数。
出于 Gas 效率的考虑,CometExt.sol 并没有将这些值存储在 string 变量中,而是存储在 immutable 的 bytes32 变量中。Solidity 不允许将 bytes32 直接转换为 strings,因此 immutable 变量会使用下方的代码动态转换为 strings。
在 Solidity 中,关于 bytes1、bytes2……以及 bytes32 数据类型的一个较少为人知的细节是,它们可以像字节数组一样在字节级别进行索引。例如:
contract Example {
bytes32 immutable x = 0x3300000000000000000000000000000000000000000000000000000000000000;
function main() external pure returns (bytes1) {
return x[0]; // returns 0x33
}
}
CometExt 在内存中初始化了一个 bytes 数组,并拷贝存储在 name32 和 symbol32 这两个 bytes32 immutable 变量中的字符,然后将其转换为 string。

调用 CometExt 中的函数
Etherscan 所知的 ABI 并不包含这些函数,因此它们不会出现在 Comet 函数列表中。然而,它们是可以被调用的,因为它们会触发 Comet 的 fallback 机制并被委托给 CometExt 合约。
这是一个使用 Foundry 的 cast 调用 name() 和 symbol() 的示例。

结论
CometV3 的表现类似于一个代表贷方正向余额的 rebasing ERC-20 token。这种正向余额可以像普通的 ERC-20 token 一样转移到其他地址。approve() 函数是非标准的——它只能进行无限的 approve 或者根本不 approve。同样地,用于无 Gas 批准的 permit() 函数也是非标准的。
通过 RareSkills 了解更多
查看我们的 blockchain bootcamp 以获取更多信息。
最初发布于 2024 年 1 月 7 日