AAVE 利率模型中,几个参数之间互相影响,关系错综复杂,其中,固定利率比较绕,尤其是计算平均固定利率的公式,尤其难懂。不过,在实际的借贷中,有很多token不支持固定利率借贷,而且,在支持固定利率借贷token中,使用固定利率的借贷比例也很小,几乎不到1%,因此,对于固定利率部分,对于初学者来说可以先跳过。
在 AAVE 的利率计算中, 有几个地方值得说明:
- AAVE的利率更新是基于时间戳来计算利息的增加,- Compound是根据块数来计算利息的增加;两者触发利率更新的方式相同,都是由存款,取款,借款,还款等几个动作触发,每个块只计算一次;
- 存款利率的增加是线性的,贷款利率的增加是复利,指数级;
- 贷款收益的 reserve factor% (默认10%) 纳入平台金库;
- aToken, debtToken 中的 balanceOf方法,返回的 amount 都是存款/贷款对应的 token 数量;
如果你只需要知道结论,AAVE的几个核心流程可以简单的总结如下(代码在 LendingPool 合约中):
存款 Deposit
- 校验数据(ValidationLogic.validateDeposit)
- 更新状态: updateState
- 更新利率: updateInterestRates
- 将 token 从用户转入 LendingPool合约
- mint aToken给用户: aToken amount = token amount / liquidityIndex
- emit Deposit事件
取款 Withdraw
- 查询用户token资产数量,注意: 是token资产数量, 而不是 aToken 数量. 虽然这里调用的是 aToken.balanceOf(), 看上去好像是 aToken 的余额,但事件上,这个函数会计算当前块 aToken 对应的 token 的数量;
- 校验数据(ValidationLogic.validateWithdraw)
- 更新状态: updateState
- 更新利率: updateInterestRates
- 销毁用户的aToken,将对应的token转给用户
- emit Withdraw 事件
借款 Borrow
- 通过价格预言机, 计算出借款对应的 ETH 价值 amountInETH(AAVE中所有的token都要换算为对应的ETH价值)
- 校验数据(ValidationLogic.validateBorrow)
- 更新状态: updateState
- 至关重要的一步:mint 对应的 debtToken, AAVE 的债务是通过 debtToken 来记录的,每个token 都会有对应的 stableDebtToken, variableDebtToken, 分别对应于定息借款和活息借款
- 更新利率: updateInterestRates
- 将用户借出的token转给用户
- emit Borrow 事件
还款 Repay
- 查询定息借款和活息借款额度, 都是贷款 debtToken 对应的 token 的数量;
- 校验数据 (ValidationLogic.validateRepay)
- 更新状态: updateState
- 销毁 debtToken. 这里有一个很关键、很重要的细节,就是还款额度的计算: paybackAmount, 必须要非常仔细, 一旦出错, 就会造成重大损失
- 更新利率: updateInterestRates
- 将用户的 token 转入合约,完成还款
- emit Repay 事件
最基本的操作就是以上这几个,其他的还有清算(专门讨论), 转换借款方式(定息转活息,活息转定息)
从上面的流程可以看出:
- updateState 和 updateInterestRates 是两个非常关键的函数,所有的操作都要调用这两个方法;
- 对这两个方法的调用稍有不同,例如 存款,取款时,两个方法是一起调用的; 而借款中,在两个方法的调用之间,还有其他操作;为什么会这样呢?
所有利率的变化,都是由 updateState 和 updateInterestRates 计算并更新的,下面我们来看看这两个函数的实现细节。
updateState updateInterestRates
两个函数定义如下:
function updateState(DataTypes.ReserveData storage reserve) internal;
function updateInterestRates(
    DataTypes.ReserveData storage reserve,
    address reserveAddress,
    address aTokenAddress,
    uint256 liquidityAdded,
    uint256 liquidityTaken
  ) internal;
这两个函数在 library ReserveLogic.sol 中, 传入的参数中都有 DataTypes.ReserveData storage reserve , ReserveData 我们前面介绍过,是储存利率相关字段的结构体; 这个参数的存储类型是 storage, 说明这两个函数可以直接修改合约中的状态。
updateState
updateState 主要作用: 0. 获取最新的活息贷款数量, 即 variableDebtToken 的总数量;
- 更新存款利率指数(ReserveData.liquidityIndex)和活息贷款利率指数(ReserveData.variableBorrowIndex), 通过 _updateIndexes方法
- 将产生的增量贷款利息的一部分(reserve factor) 存入小金库, 具体实现的方式是 mint Atoken 给 treasy 地址
updateInterestRates
updateInterestRates 主要作用: 0. 更新 currentLiquidityRate;
- 更新 currentStableBorrowRate
- 更新 currentVariableBorrowRate
关键函数
_updateIndexes
一个细节是,虽然资金变化都每次都调用,但由于在一个块内,只有第一次调用时 liquidityIndex 和 variableBorrowIndex 发生变化,后续调用都不会变化,因为 timestamp 相同, calculateCompoundedInterest 和 calculateLinearInterest 都是返回 Ray(1e27).
不知道是否可以优化, 我提了一个issue: https://github.com/aave/protocol-v2/issues/237
  function _updateIndexes(
    DataTypes.ReserveData storage reserve,
    uint256 scaledVariableDebt,
    uint256 liquidityIndex,
    uint256 variableBorrowIndex,
    uint40 timestamp  // 上次更新的时间戳
  ) internal returns (uint256, uint256) {
    // currentLiquidityRate 是在 updateInterestRates 中更新的
    uint256 currentLiquidityRate = reserve.currentLiquidityRate;
    uint256 newLiquidityIndex = liquidityIndex;
    uint256 newVariableBorrowIndex = variableBorrowIndex;
    //only cumulating if there is any income being produced
    if (currentLiquidityRate > 0) {
      // 存款利率根据 currentLiquidityRate 线性累加
      // 公式: cumulatedLiquidityInterest = currentLiquidityRate * (block.timestamp-timestamp)/SECONDS_PER_YEAR
      uint256 cumulatedLiquidityInterest =
        MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);
      newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex);
      require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW);
      // 更新存款利息指数
      reserve.liquidityIndex = uint128(newLiquidityIndex);
      //as the liquidity rate might come only from stable rate loans, we need to ensure
      //that there is actual variable debt before accumulating
      if (scaledVariableDebt != 0) {
        // 计算这段时间累加的活息贷款利息, 复利方式计算
        // 计算的实现是 泰勒级数展开
        uint256 cumulatedVariableBorrowInterest =
          MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp);
        newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex);
        require(
          newVariableBorrowIndex <= type(uint128).max,
          Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW
        );
      // 更新活息贷款利息指数
        reserve.variableBorrowIndex = uint128(newVariableBorrowIndex);
      }
    }
    // 设置更新时间戳
    //solium-disable-next-line
    reserve.lastUpdateTimestamp = uint40(block.timestamp);
    return (newLiquidityIndex, newVariableBorrowIndex);
  }
updateInterestRates
更新年化利率
  function updateInterestRates(
    DataTypes.ReserveData storage reserve,
    address reserveAddress,
    address aTokenAddress,
    uint256 liquidityAdded,
    uint256 liquidityTaken
  ) internal {
    UpdateInterestRatesLocalVars memory vars;
    vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress;
    // 定息借款
    (vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress)
      .getTotalSupplyAndAvgRate();
    // 活息贷款, 折算为 token 
    //calculates the total variable debt locally using the scaled total supply instead
    //of totalSupply(), as it's noticeably cheaper. Also, the index has been
    //updated by the previous updateState() call
    vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress)
      .scaledTotalSupply()
      .rayMul(reserve.variableBorrowIndex);
    // 根据利率策略,计算新的存款年化利率 活息贷款年化利率 定息贷款年化利率. calculateInterestRates 见下文分析
    (
      vars.newLiquidityRate,
      vars.newStableRate,
      vars.newVariableRate
    ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates(
      reserveAddress,
      aTokenAddress,
      liquidityAdded,
      liquidityTaken,
      vars.totalStableDebt,
      vars.totalVariableDebt,
      vars.avgStableRate,
      reserve.configuration.getReserveFactor()
    );
    require(vars.newLiquidityRate <= type(uint128).max, Errors.RL_LIQUIDITY_RATE_OVERFLOW);
    require(vars.newStableRate <= type(uint128).max, Errors.RL_STABLE_BORROW_RATE_OVERFLOW);
    require(vars.newVariableRate <= type(uint128).max, Errors.RL_VARIABLE_BORROW_RATE_OVERFLOW);
    // 新的利率
    reserve.currentLiquidityRate = uint128(vars.newLiquidityRate);
    reserve.currentStableBorrowRate = uint128(vars.newStableRate);
    reserve.currentVariableBorrowRate = uint128(vars.newVariableRate);
    emit ReserveDataUpdated(
      reserveAddress,
      vars.newLiquidityRate,
      vars.newStableRate,
      vars.newVariableRate,
      reserve.liquidityIndex,
      reserve.variableBorrowIndex
    );
  }
calculateInterestRates
代码如下:
  struct CalcInterestRatesLocalVars {
    uint256 totalDebt;
    uint256 currentVariableBorrowRate;
    uint256 currentStableBorrowRate;
    uint256 currentLiquidityRate;
    uint256 utilizationRate;
  }
  /**
   * @dev Calculates the interest rates depending on the reserve's state and configurations.
   * NOTE This function is kept for compatibility with the previous DefaultInterestRateStrategy interface.
   * New protocol implementation uses the new calculateInterestRates() interface
   * @param reserve The address of the reserve
   * @param availableLiquidity The liquidity available in the corresponding aToken
   * @param totalStableDebt The total borrowed from the reserve a stable rate
   * @param totalVariableDebt The total borrowed from the reserve at a variable rate
   * @param averageStableBorrowRate The weighted average of all the stable rate loans
   * @param reserveFactor The reserve portion of the interest that goes to the treasury of the market
   * @return The liquidity rate, the stable borrow rate and the variable borrow rate
   **/
  function calculateInterestRates(
    address reserve,
    uint256 availableLiquidity,
    uint256 totalStableDebt,
    uint256 totalVariableDebt,
    uint256 averageStableBorrowRate,
    uint256 reserveFactor
  )
    public
    view
    override
    returns (
      uint256,
      uint256,
      uint256
    )
  {
    CalcInterestRatesLocalVars memory vars;
    // 总借款 = 活息借款 + 定息借款
    vars.totalDebt = totalStableDebt.add(totalVariableDebt);
    vars.currentVariableBorrowRate = 0;
    vars.currentStableBorrowRate = 0;
    vars.currentLiquidityRate = 0;
    // 资金利用率 = 总借款 / (总存款+总借款)
    // 忽略 rayDiv rayMul, 可以简单的认为是 div mul 即可
    vars.utilizationRate = vars.totalDebt == 0
      ? 0
      : vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt));
    vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle())
      .getMarketBorrowRate(reserve);
    // 利率曲线: 两段式
    // 资金利用率高于最佳利用率时,利率的计算
    if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) {
      uint256 excessUtilizationRateRatio =
        vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE);
      vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add(
        _stableRateSlope2.rayMul(excessUtilizationRateRatio)
      );
      vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add(
        _variableRateSlope2.rayMul(excessUtilizationRateRatio)
      );
    } else {
      vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(
        _stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE))
      );
      vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(
        vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE)
      );
    }
    // currentLiquidityRate的计算 = (加权平均借款利率/总借款) * 资金利用率 * (1-储备率)
    // 加权平均借款利率 = (活息借款*活息利率 + 定息借款*平均定息利率) / 总借款
    vars.currentLiquidityRate = _getOverallBorrowRate(
      totalStableDebt,
      totalVariableDebt,
      vars
        .currentVariableBorrowRate,
      averageStableBorrowRate
    )
      .rayMul(vars.utilizationRate)
      .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));
    return (
      vars.currentLiquidityRate,
      vars.currentStableBorrowRate,
      vars.currentVariableBorrowRate
    );
  }
  /**
   * @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable debt
   * @param totalStableDebt The total borrowed from the reserve a stable rate
   * @param totalVariableDebt The total borrowed from the reserve at a variable rate
   * @param currentVariableBorrowRate The current variable borrow rate of the reserve
   * @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans
   * @return The weighted averaged borrow rate
    加权平均借款利率: (活息借款*活息利率 + 定息借款*平均定息利率) / 总借款
   **/
  function _getOverallBorrowRate(
    uint256 totalStableDebt,
    uint256 totalVariableDebt,
    uint256 currentVariableBorrowRate,
    uint256 currentAverageStableBorrowRate
  ) internal pure returns (uint256) {
    uint256 totalDebt = totalStableDebt.add(totalVariableDebt);
    if (totalDebt == 0) return 0;
    uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate);
    uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate);
    uint256 overallBorrowRate =
      weightedVariableRate.add(weightedStableRate).rayDiv(totalDebt.wadToRay());
    return overallBorrowRate;
  }
}
为什么要先调用 updateState, 然后再调用 updateInterestRates
简单的说,是因为 liquidityIndex 和 variableBorrowIndex 是由 currentLiquidityRate 和 currentVariableBorrowRate 计算出来的,而先调用 updateState, 然后再调用 updateInterestRates,就是说,在时刻 t1, 当存款或贷款无论变化多少次,liquidityIndex 和 variableBorrowIndex只变化一次; 而 currentLiquidityRate, currentVariableBorrowRate 和 currentStableBorrowRate 会随着金额的变化而变化。
在一个块中,liquidityIndex 和 variableBorrowIndex只变化一次, 这里 AAVE 的代码不如 Compound, 如果是同一个块,多了很多无用的运算;
就是说,你存款后,下一个块才开始有利息;同理,贷款也是如此。
定息贷款的 averageStableBorrowRate
averageStableBorrowRate 对于定息贷款很重要, 影响到 资金使用率,进而影响到 存款利率。
averageStableBorrowRate 在 StableDebtToken.sol 合约中定义,在 mint 和 burn 是改变该值。
mint:
    // _calculateBalanceIncrease 返回值: 贷款本金,本息合计,利息
    (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);
    // 用户新的平均贷款利率 = 某种加权平均???
    vars.newStableRate = _usersStableRate[onBehalfOf]
      .rayMul(currentBalance.wadToRay())
      .add(vars.amountInRay.rayMul(rate))  // rate 是入参, 值为 reserve.currentStableBorrowRate
      .rayDiv(currentBalance.add(amount).wadToRay());
    require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW);
    // 每个用户都有一个_usersStableRate, 保存用户贷款时的利率
    _usersStableRate[onBehalfOf] = vars.newStableRate;
    //solium-disable-next-line
    _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp);
    // 该 token 总的定息贷款平均利率, 与上面的更新逻辑一致
    // Calculates the updated average stable rate
    vars.currentAvgStableRate = _avgStableRate = vars
      .currentAvgStableRate
      .rayMul(vars.previousSupply.wadToRay())
      .add(rate.rayMul(vars.amountInRay))
      .rayDiv(vars.nextSupply.wadToRay());
burn:
    if (previousSupply <= amount) {
      // 当定息贷款被还完时,清零
      // 由于可能存在计算误差, 这里不能使用 previousSupply - amount
      _avgStableRate = 0;
      _totalSupply = 0;
    } else {
      nextSupply = _totalSupply = previousSupply.sub(amount);
      uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay());
      uint256 secondTerm = userStableRate.rayMul(amount.wadToRay());
      // For the same reason described above, when the last user is repaying it might
      // happen that user rate * user balance > avg rate * total supply. In that case,
      // we simply set the avg rate to 0
      if (secondTerm >= firstTerm) {
        newAvgStableRate = _avgStableRate = _totalSupply = 0;
      } else {
        newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay());
      }
    }
由于定息贷款用户的利息和平台的利息是独立计算的,感觉这里两个数据会存在不一致。