Midnight periphery
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 asPriceRatifierV1, 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 withcancelRoot.
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:
- the taker is the offer's allowed taker, when one is set (
address(0)means anyone can take); - for
RateRatifierV1only, the offer's price respects the committed rate; - the offer is a leaf of the tree (Merkle proof);
- the maker has ratified the root.
Price and rate commitments
PriceRatifierV1 | RateRatifierV1 | |
|---|---|---|
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 maker | The offer, plus allowedTaker | The offer with tick replaced by rate, plus allowedTaker |
| Price | The offer's tick, fixed in the leaf | Any 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 offersA 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.
| Name | Type | Description |
|---|---|---|
height | uint256 | Height of the offer tree (at most 20, otherwise the call reverts with TreeTooHigh). |
nonce | uint128 | Must 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. |
deadline | uint256 | Timestamp 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
| Function | Returns |
|---|---|
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
| Event | Emitted 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
| Error | Reason |
|---|---|
Unauthorized | The caller or signer is neither the maker nor authorized by the maker on Midnight. |
DeadlineExpired | The signature deadline has passed. |
InvalidSignature | The signature does not recover to an address. |
InvalidNonce | The nonce is higher than rootNonce(maker, root). |
RatifiedStatusChanged | A used nonce would change the current status. |
UnauthorizedTaker | The taker is not the offer's allowed taker. |
WorsePrice | RateRatifierV1 only: the offer's price is beyond the rate's price limit. |
InvalidProof | The offer is not a leaf of the tree. |
NotRatified | The maker has not ratified the root. |
LeafIndexOutOfRange | Declared in HashLib: leafIndex does not fit in proof.length bits. |
TreeTooHigh | Declared 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,
setIsRootRatifiedorsetIsRootRatifiedWithSig, 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-timesetIsAuthorizedcall goes to Midnight. - Morpho SDK 7.0.0:
MidnightBundlesV2. SDK maker methods publish offers in oneMidnightBundlesV2call that authorizes the ratifier and ratifies the root, so the wallet callsMidnightBundlesV2instead of the ratifier. The maker first authorizesMidnightBundlesV2on Midnight.