Skip to content

Commit

Permalink
Merge branch 'main' of github.com:morpho-labs/blue into refactor/posi…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
Rubilmax committed Aug 23, 2023
2 parents 3019d91 + 582e4df commit d44b4eb
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 67 deletions.
44 changes: 28 additions & 16 deletions src/Morpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ contract Morpho is IMorpho {
require(newOwner != address(0), ErrorsLib.ZERO_ADDRESS);
owner = newOwner;

DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256("Morpho"), block.chainid, address(this)));
DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, block.chainid, address(this)));
}

/* MODIFIERS */
Expand Down 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);

position[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);
}

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

position[id][borrower].collateral -= seized.toUint128();
position[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 {
position[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
5 changes: 4 additions & 1 deletion src/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pragma solidity >=0.5.0;
/// @custom:contact [email protected]
/// @notice Interface that oracles used by Morpho must implement.
interface IOracle {
/// @notice Returns the price of the collateral asset quoted in the borrowable asset, scaled by 1e36.
/// @notice Returns the price of 1 asset of collateral token quoted in 1 asset of borrowable token, scaled by 1e36.
/// @dev It corresponds to the price of 10**(collateral decimals) assets of collateral token quoted in
/// 10**(borrowable decimals) assets of borrowable token with `36 + borrowable decimals - collateral decimals`
/// decimals of precision.
function price() external view returns (uint256);
}
2 changes: 1 addition & 1 deletion src/libraries/ConstantsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ uint256 constant LIQUIDATION_CURSOR = 0.3e18;
uint256 constant MAX_LIQUIDATION_INCENTIVE_FACTOR = 1.15e18;

/// @dev The EIP-712 typeHash for EIP712Domain.
bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");
bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");

/// @dev The EIP-712 typeHash for Authorization.
bytes32 constant AUTHORIZATION_TYPEHASH =
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/periphery/MorphoLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import {MorphoStorageLib} from "./MorphoStorageLib.sol";

library MorphoLib {
function supplyShares(IMorpho morpho, Id id, address user) internal view returns (uint256) {
bytes32[] memory slot = _array(MorphoStorageLib.userSupplySharesSlot(id, user));
bytes32[] memory slot = _array(MorphoStorageLib.positionSupplySharesSlot(id, user));
return uint256(morpho.extSloads(slot)[0]);
}

function borrowShares(IMorpho morpho, Id id, address user) internal view returns (uint256) {
bytes32[] memory slot = _array(MorphoStorageLib.userBorrowSharesAndCollateralSlot(id, user));
bytes32[] memory slot = _array(MorphoStorageLib.positionBorrowSharesAndCollateralSlot(id, user));
return uint128(uint256(morpho.extSloads(slot)[0]));
}

function collateral(IMorpho morpho, Id id, address user) internal view returns (uint256) {
bytes32[] memory slot = _array(MorphoStorageLib.userBorrowSharesAndCollateralSlot(id, user));
bytes32[] memory slot = _array(MorphoStorageLib.positionBorrowSharesAndCollateralSlot(id, user));
return uint256(morpho.extSloads(slot)[0] >> 128);
}

Expand Down
13 changes: 7 additions & 6 deletions src/libraries/periphery/MorphoStorageLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ library MorphoStorageLib {

uint256 internal constant OWNER_SLOT = 0;
uint256 internal constant FEE_RECIPIENT_SLOT = 1;
uint256 internal constant USER_SLOT = 2;
uint256 internal constant POSITION_SLOT = 2;
uint256 internal constant MARKET_SLOT = 3;
uint256 internal constant IS_IRM_ENABLED_SLOT = 4;
uint256 internal constant IS_LLTV_ENABLED_SLOT = 5;
Expand Down Expand Up @@ -46,14 +46,15 @@ library MorphoStorageLib {
return bytes32(FEE_RECIPIENT_SLOT);
}

function userSupplySharesSlot(Id id, address user) internal pure returns (bytes32) {
return
bytes32(uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, USER_SLOT))))) + SUPPLY_SHARES_OFFSET);
function positionSupplySharesSlot(Id id, address user) internal pure returns (bytes32) {
return bytes32(
uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, POSITION_SLOT))))) + SUPPLY_SHARES_OFFSET
);
}

function userBorrowSharesAndCollateralSlot(Id id, address user) internal pure returns (bytes32) {
function positionBorrowSharesAndCollateralSlot(Id id, address user) internal pure returns (bytes32) {
return bytes32(
uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, USER_SLOT)))))
uint256(keccak256(abi.encode(user, keccak256(abi.encode(id, POSITION_SLOT)))))
+ BORROW_SHARES_AND_COLLATERAL_OFFSET
);
}
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
4 changes: 2 additions & 2 deletions test/forge/periphery/MorphoStorageLib.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ contract MorphoStorageLibTest is BaseTest {
bytes32[] memory slots = new bytes32[](16);
slots[0] = MorphoStorageLib.ownerSlot();
slots[1] = MorphoStorageLib.feeRecipientSlot();
slots[2] = MorphoStorageLib.userSupplySharesSlot(id, address(this));
slots[3] = MorphoStorageLib.userBorrowSharesAndCollateralSlot(id, BORROWER);
slots[2] = MorphoStorageLib.positionSupplySharesSlot(id, address(this));
slots[3] = MorphoStorageLib.positionBorrowSharesAndCollateralSlot(id, BORROWER);
slots[4] = MorphoStorageLib.marketTotalSupplyAssetsAndSharesSlot(id);
slots[5] = MorphoStorageLib.marketTotalBorrowAssetsAndSharesSlot(id);
slots[6] = MorphoStorageLib.marketLastUpdateAndFeeSlot(id);
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.position(id, borrower.address)).collateral;

Expand Down
Loading

0 comments on commit d44b4eb

Please sign in to comment.