Tenor Migration Intent Ratifier
The Migration Intent Ratifier lets a user commit to migration parameters (such as rate, cadence, duration, and target markets) under which a position can renew or roll indefinitely. Once configured, a keeper (or any third party) can execute takes on the user's behalf, and the ratifier checks every take against those parameters before it goes through.
It pairs with the shared IntentSettler, which routes takes between users and Morpho Midnight. The same ratifier covers six callbacks (same-protocol renewals and cross-protocol migrations, on both the borrow and lend side) and supports two primary use cases:
- Auto-renewal on the borrow side: A borrower configures the three borrow callbacks (Midnight → Midnight, Blue → Midnight, Midnight → Blue) so a position rolls into a new term at maturity, or migrates between the variable-rate and fixed-rate markets, without manual intervention.
- Market making on the lend side: A lender configures the three lend callbacks (Vault → Midnight, Midnight → Vault, Midnight → Midnight) to express a round-trip policy: sit in a Vault, automatically enter a Midnight fixed-rate position to lend at least X%, exit early when the prevailing rate falls to Y% (below the entry rate), and return to the Vault. This quotes a fixed lending rate continuously while idle capital earns the variable rate.
The contract is split in two:
MigrationIntentRatifier: concrete subclass. Stores per-user params and decodesintent.data.BaseMigrationRatifier: abstract base. Owns fee config, callback discrimination, and the window/cadence/maturity/rate checks.
How a Renewal Flows
A renewal moves through three stages:
- Configure: The user sets renewal preferences for one or more
(callback, sourceMarket, targetMarket)slots and authorizes the ratifier onIntentSettler. - Validate: When a take comes in,
IntentSettlerloads the matching slot and asks the ratifier whether the take matches the user's parameters. - Execute: If validation passes,
IntentSettlerforwards the take to Morpho Midnight, which runs the callback to perform the position transition atomically.
Supported Callbacks
The six callback addresses are pinned as immutables at deployment. Adding a new callback type requires deploying a new ratifier.
| Callback (immutable) | Direction | Page |
|---|---|---|
BORROW_MIDNIGHT_RENEWAL_CALLBACK | Midnight → Midnight | Borrow Midnight Renewal |
LEND_MIDNIGHT_RENEWAL_CALLBACK | Midnight → Midnight | Lend Midnight Renewal |
BORROW_BLUE_TO_MIDNIGHT_CALLBACK | Blue → Midnight | Blue to Midnight |
LEND_VAULT_TO_MIDNIGHT_CALLBACK | Blue → Midnight | Vault to Midnight |
BORROW_MIDNIGHT_TO_BLUE_CALLBACK | Midnight → Blue | Midnight to Blue |
LEND_MIDNIGHT_TO_VAULT_CALLBACK | Midnight → Blue | Midnight to Vault |
User Setup
Setting up a renewal takes three calls:
- Authorize the ratifier on
IntentSettler:setIsAuthorized(onBehalf, ratifier, true)lets the settler route takes through this ratifier. - Authorize the callback on Morpho Midnight: The callback needs Midnight's
isAuthorizedpermission to act on the position during the take. One-time per callback. - Set per-slot params on this ratifier:
setParams(onBehalf, callback, sourceMarketId, targetMarketId, params)stores preferences for a specific(callback, source, target)tuple. Each slot is independent.clearParamsdisables one.
setParams and clearParams accept the user themselves or any address the user has authorized on Morpho Midnight. The ratifier defers entirely to Midnight's isAuthorized mapping; it does not maintain its own ACL.
A Midnight authorization granted for any other purpose (e.g. a router or callback) also grants the right to overwrite your renewal preferences. Scope Midnight authorizations to contracts you trust.
User Parameters
Each slot stores a UserMigrationParams struct:
| Field | Description |
|---|---|
interestRatePolicy | Address of an IInterestRatePolicy contract that returns the acceptable rate for the renewal context. address(0) marks the slot as unset. See Interest Rate Policies below. |
renewalWindow | Seconds before source maturity that the renewal window opens (uint32). 0 restricts the window to "at or after source maturity". Ignored for Blue → Midnight migrations. |
minDuration / maxDuration | Bounds on the target maturity, measured from block.timestamp (uint32). |
renewalCadence | Optional IRenewalCadence that restricts target maturities to a schedule. Required for Blue → Midnight migrations. |
limitRatePerSecond | Rate limit in WAD per second (uint40). Acts as a ceiling for borrowers and a floor for lenders. |
If the routes you authorize form a cycle that returns to its starting market, a keeper can renew through the full loop in one transaction. If the rates you have authorized cross across the cycle (a negative spread), every iteration extracts value. Example: lending vault → midnight at a floor of X% and midnight → vault at an exit of Y% with Y > X relocks the position at a worse rate each pass.
A one-hop loop is closed by authorizing both directions between the same markets (e.g. BORROW_BLUE_TO_MIDNIGHT_CALLBACK and BORROW_MIDNIGHT_TO_BLUE_CALLBACK). Block it with either condition:
- Timing:
BlueToMidnight.minDuration > MidnightToBlue.renewalWindowkeeps the return leg's window from opening inside the outbound leg's minimum duration. Safer default: never setminDuration <= renewalWindowon routes sharing both endpoints. - Rates:
BlueToMidnight.borrowRate < MidnightToBlue.lendRatekeeps the loop carrying a positive spread.
Multi-hop loops close the same way through chained authorizations (e.g. Midnight market A → B → Blue → A in three hops). Audit the full route graph, not just adjacent pairs.
The ratifier does not check for loop configurations; preventing them is your responsibility.
Validation
onIntentRatify is a view function. On every take it loads the user's slot and runs five checks. Any failure reverts the take.
- Params are set:
interestRatePolicyis non-zero,minDuration > 0,maxDuration >= minDuration. - Intent matches callback:
intent.datadecodes to(sourceTenorMarketId, targetTenorMarketId). These must match the source and target the callback is actually operating on. (Tenor market IDs are a Tenor-specific identifier: Midnight markets, Blue markets, and ERC-4626 vaults each have their own kind of ID.) - Fee matches config: The fee rate and recipient encoded in the callback data must equal the ratifier's effective fee for
(callback, marketId). - Window is open:
- Midnight source: allowed once
block.timestamp >= sourceMaturity - renewalWindow, and stays open thereafter. - Blue source: anchored to
renewalCadence.nearestBoundary(block.timestamp).
- Midnight source: allowed once
- Target maturity is valid: Must fall in
[block.timestamp + minDuration, block.timestamp + maxDuration], must be strictly after source maturity for Midnight → Midnight, and must align with the cadence if one is set. - Rate clears the policy and limit: The offer rate must satisfy both
interestRatePolicy.getRate(...)and the user'slimitRatePerSecond. The clamp is a ceiling for borrowers, a floor for lenders. Rate checks are post-fee for Midnight → Midnight and Blue → Midnight (interest-based fee), and pre-fee for Midnight → Blue and Midnight → Vault (flat percentage fee, currently capped at 0).
For Midnight → Midnight, the duration used to convert rate into price is targetMaturity - max(block.timestamp, sourceMaturity). Before source maturity this equals the roll period (accounting for early settlement at par); after source maturity it equals the remaining time to target.
Execution via IntentSettler
The ratifier is view-only and never holds funds. Every take goes through the shared IntentSettler, which mediates both directions of "act on a user's behalf":
- Keeper-driven take: A keeper calls
IntentSettler.take(..., intent)withintent.ratifier = MigrationIntentRatifier. The settler checksisAuthorized[intent.user][intent.ratifier], validates the intent on this ratifier, then callsMORPHO_MIDNIGHT.take(... taker = intent.user ...). - Maker-side ratification: A user posts an offer with
offer.ratifier = address(IntentSettler). When a third-party taker fills the offer on Morpho Midnight, the protocol calls back intoIntentSettler.isRatified, which validates the intent against this ratifier and returnsCALLBACK_SUCCESS.
A single isAuthorized[user][ratifier] map gates both flows. Users opt in once per ratifier.
Interest Rate Policies
Each user slot points at an IInterestRatePolicy contract that returns the acceptable rate for the renewal context. The canonical implementation is Static Rate Policy, which encodes a fixed N-point rate curve as immutables (e.g. "start at 3% APR, ramp linearly to 6% over 24 hours, plateau"). Custom policies can implement any pricing logic (per-market rates, oracle-driven rates, dynamic curves) provided they conform to the interface.
Market Making Policy
The Static Rate Policy quotes a single curve indexed by time since the renewal window opened: fine for one-off renewals, but it can't price a position's term and can't distinguish entries from exits. Market makers typically point interestRatePolicy at the Market Making Policy instead.
The Market Making Policy is a singleton that holds one curve per (user, tenorMarketId). Each point on the curve carries both a sellRate (the MM is exiting fixed exposure) and a buyRate (the MM is entering), so the two sides share a single time-to-maturity grid by construction. setCurve enforces sellRate <= buyRate at every point, which (combined with the shared grid) preserves the spread between entry and exit rates at every duration, protecting the round-trip described in the market-making use case above. The curve is indexed by time-to-maturity at the moment a take is evaluated, so the same policy can quote one rate for a 7-day position and a different rate for a 90-day position. The curve's output is still subject to the leg's limitRatePerSecond (a floor for lend offers, a ceiling for borrow offers), so the cap continues to bound what the curve can quote.
Pausable Variant (Emergency Pause)
PausableStaticRatePolicy is a pausable subclass of StaticRatePolicy. When paused, getRate() reverts with IsPaused(), which causes the ratifier's rate check to revert and blocks every renewal take pointing at that policy.
Use it as a per-route circuit breaker. Pointing a user's slot at a PausableStaticRatePolicy instance gives a designated pauser the ability to halt renewals for that intent without touching the ratifier itself:
- Any address marked as a pauser can call
pause(). - Only the policy owner can call
unpause().
The kill switch lives in the policy, scoped to whichever users opt into it.
Fees
The ratifier owner configures fees on the shared base via setFeeConfig(callback, tenorMarketId, feeRate, feeRecipient).
- Default config:
tenorMarketId = bytes32(0)is the action-level default for that callback. - Market overrides: A specific
(callback, tenorMarketId)config takes precedence when itsfeeRecipientis non-zero. - Which market keys the fee: Midnight → Midnight and Blue → Midnight: keyed by target market. Midnight → Blue and Midnight → Vault: keyed by source Midnight market.
- Caps: Midnight → Midnight and Blue → Midnight:
MAX_FEE_RATE = 0.5e18(50% of interest). Midnight → Blue and Midnight → Vault:MAX_FEE_RATE_MIDNIGHT_TO_BLUE = 0(disabled).
There is no timelock on fee changes; updates take effect on the next take.