diff --git a/contracts/aave-v2/ExitPositionsManager.sol b/contracts/aave-v2/ExitPositionsManager.sol index 7520c0b3e..e889a124d 100644 --- a/contracts/aave-v2/ExitPositionsManager.sol +++ b/contracts/aave-v2/ExitPositionsManager.sol @@ -128,6 +128,8 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { uint256 collateralTokenUnit; // The collateral token unit considering its decimals. uint256 borrowedReserveDecimals; // The number of decimals of the borrowed asset in the reserve. uint256 borrowedTokenUnit; // The unit of borrowed token considering its decimals. + uint256 closeFactor; // The close factor used during the liquidation. + bool liquidationAllowed; // Whether the liquidation is allowed or not. } // Struct to avoid stack too deep. @@ -221,23 +223,22 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { _updateIndexes(_poolTokenBorrowed); _updateIndexes(_poolTokenCollateral); - if (!borrowedMarket.isDeprecated && !_liquidationAllowed(_borrower)) - revert UnauthorisedLiquidate(); - - address tokenBorrowedAddress = market[_poolTokenBorrowed].underlyingToken; + LiquidateVars memory vars; + (vars.closeFactor, vars.liquidationAllowed) = _liquidationAllowed( + _borrower, + borrowedMarket.isDeprecated + ); + if (!vars.liquidationAllowed) revert UnauthorisedLiquidate(); uint256 amountToLiquidate = Math.min( _amount, - _getUserBorrowBalanceInOf(_poolTokenBorrowed, _borrower).percentMul( - DEFAULT_LIQUIDATION_CLOSE_FACTOR - ) // Max liquidatable debt. + _getUserBorrowBalanceInOf(_poolTokenBorrowed, _borrower).percentMul(vars.closeFactor) // Max liquidatable debt. ); - address tokenCollateralAddress = market[_poolTokenCollateral].underlyingToken; - IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); - LiquidateVars memory vars; + address tokenCollateralAddress = market[_poolTokenCollateral].underlyingToken; + address tokenBorrowedAddress = market[_poolTokenBorrowed].underlyingToken; { ILendingPool poolMem = pool; (, , vars.liquidationBonus, vars.collateralReserveDecimals, ) = poolMem @@ -316,7 +317,6 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { emit P2PBorrowDeltaUpdated(_poolToken, deltasMem.p2pBorrowDelta); ERC20 underlyingToken = ERC20(market[_poolToken].underlyingToken); - _borrowFromPool(underlyingToken, _amount); _supplyToPool(underlyingToken, _amount); @@ -691,8 +691,21 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { /// @dev Checks if the user is liquidatable. /// @param _user The user to check. - /// @return Whether the user is liquidatable or not. - function _liquidationAllowed(address _user) internal returns (bool) { - return _getUserHealthFactor(_user, address(0), 0) < HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + /// @param _isDeprecated Whether the market is deprecated or not. + /// @return closeFactor The close factor to apply. + /// @return liquidationAllowed Whether the liquidation is allowed or not. + function _liquidationAllowed(address _user, bool _isDeprecated) + internal + returns (uint256 closeFactor, bool liquidationAllowed) + { + if (_isDeprecated) { + // Allow liquidation of the whole debt. + closeFactor = MAX_BASIS_POINTS; + liquidationAllowed = true; + } else { + closeFactor = DEFAULT_LIQUIDATION_CLOSE_FACTOR; + liquidationAllowed = (_getUserHealthFactor(_user, address(0), 0) < + HEALTH_FACTOR_LIQUIDATION_THRESHOLD); + } } } diff --git a/contracts/aave-v3/ExitPositionsManager.sol b/contracts/aave-v3/ExitPositionsManager.sol index 0b78bd29c..34f444333 100644 --- a/contracts/aave-v3/ExitPositionsManager.sol +++ b/contracts/aave-v3/ExitPositionsManager.sol @@ -133,8 +133,8 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { uint256 borrowedTokenUnit; // The unit of borrowed token considering its decimals. uint256 borrowedTokenPrice; // The price of the borrowed token. uint256 amountToLiquidate; // The amount of debt token to repay. - uint256 closeFactor; - bool liquidationAllowed; + uint256 closeFactor; // The close factor used during the liquidation. + bool liquidationAllowed; // Whether the liquidation is allowed or not. } // Struct to avoid stack too deep. @@ -229,10 +229,11 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { _updateIndexes(_poolTokenCollateral); LiquidateVars memory vars; - if (!borrowedMarket.isDeprecated) { - (vars.closeFactor, vars.liquidationAllowed) = _liquidationAllowed(_borrower); - if (!vars.liquidationAllowed) revert UnauthorisedLiquidate(); - } else vars.closeFactor = DEFAULT_LIQUIDATION_CLOSE_FACTOR; + (vars.closeFactor, vars.liquidationAllowed) = _liquidationAllowed( + _borrower, + borrowedMarket.isDeprecated + ); + if (!vars.liquidationAllowed) revert UnauthorisedLiquidate(); vars.amountToLiquidate = Math.min( _amount, @@ -700,23 +701,30 @@ contract ExitPositionsManager is IExitPositionsManager, PositionsManagerUtils { /// @dev Checks if the user is liquidatable. /// @param _user The user to check. + /// @param _isDeprecated Whether the market is deprecated or not. /// @return closeFactor The close factor to apply. /// @return liquidationAllowed Whether the liquidation is allowed or not. - function _liquidationAllowed(address _user) + function _liquidationAllowed(address _user, bool _isDeprecated) internal returns (uint256 closeFactor, bool liquidationAllowed) { - uint256 healthFactor = _getUserHealthFactor(_user, address(0), 0); - address priceOracleSentinel = addressesProvider.getPriceOracleSentinel(); - - closeFactor = healthFactor > MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD - ? DEFAULT_LIQUIDATION_CLOSE_FACTOR - : MAX_LIQUIDATION_CLOSE_FACTOR; - - if (priceOracleSentinel != address(0)) - liquidationAllowed = (healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || - (IPriceOracleSentinel(priceOracleSentinel).isLiquidationAllowed() && - healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD)); - else liquidationAllowed = healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + if (_isDeprecated) { + // Allow liquidation of the whole debt. + closeFactor = MAX_BASIS_POINTS; + liquidationAllowed = true; + } else { + uint256 healthFactor = _getUserHealthFactor(_user, address(0), 0); + address priceOracleSentinel = addressesProvider.getPriceOracleSentinel(); + + closeFactor = healthFactor > MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ? DEFAULT_LIQUIDATION_CLOSE_FACTOR + : MAX_LIQUIDATION_CLOSE_FACTOR; + + if (priceOracleSentinel != address(0)) + liquidationAllowed = (healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || + (IPriceOracleSentinel(priceOracleSentinel).isLiquidationAllowed() && + healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD)); + else liquidationAllowed = healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD; + } } } diff --git a/contracts/compound/PositionsManager.sol b/contracts/compound/PositionsManager.sol index a442e2c82..d62ff2954 100644 --- a/contracts/compound/PositionsManager.sol +++ b/contracts/compound/PositionsManager.sol @@ -218,6 +218,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine { uint256 supplyBalance; uint256 borrowedPrice; uint256 amountToSeize; + uint256 closeFactor; } /// LOGIC /// @@ -500,13 +501,17 @@ contract PositionsManager is IPositionsManager, MatchingEngine { _updateP2PIndexes(_poolTokenBorrowed); _updateP2PIndexes(_poolTokenCollateral); - if (!borrowedMarket.isDeprecated && !_isLiquidatable(_borrower, address(0), 0, 0)) - revert UnauthorisedLiquidate(); - LiquidateVars memory vars; + if (borrowedMarket.isDeprecated) + vars.closeFactor = WAD; // Allow liquidation of the whole debt. + else { + if (!_isLiquidatable(_borrower, address(0), 0, 0)) revert UnauthorisedLiquidate(); + vars.closeFactor = comptroller.closeFactorMantissa(); + } + vars.borrowBalance = _getUserBorrowBalanceInOf(_poolTokenBorrowed, _borrower); - if (_amount > vars.borrowBalance.mul(comptroller.closeFactorMantissa())) + if (_amount > vars.borrowBalance.mul(vars.closeFactor)) revert AmountAboveWhatAllowedToRepay(); // Same mechanism as Compound. Liquidator cannot repay more than part of the debt (cf close factor on Compound). _safeRepayLogic(_poolTokenBorrowed, msg.sender, _borrower, _amount, 0); diff --git a/test-foundry/aave-v2/TestLiquidate.t.sol b/test-foundry/aave-v2/TestLiquidate.t.sol index 11e80115d..c92564e02 100644 --- a/test-foundry/aave-v2/TestLiquidate.t.sol +++ b/test-foundry/aave-v2/TestLiquidate.t.sol @@ -37,10 +37,10 @@ contract TestLiquidate is TestSetup { borrower1.borrow(aDai, amount); (, uint256 supplyOnPoolBefore) = morpho.supplyBalanceInOf(aUsdc, address(borrower1)); - (, uint256 borrowOnPoolbefore) = morpho.borrowBalanceInOf(aDai, address(borrower1)); + (, uint256 borrowOnPoolBefore) = morpho.borrowBalanceInOf(aDai, address(borrower1)); // Liquidate - uint256 toRepay = amount / 2; + uint256 toRepay = borrowOnPoolBefore.rayMul(pool.getReserveNormalizedVariableDebt(dai)); // Full liquidation. User liquidator = borrower3; liquidator.approve(dai, address(morpho), toRepay); liquidator.liquidate(aDai, aUsdc, address(borrower1), toRepay); @@ -48,8 +48,28 @@ contract TestLiquidate is TestSetup { (, uint256 supplyOnPoolAfter) = morpho.supplyBalanceInOf(aUsdc, address(borrower1)); (, uint256 borrowOnPoolAfter) = morpho.borrowBalanceInOf(aDai, address(borrower1)); - assertLt(borrowOnPoolAfter, borrowOnPoolbefore); - assertLt(supplyOnPoolAfter, supplyOnPoolBefore); + ExitPositionsManager.LiquidateVars memory vars; + (, , vars.liquidationBonus, vars.collateralReserveDecimals, ) = pool + .getConfiguration(usdc) + .getParamsMemory(); + uint256 collateralPrice = oracle.getAssetPrice(usdc); + vars.collateralTokenUnit = 10**vars.collateralReserveDecimals; + + { + (, , , vars.borrowedReserveDecimals, ) = pool.getConfiguration(dai).getParamsMemory(); + uint256 borrowedPrice = oracle.getAssetPrice(dai); + vars.borrowedTokenUnit = 10**vars.borrowedReserveDecimals; + + uint256 amountToSeize = (toRepay * borrowedPrice * vars.collateralTokenUnit) / + (vars.borrowedTokenUnit * collateralPrice).percentMul(vars.liquidationBonus); + + uint256 expectedSupplyOnPoolAfter = supplyOnPoolBefore - + amountToSeize.rayDiv(pool.getReserveNormalizedIncome(usdc)); + + assertApproxEqAbs(supplyOnPoolAfter, expectedSupplyOnPoolAfter, 1e10); + } + + assertEq(borrowOnPoolAfter, 0); } // A user liquidates a borrower that has not enough collateral to cover for his debt. diff --git a/test-foundry/aave-v3/TestLiquidate.t.sol b/test-foundry/aave-v3/TestLiquidate.t.sol index 2bc4ab420..99e64a518 100644 --- a/test-foundry/aave-v3/TestLiquidate.t.sol +++ b/test-foundry/aave-v3/TestLiquidate.t.sol @@ -37,10 +37,10 @@ contract TestLiquidate is TestSetup { borrower1.borrow(aDai, amount); (, uint256 supplyOnPoolBefore) = morpho.supplyBalanceInOf(aUsdc, address(borrower1)); - (, uint256 borrowOnPoolbefore) = morpho.borrowBalanceInOf(aDai, address(borrower1)); + (, uint256 borrowOnPoolBefore) = morpho.borrowBalanceInOf(aDai, address(borrower1)); // Liquidate - uint256 toRepay = amount / 2; + uint256 toRepay = borrowOnPoolBefore.rayMul(pool.getReserveNormalizedVariableDebt(dai)); // Full liquidation. User liquidator = borrower3; liquidator.approve(dai, address(morpho), toRepay); liquidator.liquidate(aDai, aUsdc, address(borrower1), toRepay); @@ -48,8 +48,28 @@ contract TestLiquidate is TestSetup { (, uint256 supplyOnPoolAfter) = morpho.supplyBalanceInOf(aUsdc, address(borrower1)); (, uint256 borrowOnPoolAfter) = morpho.borrowBalanceInOf(aDai, address(borrower1)); - assertLt(borrowOnPoolAfter, borrowOnPoolbefore); - assertLt(supplyOnPoolAfter, supplyOnPoolBefore); + ExitPositionsManager.LiquidateVars memory vars; + (, , vars.liquidationBonus, vars.collateralReserveDecimals, , ) = pool + .getConfiguration(usdc) + .getParams(); + uint256 collateralPrice = oracle.getAssetPrice(usdc); + vars.collateralTokenUnit = 10**vars.collateralReserveDecimals; + + { + (, , , vars.borrowedReserveDecimals, , ) = pool.getConfiguration(dai).getParams(); + uint256 borrowedPrice = oracle.getAssetPrice(dai); + vars.borrowedTokenUnit = 10**vars.borrowedReserveDecimals; + + uint256 amountToSeize = (toRepay * borrowedPrice * vars.collateralTokenUnit) / + (vars.borrowedTokenUnit * collateralPrice).percentMul(vars.liquidationBonus); + + uint256 expectedSupplyOnPoolAfter = supplyOnPoolBefore - + amountToSeize.rayDiv(pool.getReserveNormalizedIncome(usdc)); + + assertApproxEqAbs(supplyOnPoolAfter, expectedSupplyOnPoolAfter, 1e15); + } + + assertApproxEqAbs(borrowOnPoolAfter, 0, 1e15); } // A user liquidates a borrower that has not enough collateral to cover for his debt. diff --git a/test-foundry/compound/TestLiquidate.t.sol b/test-foundry/compound/TestLiquidate.t.sol index 358ef639a..f95dbd01e 100644 --- a/test-foundry/compound/TestLiquidate.t.sol +++ b/test-foundry/compound/TestLiquidate.t.sol @@ -38,10 +38,10 @@ contract TestLiquidate is TestSetup { moveOneBlockForwardBorrowRepay(); (, uint256 supplyOnPoolBefore) = morpho.supplyBalanceInOf(cUsdc, address(borrower1)); - (, uint256 borrowOnPoolbefore) = morpho.borrowBalanceInOf(cDai, address(borrower1)); + (, uint256 borrowOnPoolBefore) = morpho.borrowBalanceInOf(cDai, address(borrower1)); // Liquidate - uint256 toRepay = amount / 3; + uint256 toRepay = amount; // Full liquidation. User liquidator = borrower3; liquidator.approve(dai, address(morpho), toRepay); liquidator.liquidate(cDai, cUsdc, address(borrower1), toRepay); @@ -49,8 +49,19 @@ contract TestLiquidate is TestSetup { (, uint256 supplyOnPoolAfter) = morpho.supplyBalanceInOf(cUsdc, address(borrower1)); (, uint256 borrowOnPoolAfter) = morpho.borrowBalanceInOf(cDai, address(borrower1)); - assertLt(borrowOnPoolAfter, borrowOnPoolbefore); - assertLt(supplyOnPoolAfter, supplyOnPoolBefore); + uint256 collateralPrice = oracle.getUnderlyingPrice(cUsdc); + uint256 borrowedPrice = oracle.getUnderlyingPrice(cDai); + + uint256 amountToSeize = toRepay + .mul(comptroller.liquidationIncentiveMantissa()) + .mul(borrowedPrice) + .div(collateralPrice); + + uint256 expectedSupplyOnPoolAfter = supplyOnPoolBefore - + amountToSeize.div(ICToken(cUsdc).exchangeRateCurrent()); + + assertApproxEqAbs(supplyOnPoolAfter, expectedSupplyOnPoolAfter, 2); + assertApproxEqAbs(borrowOnPoolAfter, 0, 1e15); } // A user liquidates a borrower that has not enough collateral to cover for his debt.