Skip to content

Commit

Permalink
Merge pull request #77 from morpho-org/feat/lif-restriction
Browse files Browse the repository at this point in the history
  • Loading branch information
MathisGD authored Oct 7, 2024
2 parents c2cf66d + c972d1a commit 3ce8ed3
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 34 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The pre-liquidation close factor and the pre-liquidation incentive factor evolve

These functions are illustrated in the following figure:

<img width="1061" alt="pre-liquidation-cf-and-lif" src="https://github.com/user-attachments/assets/0c11c961-a046-4701-9063-9f6b84a6c3b2">
<img width="1061" alt="pre-liquidation-cf-and-lif" src="https://github.com/user-attachments/assets/7d65a88e-8187-4b90-848e-9aa5ee66b971">

The two main use-cases are:

Expand All @@ -42,7 +42,6 @@ By calling `preLiquidate` with a smart contract that implements the `IPreLiquida
More precisely, the `onPreLiquidate` function of the liquidator's smart contract will be called after the collateral withdrawal and before the debt repayment.
This mechanism eliminates the need for a flashloan.


### PreLiquidation Oracle

The `PreLiquidationParams` struct includes a `preLiquidationOracle` attribute, allowing pre-liquidation using any compatible oracle.
Expand Down
24 changes: 12 additions & 12 deletions src/PreLiquidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback {
/// @dev The following requirements should be met:
/// - preLltv < LLTV;
/// - preLCF1 <= preLCF2;
/// - WAD <= preLIF1 <= preLIF2.
/// - 1 <= preLIF1 <= preLIF2 <= 1 / LLTV.
constructor(address morpho, Id id, PreLiquidationParams memory _preLiquidationParams) {
require(IMorpho(morpho).market(id).lastUpdate != 0, ErrorsLib.NonexistentMarket());
MarketParams memory _marketParams = IMorpho(morpho).idToMarketParams(id);
require(_preLiquidationParams.preLltv < _marketParams.lltv, ErrorsLib.PreLltvTooHigh());
require(_preLiquidationParams.preLCF1 <= _preLiquidationParams.preLCF2, ErrorsLib.PreLCFDecreasing());
require(WAD <= _preLiquidationParams.preLIF1, ErrorsLib.PreLIFTooLow());
require(_preLiquidationParams.preLIF1 <= _preLiquidationParams.preLIF2, ErrorsLib.PreLIFDecreasing());
require(_preLiquidationParams.preLIF2 <= WAD.wDivDown(_marketParams.lltv), ErrorsLib.PreLIFTooHigh());

MORPHO = IMorpho(morpho);

Expand Down Expand Up @@ -140,14 +141,14 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback {
uint256 collateralPrice = IOracle(PRE_LIQUIDATION_ORACLE).price();
uint256 collateralQuoted = uint256(position.collateral).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE);
uint256 borrowed = uint256(position.borrowShares).toAssetsUp(market.totalBorrowAssets, market.totalBorrowShares);
uint256 ltv = borrowed.wDivUp(collateralQuoted);

// The following require is equivalent to checking that borrowed > collateralQuoted.wMulDown(PRE_LLTV).
require(ltv > PRE_LLTV, ErrorsLib.NotPreLiquidatablePosition());
// The two following require-statements ensure that collateralQuoted is different from zero.
require(borrowed <= collateralQuoted.wMulDown(LLTV), ErrorsLib.LiquidatablePosition());
// The following require-statement is equivalent to checking that ltv > PRE_LLTV.
require(borrowed > collateralQuoted.wMulDown(PRE_LLTV), ErrorsLib.NotPreLiquidatablePosition());

uint256 preLIF = UtilsLib.min(
(ltv - PRE_LLTV).wDivDown(LLTV - PRE_LLTV).wMulDown(PRE_LIF_2 - PRE_LIF_1) + PRE_LIF_1, PRE_LIF_2
);
uint256 ltv = borrowed.wDivUp(collateralQuoted);
uint256 preLIF = (ltv - PRE_LLTV).wDivDown(LLTV - PRE_LLTV).wMulDown(PRE_LIF_2 - PRE_LIF_1) + PRE_LIF_1;

if (seizedAssets > 0) {
uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE);
Expand All @@ -160,11 +161,10 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback {
).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
}

// Note that the pre-liquidation close factor can be greater than WAD (100%). In this case the position can be
// fully pre-liquidated.
uint256 preLCF = UtilsLib.min(
(ltv - PRE_LLTV).wDivDown(LLTV - PRE_LLTV).wMulDown(PRE_LCF_2 - PRE_LCF_1) + PRE_LCF_1, PRE_LCF_2
);
// Note that the pre-liquidation close factor can be greater than WAD (100%).
// In this case the position can be fully pre-liquidated.
uint256 preLCF = (ltv - PRE_LLTV).wDivDown(LLTV - PRE_LLTV).wMulDown(PRE_LCF_2 - PRE_LCF_1) + PRE_LCF_1;

uint256 repayableShares = uint256(position.borrowShares).wMulDown(preLCF);
require(repaidShares <= repayableShares, ErrorsLib.PreLiquidationTooLarge(repaidShares, repayableShares));

Expand Down
4 changes: 4 additions & 0 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ library ErrorsLib {

error PreLIFDecreasing();

error PreLIFTooHigh();

error InconsistentInput();

error NotPreLiquidatablePosition();

error LiquidatablePosition();

error PreLiquidationTooLarge(uint256 repaidShares, uint256 repayableShares);

error NotMorpho();
Expand Down
18 changes: 6 additions & 12 deletions test/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,15 @@ contract BaseTest is Test {
view
returns (uint256)
{
return UtilsLib.min(
(ltv - preLiquidationParams.preLltv).wDivDown(marketParams.lltv - preLiquidationParams.preLltv).wMulDown(
preLiquidationParams.preLCF2 - preLiquidationParams.preLCF1
) + preLiquidationParams.preLCF1,
preLiquidationParams.preLCF2
);
return (ltv - preLiquidationParams.preLltv).wDivDown(marketParams.lltv - preLiquidationParams.preLltv).wMulDown(
preLiquidationParams.preLCF2 - preLiquidationParams.preLCF1
) + preLiquidationParams.preLCF1;
}

function _preLIF(PreLiquidationParams memory preLiquidationParams, uint256 ltv) internal view returns (uint256) {
return UtilsLib.min(
(ltv - preLiquidationParams.preLltv).wDivDown(marketParams.lltv - preLiquidationParams.preLltv).wMulDown(
preLiquidationParams.preLIF2 - preLiquidationParams.preLIF1
) + preLiquidationParams.preLIF1,
preLiquidationParams.preLIF2
);
return (ltv - preLiquidationParams.preLltv).wDivDown(marketParams.lltv - preLiquidationParams.preLltv).wMulDown(
preLiquidationParams.preLIF2 - preLiquidationParams.preLIF1
) + preLiquidationParams.preLIF1;
}

function _getBorrowBounds(
Expand Down
30 changes: 24 additions & 6 deletions test/PreLiquidationErrorTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract PreLiquidationErrorTest is BaseTest {
preLiqOracle: marketParams.oracle
});

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLltvTooHigh.selector));
vm.expectRevert(ErrorsLib.PreLltvTooHigh.selector);
factory.createPreLiquidation(id, preLiquidationParams);
}

Expand All @@ -53,7 +53,7 @@ contract PreLiquidationErrorTest is BaseTest {
});
preLiquidationParams.preLCF2 = bound(preLiquidationParams.preLCF2, 0, preLiquidationParams.preLCF1 - 1);

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLCFDecreasing.selector));
vm.expectRevert(ErrorsLib.PreLCFDecreasing.selector);
factory.createPreLiquidation(id, preLiquidationParams);
}

Expand All @@ -69,11 +69,29 @@ contract PreLiquidationErrorTest is BaseTest {
preLiqOracle: marketParams.oracle
});

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLIFTooLow.selector));
vm.expectRevert(ErrorsLib.PreLIFTooLow.selector);
factory.createPreLiquidation(id, preLiquidationParams);
}

function testpreLIFDecreasing(PreLiquidationParams memory preLiquidationParams) public virtual {
function testHighPreLIF(PreLiquidationParams memory preLiquidationParams) public virtual {
preLiquidationParams = boundPreLiquidationParameters({
preLiquidationParams: preLiquidationParams,
minPreLltv: WAD / 100,
maxPreLltv: marketParams.lltv - 1,
minPreLCF: WAD / 100,
maxPreLCF: WAD,
minPreLIF: WAD,
maxPreLIF: type(uint256).max,
preLiqOracle: marketParams.oracle
});
preLiquidationParams.preLIF2 =
bound(preLiquidationParams.preLIF2, WAD.wDivDown(marketParams.lltv) + 1, type(uint256).max);

vm.expectRevert(ErrorsLib.PreLIFTooHigh.selector);
factory.createPreLiquidation(id, preLiquidationParams);
}

function testPreLIFDecreasing(PreLiquidationParams memory preLiquidationParams) public virtual {
preLiquidationParams = boundPreLiquidationParameters({
preLiquidationParams: preLiquidationParams,
minPreLltv: WAD / 100,
Expand All @@ -87,12 +105,12 @@ contract PreLiquidationErrorTest is BaseTest {

preLiquidationParams.preLIF2 = bound(preLiquidationParams.preLIF2, WAD, preLiquidationParams.preLIF1 - 1);

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLIFDecreasing.selector));
vm.expectRevert(ErrorsLib.PreLIFDecreasing.selector);
factory.createPreLiquidation(id, preLiquidationParams);
}

function testNonexistentMarket(PreLiquidationParams memory preLiquidationParams) public virtual {
vm.expectRevert(abi.encodeWithSelector(ErrorsLib.NonexistentMarket.selector));
vm.expectRevert(ErrorsLib.NonexistentMarket.selector);
factory.createPreLiquidation(Id.wrap(bytes32(0)), preLiquidationParams);
}

Expand Down
46 changes: 44 additions & 2 deletions test/PreLiquidationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,48 @@ contract PreLiquidationTest is BaseTest, IPreLiquidationCallback {
factory = new PreLiquidationFactory(address(MORPHO));
}

function testPreLiquidationLiquidatable(
PreLiquidationParams memory preLiquidationParams,
uint256 collateralAmount,
uint256 borrowAmount,
uint256 newPrice
) public virtual {
preLiquidationParams = boundPreLiquidationParameters({
preLiquidationParams: preLiquidationParams,
minPreLltv: WAD / 2,
maxPreLltv: marketParams.lltv - 1,
minPreLCF: WAD / 100,
maxPreLCF: WAD,
minPreLIF: WAD,
maxPreLIF: WAD.wDivDown(lltv),
preLiqOracle: marketParams.oracle
});

collateralAmount = bound(collateralAmount, minCollateral, maxCollateral);
(uint256 collateralQuoted, uint256 borrowPreLiquidationThreshold, uint256 borrowLiquidationThreshold) =
_getBorrowBounds(preLiquidationParams, marketParams, collateralAmount);
borrowAmount = bound(borrowAmount, borrowPreLiquidationThreshold + 1, borrowLiquidationThreshold);

_preparePreLiquidation(preLiquidationParams, collateralAmount, borrowAmount, LIQUIDATOR);

uint256 ltv = borrowAmount.wDivUp(collateralQuoted);
uint256 prevPrice = oracle.price();
newPrice = bound(newPrice, prevPrice / 10, prevPrice.wDivDown(marketParams.lltv).wMulDown(ltv));
oracle.setPrice(newPrice);

uint256 newLtv = borrowAmount.wDivUp(collateralAmount.mulDivDown(newPrice, ORACLE_PRICE_SCALE));
vm.assume(newLtv > marketParams.lltv);

vm.startPrank(LIQUIDATOR);
Position memory position = MORPHO.position(id, BORROWER);

uint256 closeFactor = _closeFactor(preLiquidationParams, newLtv);
uint256 repayableShares = uint256(position.borrowShares).wMulDown(closeFactor);

vm.expectRevert(ErrorsLib.LiquidatablePosition.selector);
preLiquidation.preLiquidate(BORROWER, 0, repayableShares, hex"");
}

function testPreLiquidationShares(
PreLiquidationParams memory preLiquidationParams,
uint256 collateralAmount,
Expand Down Expand Up @@ -191,7 +233,7 @@ contract PreLiquidationTest is BaseTest, IPreLiquidationCallback {

vm.startPrank(LIQUIDATOR);

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.NotPreLiquidatablePosition.selector));
vm.expectRevert(ErrorsLib.NotPreLiquidatablePosition.selector);
preLiquidation.preLiquidate(BORROWER, 0, 1, hex"");

vm.warp(block.timestamp + 12);
Expand Down Expand Up @@ -249,7 +291,7 @@ contract PreLiquidationTest is BaseTest, IPreLiquidationCallback {

vm.startPrank(LIQUIDATOR);

vm.expectRevert(abi.encodeWithSelector(ErrorsLib.NotPreLiquidatablePosition.selector));
vm.expectRevert(ErrorsLib.NotPreLiquidatablePosition.selector);
preLiquidation.preLiquidate(BORROWER, 0, 1, hex"");
}
}

0 comments on commit 3ce8ed3

Please sign in to comment.