Callbacks
Overview
In the context of Morpho, callbacks are mechanisms that enable developers to perform arbitrary actions in the intermediate stage of a transaction process (before the ERC20 token transfer
).
Those callbacks are present in the following entry points: supply
, supplyCollateral
, repay
and liquidate
.
Using them avoids having to deal with flash loans that are limited by liquidity and that perform sub-optimal back and forth token transfers. Thus, endless possibilities are unlocked and it costs less gas for integrators.
Use case
Typical use cases using callbacks are:
- (De)Leverage one's position;
- Liquidate a position without flashloans;
- Migrate from one market to another.
Visual
Below is a visual comparison between leveraging a position by hand VS using external flashloan VS using a callback:
Examples
To illustrate the use of callbacks, refer to the:
This repository contains examples including:
leverageMe
Create a leveraged position with a given leverageFactor
on the marketParams
market of Morpho for the sender.
deleverageMe
Deleverages the sender on the given marketParams
market of Morpho by repaying his debt and withdrawing his collateral. The withdrawn assets are sent to the sender.
- Leverage via Callback in Solidity (Foundry)
- Deleverage via Callback in Solidity (Foundry)
Logic
The leverageMe()
function exemplifies leveraging a position, taking the following inputs:
leverageFactor
: The factor by which to leverage.initAmountCollateral
: Initial amount of collateral (already owned by the sender before the transaction).marketParams
: The market parameters.
The flow is the following:
- Initialization: The function starts by initializing variables and calculating necessary values.
- Calling Morpho Singleton: It then calls the Morpho singleton with the callback data.
- Executing Callback: The callback data triggers the
onMorphoSupplyCollateral
function. This function:- Borrows the loan amount of loan asset via Morpho,
- Swaps the borrowed liquidity into the collateral token using ISwap.
Leverage Mechanics:
On the Morpho contract, the logic transfers the user's leverage position up to the amount of initAmountCollateral * leverageFactor
Swap
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/* IMPORTS */
import {IMorphoSupplyCollateralCallback} from "@morpho-blue/interfaces/IMorphoCallbacks.sol";
import {Id, IMorpho, MarketParams} from "@morpho-blue/interfaces/IMorpho.sol";
import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol";
import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";
import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol";
/* INTERFACES */
interface ISwap {
/// @notice Swaps loan token to collateral token.
/// @param amount The amount of loan token to swap.
/// @return returnedAmount The amount of collateral token returned.
function swapLoanToCollat(uint256 amount) external returns (uint256 returnedAmount);
}
/* CONTRACT */
contract CallbacksSnippets is IMorphoSupplyCollateralCallback {
using MorphoLib for IMorpho;
using MarketParamsLib for MarketParams;
using SafeTransferLib for ERC20;
IMorpho public immutable morpho;
ISwap public immutable swapper;
constructor(IMorpho _morpho, ISwap _swapper) {
morpho = IMorpho _morpho;
swapper = _swapper;
}
modifier onlyMorpho() {
require(msg.sender == address(morpho), "msg.sender should be Morpho");
_;
}
/// Type of collateral supply callback data.
struct SupplyCollateralData {
uint256 loanAmount;
MarketParams marketParams;
address user;
}
/* FUNCTIONS */
function onMorphoSupplyCollateral(uint256 amount, bytes calldata data) external onlyMorpho {
SupplyCollateralData memory decoded = abi.decode(data, (SupplyCollateralData));
(uint256 amountBis,) = morpho.borrow(decoded.marketParams, decoded.loanAmount, 0, decoded.user, address(this));
ERC20(decoded.marketParams.loanToken).approve(address(swapper), amount);
// Logic to Implement. The following example is a swap, but it could be something else such as a 'unwrap + stake + wrap staked' for
// wETH(wstETH) Market.
swapper.swapLoanToCollat(amountBis);
}
/// @notice Creates a leveraged position with a specified `leverageFactor` on the `marketParams` market of Morpho
/// for the sender.
/// @dev Requires the sender to hold `initAmountCollateral` and approve this contract to manage their positions on
/// Morpho (as this contract will borrow on behalf of the sender).
/// @param leverageFactor The desired leverage factor, cannot exceed the limit of 1/1-LLTV.
/// @param initAmountCollateral The initial amount of collateral held by the sender.
/// @param marketParams Parameters of the market on which to execute the leverage operation.
function leverageMe(uint256 leverageFactor, uint256 initAmountCollateral, MarketParams calldata marketParams)
public
{
// Transfer the initial collateral from the sender to this contract.
ERC20(marketParams.collateralToken).safeTransferFrom(msg.sender, address(this), initAmountCollateral);
// Calculate the final amount of collateral based on the leverage factor.
uint256 finalAmountCollateral = initAmountCollateral * leverageFactor;
// Calculate the amount of LoanToken to be borrowed and swapped against CollateralToken.
// Note: In this simplified example, the price is assumed to be `ORACLE_PRICE_SCALE`.
// In a real-world scenario:
// - The price might not equal `ORACLE_PRICE_SCALE`, and the oracle's price should be factored into the
// calculation, like this:
// (leverageFactor - 1) * initAmountCollateral.mulDivDown(ORACLE_PRICE_SCALE, IOracle(oracle).price())
// - Consideration for fees and slippage is crucial to accurately compute `loanAmount`.
uint256 loanAmount = (leverageFactor - 1) * initAmountCollateral;
// Approve the maximum amount to Morpho on behalf of the collateral token.
_approveMaxTo(marketParams.collateralToken, address(morpho));
// Supply the collateral to Morpho, initiating the leverage operation.
morpho.supplyCollateral(
marketParams,
finalAmountCollateral,
msg.sender,
abi.encode(SupplyCollateralData(loanAmount, marketParams, msg.sender))
);
}
function _approveMaxTo(address asset, address spender) internal {
if (ERC20(asset).allowance(address(this), spender) == 0) {
ERC20(asset).safeApprove(spender, type(uint256).max);
}
}
}
Logic
ThedeleverageMe()
function exemplifies deleveraging a position. It takes the following inputs:marketParams
: The market parameters.
- Total Shares Calculation: It calculates the total shares of the borrower's loan..
- Repayment Logic: The callback data triggers the onMorphoRepay function. This function:
- Withdraws the amount of collateral of the collateral token via Morpho.
- Swaps the borrowed liquidity into the loan token using ISwap.
Swap
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;
/* IMPORTS */
import {IMorphoSupplyCollateralCallback} from "@morpho-blue/interfaces/IMorphoCallbacks.sol";
import {Id, IMorpho, MarketParams} from "@morpho-blue/interfaces/IMorpho.sol";
import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol";
import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";
import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol";
/* INTERFACES */
interface ISwap {
/// @notice Swaps collateral token to loan token.
/// @param amount The amount of collateral token to swap.
/// @return returnedAmount The amount of loan token returned.
function swapCollatToLoan(uint256 amount) external returns (uint256 returnedAmount);
}
/* FUNCTIONS */
contract CallbacksSnippets is IMorphoRepayCallback {
using MorphoLib for IMorpho;
using MarketParamsLib for MarketParams;
using SafeTransferLib for ERC20;
IMorpho public immutable morpho;
ISwap public immutable swapper;
constructor(IMorpho _morpho, ISwap _swapper) {
morpho = IMorpho _morpho;
swapper = _swapper;
}
modifier onlyMorpho() {
require(msg.sender == address(morpho), "msg.sender should be Morpho");
_;
}
/// Type of repay callback data.
struct RepayData {
MarketParams marketParams;
address user;
}
function onMorphoRepay(uint256 amount, bytes calldata data) external onlyMorpho {
RepayData memory decoded = abi.decode(data, (RepayData));
uint256 toWithdraw = morpho.collateral(decoded.marketParams.id(), decoded.user);
morpho.withdrawCollateral(decoded.marketParams, toWithdraw, decoded.user, address(this));
ERC20(decoded.marketParams.collateralToken).approve(address(swapper), amount);
swapper.swapCollatToLoan(amount);
}
/// @notice Deleverages the sender on the given `marketParams` market of Morpho 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, but he needs to have approved this contract to manage their positions on Morpho (as this
/// contract will withdrawCollateral on behalf of the sender).
/// @param marketParams Parameters of the market.
function deLeverageMe(MarketParams calldata marketParams) public returns (uint256 amountRepaid) {
uint256 totalShares = morpho.borrowShares(marketParams.id(), msg.sender);
_approveMaxTo(marketParams.loanToken, address(morpho));
(amountRepaid,) =
morpho.repay(marketParams, 0, totalShares, msg.sender, abi.encode(RepayData(marketParams, msg.sender)));
ERC20(marketParams.collateralToken).safeTransfer(
msg.sender, ERC20(marketParams.collateralToken).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);
}
}
}