中文
铁叔

天地不仁 以万物为刍狗


  • 首页

  • 归档

  • 关于我

  • 公益404

  • 搜索

AAVE源代码分析 -- AAVE 利率代码分析

时间: 2021-10-20   |   分类: Defi   AAVE   | 字数: 3078 字 | 阅读: 7分钟 | 阅读次数:

AAVE 利率模型中,几个参数之间互相影响,关系错综复杂,其中,固定利率比较绕,尤其是计算平均固定利率的公式,尤其难懂。不过,在实际的借贷中,有很多token不支持固定利率借贷,而且,在支持固定利率借贷token中,使用固定利率的借贷比例也很小,几乎不到1%,因此,对于固定利率部分,对于初学者来说可以先跳过。

在 AAVE 的利率计算中, 有几个地方值得说明:

  1. AAVE 的利率更新是基于时间戳来计算利息的增加, Compound 是根据块数来计算利息的增加;两者触发利率更新的方式相同,都是由存款,取款,借款,还款等几个动作触发,每个块只计算一次;
  2. 存款利率的增加是线性的,贷款利率的增加是复利,指数级;
  3. 贷款收益的 reserve factor% (默认10%) 纳入平台金库;
  4. aToken, debtToken 中的 balanceOf 方法,返回的 amount 都是存款/贷款对应的 token 数量;

如果你只需要知道结论,AAVE的几个核心流程可以简单的总结如下(代码在 LendingPool 合约中):

存款 Deposit

  1. 校验数据(ValidationLogic.validateDeposit)
  2. 更新状态: updateState
  3. 更新利率: updateInterestRates
  4. 将 token 从用户转入 LendingPool 合约
  5. mint aToken给用户: aToken amount = token amount / liquidityIndex
  6. emit Deposit事件

取款 Withdraw

  1. 查询用户token资产数量,注意: 是token资产数量, 而不是 aToken 数量. 虽然这里调用的是 aToken.balanceOf(), 看上去好像是 aToken 的余额,但事件上,这个函数会计算当前块 aToken 对应的 token 的数量;
  2. 校验数据(ValidationLogic.validateWithdraw)
  3. 更新状态: updateState
  4. 更新利率: updateInterestRates
  5. 销毁用户的aToken,将对应的token转给用户
  6. emit Withdraw 事件

借款 Borrow

  1. 通过价格预言机, 计算出借款对应的 ETH 价值 amountInETH (AAVE中所有的token都要换算为对应的ETH价值)
  2. 校验数据(ValidationLogic.validateBorrow)
  3. 更新状态: updateState
  4. 至关重要的一步:mint 对应的 debtToken, AAVE 的债务是通过 debtToken 来记录的,每个token 都会有对应的 stableDebtToken, variableDebtToken, 分别对应于定息借款和活息借款
  5. 更新利率: updateInterestRates
  6. 将用户借出的token转给用户
  7. emit Borrow 事件

还款 Repay

  1. 查询定息借款和活息借款额度, 都是贷款 debtToken 对应的 token 的数量;
  2. 校验数据 (ValidationLogic.validateRepay)
  3. 更新状态: updateState
  4. 销毁 debtToken. 这里有一个很关键、很重要的细节,就是还款额度的计算: paybackAmount, 必须要非常仔细, 一旦出错, 就会造成重大损失
  5. 更新利率: updateInterestRates
  6. 将用户的 token 转入合约,完成还款
  7. 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 的总数量;

  1. 更新存款利率指数(ReserveData.liquidityIndex)和活息贷款利率指数(ReserveData.variableBorrowIndex), 通过 _updateIndexes 方法
  2. 将产生的增量贷款利息的一部分(reserve factor) 存入小金库, 具体实现的方式是 mint Atoken 给 treasy 地址

updateInterestRates

updateInterestRates 主要作用: 0. 更新 currentLiquidityRate;

  1. 更新 currentStableBorrowRate
  2. 更新 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());
      }
    }

由于定息贷款用户的利息和平台的利息是独立计算的,感觉这里两个数据会存在不一致。

#AAVE# #Compound# #Defi# #Solidity# #利率模型#

声明:AAVE源代码分析 -- AAVE 利率代码分析

链接:https://guotie.github.io/post/aave/aave-interest-update-code/

作者:铁叔

声明: 本博客文章除特别声明外,均采用 CC BY-NC-SA 3.0许可协议,转载请注明出处!

创作实属不易,如有帮助,那就打赏博主些许茶钱吧 ^_^
WeChat Pay

微信打赏

Alipay

支付宝打赏

AAVE源代码分析 -- AAVE 闪电贷
下一代撮合引擎 -- 基于消息驱动的并行撮合引擎
铁叔

铁叔

千里之行 始于足下

25 日志
14 分类
56 标签
GitHub twitter telegram email medium
标签云
  • Solidity
  • Defi
  • Aave
  • Compound
  • Abi
  • Dapp
  • Ethereum
  • Evm
  • Lend protocol
  • Lending
© 2010 - 2024 铁叔
Powered by - Hugo v0.119.0 / Theme by - NexT
/
0%