aave v3分析

aave v3 去中心化借贷

aave是去中心化Defi协议,用户可以在上面存款、贷款、闪电贷等。主要是依靠算法保障系统运行。官网https://aave.com
下面是以v3版本进行分析

1、存款

以在银行存款进行举例,我们将现金存入银行账户,银行会给我们一个存储凭证。同样我们将资金存到aave协议中,aave会给我们1:1 mint aToken做为凭证。

1.1、存款流程

存款流程整体分两步

  • 将用户的资产转移到aave中
  • aave给用户 mint aToken

在SupplyLogic.executeSupply中,部分代码如下

    // 1. 将资金从user转给aave的aToken中
    IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, params.amount);

    // 2. 给用户1:1的mint aToken
    bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(
      msg.sender,
      params.onBehalfOf,
      params.amount,
      reserveCache.nextLiquidityIndex
    );

1.2、存款利率

在银行存款都是有利息的,利率是固定的比如:1年定期2%、3年定期2.5%等。在aave中存款的利率是和资金使用率相关的,也就是与资金池中的钱被借出去多少有关,借出去的越多,贷款利息越高,对应的存储利息越高。所以存款利率是由资金使用率借款利率决定的。
下图是借款利率曲线图,分两个阶段。以资金最佳使用率80%做为分界线(80%的存款都借出去了),最高借款利率达79%。

1.2.1 借款利率计算

以黑色线为准计算
资金使用率为 40% 小于 80% 在第一阶段 利率 = 基本利率 + $$\frac{40\%}{80\%}$$ 4%
资金使用率为 90% 大于 80% 在第二阶段 利率 = 基本利率 + 4% + $$\frac{90\% - 80\%}{20\%}$$
75%

image.png 图1

1.2.2 存款利率计算

存款利率 = 流动性利率 = 资金使用率 借款利率 = $$\frac{借出去的钱}{总存储的钱}$$ $$\frac{借出去的钱产生的利息}{借出去的钱}$$ = $$\frac{借出去的钱产生的利息}{总存储的钱}$$
它的含义就是每存入一份钱,可产生的收益。

在DefaultReserveInterestRateStrategy.calculateInterestRates中,部分计算存款利率代码如下:

    vars.currentLiquidityRate = _getOverallBorrowRate(  // 借款利率
      params.totalStableDebt,
      params.totalVariableDebt,
      vars.currentVariableBorrowRate,
      params.averageStableBorrowRate
    ).rayMul(vars.supplyUsageRatio).percentMul(         // 资金使用率
        // 预留金,应对客户提款和其他风险,比如最多90%的资金使用率
        PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor
      );

1.3、计算用户余额

用户余额 = 用户存入的钱 + 产生的利息

image.png 图2

下面举一个例子,比如要获取 t2 时刻的用户余额

1.3.1 v1版本

b2 = m + $$\frac{mr}{1 year seconds}$$(t2 - t1) = m[1 +$$\frac{r}{1 year seconds}$$ (t2 - t1) ]
可以看出在 t2 时刻的余额等于 m 乘以一个收益率,这个收益率只与 t1 时刻的利率和时间间隔有关。
为了方便计算,aave使用累计流动性指数字段来保存这个系数的乘积,在每次更新合约状态时都会累计。
在 t1 时刻,累计流动性指数 = q = x1 x2 在 t3 时刻,累计流动性指数 = p = x1 x2 x3 那么$$\frac{p}{q}$$ 等于 $$\frac{x1 x2 x3 }{x1 x2}$$,x3就是上面m乘以的收益率
在 t3 时刻,用户余额 b3 = m * $$\frac{p}{q}$$

这种方式需要合约记录用户在 t1 时刻的累计流动性指数所以需要一个 map<address,int256>结构,这也是 aaveV1 版本的计算存储方式 v1 AToken 代码如下

// 用户存款时的累计指数
mapping (address => uint256) private userIndexes;

function calculateCumulatedBalanceInternal(
    address _user,
    uint256 _balance
) internal view returns (uint256) {
    return _balance  // 上次操作后的余额
        .wadToRay()
        .rayMul(core.getReserveNormalizedIncome(underlyingAssetAddress)) // 计算自从上次操作到现在的收益率
        .rayDiv(userIndexes[_user])  // 除以之前累计的指数
        .rayToWad();
}

1.3.2 v3版本

观察上面的 b3 = m$$\frac{p}{q}$$,可以在简化下,用户在 t1 时刻存入 m 资金,这时 q 是已知的。那直接存储$$\frac{m}{q}$$的值。在 t3 时刻计算用户余额时直接乘以 p 就可以了。
b3 = $$\frac{m}{q}$$
p
这个$$\frac{m}{q}$$也叫流动性缩放余额,这也是在 v2、v3中使用的计算方式,这样做有个好处是可以去掉 userIndexes 的存储降低 gas 费。
aaveV3 代码如下

  // 存储时用缩放余额
  function _mintScaled(
    address caller,
    address onBehalfOf,
    uint256 amount,
    uint256 index
  ) internal returns (bool) {
    // 存储的amount数量除以流动性指数
    uint256 amountScaled = amount.rayDiv(index);
    require(amountScaled != 0,Errors.INVALID_MINT_AMOUNT);
  }

  function balanceOf(
    address user
  ) public view virtual override(IncentivizedERC20,IERC20) returns (uint256) {
    // 计算用户余额时直接乘以当前指数即可,这里就不需要再除以用户的userIndex了
    return super.balanceOf(user).rayMul(POOL.getReserveNormalizedIncome(_underlyingAsset));
  }

2、取款

我们到银行取款拿着凭证输入密码取出现金。同样的在aave中取款时,使用 aToken 做为凭证取回我们存入的资产。
取回资产后资金池的总量发生了变化,导致资金使用率发生变化 --> 贷款利率发生变换 --> 存储利率发生变化。所以需要重新计算贷款利率、存款利率。
贷款利率(根据上图1计算),代码如下:

    // 如果资金使用率大于80%
    if (vars.borrowUsageRatio > OPTIMAL_USAGE_RATIO) {
      uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - OPTIMAL_USAGE_RATIO).rayDiv(
        MAX_EXCESS_USAGE_RATIO
      );
      // 稳定利率
      vars.currentStableBorrowRate +=
        _stableRateSlope1 +
        _stableRateSlope2.rayMul(excessBorrowUsageRatio);
      // 可变利率
      vars.currentVariableBorrowRate +=
        _variableRateSlope1 +
        _variableRateSlope2.rayMul(excessBorrowUsageRatio);
    } else {
      // 资金使用率小于等于80%
      vars.currentStableBorrowRate += _stableRateSlope1.rayMul(vars.borrowUsageRatio).rayDiv(
        OPTIMAL_USAGE_RATIO
      );
      vars.currentVariableBorrowRate += _variableRateSlope1.rayMul(vars.borrowUsageRatio).rayDiv(
        OPTIMAL_USAGE_RATIO
      );
    }

3、借款

在aave中贷款利率分两种,稳定利率和可变利率。
稳定利率: 在借入资产后到归还借款的这段时间都是按照借入时的利率计算,不论中间有多少次存款、借款操作导致资金使用率变化。(如果贷款利率比存款利率还低会被官方强制 rebalance)。

可变利率: 其他用户的存款、借款会导致资金的使用率变化,这就会导致借款利率发生变化,可变利率就是当其他用户改变资金利率用时,以当时的借款利率计算利息累计和。

借款条件:在银行借款需要有一定的条件,比如要有稳定的工作,稳定的居住所,民事偿还能力,这样就可以无需抵押借款。但是在去中心化里它本身就是匿名,隐藏个人信息,所以无法提供信用贷。
在aave中只需要你存储的资产价值大于你要借款的资产价值即可,也就是超额抵押。比如你存储了价值 1000eth 的资产,你只能贷款 800eth 的另一种资产。
这里存在一个问题,既然是超额抵押那就说明了我有超过 800eth 的资产,那我为啥还有贷款呢,直接兑换成想要的货币不就行了。 有几个使用情况:
1、比如我有 1000eth,存入aave赚利息,同时我再借出 800eth 做其他投资赚取额外收益。 2、我有好多币种,存储在aave中赚取利息并且很看好它们的未来趋势不想售卖,但是又急需另一种币度应对短暂问题。

3.1、借款利息计算方式

存款和可变利率借款的存储计算方式一样,都是通过累计指数与缩放余额计算。 但它俩利息的计算方式不同,存款是单利计算,借款是复利计算。
比如在 t1-t2 时间段内,本金 m,利率是 r,最终计算出的余额是不一样的。

3.1.1 单利计算

b = m + $$\frac{mr}{1 year seconds}$$ \ (t2 - t1)

3.1.1 复利计算

可以做个简单推导

利率 R = 5%
本金 b0 = 10000元
复利周期:天
求: 存3天后的的余额

R = 5% b0 = 10000 元

b1= $ \frac{\left(b0\cdot\ R\right)}{365}1 + b0$ => b0$(\frac{R}{365}+1)$

b2= $ \frac{\left(b1\cdot\ R\right)}{365}1 + b1$ => b1$(\frac{R}{365}+1)$

b3= $ \frac{\left(b2\cdot\ R\right)}{365}1 + b2$ => b2$(\frac{R}{365}+1)$

b3 = b0 * $(\frac{R}{365}+1)^{3}$

所以在 t1-t2 时间段内的余额如下 b = m * $(\frac{r}{1 year seconds}+1)^{t2-t1}$ v3 合约代码,在MathUtils.calculateCompoundedInterest中,为节省gas将指数运算简化成如下形式。
(1+x)^n = 1+n*x+[n/2*(n-1)]*x^2+[n/6*(n-1)*(n-2)*x^3...

  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;
    }
    uint256 expMinusOne;
    uint256 expMinusTwo;
    uint256 basePowerTwo;
    uint256 basePowerThree;
    unchecked {
      // n-1
      expMinusOne = exp - 1;
      // n-2
      expMinusTwo = exp > 2 ? exp - 2 : 0;
      // x^2
      basePowerTwo = rate.rayMul(rate) / (SECONDS_PER_YEAR * SECONDS_PER_YEAR);
      // x^3
      basePowerThree = basePowerTwo.rayMul(rate) / SECONDS_PER_YEAR;
    }
    // 第二项
    // n * (n-1) * x^2
    uint256 secondTerm = exp * expMinusOne * basePowerTwo;
    unchecked {
      secondTerm /= 2;
    }
    // 第三项
    // n * (n-1) * (n-2) * x^3
    uint256 thirdTerm = exp * expMinusOne * expMinusTwo * basePowerThree;
    unchecked {
      thirdTerm /= 6;
    }
    // 1+nx+[n/2(n-1)]x^2+[n/6(n-1)*(n-2)*x^3
    return WadRayMath.RAY + (rate * exp) / SECONDS_PER_YEAR + secondTerm + thirdTerm;
  }

计算贷款余额 = 本金(缩放余额) * variableBorrowIndex(借款指数) * calculateCompoundedInterest(xxx)

  // 计算用户借款总额 = 本金 * 利息率
  userTotalDebt = userTotalDebt.rayMul(reserve.getNormalizedDebt());

  // 获取借款利率
  function getNormalizedDebt(
    DataTypes.ReserveData storage reserve
  ) internal view returns (uint256) {
    uint40 timestamp = reserve.lastUpdateTimestamp;

    //solium-disable-next-line
    if (timestamp == block.timestamp) {
      //if the index was updated in the same block, no need to perform any calculation
      return reserve.variableBorrowIndex;
    } else {
      return
        // variableBorrowIndex(借款指数) * calculateCompoundedInterest(xxx)
        MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
          reserve.variableBorrowIndex
        );
    }
  }

3.2、稳定利率

稳定利率计算余额时需要用户初始借款时的利率,无论后面怎么变都以当时的利率计算利息,代码如下:

  function balanceOf(address account) public view virtual override returns (uint256) {
    // 计算上次操作后的本金
    uint256 accountBalance = super.balanceOf(account);
    // 上次操作后的稳定利率
    uint256 stableRate = _userState[account].additionalData;
    if (accountBalance == 0) {
      return 0;
    }
    // 输入参数: 稳定利率,上次时间戳
    // 输出: 该时间段内利息增长指数
    uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest(
      stableRate,
      _timestamps[account]
    );
    // 再乘以本金就是当前余额,注意这里不需要再乘以index指数了,因为稳定利率借款不使用缩放存储。借多少就是多少。
    return accountBalance.rayMul(cumulatedInterest);
  }

3.3 超额抵押

aave协议中借款使用的是超额抵押,比如你想借价值 800eth 的资产,需要存储价值 1000eth 的资产到协议中。在贷款时会遍历所有资金池看用户是否有足够的抵押。代码如下

function calculateUserAccountData(
    mapping(address => DataTypes.ReserveData) storage reservesData,
    mapping(uint256 => address) storage reservesList,
    mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
    DataTypes.CalculateUserAccountDataParams memory params
  ) internal view returns (uint256,uint256,uint256,uint256,uint256,bool) {
    // 遍历所有资产,累计抵押资产
    // ...中间略
    // 健康度计算
    vars.healthFactor = (vars.totalDebtInBaseCurrency == 0)
      ? type(uint256).max
      : (vars.totalCollateralInBaseCurrency.percentMul(vars.avgLiquidationThreshold)).wadDiv(
        vars.totalDebtInBaseCurrency
      );
    return (
      vars.totalCollateralInBaseCurrency,
      vars.totalDebtInBaseCurrency,
      vars.avgLtv,
      vars.avgLiquidationThreshold,
      vars.healthFactor,
      vars.hasZeroLtvCollateral
    );

4、还款

还款之后资金使用率发生变化,需要重新计算借贷利率。

6 隔离资产

被隔离的资产风险系数比较高,或者出现问题时影响比较大。所以在使用隔离资产做抵押贷款时有一些限制,具体如下:

  1. 隔离资产有借入贷款上限(使用该资产最多能借入多少$)
  2. 使用隔离资产 只能借入由官方指定的稳定币
  3. 隔离资产是官方投票得出来的
  4. 进入隔离模式需要只存入一种隔离资产

7 EMode 模式

当抵押品和借入的资产有相关性时,比如 usdt dai usdc 都和美元锚定,如果美元跌了 usdt dai usdc 都会跌。这种情况下在计算健康度时也不会有太大影响 $$\frac{抵押资产价值清算阈值}{借入资产价值}$$ 所以这种情况下可以提升一下Ltv,清算阈值、手续费等,这样可以提升贷款能力。 目前有两种类型 稳定币 *ETH相关

8、清算

8.1、基本逻辑

当用户抵押的资产价值,不足借款价值时(健康度小于1),会触发清算机制。清算人归还借款,然后将借款人的抵押资产转给清算人。

清算计算逻辑如下:
1、存入10eth,借入价值5eth 的dai. 在清算时只能清算50%的dai奖金是清算价值的5% 也就是2.5eth * 5% = 0.125eth 所以清算人一共获得2.5eth + 0.125eth

2、存入5eth和价值5eth的fyi,借入5eth价值的dai。在清算时 归还50%的dai,fyi的清算奖金是15% eth 清算奖金是 5%,所以选用 fyi 的奖金(只能选一种) 2.5eth * 0.15 = 0.375eth 所以清算人一共会得到价值 2.5eth + 0.375eth价值的fyi资产

8.2 LTV

贷款-抵押比,比如要抵押房子去借款房子价值100w,只能借款70w,ltv=70%

8.3 清算阈值

当房子价值跌倒78w时,就要触发清算机制,清算阈值就是78%

8.4 健康度

用来衡量用户当前资金安全度,是否有抵押不足的情况。 健康度 = $$\frac{抵押资产价值*清算阈值}{借入资产价值}$$ 代码如下:

    vars.healthFactor = (vars.totalDebtInBaseCurrency == 0) // 没有借款,健康度最高
      ? type(uint256).max
      : (vars.totalCollateralInBaseCurrency.percentMul(vars.avgLiquidationThreshold)).wadDiv(
        vars.totalDebtInBaseCurrency
      );

9、闪电贷

在一个交易中,完成借款和还款。同时提供一些交易手续费。 用户实现自定义合约,然后用户发送交易到aave合约,将控制权交给aave闪电贷合约,在闪电贷合约中将资金转移给用户,然后再调用户自定义的合约,调用完成后检查状态,将用户贷款转附带利息转给 aave。如果这中间发生异常则终止交易。转回来的收益一部分给官方,一部分做为收益存入流动性池子。还有一种模式在闪电贷结束后,不需要归还而是转为在aave中的借款,这种模式下没有闪电贷的利息。

10、跨链桥

v3版本支持垮链,只有在白名单中的地址可以操作如下步骤 1、铸造aToken,参与存储利息收益 2、将aToken对应的标的资产转到资金池

点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
打野工程师
打野工程师
江湖只有他的大名,没有他的介绍。