Callbacks
Understand how Midnight callbacks let offers source assets from an external contract at settlement while capital can remain supplied to Morpho Blue.
A resting offer normally requires the maker to keep capital available in their wallet, where it cannot simultaneously earn Morpho Blue supply yield. A callback lets an offer source its assets from an external contract at settlement instead, so a maker can keep the capital earning on Morpho Blue until a taker arrives. This page explains what callbacks are, how the Morpho Blue lend callback works onchain, and how Morpho's router recognizes and repurposes that liquidity.
The idle-capital problem
An offer is a standing commitment. A lender who publishes a buy offer on Midnight is promising to hand over loan tokens the moment a borrower takes it. Without a callback, the loan tokens remain in the maker's wallet until settlement, so the maker must maintain sufficient balance and allowance for the offer to fill. Those assets cannot simultaneously earn Morpho Blue supply yield while backing the offer.
Callbacks remove that dead time. Instead of relying on the maker's wallet balance at settlement, the offer names a contract that will produce the assets on demand, at the instant of settlement. Between publishing and being taken, the capital is free to do something productive. For the Blue-buy callback covered below, that "something productive" is supplying the same loan tokens to a Morpho Blue market, where the position can accrue interest at the market's variable supply rate until the offer is taken. That rate changes with market conditions and may fall to zero.
What a callback is
Every onchain Midnight Offer includes two callback fields:
callback: the address of the contract Midnight calls during settlementcallbackData: opaque bytes passed through unchanged for that contract to interpret
A callback-free offer sets callback to the zero address and callbackData to 0x. The REST API serializes callbackData as callback_data.
Callbacks play different roles depending on the offer side. For a buy offer - the lend flow covered on this page - a nonzero callback becomes the buyer callback. Midnight calls onBuy so the callback can supply the loan tokens instead of the maker's wallet. For a sell offer, the nonzero callback becomes the seller callback: Midnight transfers the loan tokens first, then calls onSell. A taker can separately provide a taker callback. Each path remains atomic: if a required callback step fails, the whole take reverts.
For a buy offer, a lend offer, the hook Midnight calls is onBuy. The callback receives how many loan tokens the fill needs (buyerAssets), sources exactly that amount, approves Midnight to pull it, and returns a success value. Nothing about this requires the maker's capital to have been idle; it only has to be reachable by the callback when the moment comes.
The Blue-buy callback
The reference implementation is BlueBuyCallback.sol. It is a small, ownable contract that holds a Morpho Blue supply position and sources a buy offer's assets by withdrawing from it. The maker creates it through the supported chain-specific BlueBuyCallbackFactory, deposits assets into its Blue position, and points their offers at it.
For Morpho's API and router to classify the callback as blue_buy, the factory must register the callback for that maker before the offer is posted. An independently deployed contract can implement the same callback hook and settle onchain, but it will not receive this router/API treatment; router validation can report blue_callback.
Its state is deliberately minimal:
address public immutable OWNER; // the maker; the only buyer this callback will fund
address public immutable MIDNIGHT; // the Midnight contract; the only address allowed to call onBuy
address public immutable BLUE; // the Morpho Blue deployment funds are withdrawn from
uint256 public nonce; // anti-replay for signed Blue authorizationsThe settlement hook is the whole story (simplified from source):
function onBuy(
bytes32,
Market memory market,
uint256 buyerAssets, // exactly what this fill needs
uint256,
uint256,
address buyer,
bytes memory data // ABI-encoded Blue MarketParams
) external returns (bytes32) {
require(msg.sender == MIDNIGHT, NotMidnight()); // only Midnight may invoke the callback
require(buyer == OWNER, NotOwnerBuyer()); // only funds transactions where the owner is the buyer
MarketParams memory marketParams = abi.decode(data, (MarketParams));
require(marketParams.loanToken == market.loanToken, InconsistentLoanToken());
// Withdraw the funds from THIS contract's own Blue supply position…
IMorpho(BLUE).withdraw(marketParams, buyerAssets, 0, address(this), address(this));
// …and let Midnight pull them to settle the take.
ERC20Lib.safeApprove(market.loanToken, MIDNIGHT, buyerAssets);
return CALLBACK_SUCCESS;
}A few details in that function are the design decisions that make the feature safe and legible:
- The Blue position is owned by the callback contract, not the maker's wallet. The
withdrawcall passesaddress(this)as both the position owner and the receiver, so the callback draws on its own supply. This keeps the earning position isolated behind one purpose-built contract with known behavior, which is exactly what lets a stranger's take route through it safely. - Only Midnight can call it, and only for the owner's buys.
NotMidnightandNotOwnerBuyermean the callback can never be tricked into withdrawing for anyone else's fill. - Loan tokens must match.
InconsistentLoanTokenrejects anycallback_datawhose Blue market pays out a different token than the offer settles in. - It is atomic. The withdraw, the approval, and Midnight's pull all happen inside the taker's single transaction. If the Blue position cannot cover
buyerAssets, thewithdrawreverts and the take reverts with it. There is no partial or half-funded state.
Three housekeeping functions round it out: setAuthorization manages a Blue authorization directly, setAuthorizationWithSig performs a signed authorization update (with nonce guarding against replay), and skim sweeps any stray token balance back to the owner.
To publish, cancel and recover these offers step by step, see Lend from a Morpho Blue position.
Which callback an offer uses
BlueBuyCallbackFactory keys each callback on an owner and a salt: createBlueBuyCallback(owner, salt). The salt is any bytes32 value the caller chooses. The first call for a given owner and salt deploys a new BlueBuyCallback, and every later call with the same pair returns that same callback. The factory records it in callbackOf(owner, salt).
The factory deploys with CREATE2, so the callback address is known before deployment from the chain's factory, the owner and the salt. Offers name that address in callback before they are signed, so an integration computes it first. The Morpho SDK provides getMidnightBlueBuyCallbackAddress for this.
The salt decides which offers share a callback. Reusing a salt puts several offer groups on one callback. A new salt gives an offer group its own callback. The contracts allow both. A callback holds a separate Blue supply position in each Blue market it is funded in, and each offer withdraws from the market named in its callbackData.
How the router recognizes the liquidity
A callback would be useless to integrators if the money it can produce were invisible until execution. The point of routing is to know, before anyone commits a transaction, how much of an offer is actually fillable.
Callback-backed offers are legible. The offer itself advertises its funding source: the callback address and the callback_data (the ABI-encoded Blue MarketParams) travel with the offer. The Morpho API surfaces this as a typed group - an offer group with callback.type = "blue_buy" exposes the callback contract and the Blue market it draws on:
GET /v0/midnight/users/{maker}/offer-groups?callback_type=blue_buy
// The callback metadata the router reads liquidity against:
{
"callback": {
"type": "blue_buy",
"callback_address": "0x...callback", // the BlueBuyCallback contract
"market_id": "0x...blueMarket", // the Blue position to inspect
"market_params": { "loan_token": "0x...", "collateral_token": "0x...", "...": "..." }
}
}From there, the router does not take the offer's advertised size at face value. For each callback-backed offer, it computes the callback-funded capacity that can actually be sourced right now as the minimum of three onchain quantities:
- remaining group capacity: how much of the offer group is still unconsumed
- callback's Blue supply:
supplyAssetsof the callback contract's Blue position - Blue market liquidity: unborrowed liquidity available to withdraw this block
This minimum caps that individual callback-backed offer. It is trustless: no promise from the maker, just three reads reconciled into one number. A quote can contain this offer together with other offers and intentional fallback excess, so the quote's available_assets is the aggregate capacity of the returned takeable-offer caps and can exceed the requested target. It is not this callback's three-way minimum. When a taker acts on the quote, their SDK or bundler constructs and submits a target-aware transaction from the returned takeable offers, limiting execution to the requested amount despite any fallback excess. Midnight then calls onBuy, and assets supplied to Blue - and potentially accruing interest at its variable market rate - are withdrawn and routed into settlement.
Sizing callback-backed offers
Two limits apply to a callback-backed offer:
- the offer group's cap,
maxAssetsormaxUnits, as for any offer - what the callback can produce at settlement: its Blue supply, limited by the Blue market's liquidity.
onBuywithdrawsbuyerAssetsfrom the callback's own Blue position, and the take reverts if it can't.
The callback keeps no per-offer accounting. Every offer that names it and the same Blue market in callbackData draws on the same Blue position, up to its own group's cap.
This gives two ways to size the offers:
| Behavior | Capped offer group | Uncapped offer group ( maxAssets set to maxUint128) |
|---|---|---|
| What limits fills | The group's cap or the callback's Blue supply, whichever is lower | The callback's Blue supply |
| Interest that may accrue on Blue while the offer waits | Can be lent only up to the cap. Raising the cap means publishing a new offer group. | Any interest that accrues can be lent with no new signature |
| Assets added to or withdrawn from the callback | Fill capacity follows them, up to the cap | Fill capacity follows them |
| Several offer groups on one callback and Blue market | Each group takes at most its cap, and all groups share one balance | Any group can take the whole balance, including assets meant for the other groups |
Within one market's offer book, the router splits a shared callback's Blue supply between the maker's offers in priority order, instead of counting it in full for each offer.
How the Morpho app uses callbacks
When a lender keeps the assets of a lend limit order on Morpho Blue, the Morpho app sets each order (one offer group) up this way:
- One callback per order. The salt is the order's offer group id, computed with the callback fields left empty. All offers of one order, including a multi-market order, share one callback. Each order's offers carry their creation time, so in practice every new order gets a new salt and a new callback.
- Uncapped offers. The app creates the callback, posts the offers with
maxAssetsset tomaxUint128, and only then deposits the amount the lender enters into the callback's Blue position. The order can fill for whatever its callback holds. - Cancelling. Cancelling an order cancels its offer group, then withdraws the callback's Blue position to the lender when the withdrawal can go through. If it can't, for example because the Blue market lacks liquidity, only the cancel is sent and the assets stay in the callback. For an expired or cancelled order, the app offers a "Withdraw order" action for whatever the callback still holds.
What this setup gives:
- The order can lend its deposit plus any interest that accrues on Blue while it waits, without a new signature.
- A fill on one order only uses that order's assets, never assets meant for the lender's other open orders.
What it costs:
- Each new order deploys a new callback contract, which costs gas.
- Withdrawing assets from the callback shrinks the open order by the same amount.
How callback-backed settlement works
A Blue-backed offer follows this settlement flow:
- the callback only ever withdraws from its own isolated Blue position
- It will only act for the maker who owns it and only when the Midnight contract is the caller
- The settlement token is checked against the Blue market
- The entire funding step lives inside the taker's transaction so a shortfall reverts cleanly rather than settling half a fill
The taker experiences a Blue-backed offer through the same take flow as a wallet-funded offer: they submit a take and either get filled at the quoted terms or the transaction reverts.