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 12 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.
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
20 changes: 13 additions & 7 deletions src/interfaces/IMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,23 @@ interface IMorpho {

/// @notice Liquidates the given `seized` assets to the given `market` of the given `borrower`'s position,
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
/// optionally calling back the caller's `onMorphoLiquidate` function with the given `data`.
Jean-Grimal 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 Either `seized` or `repaidShares` should be zero.
/// Seizing more than the collateral balance will underflow and revert without any error message.
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved
/// @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 shares 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 seizedCollateral The amount of assets repaid.
/// @return repaidAssets The amount of repaid asset.
function liquidate(
MarketParams memory marketParams,
address borrower,
uint256 seizedAssets,
uint256 shares,
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
bytes memory data
) external returns (uint256 seizedCollateral, uint256 repaidAssets);
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

/// @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 @@ -145,9 +145,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 testLiquidateZeroAmount(uint256 seized, uint256 sharesRepaid) public {
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
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 maxRepaidShaires = amountCollateral.mulDivDown(priceCollateral, ORACLE_PRICE_SCALE).wDivDown(
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved
_liquidationIncentive(market.lltv)
);
vm.assume(maxRepaidShaires != 0);
sharesRepaid = bound(sharesRepaid, 1, min(maxRepaidShaires, expectedBorrowShares));
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved
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
56 changes: 31 additions & 25 deletions test/morpho_tests.tree
Original file line number Diff line number Diff line change
Expand Up @@ -206,31 +206,37 @@
├── 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 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]
├── 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]
├── 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 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)
├── if data.length > 0
│ └── it should call sender's onMorphoLiquidate callback
└── it should transfer repaid of borrowable asset from the sender the Morpho
├── when both assets and shares are null or both assets and shares are not null
│ └─ revert with INCONSISTENT_INPUT
└── when one of assets or shares is null and one of assets or shares is not null
├── when the assets to seized is zero
│ └── revert with ZERO_ASSETS
└── when the assets to seized is not zero
├── it should accrue the interests
├── when position is healthy
│ └── revert with HEALTHY_POSITION
└── when the position is not healthy
├── when assets is not zero
│ ├── it should compute repaid = seized.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(incentive)
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
│ └── it should compute repaidShares = repaid.toSharesDown(totalBorrow[market.id], totalBorrowShares[market.id]);
├── when repaidShares is not zero
│ ├── it should compute repaid = sharesRepaid.toAssetsDown(totalBorrow[id], totalBorrowShares[id]);
│ └── it should compute seized = assetsRepaid.wMulDown(liquidationIncentiveFactor(market.lltv)).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
├── it should remove repaidShares from totalBorrowShares[market.id]
├── it should remove repaid from totalBorrow[market.id]
├── it should remove seized 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(totalBorrow[market.id], totalBorrowShares[market.id])
│ ├── it should remove badDebt from totalSupply[market.id]
│ ├── it should remove badDebt from totalBorrow[market.id]
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
│ ├── 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)
├── if data.length > 0
│ └── it should call sender's onMorphoLiquidate callback
└── it should transfer repaid of borrowable asset from the sender the Morpho
.
└── flashLoan(address token, uint256 assets, bytes calldata data) external
├── it should transfer assets of token from Morpho to the sender
Expand Down