存款

首先我们假设小明在aave里面存了10000usdt,存的时候年化收益率是5%,那么半年后其存款的利息是多少呢?常规的计算方式如下:

利息=10000*5%*(存款的时长/一年的时长)

这么做有什么问题呢?假设现在的存款利率有变化,存的时候是5%,过了两个月变成了6%,那么现在到了第6个月用户提款的时候其利息就会变为:

利息=10000*0.05*(2/12)+10000*0.06*(4/12)

如果利率多变动几次,分步计算的数量会更多,并且需要把用户存款的时间点,以及每次利率变化的时间点和当时对应的利率记录下来,这样才能完整的计算利息。

aave的利率本身是动态的,每次有人存款,取款,借贷,还款都会触发利率的变化,如果把这些都记录下来的话对于智能合约来说是个很大的负担,不仅耗费存储量空间,在最终结息时候的计算量也是巨大的,随着合约的运行,其gas费用会越来越高!

liquidityIndex

aave使用了引入流动性指数这个参数来解决这个问题,aave中存储的每一种资产都有一个liquidityIndex的变量记录储备资产的累积流动性增长率。

一个用户最终的结息金额和三个条件相关:本金,储蓄时间,利率。实际上我们的期望是不管我存了多长时间,结息的时候乘以存款的最终利率是就好,打个比方在利率不变的情况下,年化利率5%,5%/12就是每个月的利率,存半年的最终利率就是5%/12*6=2.5%,那如果每个月利率都在变的情况下最终利率怎么算呢,按照上面的举例前面两个月是5%,后面4个月是6%,半年后取款的最终利率是多少(aave对利率计算的时间精确到秒,这里方便说明使用月代替),计算方式如下:

 

把时间作为权重,乘以相应的利率在累加就是最后的利率。结息的时候用本金乘以最终的利率就可以得到最后的利息金额。

这意味着每一种资产只需要记录3个值,当前的利率(currentLiquidityRate);上一次更新利率的时间戳(lastUpdateTimestamp);从存款开始到上一次更新利率所累计的总利率(liquidityIndex

liquidityIndex的初始值为1,代表尚未产生任何利润,lastUpdateTimestamp就是存款发生的时间,随着时间的推移,liquidityIndex的值会逐渐变大,因为每时每刻都在累积利息,但其并不会被更新,直到发生了利率变化,比如当前资产发生了存取,借贷,资金池的金额发生变化就会促使利率发生变化。这个时候就会发生liquidityIndex的累加,还是用上面的例子,前面两个月利率是5%:

后面4个月利率是6%则:

liquidityIndex=1.0083+\frac{0.06}{12}\times 4=1.0283

如果此时取款,直接用本金乘以liquidityIndex就可以得到本息的金额。

我们进一步思考一个问题,如果每次利率改变的时候都要计算复利呢,比如上面的例子,0.83%的利润需要带入到下一轮作为本金计算利息,该如何做呢?

其实只需要把上面的公司稍微改变一下即可

后面4个月利率是6%则后面4个月对于上一轮本金的增长率为
LiquidityInterest=1+\frac{0.06}{12}\times 4=1.02
那么6个月后总的利率就是

liquidityIndex=1.0083\times1.02=1.028466

比之前略多,实际上aave就是采用复利的方式计算liquidityIndex

scaledBalance

细心的同学此时可能会有个疑问,上面的例子阐述了一种资产一个用户进行储蓄时的利息计算,如果有多个用户在不同的时间点存入金额,只用一个公共的liquidityIndex,如何精准计算每个人的利息呢?这就涉及到scaledBalance参数的应用,前面说到影响最终利息的三大要素本金,储蓄时间,利率,scaledBalance这个参数就是本金的变种,每个用户都会存储一份。接着上面的示例,假设现在的liquidityIndex已经从最初的1变成了1.01,那么是不是我存入10000立马就能取出10100出来呢,显然是太美好的想象,实际上每个用户存入资金的时候都会根据当前的liquidityIndex对本金进行换算,得到scaledBalance。比如此时的liquidityIndex为1.01,存入10000元,换算出的scaledBalance为10000/1.01=9900.99,真正计算本息的时候是用scaledBalance*liquidityIndex,所以存入立刻取出是不可能有任何利润的。

最后我们用示例总结下上面的内容:

场景:三个用户在不同时间存款
时间线

T0: liquidityIndex = 1.0, Alice 存入 10,000 USDC
T6: liquidityIndex = 1.025, Bob 存入 5,000 USDC  
T12: liquidityIndex = 1.051, Charlie 存入 8,000 USDC
T18: liquidityIndex = 1.078, 查询所有用户余额

计算过程

Alice:
- scaledBalance = 10,000 ÷ 1.0 = 10,000
- 最终余额 = 10,000 × 1.078 = 10,780 USDC
- 利息收入 = 780 USDC

Bob:
- scaledBalance = 5,000 ÷ 1.025 = 4,878.05
- 最终余额 = 4,878.05 × 1.078 = 5,258.54 USDC
- 利息收入 = 258.54 USDC

Charlie:
- scaledBalance = 8,000 ÷ 1.051 = 7,611.80
- 最终余额 = 7,611.80 × 1.078 = 8,205.52 USDC
- 利息收入 = 205.52 USD

借款

有了存款利率的原理做基础,理解借款利率的原理就很容易了,相应的借款最终利息的三要素借款金额,利率,时间,借款也有自己的变量存储随时间不断变大的累计利率variableBorrowIndex和经过转换后的借款金额scaledDebt,基本思想和存款基本一致!

总还款金额=variableBorrowIndex*scaledDebt

但是这里我们需要思考一个问题,在存款中每一次更新利率的时候都会对liquidityIndex都会进行复利计算,也就是

liquidityIndex =currLiquidityIndex * calculateLinearInterest

但是这样的计算方式整的足够精确吗?比如我的本金是10000,年化收益率是5%,3天后由于利率的变化,我们进行了一次累积利率的计算

calculateLinearInterest=1+3*5%/365

但实际上我的10000存款并不是第三天利率变化的时候才产生利息的,其每一秒都在不断地产生利息,然后不断的利滚利才对,正确的做法应该是加上一天的有n秒
calculateLinearInterest=(1+0.05\times \frac{3\times n }{365 \times n })^{3 \times n}

这就是variableBorrowIndex 借款累计利率的计算方式,其精确程度比存款的要高得多,这么做得结果就是用户存款获取得利润略少,而借款需要付出得利息是按照秒级利滚利的,怎么说呢,符合银行的盈利模式。

合约代码分析

有了前面的数学推导做铺垫,我们进一步分析合约代码,对于非程序员的同学了解其数学原理已经足够,后面可以跳过!

存款利息

function _updateIndexes(
    DataTypes.ReserveData storage reserve,
    DataTypes.ReserveCache memory reserveCache
  ) internal {
    // Only cumulating on the supply side if there is any income being produced
    // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0),
    // as liquidity index should not be updated
    if (reserveCache.currLiquidityRate != 0) {
      uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
        reserveCache.currLiquidityRate,
        reserveCache.reserveLastUpdateTimestamp
      );
      reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(
        reserveCache.currLiquidityIndex
      );
      reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128();
    }

    // Variable borrow index only gets updated if there is any variable debt.
    // reserveCache.currVariableBorrowRate != 0 is not a correct validation,
    // because a positive base variable rate can be stored on
    // reserveCache.currVariableBorrowRate, but the index should not increase
    if (reserveCache.currScaledVariableDebt != 0) {
      uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(
        reserveCache.currVariableBorrowRate,
        reserveCache.reserveLastUpdateTimestamp
      );
      reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(
        reserveCache.currVariableBorrowIndex
      );
      reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128();
    }
  }

这段代码是 Aave V3 协议中负责更新储备金索引的核心方法。分为两个部分,上面是计算存款利率,下面是计算借款利率,首先是非0判断,当该资产池没有任何借贷时,利率为0,此时无需更新利率。

第一步是通过calculateLinearInterest计算从上次利率更新的时间点到现在所产生的利率累积。

  uint256 internal constant RAY = 1e27;  
  function calculateLinearInterest(
    uint256 rate,
    uint40 lastUpdateTimestamp
  ) internal view returns (uint256) {
    //solium-disable-next-line
    uint256 result = rate * (block.timestamp - uint256(lastUpdateTimestamp));
    unchecked {
      result = result / SECONDS_PER_YEAR;
    }

    return WadRayMath.RAY + result;
  }

aave对于利率时间的处理精确到了秒,利率的精度为27位小数,上面的代码翻译成公式就是

当前累计利率=1 +(从上次更新利率到现在的秒数 / 一年的总秒数)* 当前的利率

第二部分就是计算总的liquidityIndex

  function wadMul(uint256 a, uint256 b) internal pure returns (uint256 c) {
    // to avoid overflow, a <= (type(uint256).max - HALF_WAD) / b
    assembly {
      if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_WAD), b))))) {
        revert(0, 0)
      }

      c := div(add(mul(a, b), HALF_WAD), WAD)
    }
  }

翻译成公式就是

liquidityIndex = \frac{ currLiquidityIndex \times calculateLinearInterest+0.5 \times10^{27}}{10^{27}}

按照之前的推导currLiquidityIndex和culateLinearInterest直接相乘就能得到总的li利率累积,先加上0.5 \times10^{27}再除以10^{27}是出于四舍五入的考虑

假设a*b=3.456 如果保留两位小数会直接把最后一位舍弃变成3.45,如果想要达到四舍五入的效果可以现在最后一位加上五也就是3.456+0.005=3.461,然后舍弃最后一位变成3.46。

借款利息

根据前面的公式推导可以看出,借款利息和存款利息的计算方式有些许不同,存款利息是每次利率变动的时候进行复利计算,而借款利息则会计算每一秒的滚动利息。

  function calculateCompoundedInterest(
    uint256 rate,
    uint40 lastUpdateTimestamp,
    uint256 currentTimestamp
  ) internal pure returns (uint256) {
    //solium-disable-next-line
    uint256 exp = currentTimestamp - uint256(lastUpdateTimestamp);

    if (exp == 0) {
      return WadRayMath.RAY;
    }

    unchecked {
      // this can't overflow because rate is always fits in 128 bits and exp always fits in 40 bits
      uint256 x = (rate * exp) / SECONDS_PER_YEAR;

      return WadRayMath.RAY + x + x.rayMul(x / 2 + x.rayMul(x / 6));
    }
  }

rate 是当前的利率;exp为时间差,单位是秒;SECONDS_PER_YEAR是一年的秒数
理想状态下计算的是

(1+\frac{rate\times exp}{secondPerYear})^{rate}

由于区块链中无法直接计算指数函数(1+r)^{t},这里只能计算近似值,使用泰勒展开到三阶
(1+r)^{t}\approx 1+rt+\frac{rt^{2}}{2!}+\frac{rt^{3}}{3!}

这个公式懂的自然懂,不懂的自己去复习下微积分!!!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐