diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index b24eb0e8..153c45d0 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -431,6 +431,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { uint256 purchaseAmount, bool receiveAToken ) external override { + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index dcc3f268..643ad42f 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -21,6 +21,7 @@ import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; import {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; +import {ValidationLogic} from '../libraries/logic/ValidationLogic.sol'; /** * @title LendingPoolLiquidationManager contract @@ -89,17 +90,6 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 swappedCollateralAmount ); - enum LiquidationErrors { - NO_ERROR, - NO_COLLATERAL_AVAILABLE, - COLLATERAL_CANNOT_BE_LIQUIDATED, - CURRRENCY_NOT_BORROWED, - HEALTH_FACTOR_ABOVE_THRESHOLD, - NOT_ENOUGH_LIQUIDITY, - HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD, - INVALID_EQUAL_ASSETS_TO_SWAP - } - struct LiquidationCallLocalVars { uint256 userCollateralBalance; uint256 userStableDebt; @@ -116,6 +106,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { IAToken collateralAtoken; bool isCollateralEnabled; address principalAToken; + uint256 errorCode; + string errorMsg; } /** @@ -142,8 +134,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 purchaseAmount, bool receiveAToken ) external returns (uint256, string memory) { - ReserveLogic.ReserveData storage principalReserve = reserves[principal]; ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; + ReserveLogic.ReserveData storage principalReserve = reserves[principal]; UserConfiguration.Map storage userConfig = usersConfig[user]; LiquidationCallLocalVars memory vars; @@ -156,43 +148,29 @@ contract LendingPoolLiquidationManager is VersionedInitializable { addressesProvider.getPriceOracle() ); - if (vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { - return ( - uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), - Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD - ); - } - - vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); - - vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); - - vars.isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && - userConfig.isUsingAsCollateral(collateralReserve.id); - - //if collateral isn't enabled as collateral by user, it cannot be liquidated - if (!vars.isCollateralEnabled) { - return ( - uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), - Errors.COLLATERAL_CANNOT_BE_LIQUIDATED - ); - } - //if the user hasn't borrowed the specific currency defined by asset, it cannot be liquidated (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt( user, principalReserve ); - if (vars.userStableDebt == 0 && vars.userVariableDebt == 0) { - return ( - uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), - Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER - ); + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( + collateralReserve, + principalReserve, + userConfig, + vars.healthFactor, + vars.userStableDebt, + vars.userVariableDebt + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); } - //all clear - calculate the max principal amount that can be liquidated + vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); + + vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt).percentMul( LIQUIDATION_CLOSE_FACTOR_PERCENT ); @@ -228,7 +206,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { ); if (currentAvailableCollateral < vars.maxCollateralToLiquidate) { return ( - uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY), + uint256(Errors.LiquidationErrors.NOT_ENOUGH_LIQUIDITY), Errors.NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE ); } @@ -295,7 +273,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { receiveAToken ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** @@ -318,9 +296,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { address receiver, bytes calldata params ) external returns (uint256, string memory) { - ReserveLogic.ReserveData storage debtReserve = reserves[principal]; ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; - + ReserveLogic.ReserveData storage debtReserve = reserves[principal]; UserConfiguration.Map storage userConfig = usersConfig[user]; LiquidationCallLocalVars memory vars; @@ -333,36 +310,20 @@ contract LendingPoolLiquidationManager is VersionedInitializable { addressesProvider.getPriceOracle() ); - if ( - msg.sender != user && vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD - ) { - return ( - uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), - Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD - ); - } - - if (msg.sender != user) { - vars.isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && - userConfig.isUsingAsCollateral(collateralReserve.id); - - //if collateral isn't enabled as collateral by user, it cannot be liquidated - if (!vars.isCollateralEnabled) { - return ( - uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), - Errors.COLLATERAL_CANNOT_BE_LIQUIDATED - ); - } - } - (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve); - if (vars.userStableDebt == 0 && vars.userVariableDebt == 0) { - return ( - uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), - Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER - ); + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateRepayWithCollateral( + collateralReserve, + debtReserve, + userConfig, + user, + vars.healthFactor, + vars.userStableDebt, + vars.userVariableDebt + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); } vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); @@ -447,7 +408,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { vars.maxCollateralToLiquidate ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } struct AvailableCollateralToLiquidateLocalVars { @@ -534,7 +495,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { bytes calldata params ) external returns (uint256, string memory) { if (fromAsset == toAsset) { - return (uint256(LiquidationErrors.INVALID_EQUAL_ASSETS_TO_SWAP), Errors.INVALID_EQUAL_ASSETS_TO_SWAP); + return (uint256(Errors.LiquidationErrors.INVALID_EQUAL_ASSETS_TO_SWAP), Errors.INVALID_EQUAL_ASSETS_TO_SWAP); } ReserveLogic.ReserveData storage fromReserve = reserves[fromAsset]; @@ -580,11 +541,11 @@ contract LendingPoolLiquidationManager is VersionedInitializable { if (healthFactor < GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { return ( - uint256(LiquidationErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD), + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD), Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); } - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } } diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index f97b1cbe..3ab451b1 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -76,4 +76,16 @@ library Errors { string public constant MULTIPLICATION_OVERFLOW = '44'; string public constant ADDITION_OVERFLOW = '45'; string public constant DIVISION_BY_ZERO = '46'; + + enum LiquidationErrors { + NO_ERROR, + NO_COLLATERAL_AVAILABLE, + COLLATERAL_CANNOT_BE_LIQUIDATED, + CURRRENCY_NOT_BORROWED, + HEALTH_FACTOR_ABOVE_THRESHOLD, + NOT_ENOUGH_LIQUIDITY, + NO_ACTIVE_RESERVE, + HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD, + INVALID_EQUAL_ASSETS_TO_SWAP + } } diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index b88c065b..84c1d861 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -13,6 +13,7 @@ import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; import {UserConfiguration} from '../configuration/UserConfiguration.sol'; import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; import {Errors} from '../helpers/Errors.sol'; +import {Helpers} from '../helpers/Helpers.sol'; /** * @title ReserveLogic library @@ -329,4 +330,116 @@ library ValidationLogic { require(premium > 0, Errors.REQUESTED_AMOUNT_TOO_SMALL); require(mode <= uint256(ReserveLogic.InterestRateMode.VARIABLE), Errors.INVALID_FLASHLOAN_MODE); } + + /** + * @dev Validates the liquidationCall() action + * @param collateralReserve The reserve data of the collateral + * @param principalReserve The reserve data of the principal + * @param userConfig The user configuration + * @param userHealthFactor The user's health factor + * @param userStableDebt Total stable debt balance of the user + * @param userVariableDebt Total variable debt balance of the user + **/ + function validateLiquidationCall( + ReserveLogic.ReserveData storage collateralReserve, + ReserveLogic.ReserveData storage principalReserve, + UserConfiguration.Map storage userConfig, + uint256 userHealthFactor, + uint256 userStableDebt, + uint256 userVariableDebt + ) internal view returns(uint256, string memory) { + if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { + return ( + uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), + Errors.NO_ACTIVE_RESERVE + ); + } + + if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD + ); + } + + bool isCollateralEnabled = + collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.id); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!isCollateralEnabled) { + return ( + uint256(Errors.LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + Errors.COLLATERAL_CANNOT_BE_LIQUIDATED + ); + } + + if (userStableDebt == 0 && userVariableDebt == 0) { + return ( + uint256(Errors.LiquidationErrors.CURRRENCY_NOT_BORROWED), + Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + } + + /** + * @dev Validates the repayWithCollateral() action + * @param collateralReserve The reserve data of the collateral + * @param principalReserve The reserve data of the principal + * @param userConfig The user configuration + * @param user The address of the user + * @param userHealthFactor The user's health factor + * @param userStableDebt Total stable debt balance of the user + * @param userVariableDebt Total variable debt balance of the user + **/ + function validateRepayWithCollateral( + ReserveLogic.ReserveData storage collateralReserve, + ReserveLogic.ReserveData storage principalReserve, + UserConfiguration.Map storage userConfig, + address user, + uint256 userHealthFactor, + uint256 userStableDebt, + uint256 userVariableDebt + ) internal view returns(uint256, string memory) { + if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { + return ( + uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), + Errors.NO_ACTIVE_RESERVE + ); + } + + if ( + msg.sender != user && userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD + ); + } + + if (msg.sender != user) { + bool isCollateralEnabled = + collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.id); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!isCollateralEnabled) { + return ( + uint256(Errors.LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + Errors.COLLATERAL_CANNOT_BE_LIQUIDATED + ); + } + } + + if (userStableDebt == 0 && userVariableDebt == 0) { + return ( + uint256(Errors.LiquidationErrors.CURRRENCY_NOT_BORROWED), + Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + } } diff --git a/test/liquidation-underlying.spec.ts b/test/liquidation-underlying.spec.ts index e40ec406..26f10ced 100644 --- a/test/liquidation-underlying.spec.ts +++ b/test/liquidation-underlying.spec.ts @@ -7,6 +7,7 @@ import {makeSuite} from './helpers/make-suite'; import {ProtocolErrors, RateMode} from '../helpers/types'; import {calcExpectedStableDebtTokenBalance} from './helpers/utils/calculations'; import {getUserData} from './helpers/utils/helpers'; +import {parseEther} from 'ethers/lib/utils'; const chai = require('chai'); @@ -23,6 +24,26 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', BigNumber.config({DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP}); }); + it("It's not possible to liquidate on a non-active collateral or a non active principal", async () => { + const {configurator, weth, pool, users, dai} = testEnv; + const user = users[1]; + await configurator.deactivateReserve(weth.address); + + await expect( + pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(weth.address); + + await configurator.deactivateReserve(dai.address); + + await expect( + pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(dai.address); + }); + it('LIQUIDATION - Deposits WETH, borrows DAI', async () => { const {dai, weth, users, pool, oracle} = testEnv; const depositor = users[0]; diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index 9c169f10..abe7b680 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -39,6 +39,44 @@ export const expectRepayWithCollateralEvent = ( }; makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { + it("It's not possible to repayWithCollateral() on a non-active collateral or a non active principal", async () => { + const {configurator, weth, pool, users, dai, mockSwapAdapter} = testEnv; + const user = users[1]; + await configurator.deactivateReserve(weth.address); + + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + parseEther('100'), + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(weth.address); + + await configurator.deactivateReserve(dai.address); + + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + parseEther('100'), + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(dai.address); + }); + it('User 1 provides some liquidity for others to borrow', async () => { const {pool, weth, dai, usdc, deployer} = testEnv;