Skip to content

Commit

Permalink
Merge pull request #15 from morpho-org/feat/callbacks
Browse files Browse the repository at this point in the history
Feat/callbacks
  • Loading branch information
tomrpl authored Nov 16, 2023
2 parents 938a21f + 7f2e7a4 commit 78adf91
Show file tree
Hide file tree
Showing 13 changed files with 584 additions and 104 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/metamorpho"]
path = lib/metamorpho
url = https://github.com/morpho-org/metamorpho
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ script = "/dev/null"
[profile.test]
via-ir = false

fuzz.max_test_rejects = 100000

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at e0e9ff
7 changes: 3 additions & 4 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
@forge-std/=lib/morpho-blue/lib/forge-std/src/

@morpho-blue/=lib/morpho-blue/src/
@morpho-blue-test/=lib/morpho-blue/test/
@morpho-blue-test/=lib/morpho-blue/test/forge/

@openzeppelin4/=lib/openzeppelin-contracts/contracts/
@openzeppelin5/=lib/metamorpho/lib/openzeppelin-contracts/contracts/
@openzeppelin/=lib/metamorpho/lib/openzeppelin-contracts/contracts/

@snippets/=src/

@solmate/=lib/morpho-blue/lib/solmate/src/
solmate/=lib/morpho-blue/lib/permit2/lib/solmate/
@solmate/=lib/solmate/src/

@metamorpho/=lib/metamorpho/src/
@metamorpho-test/=lib/metamorpho/test/forge/
1 change: 1 addition & 0 deletions src/blue/BlueSnippets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Id, IMorpho, MarketParams, Market} from "@morpho-blue/interfaces/IMorpho
import {IERC20} from "@morpho-blue/interfaces/IERC20.sol";
import {IIrm} from "@morpho-blue/interfaces/IIrm.sol";
import {IOracle} from "@morpho-blue/interfaces/IOracle.sol";

import {ERC20} from "@openzeppelin4/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin4/token/ERC20/utils/SafeERC20.sol";
import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol";
Expand Down
168 changes: 168 additions & 0 deletions src/blue/CallbacksSnippets.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import {SwapMock} from "@snippets/blue/mocks/SwapMock.sol";
import {
IMorphoSupplyCollateralCallback,
IMorphoLiquidateCallback,
IMorphoRepayCallback
} from "@morpho-blue/interfaces/IMorphoCallbacks.sol";

import {Id, IMorpho, MarketParams, Market} from "@morpho-blue/interfaces/IMorpho.sol";
import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol";
import {MathLib} from "@morpho-blue/libraries/MathLib.sol";
import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol";
import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";

/*
The SwapMock 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.
The introduction of the SwapMock contract is to showcase the functioning of leverage on Morpho Blue (using callbacks)
without highlighting any known DEX.
Therefore, SwapMock must be replaced (by the swap of your choice) in your implementation. The functions
`swapCollatToLoan` and `swapLoanToCollat` must as well be adapted to match the ones of the chosen swap contract.
One should be aware that has to be taken into account on potential swap:
1. slippage
2. fees
add a definition of what snippets are
*/
contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallback, IMorphoLiquidateCallback {
using MathLib for uint256;
using MorphoLib for IMorpho;
using MarketParamsLib for MarketParams;
using SafeTransferLib for ERC20;

IMorpho public immutable morpho;
SwapMock swapMock;

constructor(address morphoAddress) {
morpho = IMorpho(morphoAddress);
}

/*
Callbacks
Reminder: for a given market, one can leverage his position up to a leverageFactor = 1/1-LLTV,
Example : with a LLTV of 80% -> 5 is the max leverage factor
*/

function onMorphoSupplyCollateral(uint256 amount, bytes calldata data) external onlyMorpho {
(uint256 toBorrow, MarketParams memory marketParams, address user) =
abi.decode(data, (uint256, MarketParams, address));
(uint256 amountBis,) = morpho.borrow(marketParams, toBorrow, 0, user, address(this));

ERC20(marketParams.loanToken).approve(address(swapMock), amount);

// Logic to Implement. Following example is a swap, could be a 'unwrap + stake + wrap staked' for
// wETH(wstETH) Market
// _approveMaxTo(marketParams.);
swapMock.swapLoanToCollat(amountBis);
}

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

function onMorphoRepay(uint256 amount, bytes calldata data) external onlyMorpho {
(MarketParams memory marketParams, address user) = abi.decode(data, (MarketParams, address));
uint256 toWithdraw = morpho.collateral(marketParams.id(), user);

morpho.withdrawCollateral(marketParams, toWithdraw, user, address(this));

ERC20(marketParams.collateralToken).approve(address(swapMock), amount);
swapMock.swapCollatToLoan(amount);
}

function leverageMe(
uint256 leverageFactor,
uint256 initAmountCollateral,
SwapMock _swapMock,
MarketParams calldata marketParams
) public {
_setSwapMock(_swapMock);

ERC20(marketParams.collateralToken).safeTransferFrom(msg.sender, address(this), initAmountCollateral);

uint256 finalAmountcollateral = initAmountCollateral * leverageFactor;

// The amount of LoanToken to be borrowed (and then swapped against collateralToken) to perform the callback is
// the following :

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

// However here 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
// 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`.

uint256 loanAmount = (leverageFactor - 1) * initAmountCollateral;

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

morpho.supplyCollateral(
marketParams, finalAmountcollateral, msg.sender, abi.encode(loanAmount, marketParams, msg.sender)
);
}

function liquidateWithoutCollat(
address borrower,
uint256 loanAmountToRepay,
uint256 assetsToSeize,
SwapMock _swapMock,
MarketParams calldata marketParams
) public returns (uint256 seizedAssets, uint256 repaidAssets) {
_setSwapMock(_swapMock);

_approveMaxTo(address(marketParams.collateralToken), address(this));

uint256 repaidShares;

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

function deLeverageMe(SwapMock _swapMock, MarketParams calldata marketParams)
public
returns (uint256 amountRepayed)
{
_setSwapMock(_swapMock);

uint256 totalShares = morpho.borrowShares(marketParams.id(), msg.sender);

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

(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)
);
}

modifier onlyMorpho() {
require(msg.sender == address(morpho), "msg.sender should be Morpho Blue");
_;
}

function _approveMaxTo(address asset, address spender) internal {
if (ERC20(asset).allowance(address(this), spender) == 0) {
ERC20(asset).approve(spender, type(uint256).max);
}
}

function _setSwapMock(SwapMock _swapMock) public {
swapMock = _swapMock;
}
}
43 changes: 43 additions & 0 deletions src/blue/mocks/SwapMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import {ORACLE_PRICE_SCALE} from "@morpho-blue/libraries/ConstantsLib.sol";

import "@morpho-blue/mocks/ERC20Mock.sol";
import {IOracle} from "@morpho-blue/interfaces/IOracle.sol";

import "@morpho-blue/libraries/MathLib.sol";

contract SwapMock {
using MathLib for uint256;

ERC20Mock public immutable collateralToken;
ERC20Mock public immutable loanToken;

address public immutable oracle;

constructor(address collateralAddress, address loanAddress, address oracleAddress) {
collateralToken = ERC20Mock(collateralAddress);
loanToken = ERC20Mock(loanAddress);

oracle = oracleAddress;
}

function swapCollatToLoan(uint256 amount) external returns (uint256 returnedAmount) {
returnedAmount = amount.mulDivDown(IOracle(oracle).price(), ORACLE_PRICE_SCALE);

collateralToken.transferFrom(msg.sender, address(this), amount);

loanToken.setBalance(address(this), returnedAmount);
loanToken.transfer(msg.sender, returnedAmount);
}

function swapLoanToCollat(uint256 amount) external returns (uint256 returnedAmount) {
returnedAmount = amount.mulDivDown(ORACLE_PRICE_SCALE, IOracle(oracle).price());

loanToken.transferFrom(msg.sender, address(this), amount);

collateralToken.setBalance(address(this), returnedAmount);
collateralToken.transfer(msg.sender, returnedAmount);
}
}
38 changes: 20 additions & 18 deletions src/metamorpho/MetamorphoSnippets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import {IrmMock} from "@metamorpho/mocks/IrmMock.sol";
import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol";
import {MathLib, WAD} from "@morpho-blue/libraries/MathLib.sol";

import {Math} from "@openzeppelin5/utils/math/Math.sol";
import {ERC20} from "@openzeppelin5/token/ERC20/ERC20.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";

contract MetamorphoSnippets {
uint256 constant FEE = 0.2 ether; // 20%

IMetaMorpho public immutable vault;
IMorpho public immutable morpho;
IMetaMorpho public immutable vault;

using MathLib for uint256;
using Math for uint256;
Expand All @@ -36,15 +34,15 @@ contract MetamorphoSnippets {
totalAssets = vault.lastTotalAssets();
}

/// @dev note that one can adapt the address in the call to the morpho contract
function vaultAmountInMarket(MarketParams memory marketParams) public view returns (uint256 vaultAmount) {
function vaultAssetsInMarket(MarketParams memory marketParams) public view returns (uint256 vaultAmount) {
vaultAmount = morpho.expectedSupplyAssets(marketParams, address(vault));
}

function totalSharesUserVault(address user) public view returns (uint256 totalSharesUser) {
totalSharesUser = vault.balanceOf(user);
}

// The following function will return the current supply queue of the vault
function supplyQueueVault() public view returns (Id[] memory supplyQueueList) {
uint256 queueLength = vault.supplyQueueLength();
supplyQueueList = new Id[](queueLength);
Expand All @@ -54,6 +52,7 @@ contract MetamorphoSnippets {
return supplyQueueList;
}

// The following function will return the current withdraw queue of the vault
function withdrawQueueVault() public view returns (Id[] memory withdrawQueueList) {
uint256 queueLength = vault.supplyQueueLength();
withdrawQueueList = new Id[](queueLength);
Expand All @@ -68,7 +67,6 @@ contract MetamorphoSnippets {
cap = vault.config(id).cap;
}

// TO TEST
function supplyAPRMarket(MarketParams memory marketParams, Market memory market)
public
view
Expand All @@ -85,11 +83,6 @@ contract MetamorphoSnippets {
supplyRate = borrowRate.wMulDown(1 ether - market.fee).wMulDown(utilization);
}

// TODO: edit comment + Test function
// same function as Morpho Blue Snippets
// a amount at 6%, B amount at 3 %:
// (a*6%) + (B*3%) / (a+b+ IDLE)

function supplyAPRVault() public view returns (uint256 avgSupplyRate) {
uint256 ratio;
uint256 queueLength = vault.withdrawQueueLength();
Expand All @@ -100,15 +93,14 @@ contract MetamorphoSnippets {
for (uint256 i; i < queueLength; ++i) {
Id idMarket = vault.withdrawQueue(i);

// To change once the cantina-review branch is merged
(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) =
(morpho.idToMarketParams(idMarket));

MarketParams memory marketParams = MarketParams(loanToken, collateralToken, oracle, irm, lltv);
Market memory market = morpho.market(idMarket);

uint256 currentSupplyAPR = supplyAPRMarket(marketParams, market);
uint256 vaultAsset = vaultAmountInMarket(marketParams);
uint256 vaultAsset = vaultAssetsInMarket(marketParams);
ratio += currentSupplyAPR.wMulDown(vaultAsset);
}

Expand All @@ -122,14 +114,24 @@ contract MetamorphoSnippets {
shares = vault.deposit(assets, onBehalf);
}

function withdrawFromVault(uint256 assets, address onBehalf) public returns (uint256 redeemed) {
// withdraw from the vault a nb of asset
function withdrawFromVaultAmount(uint256 assets, address onBehalf) public returns (uint256 redeemed) {
address receiver = onBehalf;
redeemed = vault.withdraw(assets, receiver, onBehalf);
}

function redeemAllFromVault(address receiver) public returns (uint256 redeemed) {
// maxWithdraw from the vault
function withdrawFromVaultAll(address onBehalf) public returns (uint256 redeemed) {
address receiver = onBehalf;
uint256 assets = vault.maxWithdraw(address(this));
redeemed = vault.withdraw(assets, receiver, onBehalf);
}

// maxRedeem from the vault
function redeemAllFromVault(address onBehalf) public returns (uint256 redeemed) {
address receiver = onBehalf;
uint256 maxToRedeem = vault.maxRedeem(address(this));
redeemed = vault.redeem(maxToRedeem, receiver, address(this));
redeemed = vault.redeem(maxToRedeem, receiver, onBehalf);
}

// TODO:
Expand Down
4 changes: 2 additions & 2 deletions test/forge/blue/TestBlueSnippets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {MathLib} from "@morpho-blue/libraries/MathLib.sol";
import {SharesMathLib} from "@morpho-blue/libraries/SharesMathLib.sol";

// we need to import everything in there
import "@morpho-blue-test/forge/BaseTest.sol";
import "@morpho-blue-test/BaseTest.sol";

contract TestIntegrationSnippets is BaseTest {
using MathLib for uint256;
Expand Down Expand Up @@ -130,7 +130,7 @@ contract TestIntegrationSnippets is BaseTest {
vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets);

(uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketBalances(marketParams);
uint256 borrowTrue = irm.borrowRate(marketParams, market);
uint256 borrowTrue = irm.borrowRateView(marketParams, market);
uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets);

uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization);
Expand Down
Loading

0 comments on commit 78adf91

Please sign in to comment.