diff --git a/src/contracts/helpers/LiquidationDataProvider.sol b/src/contracts/helpers/LiquidationDataProvider.sol new file mode 100644 index 00000000..ff3d039d --- /dev/null +++ b/src/contracts/helpers/LiquidationDataProvider.sol @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; + +import {IPool} from '../interfaces/IPool.sol'; +import {IPoolAddressesProvider} from '../interfaces/IPoolAddressesProvider.sol'; +import {IPriceOracleSentinel} from '../interfaces/IPriceOracleSentinel.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; + +import {ValidationLogic} from '../protocol/libraries/logic/ValidationLogic.sol'; +import {LiquidationLogic} from '../protocol/libraries/logic/LiquidationLogic.sol'; +import {ReserveConfiguration} from '../protocol/libraries/configuration/ReserveConfiguration.sol'; +import {UserConfiguration} from '../protocol/libraries/configuration/UserConfiguration.sol'; +import {EModeConfiguration} from '../protocol/libraries/configuration/EModeConfiguration.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol'; + +import {ILiquidationDataProvider} from './interfaces/ILiquidationDataProvider.sol'; + +/** + * @title LiquidationDataProvider + * @author BGD Labs + * @notice Utility contract to fetch liquidation parameters. + */ +contract LiquidationDataProvider is ILiquidationDataProvider { + using PercentageMath for uint256; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + using UserConfiguration for DataTypes.UserConfigurationMap; + + /* PUBLIC VARIABLES */ + + /// @inheritdoc ILiquidationDataProvider + IPoolAddressesProvider public immutable override ADDRESSES_PROVIDER; + + /// @inheritdoc ILiquidationDataProvider + IPool public immutable override POOL; + + /* CONSTRUCTOR */ + + constructor(address pool, address addressesProvider) { + ADDRESSES_PROVIDER = IPoolAddressesProvider(addressesProvider); + POOL = IPool(pool); + } + + /* EXTERNAL AND PUBLIC FUNCTIONS */ + + /// @inheritdoc ILiquidationDataProvider + function getUserPositionFullInfo( + address user + ) public view override returns (UserPositionFullInfo memory) { + UserPositionFullInfo memory userInfo; + ( + userInfo.totalCollateralInBaseCurrency, + userInfo.totalDebtInBaseCurrency, + userInfo.availableBorrowsInBaseCurrency, + userInfo.currentLiquidationThreshold, + userInfo.ltv, + userInfo.healthFactor + ) = POOL.getUserAccountData(user); + + return userInfo; + } + + /// @inheritdoc ILiquidationDataProvider + function getCollateralFullInfo( + address user, + address collateralAsset + ) external view override returns (CollateralFullInfo memory) { + return _getCollateralFullInfo(user, collateralAsset, ADDRESSES_PROVIDER.getPriceOracle()); + } + + /// @inheritdoc ILiquidationDataProvider + function getDebtFullInfo( + address user, + address debtAsset + ) external view override returns (DebtFullInfo memory) { + return _getDebtFullInfo(user, debtAsset, ADDRESSES_PROVIDER.getPriceOracle()); + } + + /// @inheritdoc ILiquidationDataProvider + function getLiquidationInfo( + address user, + address collateralAsset, + address debtAsset + ) public view override returns (LiquidationInfo memory) { + return getLiquidationInfo(user, collateralAsset, debtAsset, type(uint256).max); + } + + /// @inheritdoc ILiquidationDataProvider + function getLiquidationInfo( + address user, + address collateralAsset, + address debtAsset, + uint256 debtLiquidationAmount + ) public view override returns (LiquidationInfo memory) { + LiquidationInfo memory liquidationInfo; + GetLiquidationInfoLocalVars memory localVars; + + liquidationInfo.userInfo = getUserPositionFullInfo(user); + + { + address oracle = ADDRESSES_PROVIDER.getPriceOracle(); + liquidationInfo.collateralInfo = _getCollateralFullInfo(user, collateralAsset, oracle); + liquidationInfo.debtInfo = _getDebtFullInfo(user, debtAsset, oracle); + } + + if (liquidationInfo.debtInfo.debtBalance == 0) { + return liquidationInfo; + } + + if (!_canLiquidateThisHealthFactor(liquidationInfo.userInfo.healthFactor)) { + return liquidationInfo; + } + + DataTypes.ReserveDataLegacy memory collateralReserveData = POOL.getReserveData(collateralAsset); + DataTypes.ReserveDataLegacy memory debtReserveData = POOL.getReserveData(debtAsset); + + if ( + !_isReserveReadyForLiquidations({ + reserveAsset: collateralAsset, + isCollateral: true, + reserveConfiguration: collateralReserveData.configuration + }) || + !_isReserveReadyForLiquidations({ + reserveAsset: debtAsset, + isCollateral: false, + reserveConfiguration: debtReserveData.configuration + }) + ) { + return liquidationInfo; + } + + if (!_isCollateralEnabledForUser(user, collateralReserveData.id)) { + return liquidationInfo; + } + + localVars.liquidationBonus = _getLiquidationBonus( + user, + collateralReserveData.id, + collateralReserveData.configuration + ); + + localVars.maxDebtToLiquidate = _getMaxDebtToLiquidate( + liquidationInfo.userInfo, + liquidationInfo.collateralInfo, + liquidationInfo.debtInfo, + debtLiquidationAmount + ); + + ( + localVars.collateralAmountToLiquidate, + localVars.debtAmountToLiquidate, + localVars.liquidationProtocolFee + ) = _getAvailableCollateralAndDebtToLiquidate( + localVars.maxDebtToLiquidate, + localVars.liquidationBonus, + liquidationInfo.collateralInfo, + liquidationInfo.debtInfo, + collateralReserveData.configuration + ); + + ( + liquidationInfo.maxCollateralToLiquidate, + liquidationInfo.maxDebtToLiquidate, + liquidationInfo.liquidationProtocolFee + ) = _adjustAmountsForGoodLeftovers( + localVars.collateralAmountToLiquidate, + localVars.debtAmountToLiquidate, + localVars.liquidationProtocolFee, + localVars.liquidationBonus, + liquidationInfo.collateralInfo, + liquidationInfo.debtInfo, + collateralReserveData.configuration + ); + + if ( + liquidationInfo.maxDebtToLiquidate != 0 && + liquidationInfo.maxDebtToLiquidate == liquidationInfo.debtInfo.debtBalance + ) { + liquidationInfo.amountToPassToLiquidationCall = type(uint256).max; + } else { + liquidationInfo.amountToPassToLiquidationCall = liquidationInfo.maxDebtToLiquidate; + } + + return liquidationInfo; + } + + /* PRIVATE FUNCTIONS */ + + function _adjustAmountsForGoodLeftovers( + uint256 collateralAmountToLiquidate, + uint256 debtAmountToLiquidate, + uint256 liquidationProtocolFee, + uint256 liquidationBonus, + CollateralFullInfo memory collateralInfo, + DebtFullInfo memory debtInfo, + DataTypes.ReserveConfigurationMap memory collateralConfiguration + ) private pure returns (uint256, uint256, uint256) { + AdjustAmountsForGoodLeftoversLocalVars memory localVars; + + if ( + collateralAmountToLiquidate + liquidationProtocolFee < collateralInfo.collateralBalance && + debtAmountToLiquidate < debtInfo.debtBalance + ) { + localVars.collateralLeftoverInBaseCurrency = + ((collateralInfo.collateralBalance - collateralAmountToLiquidate - liquidationProtocolFee) * + collateralInfo.price) / + collateralInfo.assetUnit; + + localVars.debtLeftoverInBaseCurrency = + ((debtInfo.debtBalance - debtAmountToLiquidate) * debtInfo.price) / + debtInfo.assetUnit; + + if ( + localVars.collateralLeftoverInBaseCurrency < LiquidationLogic.MIN_LEFTOVER_BASE || + localVars.debtLeftoverInBaseCurrency < LiquidationLogic.MIN_LEFTOVER_BASE + ) { + localVars.collateralDecreaseAmountInBaseCurrency = localVars + .collateralLeftoverInBaseCurrency < LiquidationLogic.MIN_LEFTOVER_BASE + ? LiquidationLogic.MIN_LEFTOVER_BASE - localVars.collateralLeftoverInBaseCurrency + : 0; + + localVars.debtDecreaseAmountInBaseCurrency = localVars.debtLeftoverInBaseCurrency < + LiquidationLogic.MIN_LEFTOVER_BASE + ? LiquidationLogic.MIN_LEFTOVER_BASE - localVars.debtLeftoverInBaseCurrency + : 0; + + if ( + localVars.collateralDecreaseAmountInBaseCurrency > + localVars.debtDecreaseAmountInBaseCurrency + ) { + localVars.collateralDecreaseAmount = + (localVars.collateralDecreaseAmountInBaseCurrency * collateralInfo.assetUnit) / + collateralInfo.price; + + collateralAmountToLiquidate -= localVars.collateralDecreaseAmount; + + debtAmountToLiquidate = ((collateralInfo.price * + collateralAmountToLiquidate * + debtInfo.assetUnit) / (debtInfo.price * collateralInfo.assetUnit)).percentDiv( + liquidationBonus + ); + } else { + localVars.debtDecreaseAmount = + (localVars.debtDecreaseAmountInBaseCurrency * debtInfo.assetUnit) / + debtInfo.price; + + debtAmountToLiquidate -= localVars.debtDecreaseAmount; + + collateralAmountToLiquidate = ((debtInfo.price * + debtAmountToLiquidate * + collateralInfo.assetUnit) / (collateralInfo.price * debtInfo.assetUnit)).percentMul( + liquidationBonus + ); + } + + localVars.liquidationProtocolFeePercentage = collateralConfiguration + .getLiquidationProtocolFee(); + + if (localVars.liquidationProtocolFeePercentage != 0) { + localVars.bonusCollateral = + collateralAmountToLiquidate - + collateralAmountToLiquidate.percentDiv(liquidationBonus); + + liquidationProtocolFee = localVars.bonusCollateral.percentMul( + localVars.liquidationProtocolFeePercentage + ); + + collateralAmountToLiquidate -= liquidationProtocolFee; + } + } + } + + return (collateralAmountToLiquidate, debtAmountToLiquidate, liquidationProtocolFee); + } + + function _getAvailableCollateralAndDebtToLiquidate( + uint256 maxDebtToLiquidate, + uint256 liquidationBonus, + CollateralFullInfo memory collateralInfo, + DebtFullInfo memory debtInfo, + DataTypes.ReserveConfigurationMap memory collateralConfiguration + ) private pure returns (uint256, uint256, uint256) { + uint256 liquidationProtocolFeePercentage = collateralConfiguration.getLiquidationProtocolFee(); + + uint256 maxBaseCollateral = (debtInfo.price * maxDebtToLiquidate * collateralInfo.assetUnit) / + (collateralInfo.price * debtInfo.assetUnit); + + uint256 maxCollateralToLiquidate = maxBaseCollateral.percentMul(liquidationBonus); + + uint256 collateralAmountToLiquidate; + uint256 debtAmountToLiquidate; + if (maxCollateralToLiquidate > collateralInfo.collateralBalance) { + collateralAmountToLiquidate = collateralInfo.collateralBalance; + + debtAmountToLiquidate = ((collateralInfo.price * + collateralAmountToLiquidate * + debtInfo.assetUnit) / (debtInfo.price * collateralInfo.assetUnit)).percentDiv( + liquidationBonus + ); + } else { + collateralAmountToLiquidate = maxCollateralToLiquidate; + debtAmountToLiquidate = maxDebtToLiquidate; + } + + uint256 liquidationProtocolFee; + if (liquidationProtocolFeePercentage != 0) { + uint256 bonusCollateral = collateralAmountToLiquidate - + collateralAmountToLiquidate.percentDiv(liquidationBonus); + + liquidationProtocolFee = bonusCollateral.percentMul(liquidationProtocolFeePercentage); + + collateralAmountToLiquidate -= liquidationProtocolFee; + } + + return (collateralAmountToLiquidate, debtAmountToLiquidate, liquidationProtocolFee); + } + + function _getMaxDebtToLiquidate( + UserPositionFullInfo memory userInfo, + CollateralFullInfo memory collateralInfo, + DebtFullInfo memory debtInfo, + uint256 debtLiquidationAmount + ) private pure returns (uint256) { + uint256 maxDebtToLiquidate = debtInfo.debtBalance; + + if ( + collateralInfo.collateralBalanceInBaseCurrency >= + LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + debtInfo.debtBalanceInBaseCurrency >= LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && + userInfo.healthFactor > LiquidationLogic.CLOSE_FACTOR_HF_THRESHOLD + ) { + uint256 totalDefaultLiquidatableDebtInBaseCurrency = userInfo + .totalDebtInBaseCurrency + .percentMul(LiquidationLogic.DEFAULT_LIQUIDATION_CLOSE_FACTOR); + + if (debtInfo.debtBalanceInBaseCurrency > totalDefaultLiquidatableDebtInBaseCurrency) { + maxDebtToLiquidate = + (totalDefaultLiquidatableDebtInBaseCurrency * debtInfo.assetUnit) / + debtInfo.price; + } + } + + return maxDebtToLiquidate < debtLiquidationAmount ? maxDebtToLiquidate : debtLiquidationAmount; + } + + function _getLiquidationBonus( + address user, + uint16 collateralId, + DataTypes.ReserveConfigurationMap memory collateralConfiguration + ) private view returns (uint256) { + uint256 userEModeCategory = POOL.getUserEMode(user); + + uint128 collateralBitmap = POOL.getEModeCategoryCollateralBitmap(uint8(userEModeCategory)); + + if ( + userEModeCategory != 0 && + EModeConfiguration.isReserveEnabledOnBitmap(collateralBitmap, collateralId) + ) { + DataTypes.EModeCategoryLegacy memory eModeCategory = POOL.getEModeCategoryData( + uint8(userEModeCategory) + ); + + return eModeCategory.liquidationBonus; + } else { + return collateralConfiguration.getLiquidationBonus(); + } + } + + function _isCollateralEnabledForUser( + address user, + uint16 collateralId + ) private view returns (bool) { + DataTypes.UserConfigurationMap memory userConfiguration = POOL.getUserConfiguration(user); + + return userConfiguration.isUsingAsCollateral(collateralId); + } + + function _canLiquidateThisHealthFactor(uint256 healthFactor) private view returns (bool) { + address priceOracleSentinel = ADDRESSES_PROVIDER.getPriceOracleSentinel(); + + if (healthFactor >= ValidationLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return false; + } + + if ( + priceOracleSentinel != address(0) && + healthFactor >= ValidationLogic.MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD && + !IPriceOracleSentinel(priceOracleSentinel).isLiquidationAllowed() + ) { + return false; + } + + return true; + } + + function _isReserveReadyForLiquidations( + address reserveAsset, + bool isCollateral, + DataTypes.ReserveConfigurationMap memory reserveConfiguration + ) private view returns (bool) { + bool isReserveActive = reserveConfiguration.getActive(); + bool isReservePaused = reserveConfiguration.getPaused(); + + bool areLiquidationsAllowed = POOL.getLiquidationGracePeriod(reserveAsset) < + uint40(block.timestamp); + + return + isReserveActive && + !isReservePaused && + areLiquidationsAllowed && + (isCollateral ? reserveConfiguration.getLiquidationThreshold() != 0 : true); + } + + function _getCollateralFullInfo( + address user, + address reserveAsset, + address oracle + ) private view returns (CollateralFullInfo memory) { + CollateralFullInfo memory collateralInfo; + + collateralInfo.assetUnit = 10 ** IERC20Detailed(reserveAsset).decimals(); + collateralInfo.price = IPriceOracleGetter(oracle).getAssetPrice(reserveAsset); + + collateralInfo.aToken = POOL.getReserveAToken(reserveAsset); + + collateralInfo.collateralBalance = IERC20Detailed(collateralInfo.aToken).balanceOf(user); + + collateralInfo.collateralBalanceInBaseCurrency = + (collateralInfo.collateralBalance * collateralInfo.price) / + collateralInfo.assetUnit; + + return collateralInfo; + } + + function _getDebtFullInfo( + address user, + address reserveAsset, + address oracle + ) private view returns (DebtFullInfo memory) { + DebtFullInfo memory debtInfo; + + debtInfo.assetUnit = 10 ** IERC20Detailed(reserveAsset).decimals(); + debtInfo.price = IPriceOracleGetter(oracle).getAssetPrice(reserveAsset); + + debtInfo.variableDebtToken = POOL.getReserveVariableDebtToken(reserveAsset); + + debtInfo.debtBalance = IERC20Detailed(debtInfo.variableDebtToken).balanceOf(user); + + debtInfo.debtBalanceInBaseCurrency = + (debtInfo.debtBalance * debtInfo.price) / + debtInfo.assetUnit; + + return debtInfo; + } +} diff --git a/src/contracts/helpers/interfaces/ILiquidationDataProvider.sol b/src/contracts/helpers/interfaces/ILiquidationDataProvider.sol new file mode 100644 index 00000000..2f20f156 --- /dev/null +++ b/src/contracts/helpers/interfaces/ILiquidationDataProvider.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPoolAddressesProvider} from '../../interfaces/IPoolAddressesProvider.sol'; +import {IPool} from '../../interfaces/IPool.sol'; + +interface ILiquidationDataProvider { + /* STRUCTS */ + + struct UserPositionFullInfo { + uint256 totalCollateralInBaseCurrency; + uint256 totalDebtInBaseCurrency; + uint256 availableBorrowsInBaseCurrency; + uint256 currentLiquidationThreshold; + uint256 ltv; + uint256 healthFactor; + } + + struct CollateralFullInfo { + address aToken; + uint256 collateralBalance; + uint256 collateralBalanceInBaseCurrency; + uint256 price; + uint256 assetUnit; + } + + struct DebtFullInfo { + address variableDebtToken; + uint256 debtBalance; + uint256 debtBalanceInBaseCurrency; + uint256 price; + uint256 assetUnit; + } + + struct LiquidationInfo { + UserPositionFullInfo userInfo; + CollateralFullInfo collateralInfo; + DebtFullInfo debtInfo; + uint256 maxCollateralToLiquidate; + uint256 maxDebtToLiquidate; + uint256 liquidationProtocolFee; + uint256 amountToPassToLiquidationCall; + } + + struct GetLiquidationInfoLocalVars { + uint256 liquidationBonus; + uint256 maxDebtToLiquidate; + uint256 collateralAmountToLiquidate; + uint256 debtAmountToLiquidate; + uint256 liquidationProtocolFee; + } + + struct AdjustAmountsForGoodLeftoversLocalVars { + uint256 collateralLeftoverInBaseCurrency; + uint256 debtLeftoverInBaseCurrency; + uint256 collateralDecreaseAmountInBaseCurrency; + uint256 debtDecreaseAmountInBaseCurrency; + uint256 collateralDecreaseAmount; + uint256 debtDecreaseAmount; + uint256 liquidationProtocolFeePercentage; + uint256 bonusCollateral; + } + + /* PUBLIC VARIABLES */ + + /// @notice The address of the PoolAddressesProvider + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); + + /// @notice The address of the Pool + function POOL() external view returns (IPool); + + /* EXTERNAL AND PUBLIC FUNCTIONS */ + + /// @notice Returns the user position full information + /// @param user The user address + /// @return The user position full information + function getUserPositionFullInfo( + address user + ) external view returns (UserPositionFullInfo memory); + + /// @notice Returns the collateral full information for a user + /// @param user The user address + /// @param collateralAsset The collateral asset address + /// @return The collateral full information + function getCollateralFullInfo( + address user, + address collateralAsset + ) external view returns (CollateralFullInfo memory); + + /// @notice Returns the debt full information for a user + /// @param user The user address + /// @param debtAsset The debt asset address + /// @return The debt full information + function getDebtFullInfo( + address user, + address debtAsset + ) external view returns (DebtFullInfo memory); + + /// @notice Returns the liquidation information for a user + /// @param user The user address + /// @param collateralAsset The collateral asset address + /// @param debtAsset The debt asset address + /// @return The liquidation information + function getLiquidationInfo( + address user, + address collateralAsset, + address debtAsset + ) external view returns (LiquidationInfo memory); + + /// @notice Returns the liquidation information for a user for a specific max debt amount + /// @param user The user address + /// @param collateralAsset The collateral asset address + /// @param debtAsset The debt asset address + /// @param debtLiquidationAmount The maximum debt amount to be liquidated + /// @return The liquidation information + function getLiquidationInfo( + address user, + address collateralAsset, + address debtAsset, + uint256 debtLiquidationAmount + ) external view returns (LiquidationInfo memory); +} diff --git a/tests/helpers/LiquidationHelper.sol b/tests/helpers/LiquidationHelper.sol index f405c6fe..4970e8a6 100644 --- a/tests/helpers/LiquidationHelper.sol +++ b/tests/helpers/LiquidationHelper.sol @@ -19,19 +19,6 @@ library LiquidationHelper { using PercentageMath for uint256; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; - /** - * @notice Returns the required amount of borrows in base currency to reach a certain healthfactor - */ - function _getRequiredBorrowsForHfBelow( - IPool pool, - address user, - uint256 desiredHf - ) internal view returns (uint256) { - (uint256 totalCollateralBase, , , uint256 currentLiquidationThreshold, , ) = pool - .getUserAccountData(user); - return (totalCollateralBase.percentMul(currentLiquidationThreshold + 1) * 1e18) / desiredHf; - } - struct LocalVars { address user; uint256 liquidationBonus; @@ -40,25 +27,6 @@ library LiquidationHelper { address vToken; } - function _getLiquidationParams( - IPool pool, - address user, - address collateralAsset, - address debtAsset, - uint256 liquidationAmount - ) internal view returns (uint256, uint256, uint256, uint256) { - uint256 maxLiquidatableDebt = _getMaxLiquidatableDebt(pool, user, collateralAsset, debtAsset); - return - _getLiquidationParams( - pool, - user, - collateralAsset, - debtAsset, - liquidationAmount, - maxLiquidatableDebt - ); - } - /** * replicates LiquidationLogic._calculateAvailableCollateralToLiquidate without direct storage access */ @@ -100,46 +68,4 @@ library LiquidationHelper { local.liquidationBonus ); } - - function _getMaxLiquidatableDebt( - IPool pool, - address user, - address collateralAsset, - address debtAsset - ) internal view returns (uint256) { - (, uint256 totalDebtInBaseCurrency, , , , uint256 healthFactor) = pool.getUserAccountData(user); - address oracle = pool.ADDRESSES_PROVIDER().getPriceOracle(); - uint256 reserveCollateralInBaseCurrency; - { - address aToken = pool.getReserveAToken(collateralAsset); - uint256 maxLiquidatableCollateral = IERC20Detailed(aToken).balanceOf(user); - uint256 collateralAssetUnits = 10 ** IERC20Detailed(aToken).decimals(); - uint256 collateralAssetPrice = IAaveOracle(oracle).getAssetPrice(collateralAsset); - reserveCollateralInBaseCurrency = - (collateralAssetPrice * maxLiquidatableCollateral) / - collateralAssetUnits; - } - address vToken = pool.getReserveVariableDebtToken(debtAsset); - uint256 maxLiquidatableDebt = IERC20Detailed(vToken).balanceOf(user); - uint256 debtAssetUnits = 10 ** IERC20Detailed(vToken).decimals(); - uint256 debtAssetPrice = IAaveOracle(oracle).getAssetPrice(debtAsset); - uint256 reserveDebtInBaseCurrency = (debtAssetPrice * maxLiquidatableDebt) / debtAssetUnits; - - if ( - reserveDebtInBaseCurrency >= LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && - reserveCollateralInBaseCurrency >= LiquidationLogic.MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && - healthFactor > LiquidationLogic.CLOSE_FACTOR_HF_THRESHOLD - ) { - uint256 totalDefaultLiquidatableDebtInBaseCurrency = totalDebtInBaseCurrency.percentMul( - LiquidationLogic.DEFAULT_LIQUIDATION_CLOSE_FACTOR - ); - - if (reserveDebtInBaseCurrency > totalDefaultLiquidatableDebtInBaseCurrency) { - maxLiquidatableDebt = - (totalDefaultLiquidatableDebtInBaseCurrency * debtAssetUnits) / - debtAssetPrice; - } - } - return maxLiquidatableDebt; - } } diff --git a/tests/protocol/pool/Pool.Deficit.sol b/tests/protocol/pool/Pool.Deficit.sol index 2fc453aa..216c11cf 100644 --- a/tests/protocol/pool/Pool.Deficit.sol +++ b/tests/protocol/pool/Pool.Deficit.sol @@ -222,7 +222,7 @@ contract PoolDeficitTests is TestnetProcedures { _checkIrInvariant(tokenList.usdx); } - function _checkIrInvariant(address asset) internal { + function _checkIrInvariant(address asset) internal view { DataTypes.ReserveDataLegacy memory reserveData = contracts.poolProxy.getReserveData(asset); assertLt( reserveData.currentLiquidityRate * IERC20(reserveData.aTokenAddress).totalSupply(), diff --git a/tests/protocol/pool/Pool.EMode.sol b/tests/protocol/pool/Pool.EMode.sol index 3263cc54..78fb69b7 100644 --- a/tests/protocol/pool/Pool.EMode.sol +++ b/tests/protocol/pool/Pool.EMode.sol @@ -16,7 +16,6 @@ import {PercentageMath} from '../../../src/contracts/protocol/libraries/math/Per import {IAaveOracle} from '../../../src/contracts/interfaces/IAaveOracle.sol'; import {TestnetProcedures} from '../../utils/TestnetProcedures.sol'; import {TestnetERC20} from '../../../src/contracts/mocks/testnet-helpers/TestnetERC20.sol'; -import {LiquidationHelper} from '../../helpers/LiquidationHelper.sol'; contract PoolEModeTests is TestnetProcedures { using stdStorage for StdStorage; diff --git a/tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol b/tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol index 8fc4fe49..6213e0aa 100644 --- a/tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol +++ b/tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol @@ -21,6 +21,7 @@ import {DataTypes} from '../../../src/contracts/protocol/libraries/types/DataTyp import {PercentageMath} from '../../../src/contracts/protocol/libraries/math/PercentageMath.sol'; import {WadRayMath} from '../../../src/contracts/protocol/libraries/math/WadRayMath.sol'; import {TestnetProcedures} from '../../utils/TestnetProcedures.sol'; +import {LiquidationDataProvider} from '../../../src/contracts/helpers/LiquidationDataProvider.sol'; import {LiquidationHelper} from '../../helpers/LiquidationHelper.sol'; contract PoolLiquidationCloseFactorTests is TestnetProcedures { @@ -38,6 +39,7 @@ contract PoolLiquidationCloseFactorTests is TestnetProcedures { PriceOracleSentinel internal priceOracleSentinel; SequencerOracle internal sequencerOracleMock; + LiquidationDataProvider internal liquidationDataProvider; event IsolationModeTotalDebtUpdated(address indexed asset, uint256 totalDebt); @@ -46,6 +48,11 @@ contract PoolLiquidationCloseFactorTests is TestnetProcedures { _addBorrowableLiquidity(); _fundLiquidator(); + + liquidationDataProvider = new LiquidationDataProvider( + address(contracts.poolProxy), + address(contracts.poolAddressesProvider) + ); } // ## Fuzzing suite ## @@ -172,6 +179,74 @@ contract PoolLiquidationCloseFactorTests is TestnetProcedures { ); } + // on aave v3.3 in certain edge scenarios, liquidation uint.max reverts on cf 50% + // the liquidationprovider should always return valid values + function test_liquidationdataprovider_edge_range() external { + // borrow supply 4k + _supplyToPool(tokenList.usdx, bob, 8000e6); + vm.prank(bob); + contracts.poolProxy.borrow(tokenList.usdx, 4200e6, 2, 0, bob); + _borrowToBeBelowHf(bob, tokenList.weth, 0.98 ether); + + vm.startPrank(liquidator); + IERC20Detailed(tokenList.usdx).approve(address(contracts.poolProxy), type(uint256).max); + + vm.expectRevert(bytes(Errors.MUST_NOT_LEAVE_DUST)); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.usdx, + bob, + type(uint256).max, + false + ); + + // call with exact input + LiquidationDataProvider.LiquidationInfo memory liquidationInfo = liquidationDataProvider + .getLiquidationInfo(bob, tokenList.usdx, tokenList.usdx); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.usdx, + bob, + liquidationInfo.maxDebtToLiquidate, + false + ); + } + + // on aave v3.3 in certain edge scenarios, liquidation uint.max reverts on cf 50% + // the liquidationprovider should always return valid values + function test_liquidationdataprovider_edge_range_reverse() external { + // borrow supply 4k + _supplyToPool(tokenList.usdx, bob, 4200e6); + uint256 amount = (4000e8 * (10 ** IERC20Detailed(tokenList.weth).decimals())) / + contracts.aaveOracle.getAssetPrice(tokenList.weth); + _supplyToPool(tokenList.weth, bob, amount); + vm.prank(bob); + _borrowToBeBelowHf(bob, tokenList.usdx, 0.98 ether); + + vm.startPrank(liquidator); + IERC20Detailed(tokenList.usdx).approve(address(contracts.poolProxy), type(uint256).max); + + vm.expectRevert(bytes(Errors.MUST_NOT_LEAVE_DUST)); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.usdx, + bob, + type(uint256).max, + false + ); + + // call with exact input + LiquidationDataProvider.LiquidationInfo memory liquidationInfo = liquidationDataProvider + .getLiquidationInfo(bob, tokenList.usdx, tokenList.usdx); + contracts.poolProxy.liquidationCall( + tokenList.usdx, + tokenList.usdx, + bob, + liquidationInfo.maxDebtToLiquidate, + false + ); + } + function _liquidateAndValidateCloseFactor( address collateralAsset, address debtAsset, @@ -191,29 +266,29 @@ contract PoolLiquidationCloseFactorTests is TestnetProcedures { contracts.aaveOracle.getAssetPrice(debtAsset) ); // then we calculate the exact amounts - (uint256 collateralAmount, uint256 debtAmount, , ) = LiquidationHelper._getLiquidationParams( - contracts.poolProxy, - bob, - collateralAsset, - debtAsset, - amountToLiquidate - ); + LiquidationDataProvider.LiquidationInfo memory liquidationInfo = liquidationDataProvider + .getLiquidationInfo(bob, collateralAsset, debtAsset, amountToLiquidate); uint256 balanceBefore = IERC20Detailed(collateralAsset).balanceOf(liquidator); vm.prank(liquidator); IERC20Detailed(debtAsset).approve(address(contracts.poolProxy), type(uint256).max); vm.prank(liquidator); contracts.poolProxy.liquidationCall(collateralAsset, debtAsset, bob, amountToLiquidate, false); uint256 balanceAfter = IERC20Detailed(collateralAsset).balanceOf(liquidator); - assertEq(balanceAfter - balanceBefore, collateralAmount, 'WRONG_BALANCE'); - assertApproxEqAbs(debtAmountAt100.percentMul(closeFactor), debtAmount, 1, 'WRONG_CLOSE_FACTOR'); + assertEq( + balanceAfter - balanceBefore, + liquidationInfo.maxCollateralToLiquidate, + 'WRONG_BALANCE' + ); + assertApproxEqAbs( + debtAmountAt100.percentMul(closeFactor), + liquidationInfo.maxDebtToLiquidate, + 1, + 'WRONG_CLOSE_FACTOR' + ); } function _borrowToBeBelowHf(address user, address assetToBorrow, uint256 desiredhf) internal { - uint256 requiredBorrowsInBase = LiquidationHelper._getRequiredBorrowsForHfBelow( - contracts.poolProxy, - user, - desiredhf - ); + uint256 requiredBorrowsInBase = _getRequiredBorrowsForHfBelow(user, desiredhf); uint256 amount = (requiredBorrowsInBase * (10 ** IERC20Detailed(assetToBorrow).decimals())) / contracts.aaveOracle.getAssetPrice(assetToBorrow); vm.mockCall( @@ -245,4 +320,24 @@ contract PoolLiquidationCloseFactorTests is TestnetProcedures { contracts.poolProxy.supply(erc20, amount, user, 0); vm.stopPrank(); } + + /** + * @notice Returns the required amount of borrows in base currency to reach a certain healthfactor + */ + function _getRequiredBorrowsForHfBelow( + address user, + uint256 desiredHf + ) internal view returns (uint256) { + ( + uint256 totalCollateralBase, + uint256 totalBorrowsBase, + , + uint256 currentLiquidationThreshold, + , + + ) = contracts.poolProxy.getUserAccountData(user); + return + ((totalCollateralBase.percentMul(currentLiquidationThreshold + 1) * 1e18) / desiredHf) - + totalBorrowsBase; + } } diff --git a/tests/protocol/pool/Pool.Liquidations.t.sol b/tests/protocol/pool/Pool.Liquidations.t.sol index 39b80b61..8eb1ae3a 100644 --- a/tests/protocol/pool/Pool.Liquidations.t.sol +++ b/tests/protocol/pool/Pool.Liquidations.t.sol @@ -21,7 +21,7 @@ import {DataTypes} from '../../../src/contracts/protocol/libraries/types/DataTyp import {PercentageMath} from '../../../src/contracts/protocol/libraries/math/PercentageMath.sol'; import {WadRayMath} from '../../../src/contracts/protocol/libraries/math/WadRayMath.sol'; import {TestnetProcedures} from '../../utils/TestnetProcedures.sol'; -import {LiquidationHelper} from '../../helpers/LiquidationHelper.sol'; +import {LiquidationDataProvider} from '../../../src/contracts/helpers/LiquidationDataProvider.sol'; contract PoolLiquidationTests is TestnetProcedures { using stdStorage for StdStorage; @@ -39,6 +39,7 @@ contract PoolLiquidationTests is TestnetProcedures { PriceOracleSentinel internal priceOracleSentinel; SequencerOracle internal sequencerOracleMock; + LiquidationDataProvider internal liquidationDataProvider; event IsolationModeTotalDebtUpdated(address indexed asset, uint256 totalDebt); @@ -65,6 +66,10 @@ contract PoolLiquidationTests is TestnetProcedures { ISequencerOracle(address(sequencerOracleMock)), 1 days ); + liquidationDataProvider = new LiquidationDataProvider( + address(contracts.poolProxy), + address(contracts.poolAddressesProvider) + ); vm.prank(poolAdmin); sequencerOracleMock.setAnswer(false, 0); @@ -81,13 +86,8 @@ contract PoolLiquidationTests is TestnetProcedures { uint256 actualCollateralToLiquidate; bool receiveAToken; uint256 liquidationAmountInput; - address collateralPriceSource; - address debtPriceSource; - uint256 liquidationBonus; uint256 userCollateralBalance; uint256 priceImpactPercent; - uint256 liquidationProtocolFeeAmount; - uint256 collateralToLiquidateInBaseCurrency; uint256 totalCollateralInBaseCurrency; uint256 totalDebtInBaseCurrency; uint256 healthFactor; @@ -966,22 +966,8 @@ contract PoolLiquidationTests is TestnetProcedures { uint256 priceImpact ) internal returns (LiquidationInput memory) { LiquidationInput memory params; - params.collateralAsset = collateralAsset; - params.debtAsset = debtAsset; - params.user = user; - params.liquidationAmountInput = liquidationAmount; - ( - params.liquidationBonus, - params.collateralPriceSource, - params.debtPriceSource - ) = _getLiquidationBonus(params.user, params.collateralAsset, params.debtAsset); - params.receiveAToken = false; - (address aToken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses( - params.collateralAsset - ); - params.userCollateralBalance = IAToken(aToken).balanceOf(params.user); - params.priceImpactPercent = priceImpact; + params.priceImpactPercent = priceImpact; // This test expects oracle source is MockAggregator.sol stdstore .target(IAaveOracle(report.aaveOracle).getSourceOfAsset(priceImpactSource)) @@ -993,38 +979,24 @@ contract PoolLiquidationTests is TestnetProcedures { ) ); - ( - params.totalCollateralInBaseCurrency, - params.totalDebtInBaseCurrency, - , - , - , - params.healthFactor - ) = contracts.poolProxy.getUserAccountData(params.user); + LiquidationDataProvider.LiquidationInfo memory liquidationInfo = liquidationDataProvider + .getLiquidationInfo(user, collateralAsset, debtAsset, liquidationAmount); - uint256 maxLiquidatableDebt = LiquidationHelper._getMaxLiquidatableDebt( - contracts.poolProxy, - user, - params.collateralAsset, - params.debtAsset - ); + params.collateralAsset = collateralAsset; + params.debtAsset = debtAsset; + params.user = user; + params.liquidationAmountInput = liquidationAmount; + params.receiveAToken = false; - params.actualDebtToLiquidate = params.liquidationAmountInput > maxLiquidatableDebt - ? maxLiquidatableDebt - : params.liquidationAmountInput; + params.userCollateralBalance = liquidationInfo.collateralInfo.collateralBalance; + + params.totalCollateralInBaseCurrency = liquidationInfo.userInfo.totalCollateralInBaseCurrency; + params.totalDebtInBaseCurrency = liquidationInfo.userInfo.totalDebtInBaseCurrency; + params.healthFactor = liquidationInfo.userInfo.healthFactor; + + params.actualCollateralToLiquidate = liquidationInfo.maxCollateralToLiquidate; + params.actualDebtToLiquidate = liquidationInfo.maxDebtToLiquidate; - ( - params.actualCollateralToLiquidate, - params.actualDebtToLiquidate, - params.liquidationProtocolFeeAmount, - params.collateralToLiquidateInBaseCurrency - ) = LiquidationHelper._getLiquidationParams( - contracts.poolProxy, - params.user, - params.collateralAsset, - params.debtAsset, - params.actualDebtToLiquidate - ); return params; } @@ -1283,25 +1255,6 @@ contract PoolLiquidationTests is TestnetProcedures { vm.stopPrank(); } - function _getLiquidationBonus( - address user, - address collateralAsset, - address debtAsset - ) internal view returns (uint256, address, address) { - uint256 id = contracts.poolProxy.getUserEMode(user); - if (id != 0) { - DataTypes.CollateralConfig memory cfg = contracts.poolProxy.getEModeCategoryCollateralConfig( - uint8(id) - ); - return (cfg.liquidationBonus, collateralAsset, debtAsset); - } else { - DataTypes.ReserveConfigurationMap memory conf = contracts.poolProxy.getConfiguration( - debtAsset - ); - return (conf.getLiquidationBonus(), collateralAsset, debtAsset); - } - } - function _afterLiquidationChecksVariable( LiquidationInput memory params, address liquidator,