// SPDX-License-Identifier: agpl-3.0 pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {SafeMath} from '../../../dependencies/openzeppelin/contracts/SafeMath.sol'; import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol'; import {IScaledBalanceToken} from '../../../interfaces/IScaledBalanceToken.sol'; import {ReserveLogic} from './ReserveLogic.sol'; import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; import {UserConfiguration} from '../configuration/UserConfiguration.sol'; import {WadRayMath} from '../math/WadRayMath.sol'; import {PercentageMath} from '../math/PercentageMath.sol'; import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; import {DataTypes} from '../types/DataTypes.sol'; /** * @title GenericLogic library * @author Aave * @title Implements protocol-level logic to calculate and validate the state of a user */ library GenericLogic { using ReserveLogic for DataTypes.ReserveData; using SafeMath for uint256; using WadRayMath for uint256; using PercentageMath for uint256; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using UserConfiguration for DataTypes.UserConfigurationMap; uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1 ether; struct CalculateUserAccountDataVars { uint256 assetPrice; uint256 assetUnit; uint256 userBalance; uint256 userBalanceETH; uint256 userDebt; uint256 userStableDebt; uint256 userDebtETH; uint256 decimals; uint256 ltv; uint256 liquidationThreshold; uint256 i; uint256 healthFactor; uint256 totalCollateralInETH; uint256 totalDebtInETH; uint256 avgLtv; uint256 avgUncappedLtv; uint256 avgLiquidationThreshold; uint256 reservesLength; uint256 normalizedIncome; uint256 normalizedDebt; uint256 exposureCap; uint256 aTokenSupply; bool healthFactorBelowThreshold; address currentReserveAddress; bool usageAsCollateralEnabled; bool userUsesReserveAsCollateral; bool exposureCapCrossed; } /** * @dev Calculates the user data across the reserves. * this includes the total liquidity/collateral/borrow balances in ETH, * the average Loan To Value, the average Liquidation Ratio, and the Health factor. * @param user The address of the user * @param reservesData Data of all the reserves * @param userConfig The configuration of the user * @param reserves The list of the available reserves * @param oracle The price oracle address * @return The total collateral and total debt of the user in ETH, the avg ltv, liquidation threshold, the HF and the uncapped avg ltv (without exposure ceiling) **/ function calculateUserAccountData( address user, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap memory userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) internal view returns ( uint256, uint256, uint256, uint256, uint256, uint256 ) { CalculateUserAccountDataVars memory vars; if (userConfig.isEmpty()) { return (0, 0, 0, 0, uint256(-1), 0); } for (vars.i = 0; vars.i < reservesCount; vars.i++) { if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) { continue; } vars.currentReserveAddress = reserves[vars.i]; if (vars.currentReserveAddress == address(0)) continue; DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); vars.exposureCap = currentReserve.configuration.getExposureCap(); vars.assetUnit = 10**vars.decimals; vars.assetPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress); if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) { vars.normalizedIncome = currentReserve.getNormalizedIncome(); if (vars.exposureCap != 0) { (vars.userBalance, vars.aTokenSupply) = IScaledBalanceToken(currentReserve.aTokenAddress) .getScaledUserBalanceAndSupply(user); vars.userBalance = vars.userBalance.rayMul(vars.normalizedIncome); vars.aTokenSupply = vars.aTokenSupply.rayMul(vars.normalizedIncome); } else { vars.userBalance = IScaledBalanceToken(currentReserve.aTokenAddress).scaledBalanceOf( user ); vars.userBalance = vars.userBalance.rayMul(vars.normalizedIncome); vars.aTokenSupply = 0; } vars.userBalanceETH = vars.assetPrice.mul(vars.userBalance).div(vars.assetUnit); vars.totalCollateralInETH = vars.totalCollateralInETH.add(vars.userBalanceETH); vars.exposureCapCrossed = vars.exposureCap != 0 && vars.aTokenSupply.div(10**vars.decimals) > vars.exposureCap; vars.avgLtv = vars.avgLtv.add( vars.exposureCapCrossed ? 0 : vars.userBalanceETH.mul(vars.ltv) ); vars.avgUncappedLtv = vars.avgUncappedLtv.add(vars.userBalanceETH.mul(vars.ltv)); vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add( vars.userBalanceETH.mul(vars.liquidationThreshold) ); } if (userConfig.isBorrowing(vars.i)) { vars.userStableDebt = IERC20(currentReserve.stableDebtTokenAddress).balanceOf(user); vars.userDebt = IScaledBalanceToken(currentReserve.variableDebtTokenAddress) .scaledBalanceOf(user); if (vars.userDebt > 0) { vars.normalizedDebt = currentReserve.getNormalizedDebt(); vars.userDebt = vars.userDebt.rayMul(vars.normalizedDebt); } vars.userDebt = vars.userDebt.add(vars.userStableDebt); vars.userDebtETH = vars.assetPrice.mul(vars.userDebt).div(vars.assetUnit); vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); } } vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0; vars.avgUncappedLtv = vars.totalCollateralInETH > 0 ? vars.avgUncappedLtv.div(vars.totalCollateralInETH) : 0; vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0 ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH) : 0; vars.healthFactor = calculateHealthFactorFromBalances( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLiquidationThreshold ); return ( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLtv, vars.avgLiquidationThreshold, vars.healthFactor, vars.avgUncappedLtv ); } /** * @dev Calculates the health factor from the corresponding balances * @param totalCollateralInETH The total collateral in ETH * @param totalDebtInETH The total debt in ETH * @param liquidationThreshold The avg liquidation threshold * @return The health factor calculated from the balances provided **/ function calculateHealthFactorFromBalances( uint256 totalCollateralInETH, uint256 totalDebtInETH, uint256 liquidationThreshold ) internal pure returns (uint256) { if (totalDebtInETH == 0) return uint256(-1); return (totalCollateralInETH.percentMul(liquidationThreshold)).wadDiv(totalDebtInETH); } /** * @dev Calculates the equivalent amount in ETH that an user can borrow, depending on the available collateral and the * average Loan To Value * @param totalCollateralInETH The total collateral in ETH * @param totalDebtInETH The total borrow balance * @param ltv The average loan to value * @return the amount available to borrow in ETH for the user **/ function calculateAvailableBorrowsETH( uint256 totalCollateralInETH, uint256 totalDebtInETH, uint256 ltv ) internal pure returns (uint256) { uint256 availableBorrowsETH = totalCollateralInETH.percentMul(ltv); if (availableBorrowsETH < totalDebtInETH) { return 0; } availableBorrowsETH = availableBorrowsETH.sub(totalDebtInETH); return availableBorrowsETH; } /** * @dev proxy call for calculateUserAccountData as external function. * Used in LendingPool to work around contract size limit issues * @param user The address of the user * @param reservesData Data of all the reserves * @param userConfig The configuration of the user * @param reserves The list of the available reserves * @param oracle The price oracle address * @return The total collateral and total debt of the user in ETH, the avg ltv, liquidation threshold and the HF **/ function getUserAccountData( address user, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap memory userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) external view returns ( uint256, uint256, uint256, uint256, uint256, uint256 ) { return calculateUserAccountData(user, reservesData, userConfig, reserves, reservesCount, oracle); } }