aave v3 用户借贷详情计算(calculateUserAccountData)合约代码解析

calculateUserAccountData 是Aave V3协议中计算用户账户数据的核心方法,在整个借贷过程中,健康因子监控,借款限额,健康因子监控等多个环节都会涉及到。

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) {
  if (params.userConfig.isEmpty()) {
    return (0, 0, 0, 0, type(uint256).max, false);
  }
  CalculateUserAccountDataVars memory vars;
  if (params.userEModeCategory != 0) {
    vars.eModeLtv = eModeCategories[params.userEModeCategory].ltv;
    vars.eModeLiqThreshold = eModeCategories[params.userEModeCategory].liquidationThreshold;
    vars.eModeCollateralBitmap = eModeCategories[params.userEModeCategory].collateralBitmap;
  }
  uint256 userConfigCache = params.userConfig.data;
  bool isBorrowed = false;
  bool isEnabledAsCollateral = false;
  while (userConfigCache != 0) {
    (userConfigCache, isBorrowed, isEnabledAsCollateral) = UserConfiguration.getNextFlags(
      userConfigCache
    );
    if (isEnabledAsCollateral || isBorrowed) {
      vars.currentReserveAddress = reservesList[vars.i];
      if (vars.currentReserveAddress != address(0)) {
        DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress];
        (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve
          .configuration
          .getParams();
        unchecked {
          vars.assetUnit = 10 ** vars.decimals;
        }
        vars.assetPrice = IPriceOracleGetter(params.oracle).getAssetPrice(
          vars.currentReserveAddress
        );
        if (vars.liquidationThreshold != 0 && isEnabledAsCollateral) {
          vars.userBalanceInBaseCurrency = _getUserBalanceInBaseCurrency(
            params.user,
            currentReserve,
            vars.assetPrice,
            vars.assetUnit
          );
          vars.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency;
          vars.isInEModeCategory =
            params.userEModeCategory != 0 &&
            EModeConfiguration.isReserveEnabledOnBitmap(vars.eModeCollateralBitmap, vars.i);
          if (vars.ltv != 0) {
            vars.avgLtv +=
              vars.userBalanceInBaseCurrency *
              (vars.isInEModeCategory ? vars.eModeLtv : vars.ltv);
          } else {
            vars.hasZeroLtvCollateral = true;
          }
          vars.avgLiquidationThreshold +=
            vars.userBalanceInBaseCurrency *
            (vars.isInEModeCategory ? vars.eModeLiqThreshold : vars.liquidationThreshold);
        }
        if (isBorrowed) {
          vars.totalDebtInBaseCurrency += _getUserDebtInBaseCurrency(
            params.user,
            currentReserve,
            vars.assetPrice,
            vars.assetUnit
          );
        }
      }
    }
    unchecked {
      ++vars.i;
    }
  }
  // @note At this point, `avgLiquidationThreshold` represents
  // `SUM(collateral_base_value_i * liquidation_threshold_i)` for all collateral assets.
  // It has 8 decimals (base currency) + 2 decimals (percentage) = 10 decimals.
  // healthFactor has 18 decimals
  // healthFactor = (avgLiquidationThreshold * WAD / totalDebtInBaseCurrency) / 100_00
  // 18 decimals = (10 decimals * 18 decimals / 8 decimals) / 2 decimals = 18 decimals
  vars.healthFactor = (vars.totalDebtInBaseCurrency == 0)
    ? type(uint256).max
    : vars.avgLiquidationThreshold.wadDiv(vars.totalDebtInBaseCurrency) / 100_00;
  unchecked {
    vars.avgLtv = vars.totalCollateralInBaseCurrency != 0
      ? vars.avgLtv / vars.totalCollateralInBaseCurrency
      : 0;
    vars.avgLiquidationThreshold = vars.totalCollateralInBaseCurrency != 0
      ? vars.avgLiquidationThreshold / vars.totalCollateralInBaseCurrency
      : 0;
  }
  return (
    vars.totalCollateralInBaseCurrency,
    vars.totalDebtInBaseCurrency,
    vars.avgLtv,
    vars.avgLiquidationThreshold,
    vars.healthFactor,
    vars.hasZeroLtvCollateral
  );
}

该方法一共有6个返回值:

  • totalCollateralInBaseCurrency : 用户所有抵押品的总价值(美元)
  • totalDebtInBaseCurrency : 用户所有债务的总价值(美元)
  • avgLtv : 加权平均LTV
  • avgLiquidationThreshold : 加权平均清算阈值
  • healthFactor : 健康因子
  • hasZeroLtvCollateral : 是否有LTV为0的抵押品

首先,我们稍微回忆一下健康因子(Health Factor)和贷款价值比(LTV)基础概念:

抵押品 :1 ETH,当前价格 = $2,000
债务 :1,000 USDC(稳定币)
假设ETH的LTV = 80%,则:

最大可借金额 = 抵押品价值 × LTV= $2,000 × 80% = $1,600

用户当前借款 :1,000 USDC($1,000)

剩余借款额度 :$1,600 - $1,000 = $600

这意味着用户最多还能借$600的USDC。

假设ETH的清算阈值 = 85%,则

健康因子 = (抵押品价值 × 清算阈值) / 债务价值= (2000 × 85%) / 1000= 1700 / 1000 = 1.7

HF = 1.7 > 1.0 此时为安全状态,当HF < 1.0时会被清算

1. 空配置检查

if (params.userConfig.isEmpty()) {
    return (0, 0, 0, 0, type(uint256).max, false);
}

如果用户没有配置任何资产,直接返回默认值(健康因子为最大值)。

2. 效率模式(E-Mode)配置

if (params.userEModeCategory != 0) {
  vars.eModeLtv = eModeCategories[params.userEModeCategory].ltv;
  vars.eModeLiqThreshold = eModeCategories[params.userEModeCategory].liquidationThreshold;
  vars.eModeCollateralBitmap = eModeCategories[params.userEModeCategory].collateralBitmap;
}

如果用户启用了效率模式,获取对应的LTV、清算阈值和抵押品位图。

比如:userEModeCategory 为1,代表稳定币模式,则得到如下返回:

  • ltv = 90% (比普通模式更高)
  • liquidationThreshold = 93% (比普通模式更高)
  • collateralBitmap 代表在特定效率模式(E-Mode)类别中,哪些资产可以作为抵押品,比如userEModeCategory 为1时,返回0b101,允许USDC(位0)和USDT(位2)作为抵押品

3. 遍历用户配置

接下来是一个大循环,通过位运算遍历用户配置中的所有资产,一个个检查当前资产是否被用户作为抵押品或者被借款!

while (userConfigCache != 0) {
    (userConfigCache, isBorrowed, isEnabledAsCollateral) = UserConfiguration.getNextFlags(userConfigCache);
    // 处理每个资产
}

我们看下getNextFlags的处理

function getNextFlags(uint256 data) internal pure returns (uint256, bool, bool) {
  bool isBorrowed = data & 1 == 1;
  bool isEnabledAsCollateral = data & 2 == 2;
  return (data >> 2, isBorrowed, isEnabledAsCollateral);
}

userConfigCache:前面说过这是一个 256 位的位图,用于高效存储用户在所有储备资产上的状态,每个资产占用 2 位 第 1 位表示用户是否借贷该资产;第 2 位表示用户是否将该资产用作抵押品,这里返回将输入位图右移2位后的结果,移除了当前处理的资产位对(2位),指向下一个资产的位对
isBorrowed:指示当前资产是否被借入
isEnabledAsCollateral:指示当前资产是否被用作抵押品

下面这部分是抵押品计算

if (vars.liquidationThreshold != 0 && isEnabledAsCollateral) {
    vars.userBalanceInBaseCurrency = _getUserBalanceInBaseCurrency(...);
    vars.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency;
    
    // 效率模式检查
    vars.isInEModeCategory = params.userEModeCategory != 0 && 
        EModeConfiguration.isReserveEnabledOnBitmap(vars.eModeCollateralBitmap, vars.i);
    
    // LTV计算
    if (vars.ltv != 0) {
        vars.avgLtv += vars.userBalanceInBaseCurrency * 
            (vars.isInEModeCategory ? vars.eModeLtv : vars.ltv);
    } else {
        vars.hasZeroLtvCollateral = true;
    }
    
    // 清算阈值计算
    vars.avgLiquidationThreshold += vars.userBalanceInBaseCurrency * 
        (vars.isInEModeCategory ? vars.eModeLiqThreshold : vars.liquidationThreshold);
}

liquidationThreshold 是清算阈值;当用户的健康因子低于此阈值时,头寸会被清算,通常比LTV高,为清算提供缓冲空间。

当清算阈值不为0并且当前用户抵押了该资产

第一步是计算vars.totalCollateralInBaseCurrency 也就是用户抵押的当前资产值多少钱(一般以美元为基础),为我们看一下_getUserBalanceInBaseCurrency这个方法:

function _getUserBalanceInBaseCurrency(
  address user,
  DataTypes.ReserveData storage reserve,
  uint256 assetPrice,
  uint256 assetUnit
) private view returns (uint256) {
  uint256 balance = (
    IScaledBalanceToken(reserve.aTokenAddress).scaledBalanceOf(user).getATokenBalance(
      reserve.getNormalizedIncome()
    )
  ) * assetPrice;
  unchecked {
    return balance / assetUnit;
  }
}

总共分三步:

  1. 获取用户的aToken余额 - 通过 IScaledBalanceToken(reserve.aTokenAddress).scaledBalanceOf(user) 获取用户的缩放余额
  2. 转换为实际余额 - 使用 getATokenBalance(reserve.getNormalizedIncome()) 将缩放余额转换为实际余额(reserve.getNormalizedIncome()返回的是当前资产的liquidityIndex) ,由aave v3 存款与借款利息的计算方式 这篇文章可知二者相乘得到的是累加利息后的余额。
  3. 转换为基础货币价值 - 将余额乘以资产价格assetPrice(assetPrice是从预言机中获取的当前资产价格),然后除以资产单位,得到基础货币价值。

vars.totalCollateralInBaseCurrency 初始值是0,循环的过程中将用户所有的抵押资产的价值累加到其上。

vars.isInEModeCategory代表当前资产是否属于用户设置的效率模式,打个比方:
用户启用"稳定币EMode"(category=1),当前资产是ETH,则返回false,因为ETH不属于稳定币,如果当前资产是USTD则返回true

vars.avgLtv 是当前用户在aave中所有资产的平均LTV,在循环内部累加用户在当前资产上抵押的金额和当前资产LTV的乘积,也就是

 vars.avgLtv += vars.userBalanceInBaseCurrency * (vars.isInEModeCategory ? vars.eModeLtv : vars.ltv);

如果当前资产属于当前的效率模式,那么乘以当前效率的LTV否则乘以资产的LTV

如果LTV为0,表示该资产不能作为抵押品进行借贷那么设置vars.isInEModeCategory为1
vars.isInEModeCategory标着当前用户是否有抵押了已经不能被抵押的资产。

vars.avgLiquidationThreshold代表当前用户在aave中所有资产的平均清算阈值,计算方法和avgLtv 一致,在循环中只做累加

vars.avgLiquidationThreshold +=vars.userBalanceInBaseCurrency *
  (vars.isInEModeCategory ? vars.eModeLiqThreshold : vars.liquidationThreshold);

vars.totalDebtInBaseCurrency是累计用户在所有资产上的借款,换算成美金为单位,其计算过程和_getUserBalanceInBaseCurrency方法完全一致,这里不再赘述。

vars.healthFactor: 是当前用户所有资产总和的健康因子

vars.healthFactor = (vars.totalDebtInBaseCurrency == 0)
  ? type(uint256).max
  : vars.avgLiquidationThreshold.wadDiv(vars.totalDebtInBaseCurrency) / 100_00;

如果vars.totalDebtInBaseCurrency == 0代表当前用户没有任何债务,健康因子设为 type(uint256).max (最大值),表示用户账户处于绝对安全状态,没有清算风险。否则,使用清算阈值除以用户所有的负债得出健康因子。举例:

  • 用户抵押$10,000 ETH,借款$5,000
  • 加权平均清算阈值 = $10,000 × 80% = $8,000
  • 健康因子 = $8,000 ÷ $5,000 = 1.6

由于此时的健康因子大于1,代表当前的用户状态是安全的,小于是危险状态,有可能会被清算。

vars.avgLtv = vars.totalCollateralInBaseCurrency != 0
  ? vars.avgLtv / vars.totalCollateralInBaseCurrency
  : 0;
vars.avgLiquidationThreshold = vars.totalCollateralInBaseCurrency != 0
  ? vars.avgLiquidationThreshold / vars.totalCollateralInBaseCurrency
  : 0;

在循环的过程中avgLtv 累加了每个资产乘以各自LTV后的金额;avgLiquidationThreshold 累加了每个资产乘以各自清算阈值后的金额。而这两个只应该是百分比;

  • LTV : 贷款价值比,比如 75% 意味着价值 $100 的抵押品最多能借 $75
  •  清算阈值: 触发清算的抵押品价值阈值,比如 80%,当抵押品价值跌到借款额的 80% 时触发清算

所以除以用户的总抵押金额,获取当前用户总资产的LTV和清算阈值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值