03.Compound V2 code
2024-04-14 14:39:54 # 12.DeFi

Compound V2 code

架构

四大模块:

  • 核心合约:cToken合约是主要业务逻辑的合约,存款取款、借款还款等逻辑都在里面。CToken合约引入了 InterestRateModel 合约和 Comptroller 合约,因此先阅读改相关合约
  • 利率模型:利率模型的抽象合约,JInterestRateModel、umpRateModelV2、JumpRateModel、WhitePaperInterestRateModel 都是具体的利率模型实现。
  • 审计合约:审计两个字看起来很高大上,其实只是封装了一些业务检验功能,比如存款取款、借款还款前进行校验、开启抵押品等操作。
  • 治理合约:包含治理合约和治理代币Comp合约,Openzepplin也有相关实现。治理这个词也很高大上的样子,简单来说就是拥有一定量Comp代币的人可以发起提案,这些提案一般就是改改参数啥的,比如抵押因子、储备因子。

核心合约

  • CToken

    • 继承了CTokenInterface、ExponentialNoError、TokenErrorReporter,后两个用于处理定点十进制数和错误报告相关,和业务逻辑无关。而第一个定义了大量待实现的方法(管理、用户操作、管理员操作、货币市场等),并且又继承了CTokenStorage,CTokenStorage只定义了成员变量。这样实现了数据和逻辑的分离,从而保证CToken合约可以升级。当使用代理模式,用户对目标合约的所有调用都会通过Proxy合约,然后Proxy合约会delegate到逻辑合约.

      1
      abstract contract CToken is CTokenInterface, ExponentialNoError, TokenErrorReporter {}
    • CToken 是一个抽象的基础合约,没有构造函数,所以不能被部署,只能被别的合约继承,继承这个合约的正是 CErc20CEther

  • CEther&CErc20

    • CEther 用来作为cETH,它有构造函数,所以可以被部署在链上,正是用户交互的入口合约之一,而 CErc20 用来处理基于 ERC20 标准的其他代币(比如cDAI, cUSDT),但是这个合约没有提供构造函数,只有一个初始化函数,说明它是不可以被部署的。

      1
      2
      3
      contract CErc20 is CToken, CErc20Interface {}

      contract CEther is CToken {}
  • CErc20Delegate&CErc20Delegator

    • 那么如何部署 CErc20 合约呢,答案正是代理模式。

    • CErc20 相关的合约还有两个,分别是 CErc20DelegateCErc20DelegatorCErc20Delegator 是Proxy Contract,而 CErc20Delegate 是 Logic Contract,这两个合约都是可以部署的,CErc20Delegator 也是用户交互的另一个入口合约

      1
      2
      3
      contract CErc20Delegate is CErc20, CDelegateInterface {}

      contract CErc20Delegator is CTokenInterface, CErc20Interface, CDelegatorInterface {}

CToken.sol

存款

1
2
3
4
5
//@notice Sender supplies assets into the market and receives cTokens in 
function mintInternal(uint mintAmount) internal nonReentrant {
accrueInterest(); // 计算新利润
mintFresh(msg.sender, mintAmount);
}

accrueInterest()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  /**
* @notice Applies accrued interest to total borrows and reserves
* @dev This calculates interest accrued from the last checkpointed block
* up to the current block and writes new checkpoint to storage.
*/
function accrueInterest() virtual override public returns (uint) {
// 获取当前区块 currentBlockNumber 和上一次计算的区块 accrualBlockNumberPrior
uint currentBlockNumber = getBlockNumber();
uint accrualBlockNumberPrior = accrualBlockNumber;

// 如果两个区块相等,则表示当前去看已经计算过利息,无需再计算,直接返回
if (accrualBlockNumberPrior == currentBlockNumber) {
return NO_ERROR;
}

uint cashPrior = getCashPrior(); // 资金池余额
uint borrowsPrior = totalBorrows; // 总借款
uint reservesPrior = totalReserves; // 总储备金
uint borrowIndexPrior = borrowIndex; // 借款指数

// 计算:获取当前的借款利率。
uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
// 当前借款利率不得超过最大利率,最大的借款利率当前设置为 0.0005e16,即每区块 0.0005%
require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");

// 计算从上次计算之后到当前经过的区块数量。该区块数量表示还未计算利息的区块区间。
uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;

// 然后下面是一系列计算
// 1.计算区块数量时间应收的利息率(因子): 利息因子 = 当前借款利率 * 累计未计算区块间隔。
// simpleInterestFactor = borrowRate * blockDelta
// 2.计算应计利息:应计利息 = 利息因子 * 当前借款总额。
// interestAccumulated = simpleInterestFactor * totalBorrows
// 3.计算借款总额:借款总额 = 应计利息 + 当前借款总额
// totalBorrowsNew = interestAccumulated + totalBorrows
// 4.计算储备金总额:储备金总额 = 储备因子 * 应计利息 + 当前的储备金总额。
// totalReservesNew = interestAccumulated * reserveFactor + totalReserves
// 5.计算借款指数:借款指数 = 利息因子 * 当前借款指数 + 当前借款指数.
// borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior);
uint totalBorrowsNew = interestAccumulated + borrowsPrior;
uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);

// 更新当前区块高度、借款指数、借款总额、储备金总额。
accrualBlockNumber = currentBlockNumber;
borrowIndex = borrowIndexNew;
totalBorrows = totalBorrowsNew;
totalReserves = totalReservesNew;

emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);

return NO_ERROR;
}

mintFresh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  // @notice User supplies assets into the market and receives cTokens in 
function mintFresh(address minter, uint mintAmount) internal {
// 检查是否允许当前地址存入指定数量的 ETH。如果不允许,则抛出异常。
uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);
if (allowed != 0) {
revert MintComptrollerRejection(allowed);
}

// 验证当前区块高度是否等于市场保存的区块高度。如果不一致则抛出异常。
if (accrualBlockNumber != getBlockNumber()) {
revert MintFreshnessCheck();
}

// 计算当前的汇率
// exchangeRateStoredInternal 函数:首先检查当前总的供给量是否为0,
// 如果是返回初始的兑换率,否则使用 (totalCash + totalBorrows - totalReserves) / totalSupply 计算新的的兑换率。
Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});

// 计算发起者实际转入到合约中的 Token 数量。
uint actualMintAmount = doTransferIn(minter, mintAmount);

// 根据汇率计算获得的 cToken 数量
// 铸造的 cToken 数量等于真实输入到合约中的 Token 数量除以兑换率,
// 公式如下:mintTokens = actualMintAmount / exchangeRate。
uint mintTokens = div_(actualMintAmount, exchangeRate);

// 计算 cToken totalSupply;写入用户的余额
totalSupply = totalSupply + mintTokens;
accountTokens[minter] = accountTokens[minter] + mintTokens;

emit Mint(minter, actualMintAmount, mintTokens);
emit Transfer(address(this), minter, mintTokens);
}

因此,存款操作的大概就是将抵押品转入合约,根据利率获取一定量的cToken。

取款

redeemInternal()

1
2
3
4
5
function redeemInternal(uint redeemTokens) internal nonReentrant {
accrueInterest(); // 计算利息
// redeemFresh emits redeem-specific logs on errors, so we don't need to
redeemFresh(payable(msg.sender), redeemTokens, 0);
}

redeemFresh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// @notice User redeems cTokens in exchange for the underlying asset
// uint redeemTokensIn 和 uint redeemAmountIn 参数,意思你可以给定抵押品量或者给定cToken量来进行取款。二者必须有一个是0
function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal {
require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero");

// 计算当前的兑换率
Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal() });

uint redeemTokens;
uint redeemAmount;
// 如果是给定cToken量来计算
if (redeemTokensIn > 0) {
redeemTokens = redeemTokensIn;
redeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn);
} else { // 如果是给定抵押品量来计算
redeemTokens = div_(redeemAmountIn, exchangeRate);
redeemAmount = redeemAmountIn;
}

// 审计
uint allowed = comptroller.redeemAllowed(address(this), redeemer, redeemTokens);
if (allowed != 0) {
revert RedeemComptrollerRejection(allowed);
}

if (accrualBlockNumber != getBlockNumber()) {
revert RedeemFreshnessCheck();
}

// 如果Compound没有足够的金额提现,则revert
if (getCashPrior() < redeemAmount) {
revert RedeemTransferOutNotPossible();
}

// 写入:修改信息
totalSupply = totalSupply - redeemTokens;
accountTokens[redeemer] = accountTokens[redeemer] - redeemTokens;

// 写入:从合约中转出指定数量的资产到用户地址。
doTransferOut(redeemer, redeemAmount);

emit Transfer(redeemer, address(this), redeemTokens);
emit Redeem(redeemer, redeemAmount, redeemTokens);

}

借款

borrowInternal()

1
2
3
4
5
function borrowInternal(uint borrowAmount) internal nonReentrant {
accrueInterest(); // 计算利息
// borrowFresh emits borrow-specific logs on errors, so we don't need to
borrowFresh(payable(msg.sender), borrowAmount);
}

borrowFresh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  function borrowFresh(address payable borrower, uint borrowAmount) internal {
// 审计
uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount);
if (allowed != 0) {
revert BorrowComptrollerRejection(allowed);
}

// 验证当前区块高度是否等于市场保存的区块高度。如果不一致则抛出异常。
if (accrualBlockNumber != getBlockNumber()) {
revert BorrowFreshnessCheck();
}

// Compound不够钱借款了
if (getCashPrior() < borrowAmount) {
revert BorrowCashNotAvailable();
}

// 计算:查看之前借款的金额,更新借款金额
// 用户最新的借款金额 = 用户的借款金额 * 市场借款指数 / 用户的借款指数
// accountBorrowsPrev:之前借款总数,包括利息。具体过程如下:
// 1.获取包含账户借款余额和借款指数信息的借款快照。
// BorrowSnapshot storage borrowSnapshot = accountBorrows[account];
// 2.如果用户已借款数量为0,则立即返回。
// if (borrowSnapshot.principal == 0) return (MathError.NO_ERROR, 0);
// 3.计算用户最新的借款金额,并返回借款余额。
// mulUInt()、divUInt()、return (MathError.NO_ERROR, result);
uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower);
uint accountBorrowsNew = accountBorrowsPrev + borrowAmount;
// 更新Compound的借款总数
uint totalBorrowsNew = totalBorrows + borrowAmount;

// 写入:更新借款信息
accountBorrows[borrower].principal = accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = totalBorrowsNew;

// 将Token从货币市场转入指定金额到用户地址。
doTransferOut(borrower, borrowAmount);

emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew);
}

还款

repayBorrowInternal()

1
2
3
4
5
function repayBorrowInternal(uint repayAmount) internal nonReentrant {
accrueInterest(); // 计算利息
// repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to
repayBorrowFresh(msg.sender, msg.sender, repayAmount);
}

repayBorrowFresh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// @notice 还债:任何人都可以帮助你还债
// @param payer the account paying off the borrow
// @param borrower the account with the debt being payed off
// @param repayAmount the amount of underlying tokens being returned, or -1 for the full outstanding amount
function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint) {
// 审计
uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount);
if (allowed != 0) {
revert RepayBorrowComptrollerRejection(allowed);
}

// 验证当前区块高度是否等于市场保存的区块高度。如果不一致则抛出异常。
if (accrualBlockNumber != getBlockNumber()) {
revert RepayBorrowFreshnessCheck();
}

// 计算:用户的借款总数,包括利息
uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower);

// 如果还款参数为-1,即uint256的最大值,则代表我们要还完所有负债,否则偿还部分金额。
uint repayAmountFinal = repayAmount == type(uint).max ? accountBorrowsPrev : repayAmount;

// 还钱:从还款地址中转入指定金额到货币市场。
uint actualRepayAmount = doTransferIn(payer, repayAmountFinal);

// 计算:还款信息
uint accountBorrowsNew = accountBorrowsPrev - actualRepayAmount;
uint totalBorrowsNew = totalBorrows - actualRepayAmount;

// 写入:还款信息
accountBorrows[borrower].principal = accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
// 更新Compound的总借款信息
totalBorrows = totalBorrowsNew;

emit RepayBorrow(payer, borrower, actualRepayAmount, accountBorrowsNew, totalBorrowsNew);

return actualRepayAmount;
}

调用者帮借款人还款

1
2
3
4
5
function repayBorrowBehalfInternal(address borrower, uint repayAmount) internal nonReentrant {
accrueInterest(); // 计算利率
// repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to
repayBorrowFresh(msg.sender, borrower, repayAmount);
}

总结:存款、取款、借款、还款的逻辑大致就是计算利率、审计检查、转入转出

CEther.sol

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18
constructor(ComptrollerInterface comptroller_,
InterestRateModel interestRateModel_,
uint initialExchangeRateMantissa_,
string memory name_,
string memory symbol_,
uint8 decimals_,
address payable admin_) {
// 管理员是msg.sender,初始化工作需要是admin才能做
admin = payable(msg.sender);

// 调用父合约的initialize()进行初始化
initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_);

// 重新设置真正的admin
admin = admin_;
}

initialize()又跑到了CToken.sol中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 初始化某个token市场
function initialize(ComptrollerInterface comptroller_,
InterestRateModel interestRateModel_,
uint initialExchangeRateMantissa_,
string memory name_,
string memory symbol_,
uint8 decimals_) public {
// 必须是admin才能初始化,因此在CEther.sol中我们先是将admin给了msg.sender
require(msg.sender == admin, "only admin may initialize the market");
// 保证只初始化了一次
require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once");

// 设置exchange rate(汇率),并且不能为0
initialExchangeRateMantissa = initialExchangeRateMantissa_;
require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero.");

// Set the comptroller.用于审计
uint err = _setComptroller(comptroller_);
require(err == NO_ERROR, "setting comptroller failed");

// 初始化计算利息的区块号和borrow index
accrualBlockNumber = getBlockNumber(); // 会获取当前区块号
borrowIndex = mantissaOne; // mantissaOne 为一个 1e18 常量。

// 设置利率模型
// 1. 函数首先检查调用者为管理员,应计息区块号和当前区块号相同(确保是在同一个区块内进行设置的)
// 2. 然后保存当前利率模型合约地址到一个临时变量中,并检查新的地址为利率模型合约
// 3. 然后保存新的利率模型合约到利率模型合约状态变量中
// 4. 最后发出 NewMarketInterestRateModel 事件。
err = _setInterestRateModelFresh(interestRateModel_);
require(err == NO_ERROR, "setting interest rate model failed");

// 设置这个token的一系列常规信息
name = name_;
symbol = symbol_;
decimals = decimals_;

_notEntered = true; // 用于防止再次调用,重入之类
}

mint()

存款函数会新增 cToken 数量,即 totalSupply 增加了,就等于挖矿了 cToken。该操作会同时将用户的标的资产转入 cToken 合约中(数据会存储在代理合约中),并根据最新的兑换率将对应的 cToken 代币转到用户钱包地址。

1
function mint() external payable { mintInternal(msg.value);}

之后会进入到CToken.sol的mintInternal()

1
2
3
4
5
//@notice Sender supplies assets into the market and receives cTokens in 
function mintInternal(uint mintAmount) internal nonReentrant {
accrueInterest(); // 计算新利息,出现错误会抛出异常
mintFresh(msg.sender, mintAmount);
}

再进入CToken.sol存款函数的accrueInterest()

最后进入CToken.sol存款函数的mintFresh()

redeem()

然后进入CToken.sol取款函数的mintInternal()方法

1
function mint() external payable {mintInternal(msg.value);}

borrow()

然后进入CToken.sol借款的borrow()

1
2
3
4
function borrow(uint borrowAmount) external returns (uint) {
borrowInternal(borrowAmount);
return NO_ERROR;
}

repayBorrow()

然后进入CToken.sol还款的repayBorrowInternal()

1
function repayBorrow() external payable {repayBorrowInternal(msg.value);}

repayBorrowBehalf()

帮别人还钱,进入到CToken.sol还款的repayBorrowBehalfInternal()

1
function repayBorrowBehalf(address borrower) external payable {repayBorrowBehalfInternal(borrower, msg.value);}

CErc20.sol

Coumpound 会为每个 ERC20 代币部署一个货币市场,同时会生成相应的 cToken,比如 DAI 对应的 cToken 就叫 cDAI。ERC20 货币市场采用的是代理模式,以此方便某个 ERC20 代币的升级

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function initialize(address underlying_,
ComptrollerInterface comptroller_,
InterestRateModel interestRateModel_,
uint initialExchangeRateMantissa_,
string memory name_,
string memory symbol_,
uint8 decimals_) public {
// 调用父合约的初始化函数进行初始化设置
// 实例了这个底层代币
super.initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_);

// 保存对应的底层基础代币
underlying = underlying_;
// 获取它的余额来检查底层代币是否OK
EIP20Interface(underlying).totalSupply();
}

利率模型

包括两个模型

  • 直线型利率模型:WhitePaperInterestRateModel
  • 拐点型利率模型:JumpRateModelJumpRateModelV2两个不同版本的实现,曾经使用 JumpRateModel 的都已经升级为 JumpRateModelV2。所以我就直接研究 JumpRateModelV2

InterestRateModel.sol

非常简单,只定义了一个变量和两个方法

1
2
3
4
5
6
7
8
9
10
abstract contract InterestRateModel {

bool public constant isInterestRateModel = true;

// 计算: 每个区块的当前借款利率
function getBorrowRate(uint cash, uint borrows, uint reserves) virtual external view returns (uint);

// 计算: 每个区块的当前存储利率
function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual external view returns (uint);
}

WhitePaperInterestRateModel.sol

初始化

1
2
y = k*x + b
其中,k 为斜率,b 是 x 为 0 时的起点值,x 是自变量,在这里表示资金利用率,y 是因变量,表示借款利率。

在构造时候要指定基础的年化利率和利率的增长速度

  • baseRatePerYear 表示基础的年化利率,对应的 baseRatePerBlock 表示区块级利率,对应公式中的截矩 b
  • multiplierPerYear 表示利率的增长速度,对应的 multiplierPerBlock 表示利率的增长速度,也就是公式中的斜率 k
1
2
3
4
5
6
constructor(uint baseRatePerYear, uint multiplierPerYear) public {
baseRatePerBlock = baseRatePerYear / blocksPerYear;
multiplierPerBlock = multiplierPerYear / blocksPerYear;

emit NewInterestParams(baseRatePerBlock, multiplierPerBlock);
}

利用率

存储利率和贷款利率都要用到利用率。

如果借款金额为0时,利用率为0,否则利用率就等于 总借款 / (资金池余额 + 总借款 - 储备金),代码中 mul(1e18) 是为了保持结果的精度。

1
2
3
4
function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
if (borrows == 0) return 0;
return borrows * BASE / (cash + borrows - reserves);
}

借款利率

首先求出利用率,然后根据直线公式 区块级的利率增长速度 * 利用率 + 区块级的基础利率 算出借款利率。

这里 / BASE 是因为利用率和区块级的利率增长速度本身都已经扩为高精度整数了,相乘之后精度变成 36 了,所以再除以 BASE 就可以把精度降回 18。

1
2
3
4
function getBorrowRate(uint cash, uint borrows, uint reserves) override public view returns (uint) {
uint ur = utilizationRate(cash, borrows, reserves);
return (ur * multiplierPerBlock / BASE) + baseRatePerBlock;
}

存款利率

整理出来就是 资金利用率 * 借款利率 * (1 - 储备金率)

1
2
3
4
5
6
function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) override public view returns (uint) {
uint oneMinusReserveFactor = BASE - reserveFactorMantissa;
uint borrowRate = getBorrowRate(cash, borrows, reserves);
uint rateToPool = borrowRate * oneMinusReserveFactor / BASE;
return utilizationRate(cash, borrows, reserves) * rateToPool / BASE;
}

BaseJumpRateModelV2.sol

状态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
uint256 private constant BASE = 1e18; // 小数点位数,用于计算,因为solidity没有小数

address public owner; // Timelock 合约

uint public constant blocksPerYear = 2102400; // 一年大概的区块数,用于计算年利率

uint public multiplierPerBlock; // 公式 y = k * x 中的斜率 k 值

uint public baseRatePerBlock; // 利率初始值,一开始为0

uint public jumpMultiplierPerBlock; // 拐点后的斜率 k 值

uint public kink; // 拐点值,也就是资金利用率到了多少就拐点了,比如是80%。

初始化

1
2
3
4
5
  constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_, address owner_) internal {
owner = owner_;
// 使用父合约的构造函数
updateJumpRateModelInternal(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_);
}

然后调用了updateJumpRateModelInternal()

1
2
3
4
5
6
7
8
9
10
11
12
13
function updateJumpRateModelInternal(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_) internal {
// 拐点利用率之前,直线的截矩 b
baseRatePerBlock = baseRatePerYear / blocksPerYear;
// 拐点利用率之前,直线的斜率 k。
// 注意这里的 k,与直接型利率模型中的有一些细微的不同。
multiplierPerBlock = (multiplierPerYear * BASE) / (blocksPerYear * kink_);
// 拐点利用率之后,直线的斜率 k2
jumpMultiplierPerBlock = jumpMultiplierPerYear / blocksPerYear;
// 拐点时的资产利用率
kink = kink_;

emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink);
}

利用率

无论是直线利率模型,还是拐点型利率模型,在计算利用率时都是一样

借款利率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 计算区块的借款利息
function getBorrowRateInternal(uint cash, uint borrows, uint reserves) internal view returns (uint) {
// 计算:资产利用率
uint util = utilizationRate(cash, borrows, reserves);

if (util <= kink) { // 如果资金使用率没超过拐点
return ((util * multiplierPerBlock) / BASE) + baseRatePerBlock;
} else {
// 如果超过拐点,利率公式为 y = k2 * ( x - p ) + ( p * k + b),即将拐点前后两部分分别计算后相加即可。
// b, k, k2, p 分别对应:baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock
// 即 jumpMultiplierPerBlock * ( util - kink ) + ( kink * multiplierPerBlock + baseRatePerBlock)

// ( p * k + b ) kink前一部分是直线型利率
uint normalRate = ((kink * multiplierPerBlock) / BASE) + baseRatePerBlock;
// 超额利用率
uint excessUtil = util - kink;
// 多出 kink 的后一部分使用拐点斜率,两者相加
// 超额利用率 * 拐点后斜率 + 正常利率
return ((excessUtil * jumpMultiplierPerBlock) / BASE) + normalRate;
}
}

借款利率的计算很简单。首先使用 utilizationRate 计算资金利用率:

1
2
3
4
function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
if (borrows == 0) return 0;
return borrows * BASE / (cash + borrows - reserves);
}

这是计算一天的区块的利率,一天中的区块利润是单利的,而一年中的365天是复利的。按照一年2628000个区块,一天7200个区块进行计算得出:3.5%

1
2
(13385436439 / 1e18 * 7200 + 1) ** 365 - 1
0.03580119839257878

存款利率

存款利率是根据借款利率计算的,下面的代码翻译为:存款利率 = 资金使用率 * 借款利率 *(1 - 储备金率)。简单来说就是借款利息的一部分要分到储备金里进行储备。

1
2
3
4
5
6
function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual override public view returns (uint) {
uint oneMinusReserveFactor = BASE - reserveFactorMantissa;
uint borrowRate = getBorrowRateInternal(cash, borrows, reserves);
uint rateToPool = borrowRate * oneMinusReserveFactor / BASE;
return utilizationRate(cash, borrows, reserves) * rateToPool / BASE;
}

计算增值的利息,见CToken.sol存款函数的accrueInterest()

计算汇率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @notice Calculates the exchange rate from the underlying to the CToken
* @dev This function does not accrue interest before calculating the exchange rate
* @return calculated exchange rate scaled by 1e18
*/
function exchangeRateStoredInternal() virtual internal view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) { // 如果没有token被mint过
return initialExchangeRateMantissa;
} else {
// exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
// 这里totalCash、totalBorrows都是以抵押品为单位的数量
// _totalSupply是以cToken为单位的数量
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;

return exchangeRate;
}
}

计算借款金额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 计算借款金额
function borrowBalanceStoredInternal(address account) internal view returns (uint)
BorrowSnapshot storage borrowSnapshot = accountBorrows[account];

// 如果借款金额是0,那么borrowIndex就应该是0,
// 但是除0会异常,因此这里直接返回0了
if (borrowSnapshot.principal == 0) {
return 0;
}

// 计算新的借款金额
// recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
uint principalTimesIndex = borrowSnapshot.principal * borrowIndex;
return principalTimesIndex / borrowSnapshot.interestIndex;
}

JumpRateModelV2

1
2
3
4
资金使用率没超过拐点值时,利率公式和直线型的一样:
y = k*x + b
而超过拐点之后,则利率公式将变成:
y = k2*(x - p) + (k*p + b)

k2 表示拐点后的直线的斜率,简称为拐点直线斜率,p 表示拐点的 x 轴的值,也即拐点利用率,x - p 表示超额利用率,k*p + b 表示拐点时 y 的高度,简称拐点y 值。

JumpRateModelV2 合约继承自 BaseJumpRateModelV2 合约,大部分操作由后者来完成。

治理合约

包括:Governance文件夹所有(Comp.sol、GovernorAlpha.sol等)、Timelock.sol

Comp.sol

除了正常的ERC20功能,魔改增加了投票功能

将自己的投票权委托给别人,可以委托给自己。最终的投票权=自己的COMP余额+委托的COMP金额

1
2
3
function delegate(address delegatee) public {
return _delegate(msg.sender, delegatee);
}

投票权可以离线签名:EIP-712。expiry是签名到期时间

1
2
3
4
5
6
7
8
9
10
function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) public {
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Comp::delegateBySig: invalid signature");
require(nonce == nonces[signatory]++, "Comp::delegateBySig: invalid nonce");
require(block.timestamp <= expiry, "Comp::delegateBySig: signature expired");
return _delegate(signatory, delegatee);
}

获取某用户的投票数

1
2
3
4
function getCurrentVotes(address account) external view returns (uint96) {
uint32 nCheckpoints = numCheckpoints[account];
return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}

查看某个用户在之前某个区块的获得的投票数额

1
function getPriorVotes(address account, uint blockNumber) public view returns (uint96) {.....}

GovernorAlpha.sol

创建和执行提案的所有逻辑

查看提案状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum ProposalState {
Pending,
Active,
Canceled,
Defeated,
Succeeded,
Queued,
Expired,
Executed
}

function state(uint proposalId) public view returns (ProposalState) {
require(proposalCount >= proposalId && proposalId > 0, "GovernorAlpha::state: invalid proposal id");
Proposal storage proposal = proposals[proposalId];
if (proposal.canceled) {
return ProposalState.Canceled;
} else if (block.number <= proposal.startBlock) {
return ProposalState.Pending;
} else if (block.number <= proposal.endBlock) {
return ProposalState.Active;
} else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes()) {
return ProposalState.Defeated;
} else if (proposal.eta == 0) {
return ProposalState.Succeeded;
} else if (proposal.executed) {
return ProposalState.Executed;
} else if (block.timestamp >= add256(proposal.eta, timelock.GRACE_PERIOD())) {
return ProposalState.Expired;
} else {
return ProposalState.Queued;
}
}

发起提案

  • targets:执行此提案操作的合约地址
  • values:msg.value
  • signatures:调用啥方法,比如transfer()
  • calldatas:执行方法的参数,比如上面transfer()函数中的参数内容
  • description:描述这个提案用来干啥的
1
function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) {.....}

进行投票

可以自己投票,也可以签名让别人投票

1
2
3
4
5
6
7
8
9
10
11
12
function castVote(uint proposalId, bool support) public {
return _castVote(msg.sender, proposalId, support);
}

function castVoteBySig(uint proposalId, bool support, uint8 v, bytes32 r, bytes32 s) public {
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "GovernorAlpha::castVoteBySig: invalid signature");
return _castVote(signatory, proposalId, support);
}

加入队列

可以看出,只要提案投票通过,任何人都可以将提案加入到队列中

1
2
3
4
5
6
7
8
9
10
function queue(uint proposalId) public {
require(state(proposalId) == ProposalState.Succeeded, "GovernorAlpha::queue: proposal can only be queued if it is succeeded");
Proposal storage proposal = proposals[proposalId];
uint eta = add256(block.timestamp, timelock.delay());
for (uint i = 0; i < proposal.targets.length; i++) {
_queueOrRevert(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta);
}
proposal.eta = eta;
emit ProposalQueued(proposalId, eta);
}

取消提案

目前,仍然存在一个有权取消任何提案的监护人地址。它目前是由Compound Labs, Inc.自己持有。在未来,这个监护人地址可能会被删除。

在提案执行之前,提案的提出者可以取消此提案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function cancel(uint proposalId) public {
ProposalState state = state(proposalId);
require(state != ProposalState.Executed, "GovernorAlpha::cancel: cannot cancel executed proposal");

Proposal storage proposal = proposals[proposalId];
require(msg.sender == guardian || comp.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < proposalThreshold(), "GovernorAlpha::cancel: proposer above threshold");

proposal.canceled = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.cancelTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}

emit ProposalCanceled(proposalId);
}

执行提案

可以看出,只要排队完成,任何人都可以执行提案。提案的执行是将

1
2
3
4
5
6
7
8
9
function execute(uint proposalId) public payable {
require(state(proposalId) == ProposalState.Queued, "GovernorAlpha::execute: proposal can only be executed if it is queued");
Proposal storage proposal = proposals[proposalId];
proposal.executed = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.executeTransaction{value: proposal.values[i]}(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}
emit ProposalExecuted(proposalId);
}

这会跑到Timelock.sol执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) {
require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin.");

bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale.");

queuedTransactions[txHash] = false;

bytes memory callData;

if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
}

// solium-disable-next-line security/no-call-value
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Timelock::executeTransaction: Transaction execution reverted.");

emit ExecuteTransaction(txHash, target, value, signature, data, eta);

return returnData;
}

提案执行的逻辑:

1
2
bytes memory callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
(bool success,) = target.call.value(value)(callData);

Timelock.sol

拥有管理员机制

延迟

设置提案多久之后才能被投票、被加入队列、被执行等,目前设置为2天

1
2
3
4
5
6
7
8
function setDelay(uint delay_) public {
require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock.");
require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay.");
require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay.");
delay = delay_;

emit NewDelay(delay);
}

队列&交易

主要有:queueTransaction()cancelTransaction()executeTransaction()

Prev
2024-04-14 14:39:54 # 12.DeFi
Next