Skip to content

Commit

Permalink
Merge pull request #1305 from morpho-dao/refactor/liquidation-depreca…
Browse files Browse the repository at this point in the history
…ted-mkt

Refactor liquidation on deprecated market
  • Loading branch information
MerlinEgalite committed Sep 23, 2022
2 parents f799825 + 8d19785 commit cf7efc4
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 49 deletions.
41 changes: 27 additions & 14 deletions contracts/aave-v2/ExitPositionsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
}
46 changes: 27 additions & 19 deletions contracts/aave-v3/ExitPositionsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
}
13 changes: 9 additions & 4 deletions contracts/compound/PositionsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ contract PositionsManager is IPositionsManager, MatchingEngine {
uint256 supplyBalance;
uint256 borrowedPrice;
uint256 amountToSeize;
uint256 closeFactor;
}

/// LOGIC ///
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 24 additions & 4 deletions test-foundry/aave-v2/TestLiquidate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,39 @@ 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);

(, 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.
Expand Down
28 changes: 24 additions & 4 deletions test-foundry/aave-v3/TestLiquidate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,39 @@ 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);

(, 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.
Expand Down
19 changes: 15 additions & 4 deletions test-foundry/compound/TestLiquidate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,30 @@ 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);

(, 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.
Expand Down

0 comments on commit cf7efc4

Please sign in to comment.