Skip to content

Commit

Permalink
Merge pull request #21 from morpho-org/refactor/callbacks-jean
Browse files Browse the repository at this point in the history
Refactor of callback snippets
  • Loading branch information
tomrpl authored Dec 12, 2023
2 parents 45d938d + d997e63 commit 11c0ef9
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 111 deletions.
79 changes: 55 additions & 24 deletions src/blue/CallbacksSnippets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol";
import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";

import {ISwap} from "@snippets/blue/interfaces/ISwap.sol";

/*
The following swapper contract only has educational purpose. It simulates a contract allowing to swap a token against
another, with the exact price returned by an arbitrary oracle.
Expand All @@ -27,8 +29,7 @@ One should be aware that has to be taken into account on potential swap:
1. slippage
2. fees
TODOS: add a definition of what snippets are useful for
*/
*/

contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallback, IMorphoLiquidateCallback {
using MorphoLib for IMorpho;
Expand All @@ -52,10 +53,13 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb
Callbacks
Reminder: for a given market, one can leverage his position up to a leverageFactor = 1/1-LLTV,
Reminder: for a given market, one can leverage his position up to a maxLeverageFactor = 1/1-LLTV,
Example : with a LLTV of 80% -> 5 is the max leverage factor
Warning : it is strongly recommended to not use the the max leverage factor, as the position could get liquidated
at next block because of interets.
*/

function onMorphoSupplyCollateral(uint256 amount, bytes calldata data) external onlyMorpho {
Expand All @@ -72,10 +76,11 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb
}

function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external onlyMorpho {
(uint256 toSwap, MarketParams memory marketParams) = abi.decode(data, (uint256, MarketParams));
uint256 returnedAmount = swapper.swapCollatToLoan(toSwap);
require(returnedAmount > repaidAssets); // Add logic for gas cost threshold for instance
ERC20(marketParams.loanToken).approve(address(swapper), returnedAmount);
(address collateralToken) = abi.decode(data, (address));

ERC20(collateralToken).approve(address(swapper), type(uint256).max);

swapper.swapCollatToLoan(ERC20(collateralToken).balanceOf(address(this)));
}

function onMorphoRepay(uint256 amount, bytes calldata data) external onlyMorpho {
Expand All @@ -88,6 +93,13 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb
swapper.swapCollatToLoan(amount);
}

/// @notice Create a leveraged position with a given `leverageFactor` on the `marketParams` market of Morpho Blue
/// for the sendder.
/// @dev The sender needs to hold `initAmountCollateral`, and to approve this contract to manage his positions on
/// Morpho Blue.
/// @param leverageFactor The factor of leverage wanted. can't be higher than 1/1-LLTV.
/// @param initAmountCollateral The initial amount of collateral owned by the sender.
/// @param marketParams The market to perform the leverage on.
function leverageMe(uint256 leverageFactor, uint256 initAmountCollateral, MarketParams calldata marketParams)
public
{
Expand All @@ -100,10 +112,10 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb

// (leverageFactor - 1) * InitAmountCollateral.mulDivDown.(ORACLE_PRICE_SCALE, IOracle(oracle).price())

// However here we have price = `ORACLE_PRICE_SCALE`, so loanAmount = (leverageFactor - 1) *
// However in this simple example we have price = `ORACLE_PRICE_SCALE`, so loanAmount = (leverageFactor - 1) *
// InitAmountCollateral

// Warning : When using real swaps, price doesn't equal `ORACLE_PRICE_SCALE` anymore, so
// Warning : When using real swaps, price doesn't necessarily equal `ORACLE_PRICE_SCALE` anymore, so
// mulDivDown.(ORACLE_PRICE_SCALE, IOracle(oracle).price()) can't be removed from the calculus, and therefore an
// oracle should be used to compute the correct amount.
// Warning : When using real swaps, fees and slippage should also be taken into account to compute `loanAmount`.
Expand All @@ -117,20 +129,11 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb
);
}

function liquidateWithoutCollat(
address borrower,
uint256 loanAmountToRepay,
uint256 assetsToSeize,
MarketParams calldata marketParams
) public returns (uint256 seizedAssets, uint256 repaidAssets) {
_approveMaxTo(address(marketParams.collateralToken), address(this));

uint256 repaidShares;

(seizedAssets, repaidAssets) =
morpho.liquidate(marketParams, borrower, assetsToSeize, repaidShares, abi.encode(loanAmountToRepay));
}

/// @notice Create a deleverages the sender on the given `marketParams` market of Morpho Blue by repaying his debt
/// and withdrawing his collateral. The withdrawn assets are sent to the sender.
/// @dev If the sender has a leveraged position on `marketParams`, he doesn't need any tokens to perform this
/// operation.
/// @param marketParams The market to perform the leverage on.
function deLeverageMe(MarketParams calldata marketParams) public returns (uint256 amountRepayed) {
uint256 totalShares = morpho.borrowShares(marketParams.id(), msg.sender);

Expand All @@ -139,10 +142,38 @@ contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallb
(amountRepayed,) = morpho.repay(marketParams, 0, totalShares, msg.sender, abi.encode(marketParams, msg.sender));

ERC20(marketParams.collateralToken).safeTransfer(
msg.sender, ERC20(marketParams.collateralToken).balanceOf(msg.sender)
msg.sender, ERC20(marketParams.collateralToken).balanceOf(address(this))
);
}

/// @notice Fully liquidates the borrow position of `borrower` on the given `marketParams` market of Morpho Blue and
/// sends the profit of the liquidation to the sender.
/// @dev Thanks to callbacks, the sender doesn't need to hold any tokens to perform this operation.
/// @param marketParams The market to perform the liquidation on.
/// @param borrower The owner of the liquidable borrow position.
/// @param seizeFullCollat Pass `True` to seize all the collateral of `borrower`. Pass `False` to repay all of the
/// `borrower`'s debt.
function fullLiquidationWithoutCollat(MarketParams calldata marketParams, address borrower, bool seizeFullCollat)
public
returns (uint256 seizedAssets, uint256 repaidAssets)
{
Id id = marketParams.id();

uint256 seizedCollateral;
uint256 repaidShares;

if (seizeFullCollat) seizedCollateral = morpho.collateral(id, borrower);
else repaidShares = morpho.borrowShares(id, borrower);

_approveMaxTo(marketParams.loanToken, address(morpho));

(seizedAssets, repaidAssets) = morpho.liquidate(
marketParams, borrower, seizedCollateral, repaidShares, abi.encode(marketParams.collateralToken)
);

ERC20(marketParams.loanToken).safeTransfer(msg.sender, ERC20(marketParams.loanToken).balanceOf(address(this)));
}

function _approveMaxTo(address asset, address spender) internal {
if (ERC20(asset).allowance(address(this), spender) == 0) {
ERC20(asset).safeApprove(spender, type(uint256).max);
Expand Down
156 changes: 69 additions & 87 deletions test/forge/blue/TestCallbacksSnippets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "@morpho-blue-test/BaseTest.sol";
import {ISwap} from "@snippets/blue/interfaces/ISwap.sol";
import {SwapMock} from "@snippets/blue/mocks/SwapMock.sol";
import {CallbacksSnippets} from "@snippets/blue/CallbacksSnippets.sol";
import {ERC20} from "@solmate/utils/SafeTransferLib.sol";

contract CallbacksIntegrationTest is BaseTest {
using MathLib for uint256;
Expand Down Expand Up @@ -33,23 +34,20 @@ contract CallbacksIntegrationTest is BaseTest {
vm.stopPrank();
}

function testLeverageMe(uint256 initAmountCollateral) public {
// INITIALISATION

uint256 leverageFactor = 4; // nb to set
function testLeverageMe(uint256 initAmountCollateral, uint256 leverageFactor) public {
uint256 maxLeverageFactor = WAD / (WAD - marketParams.lltv);

leverageFactor = bound(leverageFactor, 2, maxLeverageFactor);
initAmountCollateral = bound(initAmountCollateral, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT / leverageFactor);
uint256 finalAmountCollateral = initAmountCollateral * leverageFactor;

oracle.setPrice(ORACLE_PRICE_SCALE);
loanToken.setBalance(SUPPLIER, finalAmountCollateral);
collateralToken.setBalance(USER, initAmountCollateral);

// supplying enough liquidity in the market
vm.startPrank(SUPPLIER);
loanToken.setBalance(address(SUPPLIER), finalAmountCollateral);
morpho.supply(marketParams, finalAmountCollateral, 0, address(SUPPLIER), hex"");
vm.stopPrank();
vm.prank(SUPPLIER);
morpho.supply(marketParams, finalAmountCollateral, 0, SUPPLIER, hex"");

collateralToken.setBalance(USER, initAmountCollateral);
vm.prank(USER);
snippets.leverageMe(leverageFactor, initAmountCollateral, marketParams);

Expand All @@ -60,24 +58,22 @@ contract CallbacksIntegrationTest is BaseTest {
assertEq(morpho.expectedBorrowAssets(marketParams, USER), loanAmount, "no collateral");
}

function testDeLeverageMe(uint256 initAmountCollateral) public {
/// same as testLeverageMe

uint256 leverageFactor = 4; // nb to set
function testDeLeverageMe(uint256 initAmountCollateral, uint256 leverageFactor) public {
uint256 maxLeverageFactor = WAD / (WAD - marketParams.lltv);

leverageFactor = bound(leverageFactor, 2, maxLeverageFactor);
initAmountCollateral = bound(initAmountCollateral, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT / leverageFactor);
uint256 finalAmountCollateral = initAmountCollateral * leverageFactor;

oracle.setPrice(ORACLE_PRICE_SCALE);
loanToken.setBalance(SUPPLIER, finalAmountCollateral);
collateralToken.setBalance(USER, initAmountCollateral);

vm.startPrank(SUPPLIER);
loanToken.setBalance(address(SUPPLIER), finalAmountCollateral);
morpho.supply(marketParams, finalAmountCollateral, 0, address(SUPPLIER), hex"");
vm.stopPrank();
vm.prank(SUPPLIER);
morpho.supply(marketParams, finalAmountCollateral, 0, SUPPLIER, hex"");

uint256 loanAmount = initAmountCollateral * (leverageFactor - 1);

collateralToken.setBalance(USER, initAmountCollateral);
vm.prank(USER);
snippets.leverageMe(leverageFactor, initAmountCollateral, marketParams);

Expand All @@ -91,6 +87,9 @@ contract CallbacksIntegrationTest is BaseTest {

assertEq(morpho.borrowShares(marketParams.id(), USER), 0, "no borrow");
assertEq(amountRepayed, loanAmount, "no repaid");
assertEq(
ERC20(marketParams.collateralToken).balanceOf(USER), initAmountCollateral, "user didn't get back his assets"
);
}

struct LiquidateTestParams {
Expand All @@ -101,72 +100,55 @@ contract CallbacksIntegrationTest is BaseTest {
uint256 lltv;
}

// TODOS: implement the following function
// function testLiquidateWithoutCollateral(LiquidateTestParams memory params, uint256 amountSeized) public {
// _setLltv(_boundTestLltv(params.lltv));
// (params.amountCollateral, params.amountBorrowed, params.priceCollateral) =
// _boundUnhealthyPosition(params.amountCollateral, params.amountBorrowed, params.priceCollateral);

// vm.assume(params.amountCollateral > 1);

// params.amountSupplied =
// bound(params.amountSupplied, params.amountBorrowed, params.amountBorrowed + MAX_TEST_AMOUNT);
// _supply(params.amountSupplied);

// collateralToken.setBalance(BORROWER, params.amountCollateral);

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

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

// oracle.setPrice(params.priceCollateral);

// // uint256 borrowShares = morpho.borrowShares(id, BORROWER);
// uint256 liquidationIncentiveFactor = _liquidationIncentiveFactor(marketParams.lltv);
// uint256 maxSeized = params.amountBorrowed.wMulDown(liquidationIncentiveFactor).mulDivDown(
// ORACLE_PRICE_SCALE, params.priceCollateral
// );
// vm.assume(maxSeized != 0);

// amountSeized = bound(amountSeized, 1, Math.min(maxSeized, params.amountCollateral - 1));

// uint256 expectedRepaid =
// amountSeized.mulDivUp(params.priceCollateral, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor);
// // uint256 expectedRepaidShares =
// // expectedRepaid.toSharesDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id));

// vm.startPrank(address(snippets));
// loanToken.approve(address(morpho), type(uint256).max);
// loanToken.approve(address(swapMock), type(uint256).max);
// collateralToken.approve(address(morpho), type(uint256).max);
// collateralToken.approve(address(swapMock), type(uint256).max);
// loanToken.approve(address(snippets), type(uint256).max);
// collateralToken.approve(address(snippets), type(uint256).max);
// loanToken.setBalance(address(snippets), params.amountBorrowed);

// // vm.prank(LIQUIDATOR);

// (uint256 returnSeized, uint256 returnRepaid) =
// snippets.liquidateWithoutCollat(BORROWER, params.amountBorrowed, amountSeized, swapMock, marketParams);
// // morpho.liquidate(marketParams, BORROWER, amountSeized, 0, hex"");
// // uint256 expectedCollateral = params.amountCollateral - amountSeized;
// // uint256 expectedBorrowed = params.amountBorrowed - expectedRepaid;
// // uint256 expectedBorrowShares = borrowShares - expectedRepaidShares;

// assertEq(returnSeized, amountSeized, "returned seized amount");
// assertEq(returnRepaid, expectedRepaid, "returned asset amount");
// // assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares");
// // assertEq(morpho.totalBorrowAssets(id), expectedBorrowed, "total borrow");
// // assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares");
// // assertEq(morpho.collateral(id, BORROWER), expectedCollateral, "collateral");
// // assertEq(loanToken.balanceOf(BORROWER), params.amountBorrowed, "borrower balance");
// // assertEq(loanToken.balanceOf(LIQUIDATOR), expectedBorrowed, "liquidator balance");
// // assertEq(loanToken.balanceOf(address(morpho)), params.amountSupplied - expectedBorrowed, "morpho
// balance");
// // assertEq(collateralToken.balanceOf(address(morpho)), expectedCollateral, "morpho collateral balance");
// // assertEq(collateralToken.balanceOf(LIQUIDATOR), amountSeized, "liquidator collateral balance");
// }
function testLiquidateSeizeAllCollateral(uint256 borrowAmount) public {
borrowAmount = bound(borrowAmount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);

uint256 collateralAmount = borrowAmount.wDivUp(marketParams.lltv);

oracle.setPrice(ORACLE_PRICE_SCALE);
loanToken.setBalance(SUPPLIER, borrowAmount);
collateralToken.setBalance(BORROWER, collateralAmount);

vm.prank(SUPPLIER);
morpho.supply(marketParams, borrowAmount, 0, SUPPLIER, hex"");

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

oracle.setPrice(ORACLE_PRICE_SCALE / 2);

vm.prank(LIQUIDATOR);
snippets.fullLiquidationWithoutCollat(marketParams, BORROWER, true);

assertEq(morpho.collateral(marketParams.id(), BORROWER), 0, "not fully liquididated");
assertGt(ERC20(marketParams.loanToken).balanceOf(LIQUIDATOR), 0, "Liquidator didn't receive profit");
}

function testLiquidateRepayAllShares(uint256 borrowAmount) public {
borrowAmount = bound(borrowAmount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);

uint256 collateralAmount = borrowAmount.wDivUp(marketParams.lltv);

oracle.setPrice(ORACLE_PRICE_SCALE);
loanToken.setBalance(SUPPLIER, borrowAmount);
collateralToken.setBalance(BORROWER, collateralAmount);

vm.prank(SUPPLIER);
morpho.supply(marketParams, borrowAmount, 0, SUPPLIER, hex"");

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

oracle.setPrice(ORACLE_PRICE_SCALE.wMulDown(0.95e18));

vm.prank(LIQUIDATOR);
snippets.fullLiquidationWithoutCollat(marketParams, BORROWER, false);

assertEq(morpho.borrowShares(marketParams.id(), BORROWER), 0, "not fully liquididated");
assertGt(ERC20(marketParams.loanToken).balanceOf(LIQUIDATOR), 0, "Liquidator didn't receive profit");
}
}

0 comments on commit 11c0ef9

Please sign in to comment.