diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 43bfb554..b773a638 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -231,6 +231,27 @@ interface ILendingPool { bool receiveAToken ) external; + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it + * - The owner can repay with his collateral at any point, no matter the health factor + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below + * @param collateral The address of the collateral asset + * @param principal The address of the owed asset + * @param user Address of the borrower + * @param principalAmount Amount of the debt to repay. type(uint256).max to repay the maximum possible + * @param receiver Address of the contract receiving the collateral to swap + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external; + /** * @dev allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index b1b20a5a..1d266080 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -455,6 +455,49 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { } } + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it + * - The owner can repay with his collateral at any point, no matter the health factor + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below + * @param collateral The address of the collateral asset + * @param principal The address of the owed asset + * @param user Address of the borrower + * @param principalAmount Amount of the debt to repay. type(uint256).max to repay the maximum possible + * @param receiver Address of the contract receiving the collateral to swap + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external override nonReentrant { + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); + + //solium-disable-next-line + (bool success, bytes memory result) = liquidationManager.delegatecall( + abi.encodeWithSignature( + 'repayWithCollateral(address,address,address,uint256,address,bytes)', + collateral, + principal, + user, + principalAmount, + receiver, + params + ) + ); + require(success, 'FAILED_REPAY_WITH_COLLATERAL'); + + (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); + + if (returnCode != 0) { + revert(string(abi.encodePacked(returnMessage))); + } + } + /** * @dev allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts @@ -519,115 +562,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { emit FlashLoan(receiverAddress, asset, amount, amountFee); } - /** - * @dev flashes collateral, by both a flash liquidator or the user owning it. - * @param collateralAsset The address of the collateral asset. - * @param debtAsset The address of the debt asset. - * @param collateralAmount Collateral amount to flash. - * @param user Address of the user owning the collateral. - * @param receiverAddress Address of the contract receiving the collateral. - * @param debtMode Numeric variable, managing how to operate with the debt side. - * 1 -> With final repayment, to do it on the stable debt. - * 2 -> With final repayment, to do it on the variable debt. - * 3 -> On movement of the debt to the liquidator, to move the stable debt - * 4 -> On movement of the debt to the liquidator, to move the variable debt - * @param receiveAToken "true" to send aToken to the receiver contract, "false" to send underlying tokens. - * @param referralCode Integrators are assigned a referral code and can potentially receive rewards. - **/ - function flashCollateral( - address collateralAsset, - address debtAsset, - uint256 collateralAmount, - address user, - address receiverAddress, - uint256 debtMode, - bool receiveAToken, - uint16 referralCode - ) external override { - require(debtMode > 0, 'INVALID_DEBT_FLAG'); - - ReserveLogic.ReserveData storage collateralReserve = _reserves[collateralAsset]; - ReserveLogic.ReserveData storage debtReserve = _reserves[debtAsset]; - - address collateralAToken = collateralReserve.aTokenAddress; - uint256 availableCollateral = IERC20(collateralAToken).balanceOf(user); - - require(collateralAmount <= availableCollateral, 'NOT_ENOUGH_BALANCE'); - - address oracle = addressesProvider.getPriceOracle(); - (, , , , healthFactor) = GenericLogic.calculateUserAccountData( - user, - _reserves, - _usersConfig[user], - _reservesList, - oracle - ); - - if (healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && msg.sender != user) { - revert('INVALID_FLASH_COLLATERAL_BY_NON_OWNER'); - } - - uint256 amountToFlash = (msg.sender == user || healthFactor < 0.98 ether) // TODO: better constant - ? collateralAmount - : collateralAmount.div(2); // TODO: better constant - - // If liquidator reclaims the aToken, he receives the equivalent atoken amount, - // otherwise receives the underlying asset - if (receiveAToken) { - IAToken(collateralAToken).transferOnLiquidation(user, receiverAddress, amountToFlash); - } else { - collateralReserve.updateCumulativeIndexesAndTimestamp(); - collateralReserve.updateInterestRates(collateral, aTokenAddress, 0, collateralAmount); - - // Burn of aToken and send the underlying to the receiver - IAToken(aTokenAddress).burn(user, receiver, collateralAmount); - } - - // Notifies the receiver to proceed, sending the underlying or the aToken amount already transferred - IFlashLoanReceiver(receiverAddress).executeOperation( - collateralAsset, - aTokenAddress, - (!receiveAToken) ? collateralAmount : 0, - receiverAToken ? aTokenAmount : 0, - params - ); - - // Calculation of the minimum amount of the debt asset to be received - uint256 debtAmountNeeded = oracle - .getAssetPrice(collateralAsset) - .mul(collateralAmount) - .mul(10**debtReserve.configuration.getDecimals()) - .div(oracle.getAssetPrice(debtAsset).mul(10**collateralReserve.configuration.getDecimals())) - .percentDiv(collateralReserve.configuration.getLiquidationBonus()); - - // Or the debt is transferred to the msg.sender, or funds are transferred from the receiver to repay the debt - if (debtMode > 2) { - (uint256 userStableDebt, uint256 userVariableDebt) = Helpers.getUserCurrentDebt( - user, - debtReserve - ); - - uint256 debtToTransfer; - if (debtMode.div(3) == 1) { - // stable - debtToTransfer = (userStableDebt > debtAmountNeeded) ? debtAmountNeeded : userStableDebt; - IStableDebtToken(debtReserve.stableDebtTokenAddress).burn(user, debtToTransfer); - } else if (debtMode.div(3) == 2) { - // variable - debtToTransfer = (userVariableDebt > debtAmountNeeded) - ? debtAmountNeeded - : userVariableDebt; - IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn(user, debtToTransfer); - } - _executeBorrow( - BorrowLocalVars(debtAsset, msg.sender, debtToTransfer, debt, false, referralCode) - ); - } else { - IERC20(debtAsset).transferFrom(receiverAddress, address(this), debtAmountNeeded); - _executeRepay(asset, msg.sender, amount, debtMode, user); - } - } - /** * @dev accessory functions to fetch data from the core contract **/ diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index a36ed3f3..7bbf149b 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -21,6 +21,7 @@ import {Helpers} from '../libraries/helpers/Helpers.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; /** * @title LendingPoolLiquidationManager contract @@ -65,6 +66,24 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl bool receiveAToken ); + /** + @dev emitted when a borrower/liquidator repays with the borrower's collateral + @param collateral the address of the collateral being swapped to repay + @param principal the address of the reserve of the debt + @param user the borrower's address + @param liquidator the address of the liquidator, same as the one of the borrower on self-repayment + @param principalAmount the amount of the debt finally covered + @param swappedCollateralAmount the amount of collateral finally swapped + */ + event RepaidWithCollateral( + address indexed collateral, + address indexed principal, + address indexed user, + address liquidator, + uint256 principalAmount, + uint256 swappedCollateralAmount + ); + enum LiquidationErrors { NO_ERROR, NO_COLLATERAL_AVAILABLE, @@ -271,6 +290,166 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl return (uint256(LiquidationErrors.NO_ERROR), 'No errors'); } + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it. + * - The owner can repay with his collateral at any point, no matter the health factor. + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below. + * @param collateral The address of the collateral asset. + * @param principal The address of the owed asset. + * @param user Address of the borrower. + * @param principalAmount Amount of the debt to repay. + * @param receiver Address of the contract receiving the collateral to swap. + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external returns (uint256, string memory) { + ReserveLogic.ReserveData storage debtReserve = reserves[principal]; + ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; + + UserConfiguration.Map storage userConfig = usersConfig[user]; + + LiquidationCallLocalVars memory vars; + + (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + user, + reserves, + usersConfig[user], + reservesList, + addressesProvider.getPriceOracle() + ); + + if (msg.sender != user && vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + 'HEALTH_FACTOR_ABOVE_THRESHOLD' + ); + } + + if (msg.sender != user) { + vars.isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.index); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!vars.isCollateralEnabled) { + return ( + uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + '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), + 'CURRRENCY_NOT_BORROWED' + ); + } + + if (msg.sender == user || vars.healthFactor < GenericLogic.HEALTH_FACTOR_CRITICAL_THRESHOLD) { + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); + } else { + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt).percentMul( + LIQUIDATION_CLOSE_FACTOR_PERCENT + ); + } + + vars.actualAmountToLiquidate = principalAmount > vars.maxPrincipalAmountToLiquidate + ? vars.maxPrincipalAmountToLiquidate + : principalAmount; + + vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); + vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); + + ( + vars.maxCollateralToLiquidate, + vars.principalAmountNeeded + ) = calculateAvailableCollateralToLiquidate( + collateralReserve, + debtReserve, + collateral, + principal, + vars.actualAmountToLiquidate, + vars.userCollateralBalance + ); + + //if principalAmountNeeded < vars.ActualAmountToLiquidate, there isn't enough + //of collateral to cover the actual amount that is being liquidated, hence we liquidate + //a smaller amount + if (vars.principalAmountNeeded < vars.actualAmountToLiquidate) { + vars.actualAmountToLiquidate = vars.principalAmountNeeded; + } + + vars.collateralAtoken.burn(user, receiver, vars.maxCollateralToLiquidate); + + // Notifies the receiver to proceed, sending as param the underlying already transferred + IFlashLoanReceiver(receiver).executeOperation( + collateral, + address(vars.collateralAtoken), + vars.maxCollateralToLiquidate, + 0, + params + ); + + //updating debt reserve + debtReserve.updateCumulativeIndexesAndTimestamp(); + debtReserve.updateInterestRates( + principal, + debtReserve.aTokenAddress, + vars.actualAmountToLiquidate, + 0 + ); + IERC20(principal).transferFrom(receiver, debtReserve.aTokenAddress, vars.actualAmountToLiquidate); + + if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { + IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + user, + vars.actualAmountToLiquidate + ); + } else { + IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + user, + vars.userVariableDebt + ); + IStableDebtToken(debtReserve.stableDebtTokenAddress).burn( + user, + vars.actualAmountToLiquidate.sub(vars.userVariableDebt) + ); + } + + + //updating collateral reserve + collateralReserve.updateCumulativeIndexesAndTimestamp(); + collateralReserve.updateInterestRates( + collateral, + address(vars.collateralAtoken), + 0, + vars.maxCollateralToLiquidate + ); + + emit RepaidWithCollateral( + collateral, + principal, + user, + msg.sender, + vars.actualAmountToLiquidate, + vars.maxCollateralToLiquidate + ); + + return (uint256(LiquidationErrors.NO_ERROR), 'SUCCESS'); + } + struct AvailableCollateralToLiquidateLocalVars { uint256 userCompoundedBorrowBalance; uint256 liquidationBonus; diff --git a/contracts/libraries/logic/GenericLogic.sol b/contracts/libraries/logic/GenericLogic.sol index ead09d38..efe053a0 100644 --- a/contracts/libraries/logic/GenericLogic.sol +++ b/contracts/libraries/logic/GenericLogic.sol @@ -24,7 +24,8 @@ library GenericLogic { using ReserveConfiguration for ReserveConfiguration.Map; using UserConfiguration for UserConfiguration.Map; - uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1 ether; + uint256 public constant HEALTH_FACTOR_CRITICAL_THRESHOLD = 0.98 ether; struct balanceDecreaseAllowedLocalVars { uint256 decimals;