Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add repaidShares argument to liquidate function #322

Merged
merged 22 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions src/Morpho.sol
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,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 @@ -352,14 +355,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 @@ -372,13 +382,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 @@ -214,19 +214,26 @@ interface IMorpho {
function withdrawCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, address receiver)
external;

/// @notice Liquidates `seized` of collateral to `market` 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.
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
/// @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 @@ -21,7 +21,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 @@ -165,9 +165,9 @@ contract IntegrationCallbacksTest is
borrowableToken.approve(address(morpho), 0);

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

Expand Down
92 changes: 83 additions & 9 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, market));

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(market, address(this), 0, hex"");
vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT));
morpho.liquidate(market, 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(market, 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(market, BORROWER, amountSeized, hex"");
morpho.liquidate(market, BORROWER, amountSeized, 0, hex"");
}

function testLiquidateNoBadDebt(
function testLiquidateSeizedInputNoBadDebt(
uint256 amountCollateral,
uint256 amountSupplied,
uint256 amountBorrowed,
Expand Down Expand Up @@ -91,12 +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(market, BORROWER, amountSeized, hex"");
(uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(market, 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 @@ -114,6 +124,70 @@ 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(market.lltv)
);
vm.assume(maxRepaidShares != 0);
sharesRepaid = bound(sharesRepaid, 1, min(maxRepaidShares, expectedBorrowShares));
uint256 expectedRepaid = sharesRepaid.toAssetsUp(amountBorrowed, expectedBorrowShares);
uint256 expectedSeized =
expectedRepaid.wMulDown(_liquidationIncentive(market.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(market, amountCollateral, BORROWER, hex"");
morpho.borrow(market, 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(market, 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 @@ -182,10 +256,10 @@ contract IntegrationLiquidateTest is BaseTest {
amountCollateral,
params.expectedBadDebt * SharesMathLib.VIRTUAL_SHARES
);
(uint256 returnRepaid, uint256 returnRepaidShares) = morpho.liquidate(market, BORROWER, amountCollateral, hex"");
(uint256 returnSeized, uint256 returnRepaid) = morpho.liquidate(market, 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
2 changes: 1 addition & 1 deletion test/hardhat/Morpho.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe("Morpho", () => {

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

await morpho.connect(liquidator).liquidate(market, borrower.address, seized, "0x");
await morpho.connect(liquidator).liquidate(market, 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(Market memory market, address borrower, uint256 seized, bytes calldata data) external
└── liquidate(Market memory market, 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[market.id], totalBorrowShares[market.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(market.lltv))
│ └── it should compute repaidShares = assetsRepaid.toSharesDown(totalBorrow[market.id], totalBorrowShares[market.id])
├── when repaidShares is not zero
│ ├── it should compute assetsRepaid = repaidShares.toAssetsUp(totalBorrow[market.id], totalBorrowShares[market.id])
│ └── it should compute seizedAssets = assetsRepaid.wMulDown(incentiveFactor).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice)
├── it should remove repaidShares from totalBorrowShares[market.id]
├── it should remove repaid from totalBorrowAssets[market.id]
├── it should remove seized from collateral[market.id][borrower]
├── it should remove assetsRepaid from totalBorrow[market.id]
├── it should remove repaidShares from collateral[market.id][borrower]
├── if after the liquidation the borrower's collateral is 0
│ └── it should realize bad debt
│ ├── it should compute badDebt = borrowShares[market.id][borrower].toAssetsUp(totalBorrowAssets[market.id], totalBorrowShares[market.id])
│ ├── it should compute badDebt = borrowShares[market.id][borrower].toAssetsUp(totalBorrow[market.id], totalBorrowShares[market.id])
│ ├── it should remove badDebt from totalSupplyAssets[market.id]
│ ├── it should remove badDebt from totalBorrowAssets[market.id]
│ ├── it should remove borrowShares[market.id][borrower] from totalBorrowShares[market.id]
│ └── it should set borrowShares[market.id][borrower] to 0
├── it should transfer seized of collateral asset to the sender
├── it should emit Liquidate(market.id, msg.sender, borrower, repaid, repaidShares, seized, badDebtShares)
├── it should transfer repaidShares of collateral asset to the sender
├── it should emit Liquidate(market.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