Docs

Midnight periphery

Midnight repository

Periphery and ratifiers

Optional helper contracts are available in the periphery folder:

  • BlueBuyCallback - callback that funds buy offers by withdrawing loan tokens previously supplied to a Morpho Blue market on behalf of the callback contract.
  • EcrecoverAuthorizer - enables signature-based authorization changes after the helper has been authorized on Midnight for the relevant account. Signed changes are submitted through the helper and executed onchain.

A ratifier decides whether an offer can be taken. Each offer names a ratifier, and when a taker calls take, Midnight asks that ratifier to confirm the maker approved the offer, then rejects the take if it does not.

Ratifier contracts are available in the ratifiers folder.

  • PriceRatifierV1 - verifies that an offer belongs to a Merkle root ratified by its maker, and optionally restricts who can take it. See the reference below.
  • RateRatifierV1 - same as PriceRatifierV1, but the maker commits to a rate instead of a price. See the reference below.
  • SetterRatifier (legacy) - verifies that an offer belongs to a Merkle root ratified by its maker.
  • EcrecoverRatifier (legacy) - verifies that an offer belongs to a Merkle root signed (EIP-712) by its maker or by a signer the maker authorized on Midnight. The maker, or an account it authorized, can cancel a root with cancelRoot.

Morpho SDK 7.0.0 makes offers only with PriceRatifierV1 or RateRatifierV1 trees. SetterRatifier and EcrecoverRatifier stay deployed, and @morpho-org/midnight-sdk still exports their utilities.

Deployment addresses are listed on the Addresses page.

PriceRatifierV1 and RateRatifierV1

Both ratifiers let a maker approve a whole Merkle tree of offers with a single root ratification. All offers in a tree are expected to share the same maker and ratifier.

When an offer is taken, Midnight calls the ratifier's isRatified, which checks that:

  1. the taker is the offer's allowed taker, when one is set (address(0) means anyone can take);
  2. for RateRatifierV1 only, the offer's price respects the committed rate;
  3. the offer is a leaf of the tree (Merkle proof);
  4. the maker has ratified the root.

Price and rate commitments

PriceRatifierV1RateRatifierV1
ratifierData decodes to(bytes32 root, uint256 leafIndex, bytes32[] proof, address allowedTaker)(bytes32 root, uint256 leafIndex, bytes32[] proof, uint256 rate, address allowedTaker)
Leaf committed by the makerThe offer, plus allowedTakerThe offer with tick replaced by rate, plus allowedTaker
PriceThe offer's tick, fixed in the leafAny tick whose price is at or below (buy offers) or at or above (sell offers) the price limit derived from rate

rate is a WAD-scaled per-second rate, quoted as simple interest. It does not account for compounding or continuous fees. At take time, RateRatifierV1 converts it into a price limit using the remaining time to maturity:

priceLimit = WAD * WAD / (WAD + rate * timeToMaturity); // rounded down for buy offers, up for sell offers

A price beyond the limit reverts with WorsePrice.

Ratifying a root

function setIsRootRatified(address maker, bytes32 root, bool newIsRootRatified) external returns (bytes32);

Sets whether root is ratified for maker. The caller must be maker or an account authorized by maker on Midnight. Setting it back to false stops every offer in the tree from being taken. This does not consume rootNonce, so a signed ratification with the current nonce can still be submitted afterwards by an authorized caller and ratify the root again.

function setIsRootRatifiedWithSig(
    address maker,
    bytes32 root,
    uint256 height,
    bool newIsRootRatified,
    uint128 nonce,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external returns (bytes32);

Same as setIsRootRatified, using an EIP-712 signature over the offer tree. The signer and the caller must each be maker or an account authorized by maker on Midnight. The caller check prevents a third party from submitting a signed ratification before or without the maker's batch.

NameTypeDescription
heightuint256Height of the offer tree (at most 20, otherwise the call reverts with TreeTooHigh).
nonceuint128Must equal rootNonce(maker, root), which is shared by all of the maker's signers. A used nonce is accepted only if it does not change the current status, otherwise the call reverts with RatifiedStatusChanged.
deadlineuint256Timestamp after which the signature is rejected.

Both functions return keccak256("morpho.midnight.setIsRootRatifiedSuccess"). The EIP-712 domain contains only the chain id and the ratifier address.

Views

FunctionReturns
isRootRatified(maker, root)Whether root is ratified for maker.
rootNonce(maker, root)The next nonce expected by setIsRootRatifiedWithSig.
ratification(maker, root)Both values above.
MIDNIGHT()The Midnight contract the ratifier works with.
DOMAIN_SEPARATOR()The EIP-712 domain separator.

Events

EventEmitted when
SetIsRootRatified(address indexed caller, address indexed maker, bytes32 indexed root, bool newIsRootRatified)setIsRootRatified is called.
SetIsRootRatifiedWithSig(address caller, address indexed signer, address indexed maker, bytes32 indexed root, uint256 height, bool newIsRootRatified, uint128 signatureNonce, uint128 previousNonce)setIsRootRatifiedWithSig succeeds. It is also emitted when a used nonce is replayed without changing the status (signatureNonce < previousNonce), so not every emission is a state change.

Errors

ErrorReason
UnauthorizedThe caller or signer is neither the maker nor authorized by the maker on Midnight.
DeadlineExpiredThe signature deadline has passed.
InvalidSignatureThe signature does not recover to an address.
InvalidNonceThe nonce is higher than rootNonce(maker, root).
RatifiedStatusChangedA used nonce would change the current status.
UnauthorizedTakerThe taker is not the offer's allowed taker.
WorsePriceRateRatifierV1 only: the offer's price is beyond the rate's price limit.
InvalidProofThe offer is not a leaf of the tree.
NotRatifiedThe maker has not ratified the root.
LeafIndexOutOfRangeDeclared in HashLib: leafIndex does not fit in proof.length bits.
TreeTooHighDeclared in HashLib: height is above 20 in setIsRootRatifiedWithSig.

LeafIndexOutOfRange and TreeTooHigh are not declared in the ratifier interfaces, so an ABI generated from IPriceRatifierV1 or IRateRatifierV1 alone does not decode them.

Authorization and custody

Before its offers can be taken, the maker authorizes the ratifier once on Midnight with setIsAuthorized. Otherwise take reverts with RatifierUnauthorized.

The ratifiers hold no assets and make no token transfers or approvals. They have no owner and cannot be upgraded. They only store the ratification status and nonce per maker and root, and read Midnight's isAuthorized. Token approvals stay on Midnight, which moves the tokens during take.

A wallet that only allows calls to listed contracts, such as an MPC wallet policy, needs the contract that ratifies its roots as an allowed target:

  • Direct calls: the ratifier. Each tree is ratified with a direct call to the ratifier, setIsRootRatified or setIsRootRatifiedWithSig, from the maker or an account the maker authorized on Midnight. That account needs the ratifier on its allowlist whichever function is used. The one-time setIsAuthorized call goes to Midnight.
  • Morpho SDK 7.0.0: MidnightBundlesV2. SDK maker methods publish offers in one MidnightBundlesV2 call that authorizes the ratifier and ratifies the root, so the wallet calls MidnightBundlesV2 instead of the ratifier. The maker first authorizes MidnightBundlesV2 on Midnight.