diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index d2902946..92cf4e9b 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -299,6 +299,22 @@ interface ILendingPool { uint16 referralCode ) external; + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external; + /** * @dev accessory functions to fetch data from the core contract **/ diff --git a/contracts/interfaces/ISwapAdapter.sol b/contracts/interfaces/ISwapAdapter.sol index c51ee0fd..ed91f95f 100644 --- a/contracts/interfaces/ISwapAdapter.sol +++ b/contracts/interfaces/ISwapAdapter.sol @@ -2,20 +2,19 @@ pragma solidity ^0.6.8; interface ISwapAdapter { - - /** - * @dev Swaps an `amountToSwap` of an asset to another, approving a `fundsDestination` to pull the funds - * @param assetToSwapFrom Origin asset - * @param assetToSwapTo Destination asset - * @param amountToSwap How much `assetToSwapFrom` needs to be swapped - * @param fundsDestination Address that will be pulling the swapped funds - * @param params Additional variadic field to include extra params - */ - function executeOperation( - address assetToSwapFrom, - address assetToSwapTo, - uint256 amountToSwap, - address fundsDestination, - bytes calldata params - ) external; -} \ No newline at end of file + /** + * @dev Swaps an `amountToSwap` of an asset to another, approving a `fundsDestination` to pull the funds + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap How much `assetToSwapFrom` needs to be swapped + * @param fundsDestination Address that will be pulling the swapped funds + * @param params Additional variadic field to include extra params + */ + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external; +} diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index f61c6b20..5700fc69 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -20,6 +20,7 @@ import {UserConfiguration} from '../libraries/configuration/UserConfiguration.so import {IStableDebtToken} from '../tokenization/interfaces/IStableDebtToken.sol'; import {IVariableDebtToken} from '../tokenization/interfaces/IVariableDebtToken.sol'; import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; +import {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; import {LendingPoolLiquidationManager} from './LendingPoolLiquidationManager.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; @@ -584,6 +585,43 @@ contract LendingPool is VersionedInitializable, PausablePool, ILendingPool { } } + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external override { + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); + + //solium-disable-next-line + (bool success, bytes memory result) = liquidationManager.delegatecall( + abi.encodeWithSignature( + 'swapLiquidity(address,address,address,uint256,bytes)', + receiverAddress, + fromAsset, + toAsset, + amountToSwap, + params + ) + ); + require(success, Errors.FAILED_COLLATERAL_SWAP); + + (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); + + if (returnCode != 0) { + revert(string(abi.encodePacked(returnMessage))); + } + } + /** * @dev accessory functions to fetch data from the core contract **/ diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index e2080611..cccca4fa 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -22,6 +22,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 @@ -91,15 +92,6 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { uint256 swappedCollateralAmount ); - enum LiquidationErrors { - NO_ERROR, - NO_COLLATERAL_AVAILABLE, - COLLATERAL_CANNOT_BE_LIQUIDATED, - CURRRENCY_NOT_BORROWED, - HEALTH_FACTOR_ABOVE_THRESHOLD, - NOT_ENOUGH_LIQUIDITY - } - struct LiquidationCallLocalVars { uint256 userCollateralBalance; uint256 userStableDebt; @@ -115,6 +107,29 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { uint256 healthFactor; IAToken collateralAtoken; bool isCollateralEnabled; + address principalAToken; + uint256 errorCode; + string errorMsg; + } + + struct SwapLiquidityLocalVars { + uint256 healthFactor; + uint256 amountToReceive; + uint256 userBalanceBefore; + IAToken fromReserveAToken; + IAToken toReserveAToken; + uint256 errorCode; + string errorMsg; + } + + struct AvailableCollateralToLiquidateLocalVars { + uint256 userCompoundedBorrowBalance; + uint256 liquidationBonus; + uint256 collateralPrice; + uint256 principalCurrencyPrice; + uint256 maxAmountCollateralToLiquidate; + uint256 principalDecimals; + uint256 collateralDecimals; } /** @@ -141,8 +156,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { 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; @@ -155,43 +170,29 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { 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 ); @@ -227,7 +228,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { ); if (currentAvailableCollateral < vars.maxCollateralToLiquidate) { return ( - uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY), + uint256(Errors.LiquidationErrors.NOT_ENOUGH_LIQUIDITY), Errors.NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE ); } @@ -299,7 +300,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { receiveAToken ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** @@ -322,9 +323,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { 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; @@ -337,36 +337,20 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { 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); @@ -410,7 +394,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { usersConfig[user].setUsingAsCollateral(collateralReserve.id, false); } - address principalAToken = debtReserve.aTokenAddress; + vars.principalAToken = debtReserve.aTokenAddress; // Notifies the receiver to proceed, sending as param the underlying already transferred ISwapAdapter(receiver).executeOperation( @@ -423,8 +407,13 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { //updating debt reserve debtReserve.updateCumulativeIndexesAndTimestamp(); - debtReserve.updateInterestRates(principal, principalAToken, vars.actualAmountToLiquidate, 0); - IERC20(principal).transferFrom(receiver, principalAToken, vars.actualAmountToLiquidate); + debtReserve.updateInterestRates( + principal, + vars.principalAToken, + vars.actualAmountToLiquidate, + 0 + ); + IERC20(principal).transferFrom(receiver, vars.principalAToken, vars.actualAmountToLiquidate); if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( @@ -456,17 +445,100 @@ contract LendingPoolLiquidationManager is VersionedInitializable, Pausable { vars.maxCollateralToLiquidate ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } - struct AvailableCollateralToLiquidateLocalVars { - uint256 userCompoundedBorrowBalance; - uint256 liquidationBonus; - uint256 collateralPrice; - uint256 principalCurrencyPrice; - uint256 maxAmountCollateralToLiquidate; - uint256 principalDecimals; - uint256 collateralDecimals; + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external returns (uint256, string memory) { + ReserveLogic.ReserveData storage fromReserve = reserves[fromAsset]; + ReserveLogic.ReserveData storage toReserve = reserves[toAsset]; + + // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables + SwapLiquidityLocalVars memory vars; + + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateSwapLiquidity( + fromReserve, + toReserve, + fromAsset, + toAsset + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); + } + + vars.fromReserveAToken = IAToken(fromReserve.aTokenAddress); + vars.toReserveAToken = IAToken(toReserve.aTokenAddress); + + fromReserve.updateCumulativeIndexesAndTimestamp(); + toReserve.updateCumulativeIndexesAndTimestamp(); + + if (vars.fromReserveAToken.balanceOf(msg.sender) == amountToSwap) { + usersConfig[msg.sender].setUsingAsCollateral(fromReserve.id, false); + } + + fromReserve.updateInterestRates(fromAsset, address(vars.fromReserveAToken), 0, amountToSwap); + + vars.fromReserveAToken.burn( + msg.sender, + receiverAddress, + amountToSwap, + fromReserve.liquidityIndex + ); + // Notifies the receiver to proceed, sending as param the underlying already transferred + ISwapAdapter(receiverAddress).executeOperation( + fromAsset, + toAsset, + amountToSwap, + address(this), + params + ); + + vars.amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); + if (vars.amountToReceive != 0) { + IERC20(toAsset).transferFrom( + receiverAddress, + address(vars.toReserveAToken), + vars.amountToReceive + ); + vars.toReserveAToken.mint(msg.sender, vars.amountToReceive, toReserve.liquidityIndex); + toReserve.updateInterestRates( + toAsset, + address(vars.toReserveAToken), + vars.amountToReceive, + 0 + ); + } + + (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + msg.sender, + reserves, + usersConfig[msg.sender], + reservesList, + addressesProvider.getPriceOracle() + ); + + if (vars.healthFactor < GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD), + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index c341c56c..98dd62d4 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -39,8 +39,10 @@ library Errors { string public constant CALLER_NOT_LENDING_POOL_CONFIGURATOR = '27'; // 'The actual balance of the protocol is inconsistent' string public constant INVALID_FLASHLOAN_MODE = '43'; //Invalid flashloan mode selected string public constant BORROW_ALLOWANCE_ARE_NOT_ENOUGH = '54'; // User borrows on behalf, but allowance are too small - string public constant REENTRANCY_NOT_ALLOWED = '52'; + string public constant REENTRANCY_NOT_ALLOWED = '57'; string public constant FAILED_REPAY_WITH_COLLATERAL = '53'; + string public constant FAILED_COLLATERAL_SWAP = '55'; + string public constant INVALID_EQUAL_ASSETS_TO_SWAP = '56'; // require error messages - aToken string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool' @@ -78,4 +80,15 @@ library Errors { // pausable error message string public constant IS_PAUSED = '54'; // 'Pool is paused' string public constant NOT_PAUSED = '55'; // 'Pool is not paused' + 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/ReserveLogic.sol b/contracts/libraries/logic/ReserveLogic.sol index e8025b43..f8ce2220 100644 --- a/contracts/libraries/logic/ReserveLogic.sol +++ b/contracts/libraries/logic/ReserveLogic.sol @@ -187,7 +187,7 @@ library ReserveLogic { ReserveData storage reserve, uint256 totalLiquidity, uint256 amount - ) internal { + ) external { uint256 amountToLiquidityRatio = amount.wadToRay().rayDiv(totalLiquidity.wadToRay()); uint256 result = amountToLiquidityRatio.add(WadRayMath.ray()); @@ -249,7 +249,7 @@ library ReserveLogic { address aTokenAddress, uint256 liquidityAdded, uint256 liquidityTaken - ) internal { + ) external { UpdateInterestRatesLocalVars memory vars; vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress; diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index b88c065b..336e18ad 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,139 @@ 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); + } + + /** + * @dev Validates the swapLiquidity() action + * @param fromReserve The reserve data of the asset to swap from + * @param toReserve The reserve data of the asset to swap to + * @param fromAsset Address of the asset to swap from + * @param toAsset Address of the asset to swap to + **/ + function validateSwapLiquidity( + ReserveLogic.ReserveData storage fromReserve, + ReserveLogic.ReserveData storage toReserve, + address fromAsset, + address toAsset + ) internal view returns (uint256, string memory) { + if (!fromReserve.configuration.getActive() || !toReserve.configuration.getActive()) { + return (uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), Errors.NO_ACTIVE_RESERVE); + } + + if (fromAsset == toAsset) { + return ( + uint256(Errors.LiquidationErrors.INVALID_EQUAL_ASSETS_TO_SWAP), + Errors.INVALID_EQUAL_ASSETS_TO_SWAP + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + } } diff --git a/contracts/mocks/flashloan/MockSwapAdapter.sol b/contracts/mocks/flashloan/MockSwapAdapter.sol index 85c7d84b..08658d4b 100644 --- a/contracts/mocks/flashloan/MockSwapAdapter.sol +++ b/contracts/mocks/flashloan/MockSwapAdapter.sol @@ -4,57 +4,56 @@ pragma solidity ^0.6.8; import {MintableERC20} from '../tokens/MintableERC20.sol'; import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; import {ISwapAdapter} from '../../interfaces/ISwapAdapter.sol'; -import {ILendingPool} from "../../interfaces/ILendingPool.sol"; +import {ILendingPool} from '../../interfaces/ILendingPool.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; contract MockSwapAdapter is ISwapAdapter { + uint256 internal _amountToReturn; + bool internal _tryReentrancy; + ILendingPoolAddressesProvider public addressesProvider; - uint256 internal _amountToReturn; - bool internal _tryReentrancy; - ILendingPoolAddressesProvider public addressesProvider; + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); - event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + constructor(ILendingPoolAddressesProvider provider) public { + addressesProvider = provider; + } - constructor(ILendingPoolAddressesProvider provider) public { - addressesProvider = provider; + function setAmountToReturn(uint256 amount) public { + _amountToReturn = amount; + } + + function setTryReentrancy(bool tryReentrancy) public { + _tryReentrancy = tryReentrancy; + } + + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external override { + params; + IERC20(assetToSwapFrom).transfer(address(1), amountToSwap); // We don't want to keep funds here + MintableERC20(assetToSwapTo).mint(_amountToReturn); + IERC20(assetToSwapTo).approve(fundsDestination, _amountToReturn); + + if (_tryReentrancy) { + ILendingPool(fundsDestination).repayWithCollateral( + assetToSwapFrom, + assetToSwapTo, + address(1), // Doesn't matter, we just want to test the reentrancy + 1 ether, // Same + address(1), // Same + '0x' + ); } - function setAmountToReturn(uint256 amount) public { - _amountToReturn = amount; - } + emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, _amountToReturn); + } - function setTryReentrancy(bool tryReentrancy) public { - _tryReentrancy = tryReentrancy; - } - - function executeOperation( - address assetToSwapFrom, - address assetToSwapTo, - uint256 amountToSwap, - address fundsDestination, - bytes calldata params - ) external override { - params; - IERC20(assetToSwapFrom).transfer(address(1), amountToSwap); // We don't want to keep funds here - MintableERC20(assetToSwapTo).mint(_amountToReturn); - IERC20(assetToSwapTo).approve(fundsDestination, _amountToReturn); - - if (_tryReentrancy) { - ILendingPool(fundsDestination).repayWithCollateral( - assetToSwapFrom, - assetToSwapTo, - address(1), // Doesn't matter, we just want to test the reentrancy - 1 ether, // Same - address(1), // Same - "0x" - ); - } - - emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, _amountToReturn); - } - - function burnAsset(IERC20 asset, uint256 amount) public { - uint256 amountToBurn = (amount == type(uint256).max) ? asset.balanceOf(address(this)) : amount; - asset.transfer(address(0), amountToBurn); - } -} \ No newline at end of file + function burnAsset(IERC20 asset, uint256 amount) public { + uint256 amountToBurn = (amount == type(uint256).max) ? asset.balanceOf(address(this)) : amount; + asset.transfer(address(0), amountToBurn); + } +} diff --git a/deployed-contracts.json b/deployed-contracts.json index d3f80214..9981b17c 100644 --- a/deployed-contracts.json +++ b/deployed-contracts.json @@ -493,9 +493,6 @@ "MockSwapAdapter": { "buidlerevm": { "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2" - }, - "localhost": { - "address": "0x749258D38b0473d96FEcc14cC5e7DCE12d7Bd6f6" } } -} \ No newline at end of file +} diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts new file mode 100644 index 00000000..cbae27f5 --- /dev/null +++ b/test/collateral-swap.spec.ts @@ -0,0 +1,198 @@ +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {MockSwapAdapter} from '../types/MockSwapAdapter'; +import {getMockSwapAdapter} from '../helpers/contracts-helpers'; +import {ProtocolErrors} from '../helpers/types'; +import {ethers} from 'ethers'; +import {APPROVAL_AMOUNT_LENDING_POOL} from '../helpers/constants'; +import {getContractsData, getTxCostAndTimestamp} from './helpers/actions'; +import {calcExpectedATokenBalance} from './helpers/utils/calculations'; +import {waitForTx} from './__setup.spec'; +import {advanceBlock, timeLatest} from '../helpers/misc-utils'; + +const {expect} = require('chai'); + +makeSuite('LendingPool SwapDeposit function', (testEnv: TestEnv) => { + let _mockSwapAdapter = {} as MockSwapAdapter; + const {HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD} = ProtocolErrors; + + before(async () => { + _mockSwapAdapter = await getMockSwapAdapter(); + }); + + it('Deposits WETH into the reserve', async () => { + const {pool, weth, users} = testEnv; + const amountToDeposit = ethers.utils.parseEther('1'); + + for (const signer of [weth.signer, users[2].signer]) { + const connectedWETH = weth.connect(signer); + await connectedWETH.mint(amountToDeposit); + await connectedWETH.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool + .connect(signer) + .deposit(weth.address, amountToDeposit, await signer.getAddress(), '0'); + } + }); + it('User tries to swap more then he can, revert expected', async () => { + const {pool, weth, dai} = testEnv; + await expect( + pool.swapLiquidity( + _mockSwapAdapter.address, + weth.address, + dai.address, + ethers.utils.parseEther('1.1'), + '0x10' + ) + ).to.be.revertedWith('55'); + }); + + it('User tries to swap asset on equal asset, revert expected', async () => { + const {pool, weth} = testEnv; + await expect( + pool.swapLiquidity( + _mockSwapAdapter.address, + weth.address, + weth.address, + ethers.utils.parseEther('0.1'), + '0x10' + ) + ).to.be.revertedWith('56'); + }); + + it('User tries to swap more then available on the reserve', async () => { + const {pool, weth, dai, users, aEth, deployer} = testEnv; + + await pool.borrow(weth.address, ethers.utils.parseEther('0.1'), 1, 0, deployer.address); + await pool.connect(users[2].signer).withdraw(weth.address, ethers.utils.parseEther('1')); + + await expect( + pool.swapLiquidity( + _mockSwapAdapter.address, + weth.address, + dai.address, + ethers.utils.parseEther('1'), + '0x10' + ) + ).to.be.revertedWith('55'); + }); + + it('User tries to swap correct amount', async () => { + const {pool, weth, dai, aEth, aDai} = testEnv; + const userAddress = await pool.signer.getAddress(); + const amountToSwap = ethers.utils.parseEther('0.25'); + + const amountToReturn = ethers.utils.parseEther('0.5'); + await _mockSwapAdapter.setAmountToReturn(amountToReturn); + + const { + reserveData: wethReserveDataBefore, + userData: wethUserDataBefore, + } = await getContractsData(weth.address, userAddress, testEnv); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + userAddress, + testEnv + ); + + const reserveBalanceWETHBefore = await weth.balanceOf(aEth.address); + const reserveBalanceDAIBefore = await dai.balanceOf(aDai.address); + + const txReceipt = await waitForTx( + await pool.swapLiquidity( + _mockSwapAdapter.address, + weth.address, + dai.address, + amountToSwap, + '0x10' + ) + ); + const {txTimestamp} = await getTxCostAndTimestamp(txReceipt); + const userATokenBalanceWETHAfter = await aEth.balanceOf(userAddress); + const userATokenBalanceDAIAfter = await aDai.balanceOf(userAddress); + + const reserveBalanceWETHAfter = await weth.balanceOf(aEth.address); + const reserveBalanceDAIAfter = await dai.balanceOf(aDai.address); + + expect(userATokenBalanceWETHAfter.toString()).to.be.equal( + calcExpectedATokenBalance(wethReserveDataBefore, wethUserDataBefore, txTimestamp) + .minus(amountToSwap.toString()) + .toString(), + 'was burned incorrect amount of user funds' + ); + expect(userATokenBalanceDAIAfter.toString()).to.be.equal( + calcExpectedATokenBalance(daiReserveDataBefore, daiUserDataBefore, txTimestamp) + .plus(amountToReturn.toString()) + .toString(), + 'was minted incorrect amount of user funds' + ); + + expect(reserveBalanceWETHAfter.toString()).to.be.equal( + reserveBalanceWETHBefore.sub(amountToSwap).toString(), + 'was sent incorrect amount if reserve funds' + ); + expect(reserveBalanceDAIAfter.toString()).to.be.equal( + reserveBalanceDAIBefore.add(amountToReturn).toString(), + 'was received incorrect amount if reserve funds' + ); + }); + + it('User tries to drop HF below one', async () => { + const {pool, weth, dai, deployer} = testEnv; + const amountToSwap = ethers.utils.parseEther('0.3'); + + const amountToReturn = ethers.utils.parseEther('0.5'); + await _mockSwapAdapter.setAmountToReturn(amountToReturn); + + await pool.borrow(weth.address, ethers.utils.parseEther('0.3'), 1, 0, deployer.address); + + await expect( + pool.swapLiquidity(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') + ).to.be.revertedWith(HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); + }); + + it('Should set usage as collateral to false if no leftovers after swap', async () => { + const {pool, weth, dai, aEth, users} = testEnv; + const userAddress = await pool.signer.getAddress(); + + // add more liquidity to allow user 0 to swap everything he has + await weth.connect(users[2].signer).mint(ethers.utils.parseEther('1')); + await pool + .connect(users[2].signer) + .deposit(weth.address, ethers.utils.parseEther('1'), users[2].address, '0'); + + // cleanup borrowings, to be abe to swap whole weth + const amountToRepay = ethers.utils.parseEther('0.5'); + await weth.mint(amountToRepay); + await pool.repay(weth.address, amountToRepay, '1', userAddress); + const txTimestamp = (await timeLatest()).plus(100); + + const { + reserveData: wethReserveDataBefore, + userData: wethUserDataBefore, + } = await getContractsData(weth.address, userAddress, testEnv); + const amountToSwap = calcExpectedATokenBalance( + wethReserveDataBefore, + wethUserDataBefore, + txTimestamp.plus('1') + ); + + await advanceBlock(txTimestamp.toNumber()); + + await pool.swapLiquidity( + _mockSwapAdapter.address, + weth.address, + dai.address, + amountToSwap.toString(), + '0x10' + ); + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + userAddress, + testEnv + ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.equal( + false, + 'usageAsCollateralEnabled are not set to false' + ); + }); +}); diff --git a/test/flashloan.spec.ts b/test/flashloan.spec.ts index 3fb2f4b2..036a30fd 100644 --- a/test/flashloan.spec.ts +++ b/test/flashloan.spec.ts @@ -1,3 +1,5 @@ +import BigNumber from 'bignumber.js'; + import {TestEnv, makeSuite} from './helpers/make-suite'; import {APPROVAL_AMOUNT_LENDING_POOL, oneRay} from '../helpers/constants'; import { @@ -8,7 +10,6 @@ import { import {ethers} from 'ethers'; import {MockFlashLoanReceiver} from '../types/MockFlashLoanReceiver'; import {ProtocolErrors, eContractid} from '../helpers/types'; -import BigNumber from 'bignumber.js'; import {VariableDebtToken} from '../types/VariableDebtToken'; import {StableDebtToken} from '../types/StableDebtToken'; @@ -29,7 +30,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); }); - it('Deposits ETH into the reserve', async () => { + it('Deposits WETH into the reserve', async () => { const {pool, weth} = testEnv; const userAddress = await pool.signer.getAddress(); const amountToDeposit = ethers.utils.parseEther('1'); diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index d03ecb3f..3e60955d 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -720,7 +720,7 @@ const getDataBeforeAction = async ( }; }; -const getTxCostAndTimestamp = async (tx: ContractReceipt) => { +export const getTxCostAndTimestamp = async (tx: ContractReceipt) => { if (!tx.blockNumber || !tx.transactionHash || !tx.cumulativeGasUsed) { throw new Error('No tx blocknumber'); } diff --git a/test/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 23f2a374..98431d36 100644 --- a/test/helpers/utils/calculations.ts +++ b/test/helpers/utils/calculations.ts @@ -539,7 +539,7 @@ export const calcExpectedUserDataAfterBorrow = ( currentTimestamp ); expectedUserData.scaledATokenBalance = userDataBeforeAction.scaledATokenBalance; - + expectedUserData.walletBalance = userDataBeforeAction.walletBalance.plus(amountBorrowed); return expectedUserData; @@ -623,7 +623,7 @@ export const calcExpectedUserDataAfterRepay = ( txTimestamp ); expectedUserData.scaledATokenBalance = userDataBeforeAction.scaledATokenBalance; - + if (user === onBehalfOf) { expectedUserData.walletBalance = userDataBeforeAction.walletBalance.minus(totalRepaid); } else { @@ -919,16 +919,14 @@ const calcExpectedScaledATokenBalance = ( .minus(amountTaken.rayDiv(index)); }; -const calcExpectedATokenBalance = ( +export const calcExpectedATokenBalance = ( reserveDataBeforeAction: ReserveData, userDataBeforeAction: UserReserveData, currentTimestamp: BigNumber ) => { const index = calcExpectedReserveNormalizedIncome(reserveDataBeforeAction, currentTimestamp); - const { - scaledATokenBalance: scaledBalanceBeforeAction, - } = userDataBeforeAction; + const {scaledATokenBalance: scaledBalanceBeforeAction} = userDataBeforeAction; return scaledBalanceBeforeAction.rayMul(index); }; 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 69c7a8b1..41e9060f 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -40,6 +40,43 @@ export const expectRepayWithCollateralEvent = ( makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { const {IS_PAUSED} = ProtocolErrors; + 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;