Skip to main content

Tenor Migration Ratifier

The Migration 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. The user is always the offer maker: once configured, a counterparty (which may be a keeper, or anyone) can fill the user's offer with a permissionless take(), and the ratifier checks every take against those parameters before it goes through.

The Migration Ratifier is a first-class Morpho Midnight ratifier: it implements IRatifier directly, so when the user posts an offer that names it and has authorized it on Midnight, Midnight calls the ratifier's isRatified() on every take of that offer. 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:

  • MigrationRatifier: concrete subclass. Implements isRatified, stores per-user params, and decodes the take's source/target market IDs from the ratifier 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:

  1. Configure: The user authorizes the ratifier on Morpho Midnight (setIsAuthorized(ratifier, true)) and sets renewal preferences for one or more (callback, sourceMarket, targetMarket) slots.
  2. Validate: When a counterparty fills the user's offer, Morpho Midnight checks isAuthorized[maker][ratifier] and calls the ratifier's isRatified(), which loads the matching slot and checks the take against the user's parameters.
  3. Execute: If validation passes, Morpho Midnight invokes the offer's 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)DirectionPage
BORROW_MIDNIGHT_RENEWAL_CALLBACKMidnight → MidnightBorrow Midnight Renewal
LEND_MIDNIGHT_RENEWAL_CALLBACKMidnight → MidnightLend Midnight Renewal
BORROW_BLUE_TO_MIDNIGHT_CALLBACKBlue → MidnightBlue to Midnight
LEND_VAULT_TO_MIDNIGHT_CALLBACKVault → MidnightVault to Midnight
BORROW_MIDNIGHT_TO_BLUE_CALLBACKMidnight → BlueMidnight to Blue
LEND_MIDNIGHT_TO_VAULT_CALLBACKMidnight → VaultMidnight to Vault

User Setup

Setting up a renewal takes three calls:

  1. Authorize the ratifier on Morpho Midnight: setIsAuthorized(ratifier, true) marks this ratifier as authorized for the user in Midnight's native isAuthorized[user][ratifier] map, so Midnight will call it when the user's offer is taken.
  2. Authorize the callback on Morpho Midnight: The callback needs Midnight's isAuthorized permission to act on the position during the take. One-time per callback.
  3. 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. clearParams disables 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.

clearParams(onBehalf, callback, sourceTenorMarketId, targetTenorMarketId) deletes stored parameters and revokes standing consent for that route.

Authorized delegates can rewrite your params

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:

FieldDescription
interestRatePolicyAddress 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.
renewalWindowSeconds 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 / maxDurationBounds on the target maturity, measured from block.timestamp (uint32).
renewalCadenceOptional IRenewalCadence that restricts target maturities to a schedule. Required for Blue → Midnight migrations.
limitRatePerSecondRate limit in WAD per second (uint40). Acts as a ceiling for borrowers and a floor for lenders.
Avoid configurations that allow atomic loops

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.renewalWindow keeps the return leg's window from opening inside the outbound leg's minimum duration. Safer default: never set minDuration <= renewalWindow on routes sharing both endpoints.
  • Rates: BlueToMidnight.borrowRate < MidnightToBlue.lendRate keeps 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

isRatified is a view function. On every take it loads the user's slot and runs six checks. Any failure reverts the take.

  1. Params are set: interestRatePolicy is non-zero, minDuration > 0, maxDuration >= minDuration.
  2. Callback data matches the take: the ratifier data decodes 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.)
  3. Fee matches config: The fee rate and recipient encoded in the callback data must equal the ratifier's effective fee for (callback, marketId).
  4. Window is open:
    • Midnight source: allowed once block.timestamp >= sourceMaturity - renewalWindow, and stays open thereafter.
    • Blue source: anchored to renewalCadence.nearestBoundary(block.timestamp).
  5. 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.
  6. Rate clears the policy and limit: The offer rate must satisfy both interestRatePolicy.getRate(...) and the user's limitRatePerSecond. 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).

The duration used to convert rate into price depends on the flow:

  • Renewals (Midnight → Midnight): 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.
  • Entries (Blue/Vault → Midnight): targetMaturity - block.timestamp.
  • Exits (Midnight → Blue/Vault): max(sourceMaturity - block.timestamp, 0).

Rate Check Adjustments

One adjustment applies when evaluating the rate check:

  • Continuous fee on lend flows: For lend flows targeting Midnight markets, the rate check accounts for Midnight's continuous fee by reducing the effective face value. Units are scaled by (WAD - continuousFee * timeToMaturity) / WAD before rate comparison.

Validation via isRatified

The ratifier is view-only and never holds funds. The user is always the offer maker: they post an offer with offer.ratifier = MigrationRatifier and authorize it on Morpho Midnight. When a counterparty fills the offer with midnight.take(), Midnight checks isAuthorized[offer.maker][offer.ratifier], calls ratifier.isRatified(offer, ratifierData) to validate the take against the maker's stored params, and reverts if any check fails. Midnight's native isAuthorized[user][ratifier] map is the only gate; 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 its feeRecipient is non-zero.
  • Which market keys the fee: Entry and renewal flows (Midnight → Midnight, Blue → Midnight, Vault → Midnight): keyed by target Midnight market. Exit flows (Midnight → Blue, Midnight → Vault): keyed by source Midnight market.
  • Caps: Midnight → Midnight and Blue → Midnight: MAX_FEE_RATE = 0.5e18 (50% of interest). Midnight exit flows disabled (0%) — both Midnight → Blue and Midnight → Vault: MAX_FEE_RATE_FIXED_TO_VARIABLE = 0.

There is no timelock on fee changes; updates take effect on the next take.