Skip to content

Commit

Permalink
Merge pull request #322 from morpho-labs/feat/add-argument-liquidate
Browse files Browse the repository at this point in the history
Add repaidShares argument to liquidate function
  • Loading branch information
MerlinEgalite authored Aug 23, 2023
2 parents 47a0619 + e17140c commit 389cd0c
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 53 deletions.
42 changes: 27 additions & 15 deletions src/Morpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -326,13 +326,16 @@ contract Morpho is IMorpho {
/* LIQUIDATION */

/// @inheritdoc IMorpho
function liquidate(MarketParams memory marketParams, address borrower, uint256 seized, bytes calldata data)
external
returns (uint256 assetsRepaid, uint256 sharesRepaid)
{
function liquidate(
MarketParams memory marketParams,
address borrower,
uint256 seizedAssets,
uint256 repaidShares,
bytes calldata data
) external returns (uint256, uint256) {
Id id = marketParams.id();
require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);
require(seized != 0, ErrorsLib.ZERO_ASSETS);
require(UtilsLib.exactlyOneZero(seizedAssets, repaidShares), ErrorsLib.INCONSISTENT_INPUT);

_accrueInterest(marketParams, id);

Expand All @@ -344,14 +347,21 @@ contract Morpho is IMorpho {
uint256 incentiveFactor = UtilsLib.min(
MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - marketParams.lltv))
);
assetsRepaid = seized.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(incentiveFactor);
sharesRepaid = assetsRepaid.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);

user[id][borrower].borrowShares -= sharesRepaid.toUint128();
market[id].totalBorrowShares -= sharesRepaid.toUint128();
market[id].totalBorrowAssets -= assetsRepaid.toUint128();
uint256 repaidAssets;
if (seizedAssets > 0) {
repaidAssets = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(incentiveFactor);
repaidShares = repaidAssets.toSharesDown(market[id].totalBorrowAssets, market[id].totalBorrowShares);
} else {
repaidAssets = repaidShares.toAssetsUp(market[id].totalBorrowAssets, market[id].totalBorrowShares);
seizedAssets = repaidAssets.wMulDown(incentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
}

user[id][borrower].borrowShares -= repaidShares.toUint128();
market[id].totalBorrowShares -= repaidShares.toUint128();
market[id].totalBorrowAssets -= repaidAssets.toUint128();

user[id][borrower].collateral -= seized.toUint128();
user[id][borrower].collateral -= seizedAssets.toUint128();

// Realize the bad debt if needed. Note that it saves ~3k gas to do it.
uint256 badDebtShares;
Expand All @@ -364,13 +374,15 @@ contract Morpho is IMorpho {
user[id][borrower].borrowShares = 0;
}

IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seized);
IERC20(marketParams.collateralToken).safeTransfer(msg.sender, seizedAssets);

emit EventsLib.Liquidate(id, msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, badDebtShares);

emit EventsLib.Liquidate(id, msg.sender, borrower, assetsRepaid, sharesRepaid, seized, badDebtShares);
if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(repaidAssets, data);

if (data.length > 0) IMorphoLiquidateCallback(msg.sender).onMorphoLiquidate(assetsRepaid, data);
IERC20(marketParams.borrowableToken).safeTransferFrom(msg.sender, address(this), repaidAssets);

IERC20(marketParams.borrowableToken).safeTransferFrom(msg.sender, address(this), assetsRepaid);
return (seizedAssets, repaidAssets);
}

/* FLASH LOANS */
Expand Down
23 changes: 15 additions & 8 deletions src/interfaces/IMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,19 +216,26 @@ interface IMorpho {
function withdrawCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, address receiver)
external;

/// @notice Liquidates `seized` of collateral of `borrower`'s position, optionally calling back the caller's
/// `onMorphoLiquidate` function with the given `data`.
/// @notice Liquidates the given `repaidShares` of debt asset or seize the given `seized` of collateral on the given
/// `market` of the given `borrower`'s position, optionally calling back the caller's `onMorphoLiquidate` function
/// with the given `data`.
/// @dev Either `seized` or `repaidShares` should be zero.
/// @dev Seizing more than the collateral balance will underflow and revert without any error message.
/// @dev Repaying more than the borrow balance will underflow and revert without any error message.
/// @param marketParams The market of the position.
/// @param borrower The owner of the position.
/// @param seized The amount of collateral to seize.
/// @param seizedAssets The amount of collateral to seize.
/// @param repaidShares The amount of shares to repay.
/// @param data Arbitrary data to pass to the `onMorphoLiquidate` callback. Pass empty data if not needed.
/// @return assetsRepaid The amount of assets repaid.
/// @return sharesRepaid The amount of shares burned.
function liquidate(MarketParams memory marketParams, address borrower, uint256 seized, bytes memory data)
external
returns (uint256 assetsRepaid, uint256 sharesRepaid);
/// @return The amount of assets seized.
/// @return The amount of assets repaid.
function liquidate(
MarketParams memory marketParams,
address borrower,
uint256 seizedAssets,
uint256 repaidShares,
bytes memory data
) external returns (uint256, uint256);

/// @notice Executes a flash loan.
/// @param token The token to flash loan.
Expand Down
2 changes: 1 addition & 1 deletion test/forge/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ contract BaseTest is Test {
uint256 internal constant MAX_TEST_AMOUNT = 1e28;
uint256 internal constant MIN_TEST_SHARES = MIN_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES;
uint256 internal constant MAX_TEST_SHARES = MAX_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES;
uint256 internal constant MIN_COLLATERAL_PRICE = 1000;
uint256 internal constant MIN_COLLATERAL_PRICE = 1e10;
uint256 internal constant MAX_COLLATERAL_PRICE = 1e40;
uint256 internal constant MAX_COLLATERAL_ASSETS = type(uint128).max;

Expand Down
4 changes: 2 additions & 2 deletions test/forge/integration/TestIntegrationCallbacks.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ contract IntegrationCallbacksTest is
borrowableToken.approve(address(morpho), 0);

vm.expectRevert();
morpho.liquidate(marketParams, address(this), collateralAmount, hex"");
morpho.liquidate(marketParams, address(this), collateralAmount, 0, hex"");
morpho.liquidate(
marketParams, address(this), collateralAmount, abi.encode(this.testLiquidateCallback.selector, hex"")
marketParams, address(this), collateralAmount, 0, abi.encode(this.testLiquidateCallback.selector, hex"")
);
}

Expand Down
96 changes: 85 additions & 11 deletions test/forge/integration/TestIntegrationLiquidate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ contract IntegrationLiquidateTest is BaseTest {
vm.assume(neq(marketParamsFuzz, marketParams));

vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED));
morpho.liquidate(marketParamsFuzz, address(this), 1, hex"");
morpho.liquidate(marketParamsFuzz, address(this), 1, 0, hex"");
}

function testLiquidateZeroAmount() public {
vm.prank(BORROWER);

vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS));
morpho.liquidate(marketParams, address(this), 0, hex"");
vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT));
morpho.liquidate(marketParams, address(this), 0, 0, hex"");
}

function testLiquidateInconsistentInput(uint256 seized, uint256 sharesRepaid) public {
seized = bound(seized, 1, MAX_TEST_AMOUNT);
sharesRepaid = bound(sharesRepaid, 1, MAX_TEST_SHARES);

vm.prank(BORROWER);

vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT));
morpho.liquidate(marketParams, address(this), seized, sharesRepaid, hex"");
}

function testLiquidateHealthyPosition(
Expand Down Expand Up @@ -49,10 +59,10 @@ contract IntegrationLiquidateTest is BaseTest {

vm.prank(LIQUIDATOR);
vm.expectRevert(bytes(ErrorsLib.HEALTHY_POSITION));
morpho.liquidate(marketParams, BORROWER, amountSeized, hex"");
morpho.liquidate(marketParams, BORROWER, amountSeized, 0, hex"");
}

function testLiquidateNoBadDebt(
function testLiquidateSeizedInputNoBadDebt(
uint256 amountCollateral,
uint256 amountSupplied,
uint256 amountBorrowed,
Expand Down Expand Up @@ -91,13 +101,12 @@ contract IntegrationLiquidateTest is BaseTest {

vm.expectEmit(true, true, true, true, address(morpho));
emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, expectedRepaidShares, amountSeized, 0);
(uint256 returnRepaid, uint256 returnRepaidShares) =
morpho.liquidate(marketParams, BORROWER, amountSeized, hex"");
(uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(marketParams, BORROWER, amountSeized, 0, hex"");

uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0) - expectedRepaidShares;

assertEq(returnSeized, amountSeized, "returned seized amount");
assertEq(returnRepaid, expectedRepaid, "returned asset amount");
assertEq(returnRepaidShares, expectedRepaidShares, "returned shares amount");
assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares");
assertEq(morpho.totalBorrowAssets(id), amountBorrowed - expectedRepaid, "total borrow");
assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares");
Expand All @@ -115,6 +124,71 @@ contract IntegrationLiquidateTest is BaseTest {
assertEq(collateralToken.balanceOf(LIQUIDATOR), amountSeized, "liquidator collateral balance");
}

function testLiquidateSharesInputNoBadDebt(
uint256 amountCollateral,
uint256 amountSupplied,
uint256 amountBorrowed,
uint256 sharesRepaid,
uint256 priceCollateral
) public {
(amountCollateral, amountBorrowed, priceCollateral) =
_boundUnhealthyPosition(amountCollateral, amountBorrowed, priceCollateral);

vm.assume(amountCollateral > 1);

amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT);
_supply(amountSupplied);

uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0);
uint256 maxRepaidShares = amountCollateral.mulDivDown(priceCollateral, ORACLE_PRICE_SCALE).wDivDown(
_liquidationIncentive(marketParams.lltv)
);
vm.assume(maxRepaidShares != 0);
sharesRepaid = bound(sharesRepaid, 1, min(maxRepaidShares, expectedBorrowShares));
uint256 expectedRepaid = sharesRepaid.toAssetsUp(amountBorrowed, expectedBorrowShares);
uint256 expectedSeized = expectedRepaid.wMulDown(_liquidationIncentive(marketParams.lltv)).mulDivDown(
ORACLE_PRICE_SCALE, priceCollateral
);

borrowableToken.setBalance(LIQUIDATOR, amountBorrowed);
collateralToken.setBalance(BORROWER, amountCollateral);

oracle.setPrice(type(uint256).max / amountCollateral);

vm.startPrank(BORROWER);
morpho.supplyCollateral(marketParams, amountCollateral, BORROWER, hex"");
morpho.borrow(marketParams, amountBorrowed, 0, BORROWER, BORROWER);
vm.stopPrank();

oracle.setPrice(priceCollateral);

vm.prank(LIQUIDATOR);

vm.expectEmit(true, true, true, true, address(morpho));
emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, sharesRepaid, expectedSeized, 0);
(uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(marketParams, BORROWER, 0, sharesRepaid, hex"");

expectedBorrowShares = amountBorrowed.toSharesUp(0, 0) - sharesRepaid;

assertEq(returnSeized, expectedSeized, "returned seized amount");
assertEq(returnRepaid, expectedRepaid, "returned asset amount");
assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares");
assertEq(morpho.totalBorrowAssets(id), amountBorrowed - expectedRepaid, "total borrow");
assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares");
assertEq(morpho.collateral(id, BORROWER), amountCollateral - expectedSeized, "collateral");
assertEq(borrowableToken.balanceOf(BORROWER), amountBorrowed, "borrower balance");
assertEq(borrowableToken.balanceOf(LIQUIDATOR), amountBorrowed - expectedRepaid, "liquidator balance");
assertEq(
borrowableToken.balanceOf(address(morpho)),
amountSupplied - amountBorrowed + expectedRepaid,
"morpho balance"
);
assertEq(
collateralToken.balanceOf(address(morpho)), amountCollateral - expectedSeized, "morpho collateral balance"
);
assertEq(collateralToken.balanceOf(LIQUIDATOR), expectedSeized, "liquidator collateral balance");
}

struct LiquidateBadDebtTestParams {
uint256 incentive;
uint256 expectedRepaid;
Expand Down Expand Up @@ -183,11 +257,11 @@ contract IntegrationLiquidateTest is BaseTest {
amountCollateral,
params.expectedBadDebt * SharesMathLib.VIRTUAL_SHARES
);
(uint256 returnRepaid, uint256 returnRepaidShares) =
morpho.liquidate(marketParams, BORROWER, amountCollateral, hex"");
(uint256 returnSeized, uint256 returnRepaid) =
morpho.liquidate(marketParams, BORROWER, amountCollateral, 0, hex"");

assertEq(returnSeized, amountCollateral, "returned seized amount");
assertEq(returnRepaid, params.expectedRepaid, "returned asset amount");
assertEq(returnRepaidShares, params.expectedRepaidShares, "returned shares amount");
assertEq(morpho.collateral(id, BORROWER), 0, "collateral");
assertEq(borrowableToken.balanceOf(BORROWER), amountBorrowed, "borrower balance");
assertEq(borrowableToken.balanceOf(LIQUIDATOR), amountBorrowed - params.expectedRepaid, "liquidator balance");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ contract SingleMarketChangingPriceInvariantTest is InvariantBaseTest {
borrowableToken.setBalance(msg.sender, repaid);

vm.prank(msg.sender);
morpho.liquidate(marketParams, user, seized, hex"");
morpho.liquidate(marketParams, user, seized, 0, hex"");
}

function invariantSupplyShares() public {
Expand Down
2 changes: 1 addition & 1 deletion test/hardhat/Morpho.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe("Morpho", () => {

const seized = closePositions ? assets : assets.div(2);

await morpho.connect(liquidator).liquidate(marketParams, borrower.address, seized, "0x");
await morpho.connect(liquidator).liquidate(marketParams, borrower.address, seized, 0, "0x");

const remainingCollateral = (await morpho.user(id, borrower.address)).collateral;

Expand Down
31 changes: 17 additions & 14 deletions test/morpho_tests.tree
Original file line number Diff line number Diff line change
Expand Up @@ -202,35 +202,38 @@
└── when position is not healthy
└── revert with INSUFFICIENT_COLLATERAL
.
└── liquidate(MarketParams memory marketParams, address borrower, uint256 seized, bytes calldata data) external
└── liquidate(MarketParams memory marketParams, address borrower, uint256 seizedAssets, uint256 repaidShares, bytes calldata data) external
β”œβ”€β”€ when market is not created
β”‚ └── revert with MARKET_NOT_CREATED
└── when market is created
β”œβ”€β”€ when the assets to seized is zero
β”‚ └── revert with ZERO_ASSETS
└── when the assets to seized is not zero
β”œβ”€β”€ it should accrue the interest
β”œβ”€β”€ when both seizedAssets and repaidShares are null or both seizedAssets and repaidShares are not null
β”‚ └─ revert with INCONSISTENT_INPUT
└── when one of seizedAssets or repaidShares is null and one of seizedAssets or repaidShares is not null
β”œβ”€β”€ it should accrue the interests
β”œβ”€β”€ when position is healthy
β”‚ └── revert with HEALTHY_POSITION
└── when the position is not healthy
β”œβ”€β”€ it should compute repaid = seized.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(incentive)
β”œβ”€β”€ it should compute repaidShares = repaid.toSharesDown(totalBorrowAssets[marketParams.id], totalBorrowShares[marketParams.id]);
β”œβ”€β”€ it should remove repaidShares from borrowShares[market.id][borrower]
β”œβ”€β”€ when seizedAssets is not zero
β”‚ β”œβ”€β”€ it should compute assetsRepaid = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor(marketParams.lltv))
β”‚ └── it should compute repaidShares = assetsRepaid.toSharesDown(totalBorrow[marketParams.id], totalBorrowShares[market.id])
β”œβ”€β”€ when repaidShares is not zero
β”‚ β”œβ”€β”€ it should compute assetsRepaid = repaidShares.toAssetsUp(totalBorrow[marketParams.id], totalBorrowShares[marketParams.id])
β”‚ └── it should compute seizedAssets = assetsRepaid.wMulDown(incentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice)
β”œβ”€β”€ it should remove repaidShares from totalBorrowShares[marketParams.id]
β”œβ”€β”€ it should remove repaid from totalBorrowAssets[marketParams.id]
β”œβ”€β”€ it should remove seized from collateral[marketParams.id][borrower]
β”œβ”€β”€ it should remove assetsRepaid from totalBorrow[marketParams.id]
β”œβ”€β”€ it should remove repaidShares from collateral[marketParams.id][borrower]
β”œβ”€β”€ if after the liquidation the borrower's collateral is 0
β”‚ └── it should realize bad debt
β”‚ β”œβ”€β”€ it should compute badDebt = borrowShares[marketParams.id][borrower].toAssetsUp(totalBorrowAssets[marketParams.id], totalBorrowShares[marketParams.id])
β”‚ β”œβ”€β”€ it should compute badDebt = borrowShares[marketParams.id][borrower].toAssetsUp(totalBorrow[marketParams.id], totalBorrowShares[marketParams.id])
β”‚ β”œβ”€β”€ it should remove badDebt from totalSupplyAssets[marketParams.id]
β”‚ β”œβ”€β”€ it should remove badDebt from totalBorrowAssets[marketParams.id]
β”‚ β”œβ”€β”€ it should remove borrowShares[marketParams.id][borrower] from totalBorrowShares[marketParams.id]
β”‚ └── it should set borrowShares[marketParams.id][borrower] to 0
β”œβ”€β”€ it should transfer seized of collateral asset to the sender
β”œβ”€β”€ it should emit Liquidate(marketParams.id, msg.sender, borrower, repaid, repaidShares, seized, badDebtShares)
β”œβ”€β”€ it should transfer repaidShares of collateral asset to the sender
β”œβ”€β”€ it should emit Liquidate(marketParams.id, msg.sender, borrower, assetsRepaid, repaidShares, seizedAssets, badDebtShares)
β”œβ”€β”€ if data.length > 0
β”‚ └── it should call sender's onMorphoLiquidate callback
└── it should transfer repaid of borrowable asset from the sender the Morpho
└── it should transfer assetsRepaid of borrowable asset from the sender to Morpho
.
└── flashLoan(address token, uint256 assets, bytes calldata data) external
β”œβ”€β”€ it should transfer assets of token from Morpho to the sender
Expand Down

0 comments on commit 389cd0c

Please sign in to comment.