From d3f61d6c024ffd668029693b6efa234d27d04029 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 4 Oct 2024 16:02:33 +0200 Subject: [PATCH 1/9] feat: add LIF restriction --- src/PreLiquidation.sol | 6 ++++-- src/libraries/ErrorsLib.sol | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index 7264ec4..7be17f6 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -79,7 +79,7 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { /// @dev The following requirements should be met: /// - preLltv < LLTV; /// - preLCF1 <= preLCF2; - /// - WAD <= preLIF1 <= preLIF2. + /// - WAD <= 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); @@ -87,6 +87,7 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { 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); @@ -160,7 +161,8 @@ 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 + // 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 diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 7bae166..5c46196 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -16,6 +16,8 @@ library ErrorsLib { error PreLIFDecreasing(); + error PreLIFTooHigh(); + error InconsistentInput(); error NotPreLiquidatablePosition(); From 7832bb644fca4a1a10d4934260fd858039cfd292 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 4 Oct 2024 16:03:51 +0200 Subject: [PATCH 2/9] chore: reformat comment. --- src/PreLiquidation.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index 7be17f6..1726115 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -161,9 +161,8 @@ 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. + // 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 ); From 8c07ddedd73016deaee815b29e81ab6430f7d635 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 4 Oct 2024 17:11:30 +0200 Subject: [PATCH 3/9] test: add preLIFTooHigh test --- test/PreLiquidationErrorTest.sol | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/PreLiquidationErrorTest.sol b/test/PreLiquidationErrorTest.sol index 16ca922..ce4b1ef 100644 --- a/test/PreLiquidationErrorTest.sol +++ b/test/PreLiquidationErrorTest.sol @@ -73,7 +73,25 @@ contract PreLiquidationErrorTest is BaseTest { 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(abi.encodeWithSelector(ErrorsLib.PreLIFTooHigh.selector)); + factory.createPreLiquidation(id, preLiquidationParams); + } + + function testPreLIFDecreasing(PreLiquidationParams memory preLiquidationParams) public virtual { preLiquidationParams = boundPreLiquidationParameters({ preLiquidationParams: preLiquidationParams, minPreLltv: WAD / 100, From e257eed05d00311b2403eb35f5a214c29ec642bd Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Fri, 4 Oct 2024 19:51:13 +0200 Subject: [PATCH 4/9] feat: restrict ltv < LLTV --- src/PreLiquidation.sol | 11 ++++---- src/libraries/ErrorsLib.sol | 2 ++ test/BaseTest.sol | 18 +++++-------- test/PreLiquidationErrorTest.sol | 12 ++++----- test/PreLiquidationTest.sol | 46 ++++++++++++++++++++++++++++++-- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index da2dc3f..a50c318 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -146,9 +146,9 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { // The following require is equivalent to checking that borrowed > collateralQuoted.wMulDown(PRE_LLTV). require(ltv > 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 - ); + require(ltv <= LLTV, ErrorsLib.LiquidatablePosition()); + + 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); @@ -163,9 +163,8 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { // 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 - ); + 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)); diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 5c46196..38439b3 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -22,6 +22,8 @@ library ErrorsLib { error NotPreLiquidatablePosition(); + error LiquidatablePosition(); + error PreLiquidationTooLarge(uint256 repaidShares, uint256 repayableShares); error NotMorpho(); diff --git a/test/BaseTest.sol b/test/BaseTest.sol index 3fa9adb..953df08 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -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( diff --git a/test/PreLiquidationErrorTest.sol b/test/PreLiquidationErrorTest.sol index ce4b1ef..35928fe 100644 --- a/test/PreLiquidationErrorTest.sol +++ b/test/PreLiquidationErrorTest.sol @@ -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); } @@ -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); } @@ -69,7 +69,7 @@ contract PreLiquidationErrorTest is BaseTest { preLiqOracle: marketParams.oracle }); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLIFTooLow.selector)); + vm.expectRevert(ErrorsLib.PreLIFTooLow.selector); factory.createPreLiquidation(id, preLiquidationParams); } @@ -87,7 +87,7 @@ contract PreLiquidationErrorTest is BaseTest { preLiquidationParams.preLIF2 = bound(preLiquidationParams.preLIF2, WAD.wDivDown(marketParams.lltv) + 1, type(uint256).max); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PreLIFTooHigh.selector)); + vm.expectRevert(ErrorsLib.PreLIFTooHigh.selector); factory.createPreLiquidation(id, preLiquidationParams); } @@ -105,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); } diff --git a/test/PreLiquidationTest.sol b/test/PreLiquidationTest.sol index 70daf88..3545df6 100644 --- a/test/PreLiquidationTest.sol +++ b/test/PreLiquidationTest.sol @@ -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, @@ -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); @@ -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""); } } From bdcffe9f38867f0333deeb031558a107b8601c6e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 5 Oct 2024 18:35:28 +0200 Subject: [PATCH 5/9] docs: requirement of pre-liqudiation incentive factors Co-authored-by: MathisGD <74971347+MathisGD@users.noreply.github.com> Signed-off-by: Quentin Garchery --- src/PreLiquidation.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index a50c318..2de16af 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -79,7 +79,7 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { /// @dev The following requirements should be met: /// - preLltv < LLTV; /// - preLCF1 <= preLCF2; - /// - WAD <= preLIF1 <= preLIF2 <= 1 / LLTV. + /// - 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); From de168dd6cc41a733023c65d69728d5f9c1ec4981 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 5 Oct 2024 18:49:25 +0200 Subject: [PATCH 6/9] feat: rearrange requires to ensure no division by zero --- src/PreLiquidation.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index 2de16af..880c9c2 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -141,13 +141,13 @@ 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()); - require(ltv <= LLTV, ErrorsLib.LiquidatablePosition()); + // The following require is equivalent to checking that ltv > PRE_LLTV. + require(borrowed > collateralQuoted.wMulDown(PRE_LLTV), ErrorsLib.NotPreLiquidatablePosition()); + require(borrowed <= collateralQuoted.wMulDown(LLTV), ErrorsLib.LiquidatablePosition()); + // The two preceding requires ensures that collateralQuoted is different from zero. + 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) { From 1af02e4f512de69a559144ba7d4f6062cfe7964b Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sat, 5 Oct 2024 19:11:30 +0200 Subject: [PATCH 7/9] chore: update README schema --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 78e8d66..969e608 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The pre-liquidation close factor and the pre-liquidation incentive factor evolve These functions are illustrated in the following figure: -pre-liquidation-cf-and-lif +pre-liquidation-cf-and-lif The two main use-cases are: @@ -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. From 525cfdf5ae50596320df72fa284635447cfdcd1e Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sun, 6 Oct 2024 18:05:40 +0200 Subject: [PATCH 8/9] refactor: change order of require for clarity --- src/PreLiquidation.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PreLiquidation.sol b/src/PreLiquidation.sol index 880c9c2..a1f37c2 100644 --- a/src/PreLiquidation.sol +++ b/src/PreLiquidation.sol @@ -142,11 +142,11 @@ contract PreLiquidation is IPreLiquidation, IMorphoRepayCallback { uint256 collateralQuoted = uint256(position.collateral).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE); uint256 borrowed = uint256(position.borrowShares).toAssetsUp(market.totalBorrowAssets, market.totalBorrowShares); - // The following require is equivalent to checking that ltv > PRE_LLTV. - require(borrowed > collateralQuoted.wMulDown(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()); - // The two preceding requires ensures that collateralQuoted is different from zero. uint256 ltv = borrowed.wDivUp(collateralQuoted); uint256 preLIF = (ltv - PRE_LLTV).wDivDown(LLTV - PRE_LLTV).wMulDown(PRE_LIF_2 - PRE_LIF_1) + PRE_LIF_1; From c972d1ade8380c2eea11ab1bf05486b825c9435a Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Sun, 6 Oct 2024 21:49:04 +0200 Subject: [PATCH 9/9] chore: update README to pre naming --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 969e608..b53ced1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The pre-liquidation close factor and the pre-liquidation incentive factor evolve These functions are illustrated in the following figure: -pre-liquidation-cf-and-lif +pre-liquidation-cf-and-lif The two main use-cases are: