calculateUserAccountData

This function calculates and returns the following user data across all the assets:

  • totalCollateralInBaseCurrency

  • totalDebtInBaseCurrency

  • avgLTV

  • avgLiquidationThreshold

  • healthFactor

  • hasZeroLtvCollateral

Visual Aid

check if userConfig is empty

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

If all the bits in UserConfiguration is set to 0, data == 0 will evaluate to be true. This indicates that the user did not undertake any borrow or supply as collateral action.

  • returns health factor as type(uint256).max

  • returns hasZeroLTVCollateral as false

If user is in e-mode

We need to obtain e-mode specific info such as LTV, liquidationThreshold and eModeAssetPrice.

if (params.userEModeCategory != 0) {
  (vars.eModeLtv, vars.eModeLiqThreshold, vars.eModeAssetPrice) = EModeLogic
    .getEModeConfiguration(
      eModeCategories[params.userEModeCategory],
      IPriceOracleGetter(params.oracle)
    );
}

Get e-mode configuration details, by passing the user's eModeCategory id (params.userEModeCategory) into the mapping eModeCategories. This will return the EModeCategory struct.

params.userEModeCategory was obtained previously by passing _usersEModeCategory[msg.sender]

The if statement exists purely to check if a priceSource was defined in EModeCategory. If no priceSource was defined, params.oracle is returned as the oracle address.

Else params.oracle, is overwritten with the category.priceSource.

While loop

while (vars.i < params.reservesCount) {...}

Loops through all the active reserves there are in the protocol. reservesCount is defined on PoolStorage.sol

PoolStorage.sol
 // Maximum number of active reserves there have been in the protocol. It is the upper bound of the reserves list
  uint16 internal _reservesCount;

For each asset, the following is executed.

1. Check if asset isUsingAsCollateralOrBorrowing

If the asset is not being used as either, increment the counter and continue; skip the remaining block of code and moving to the next reserveIndex.

isUsingAsCollateralOrBorrowing

  • require statement performs a boundary check to ensure that reserveIndex value is within the valid range of [0 - 127].

  • If you are unclear on the bitmap manipulations, please see that section.

2. Check if zero address

// reservesList: List of reserves as a map (reserveId => reserve).
        vars.currentReserveAddress = reservesList[vars.i];

    if (vars.currentReserveAddress == address(0)) {
      unchecked {
        ++vars.i;
      }
      continue;
    }

Get the asset address of the current iteration by passing the counter into mapping reservesList

  • reservesList is defined on PoolStorage

If asset address is undefined, increment counter and continue.

Would there be gas savings by checking for zero address first, then followed by isUsingAsCollateralOrBorrow?

3. Get asset's params

Now that we have established that the asset is defined and being used by the user as either collateral or borrowing, let us obtain the following key details:

  • LTV

  • liquidationThreshold

  • decimals

  • Emode category

This is achieved via getParams, which utilizes bitmasks to extract the relevant information from the ReserveConfigurationMap; which is a bitmap.

4. Set decimals and oracle interface

  • Define the decimal precision of 1 unit if the asset (1 Ether = 10**18 | 1 USDC = 10**6)

  • Define the oracle interface

If both the user and the asset are in the same e-mode category, and vars.eModeAssetPrice !=0, use vars.eModeAssetPrice

// this was set earlier via getEModeConfiguration
vars.eModeAssetPrice = IPriceOracleGetter(params.oracle).getAssetPrice(eModePriceSource)

Else, default to using the following as the oracle interface:

IPriceOracleGetter(params.oracle).getAssetPrice(vars.currentReserveAddress);

Setting of oracles is crucial because we will be normalizing all of the user's collateral and debt to a common base currency; likely USD. This will allow us to calculate wallet-level metrics like LTV and liquidation threshold and consequently the user's health factor.

5. If asset is used as collateral

If the asset's liquidation threshold is defined and it is being used by the user as collateral, execute the following.

  • get user's balance in base CCY and increment totalCollateralInBaseCurrency

  • totalCollateralInBaseCurrency will be the sum of collateral across all asset classes, normalized into the base currency.

  • E.g. get user's total collateral in USD.

Each market has an AaveOracle contract where you can query token prices in the base currency. BaseCCY:

  • ETH on V2 mainnet/polygon

  • USD on all other markets)

// if user and asset in same emode category, returns true
vars.isInEModeCategory = EModeLogic.isInEModeCategory(params.userEModeCategory, vars.eModeAssetCategory);

If the asset's LTV is defined: avgLTV

  • calculate the user's max debt for each asset

  • the sum of these across all assets will give us the numerator for the avgLTV calculation

  • we will divide by totalCollateralInBaseCurrency at the end

avgLTV=CollateraliinbaseCCY×LTVratioitotalCollateralInBaseCurrencyavgLTV = \frac{ \sum{Collateral_i \: in \: baseCCY \: \times \: LTV ratio _i}}{totalCollateralInBaseCurrency}

Loan to Value (”LTV”) ratio defines the maximum amount of assets that can be borrowed with a specific collateral.

avgLiquidationThreshold

For each wallet, the Liquidation Threshold is calculate as the weighted average of the Liquidation Thresholds of the collateral assets and their value:

LiquidationThreshold=CollateraliinbaseCCY×LiquidationThresholdiTotalCollateralinbaseCCYLiquidation \: Threshold= \frac{ \sum{Collateral_i \: in \: baseCCY \: \times \: Liquidation \: Threshold_i}}{Total \: Collateral \: in \: baseCCY\:}

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

At this stage we simply look to obtain the numerator for the avgLiquidationThreshold calculation. Like avgLTV, the division will be done at the end, once the loop has been completed.

Liquidation threshold is the percentage at which a position is defined as undercollateralised. For example, a Liquidation threshold of 80% means that if the loan value rises above 80% of the collateral, the position is undercollateralised and could be liquidated.

6. isBorrowing

If user is borrowing this asset, calculate its value in base CCY, and increment vars.totalDebtInBaseCurrency

Exit loop

Now that we have traversed across the entire universe of assets and increments the various necessary measures like

  • totalCollateralInBaseCurrency

  • totalDebtInBaseCurrency

  • LTV, Liquidation threshold

We have the prerequisites to calculate a wallet's health factor.

First we obtain avgLtv and avgLiquidationThreshold by dividing them each against totalCollateralInBaseCurrency.

Remember, we previously only obtained their respective numerators for the weighted calculation, in the while loop.

Then the calculation for health factor:

avgLiquidationThreshold was obtained by dividing the weighted sum by totalCollateral, therefore this can be expressed as:

hf=Collaterali,baseCCY×LiquidationThresholdiTotalDebtbaseCCYhf = \frac{ \sum{Collateral_{i, baseCCY} \: \times \: Liquidation \: Threshold_i} }{TotalDebt_{baseCCY}}
  • collateral * liquidationThreshold => max debt possible for that collateral asset

  • the ratio of the sum of max debt possible against the totalDebt presently held constitutes the health factor of a wallet

If hf < 1,

  • numerator < denominator

  • Collaterali,baseCCY×LiquidationThresholdi\sum{Collateral_{i, baseCCY} \: \times \: Liquidation \: Threshold_i} < TotalDebtbaseCCY{TotalDebt_{baseCCY}}

User's current total debt exceeds his max loan value possible; hence considered undercollateralized.

Last updated