# Table of Contents
- [Tenor](#tenor)
- [Markdown page example | Tenor](#markdown-page-example-tenor)
- [Search the documentation | Tenor](#search-the-documentation-tenor)
- [About | Tenor](#about-tenor)
- [Fixed-rate borrowing | Tenor](#fixed-rate-borrowing-tenor)
- [Auto-Renewal | Tenor](#auto-renewal-tenor)
- [Liquidations | Tenor](#liquidations-tenor)
- [Maturity | Tenor](#maturity-tenor)
- [Borrow offer types | Tenor](#borrow-offer-types-tenor)
- [Fees | Tenor](#fees-tenor)
- [Exit Early | Tenor](#exit-early-tenor)
- [Create OTC offers | Tenor](#create-otc-offers-tenor)
- [Fixed-rate lending | Tenor](#fixed-rate-lending-tenor)
- [Exit Early | Tenor](#exit-early-tenor)
- [Lend offer types | Tenor](#lend-offer-types-tenor)
- [Liquidation grace periods | Tenor](#liquidation-grace-periods-tenor)
- [Gated markets | Tenor](#gated-markets-tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Request quotes | Tenor](#request-quotes-tenor)
- [Tenor](#tenor)
- [Risks | Tenor](#risks-tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Rewards | Tenor](#rewards-tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Unknown](#unknown)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Unknown](#unknown)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Tenor](#tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Supply Collateral | Tenor](#supply-collateral-tenor)
- [Lend Vault to Midnight | Tenor](#lend-vault-to-midnight-tenor)
- [Supply Vault Shares as Collateral | Tenor](#supply-vault-shares-as-collateral-tenor)
- [Tutorials | Tenor](#tutorials-tenor)
- [Withdraw Vault Shares as collateral | Tenor](#withdraw-vault-shares-as-collateral-tenor)
- [Borrow | Tenor](#borrow-tenor)
- [Auto-Renewal | Tenor](#auto-renewal-tenor)
- [Manage Position | Tenor](#manage-position-tenor)
- [Set Alerts | Tenor](#set-alerts-tenor)
- [Lend | Tenor](#lend-tenor)
- [Claim Rewards | Tenor](#claim-rewards-tenor)
- [Exit Early | Tenor](#exit-early-tenor)
- [OTC Agreements | Tenor](#otc-agreements-tenor)
- [Four Week Cadence | Tenor](#four-week-cadence-tenor)
- [Morpho Midnight Allowlist Gate | Tenor](#morpho-midnight-allowlist-gate-tenor)
- [Delayed Liquidation Gate | Tenor](#delayed-liquidation-gate-tenor)
- [Market Making Policy | Tenor](#market-making-policy-tenor)
- [Tenor Migration Intent Ratifier | Tenor](#tenor-migration-intent-ratifier-tenor)
- [Static Rate Policy | Tenor](#static-rate-policy-tenor)
- [Tenor Adapter | Tenor](#tenor-adapter-tenor)
- [Vault V2 Allowlist Gate | Tenor](#vault-v2-allowlist-gate-tenor)
- [Tenor Router | Tenor](#tenor-router-tenor)
- [Technical Documentation | Tenor](#technical-documentation-tenor)
- [SDK | Tenor](#sdk-tenor)
- [Oracle with Validation | Tenor](#oracle-with-validation-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Welcome to Tenor docs | Tenor](#welcome-to-tenor-docs-tenor)
- [Tenor](#tenor)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Markdown page example | Tenor
[Skip to main content](https://docs.tenor.finance/markdown-page#__docusaurus_skipToContent_fallback)
You don't need React to write simple standalone pages.
---
# Search the documentation | Tenor
[Skip to main content](https://docs.tenor.finance/search#__docusaurus_skipToContent_fallback)
Search the documentation
========================
[](https://www.algolia.com/)
---
# About | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/about#__docusaurus_skipToContent_fallback)
On this page
Tenor was founded in 2025 by former engineers from Morpho, Notional, and Google with deep experience in onchain lending markets and building onchain infrastructure.
Tenor & Morpho[](https://docs.tenor.finance/get-started/about#tenor--morpho "Direct link to Tenor & Morpho")
--------------------------------------------------------------------------------------------------------------
Tenor builds on top of [Morpho](https://morpho.org/)
, which acts as the infrastructure and liquidity network underpinning the Tenor platform. Read more about the [Tenor and Morpho collaboration](https://blog.tenor.finance/tenor-morpho)
.
Investors[](https://docs.tenor.finance/get-started/about#investors "Direct link to Investors")
------------------------------------------------------------------------------------------------
Tenor is backed by [Variant](https://variant.fund/)
, [Coinbase Ventures](https://www.coinbase.com/ventures)
, [Prelude](https://prelude.xyz/)
, [Nascent](https://www.nascent.xyz/)
, [Lattice Fund](https://www.lattice.fund/)
, and a group of [angel investors](https://blog.tenor.finance/tenor-seed-round)
.
Contact[](https://docs.tenor.finance/get-started/about#contact "Direct link to Contact")
------------------------------------------------------------------------------------------
Reach out at [contact@tenor.finance](mailto:contact@tenor.finance)
, or follow Tenor on [X](https://x.com/TenorFinance)
and [LinkedIn](https://www.linkedin.com/company/tenor-finance-labs/)
.
* [Tenor & Morpho](https://docs.tenor.finance/get-started/about#tenor--morpho)
* [Investors](https://docs.tenor.finance/get-started/about#investors)
* [Contact](https://docs.tenor.finance/get-started/about#contact)
---
# Fixed-rate borrowing | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/fixed-rate#__docusaurus_skipToContent_fallback)
On this page
A fixed-rate borrow position locks in both the interest rate and the term length when it is initiated. Both inputs are set once, at the moment of the match, so the cost of capital is known for the full term. Every position has an explicit end date called its [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
. The term is agreed upfront and defines exactly when repayment is due.
On Tenor[](https://docs.tenor.finance/get-started/borrow/fixed-rate#on-tenor "Direct link to On Tenor")
---------------------------------------------------------------------------------------------------------
When you initiate a fixed-rate borrow position on Tenor, you either take at the market rate or post a limit offer:
* **Take at the market rate:** borrow right away at the best rate currently available.
* **Limit offer:** set the rate you want and wait for a counterparty to match it.
See [offer types](https://docs.tenor.finance/get-started/borrow/offer-types)
for how each one works and what happens to your collateral while a limit offer waits.
Every match is peer-to-peer. You post collateral and a lender supplies the loan token at the agreed rate and term, and the position is initiated and settled onchain through [Morpho Midnight (fixed-rate, fixed-term markets)](https://docs.tenor.finance/technical-docs/architecture#midnight-markets)
smart contracts.
Each market sets its terms in advance: the loan token, the accepted collateral tokens (each with its own oracle and [LLTV](https://docs.tenor.finance/get-started/borrow/liquidations#ltv-based-liquidation)
), and the maturity are fixed and shared by everyone in the market. Only the rate and the filled amount vary from one match to the next.
### Example[](https://docs.tenor.finance/get-started/borrow/fixed-rate#example "Direct link to Example")
Consider a USDC market collateralized by cbBTC, priced by a Chainlink BTC/USD oracle, with an 86% LLTV and a maturity 30 days out. With cbBTC at $80,000, a borrower deposits **1 cbBTC** as collateral and borrows **50,000 USDC** at a **5% fixed rate** until that maturity. From the moment the position is matched:
* The borrowed 50,000 USDC accrues interest at 5% for the full 30 days. The rate is set until maturity.
* At maturity, the borrower owes **50,205.48 USDC**:
50,000×(1+30365×5%)≈50,205.48 USDC50{,}000 \\times \\left(1 + \\tfrac{30}{365} \\times 5\\%\\right) \\approx 50{,}205.48 \\text{ USDC}50,000×(1+36530×5%)≈50,205.48 USDC
* The starting LTV is 50,00080,000\=62.5%\\tfrac{50{,}000}{80{,}000} = 62.5\\%80,00050,000\=62.5%, well below the 86% LLTV. The position is liquidatable before maturity only if cbBTC falls far enough that LTV crosses the LLTV.
### At maturity[](https://docs.tenor.finance/get-started/borrow/fixed-rate#at-maturity "Direct link to At maturity")
You can repay and close the position at any time before maturity. At [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
, the outstanding debt is due. You can settle it manually, or opt in to Tenor's [auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
smart contracts to roll into a new term automatically.
A position that is neither repaid nor renewed by maturity becomes eligible for liquidation, regardless of LTV. Before maturity, the position can also be liquidated if its LTV crosses the market's LLTV. See [liquidations](https://docs.tenor.finance/get-started/borrow/liquidations)
for the full mechanics of both paths.
* [On Tenor](https://docs.tenor.finance/get-started/borrow/fixed-rate#on-tenor)
* [Example](https://docs.tenor.finance/get-started/borrow/fixed-rate#example)
* [At maturity](https://docs.tenor.finance/get-started/borrow/fixed-rate#at-maturity)
---
# Auto-Renewal | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/auto-renewal#__docusaurus_skipToContent_fallback)
On this page
Auto-renewal is an opt-in feature that rolls a borrow position into a new fixed-rate or variable-rate position at maturity, without requiring action from the borrower. You configure your preferences once when you initiate a borrow position, and third-party keepers execute the renewal on your behalf.
A borrow position that reaches [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
without being repaid or rolled becomes eligible for [post-maturity liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
on [Morpho Midnight (fixed-rate, fixed-term markets)](https://docs.tenor.finance/technical-docs/architecture#midnight-markets)
. Auto-renewal avoids this by rolling the position into a new term before maturity, as long as a matching offer settles under your configured limits.
Renewals are executed by independent keepers that monitor positions onchain and act when the terms are profitable for them. A keeper rolls your position only when it can match it against an available offer at a rate you accepted in advance.
Auto-renewal is only available for certain markets
Auto-renewal is available on [Tenor markets](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral)
, but is not enabled for OTC positions.
Renewal options[](https://docs.tenor.finance/get-started/borrow/auto-renewal#renewal-options "Direct link to Renewal options")
--------------------------------------------------------------------------------------------------------------------------------
When you initiate a borrow position on Tenor, you pick one of three renewal options. Two auto-renew the position at maturity; the third leaves repayment and rolling up to you.
* **Fixed-rate auto-renewal:** At maturity, the position rolls into a longer-dated fixed-rate market at the best available rate, up to a maximum rate you set. This option also enables the variable-rate fallback by default: if no fixed-rate offer is available at or below your maximum rate, the position migrates to a variable rate instead, then [returns to fixed-rate](https://docs.tenor.finance/get-started/borrow/auto-renewal#variable-to-fixed-migration)
once fixed-rate liquidity becomes available again.
* **Variable-rate auto-renewal:** At maturity, the position migrates directly to a Morpho Blue variable-rate market, skipping the fixed-rate roll.
* **Self-managed:** No auto-renewal. You repay or roll the position manually before maturity. A missed repayment results in liquidation to cover the unrepaid debt.
How a renewal works[](https://docs.tenor.finance/get-started/borrow/auto-renewal#how-a-renewal-works "Direct link to How a renewal works")
--------------------------------------------------------------------------------------------------------------------------------------------
### Fixed-to-fixed renewal[](https://docs.tenor.finance/get-started/borrow/auto-renewal#fixed-to-fixed-renewal "Direct link to Fixed-to-fixed renewal")
A renewal window opens a set period before maturity and stays open until the position is repaid, rolled, or liquidated. Within that window:
* You set a **maximum rate**, a ceiling on what you will pay to renew.
* The rate offered to keepers starts low and rises across the window toward your maximum. As the rate rises, more offers become available for keepers to profitably renew your position, until your maximum rate is reached.
* A keeper can roll your position as soon as it can match it against an available lend offer. When keepers compete to roll a position, renewals tend to settle close to the market rate.
If a fixed-rate offer clears under your maximum, the position rolls into a new fixed-rate term, with collateral migrated in proportion to the debt that rolls. If none does, the variable-rate fallback migrates the position to a Morpho Blue variable-rate market instead. If neither settles, the position is exposed to [post-maturity liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
.
The position rolls into a longer-dated fixed-rate market. You can configure a maximum renewal rate and a range of maturities you can renew into.


Learn more about [fixed-to-fixed renewal callbacks](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#renewal--migration)
.
### Fixed-to-variable fallback[](https://docs.tenor.finance/get-started/borrow/auto-renewal#fixed-to-variable-fallback "Direct link to Fixed-to-variable fallback")
If no fixed-rate offer clears under your maximum during the window, the position migrates to a Morpho Blue variable-rate open-term market.


Learn more about [fixed-to-variable migration callbacks](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#renewal--migration)
.
Example[](https://docs.tenor.finance/get-started/borrow/auto-renewal#example "Direct link to Example")
--------------------------------------------------------------------------------------------------------
Consider a borrower with:
* 1 cbBTC collateral, 50,000 USDC borrowed at 5% APR for 30 days.
* Fixed-rate auto-renewal enabled.
* A maximum renewal rate of 6%.
The numbers below are illustrative. During the renewal window, the rate the auction offers to keepers ramps upward over time, but the effective rate is capped at the borrower's 6% maximum, so the borrower never pays more than 6% to renew. A keeper renews the position as soon as the effective rate is high enough to match an available fixed offer in the target maturity. Here, a 5% fixed offer is available, so the renewal executes once the effective rate reaches 5%.

The position rolls into a new 30-day fixed-rate term at 5% APR. The collateral is migrated to the new market, unchanged in amount. The new outstanding debt is the previous maturity value, now accruing at 5% for the next 30 days:
50,000×(1+30365×5%)≈50,205.48 USDC50{,}000 \\times \\left(1 + \\tfrac{30}{365} \\times 5\\%\\right) \\approx 50{,}205.48 \\text{ USDC}50,000×(1+36530×5%)≈50,205.48 USDC
Edge cases[](https://docs.tenor.finance/get-started/borrow/auto-renewal#edge-cases "Direct link to Edge cases")
-----------------------------------------------------------------------------------------------------------------
It is possible the fixed-rate auto-renewal does not execute during the renewal window. There are two ways this happens:
* **Fixed-rate renewal fails:** no fixed-rate offer is available at or below your maximum rate.
* **Variable-rate fallback fails:** Morpho Blue lacks the lending liquidity to migrate the position.
When the fixed-rate renewal fails, the variable-rate fallback takes over. Only if that fallback also fails before maturity is your position exposed to [post-maturity liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
.
Auto-renewal stays permitted after maturity, until the position is repaid, rolled, or liquidated. A keeper that finds a matching offer can still roll the position past maturity, but the position is also eligible for liquidation from that point on, and whichever executes first settles the position.
Variable-to-fixed migration[](https://docs.tenor.finance/get-started/borrow/auto-renewal#variable-to-fixed-migration "Direct link to Variable-to-fixed migration")
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Variable-to-fixed migration is enabled automatically with both auto-renewal options. It is what returns a position to fixed-rate terms after it has been on a variable rate, whether the position reached variable as the fixed-rate fallback or migrated there directly under variable-rate auto-renewal. Once a position is on Morpho Blue, it migrates back to a Morpho Midnight fixed-rate market as soon as fixed-rate liquidity becomes favorable, without further action from you. Collateral and debt move atomically through the [Blue to Midnight](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight)
callback.
Both options are therefore a round trip to fixed-rate terms. They differ only in what happens at maturity: fixed-rate auto-renewal attempts a fixed-rate roll first and uses variable only as a fallback, while variable-rate auto-renewal goes straight to variable. Either way, the position returns to a fixed rate once the market supports it.
Self-managed positions[](https://docs.tenor.finance/get-started/borrow/auto-renewal#self-managed-positions "Direct link to Self-managed positions")
-----------------------------------------------------------------------------------------------------------------------------------------------------
A self-managed position has no keeper acting on your behalf, so you retain full control of the position and full responsibility for it. Before [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
, you either [repay](https://docs.tenor.finance/get-started/borrow/repay-early)
the debt or roll the position into a new term yourself.
There is no fallback if you take no action. A position that reaches maturity unrepaid and unrolled is exposed to [post-maturity liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
.
* [Renewal options](https://docs.tenor.finance/get-started/borrow/auto-renewal#renewal-options)
* [How a renewal works](https://docs.tenor.finance/get-started/borrow/auto-renewal#how-a-renewal-works)
* [Fixed-to-fixed renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal#fixed-to-fixed-renewal)
* [Fixed-to-variable fallback](https://docs.tenor.finance/get-started/borrow/auto-renewal#fixed-to-variable-fallback)
* [Example](https://docs.tenor.finance/get-started/borrow/auto-renewal#example)
* [Edge cases](https://docs.tenor.finance/get-started/borrow/auto-renewal#edge-cases)
* [Variable-to-fixed migration](https://docs.tenor.finance/get-started/borrow/auto-renewal#variable-to-fixed-migration)
* [Self-managed positions](https://docs.tenor.finance/get-started/borrow/auto-renewal#self-managed-positions)
---
# Liquidations | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/liquidations#__docusaurus_skipToContent_fallback)
On this page
A liquidation is the recovery of a borrower's collateral by a third party who repays part or all of the outstanding debt. Whether a position can be liquidated depends on its **LTV**, the ratio of debt to collateral value: each market has a fixed, immutable **Liquidation LTV (LLTV)** above which the position becomes eligible for liquidation.
On Tenor, a borrow position can be liquidated in two ways:
1. **LTV-based liquidation:** when the position's LTV ratio rises above the market's LLTV.
2. **Post-maturity liquidation:** when the position reaches [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
without being repaid or renewed, even if its LTV is still below the LLTV.
Liquidations are triggered by third-party liquidators, who repay the borrower's debt in exchange for the collateral at a discount relative to its oracle price. This discount is defined by the market's [liquidation bonus parameter](https://docs.tenor.finance/technical-docs/architecture#midnight-markets)
, which is typically set as a ratio of the LLTV.
Set liquidation notifications
Set notifications on Tenor to receive alerts when a position's health deteriorates, leaving time to act before liquidation becomes possible.
LTV-based liquidation[](https://docs.tenor.finance/get-started/borrow/liquidations#ltv-based-liquidation "Direct link to LTV-based liquidation")
--------------------------------------------------------------------------------------------------------------------------------------------------
The LLTV sets the maximum ratio of debt to collateral value that a position is allowed to reach. Once a position's LTV rises above the LLTV, it is eligible for liquidation.
LTV rises when the value of the collateral falls relative to the debt. On Morpho Midnight, debt is valued at par (the amount owed at maturity) when computing health, so LTV moves with collateral price, not with the passage of time. A borrower can reduce their LTV by adding collateral or repaying part of the debt.
### Example[](https://docs.tenor.finance/get-started/borrow/liquidations#example "Direct link to Example")
Consider a situation where:
* A borrower deposits **1 cbBTC as collateral** (valued at $80,000 with BTC at $80,000).
* The market LLTV is **86%**.
* The borrower borrows **$64,000 USDC**.
The LTV is calculated as:
LTV\=DebtCollateral Value×100%LTV = \\frac{\\text{Debt}}{\\text{Collateral Value}} \\times 100\\%LTV\=Collateral ValueDebt×100%
As the price of cbBTC falls, the collateral value drops and the LTV rises:
| cbBTC price | Collateral value | LTV | Status |
| --- | --- | --- | --- |
| $80,000 | $80,000 | 80.0% | Healthy |
| $76,000 | $76,000 | 84.2% | Healthy |
| $74,000 | $74,000 | 86.5% | Eligible for liquidation |
Once LTV exceeds the 86% LLTV, the position is eligible for liquidation.
### If a position is liquidated[](https://docs.tenor.finance/get-started/borrow/liquidations#if-a-position-is-liquidated "Direct link to If a position is liquidated")
When a position is liquidated, the liquidator repays part of the outstanding debt and seizes collateral worth the repaid amount plus the liquidation bonus, valued at the oracle price. The bonus compensates the liquidator for taking on the position and is deducted from the borrower's remaining collateral.
Before maturity, liquidations are typically partial: they bring the position's LTV back down to the LLTV rather than below it, so the bonus is sized to the actual breach rather than the whole position. The borrower keeps any collateral that is not seized, along with the borrowed funds that were already disbursed.
If the borrower's collateral, valued at the oracle price, cannot cover the outstanding debt even when the maximum liquidation bonus is applied, the unrecoverable portion is realized as bad debt. That shortfall is socialized across all lenders in the market: each lender's positions in the market are slashed proportionally at their next interaction.
Post-maturity liquidations[](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations "Direct link to Post-maturity liquidations")
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
A position that reaches maturity without being repaid or renewed is eligible for liquidation, regardless of its LTV. At that point, a portion of the collateral equal to the outstanding debt is sold to fully settle the position.
warning
Post-maturity liquidation applies even when a borrower's LTV is well below the LLTV. Enabling [auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
renews the position automatically before maturity.
Delayed liquidations[](https://docs.tenor.finance/get-started/borrow/liquidations#delayed-liquidations "Direct link to Delayed liquidations")
-----------------------------------------------------------------------------------------------------------------------------------------------
Some markets use delayed liquidations. On these markets, a position that first becomes liquidatable enters a grace period before any liquidator can act, followed by a liquidation period during which liquidators can act. The grace and liquidation periods are fixed for each market and begin only once a keeper starts the grace period.
See [Delayed Liquidations](https://docs.tenor.finance/get-started/otc/delayed-liquidations)
for the OTC use case and [DelayedLiquidationGate](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation)
for the contract details.
* [LTV-based liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#ltv-based-liquidation)
* [Example](https://docs.tenor.finance/get-started/borrow/liquidations#example)
* [If a position is liquidated](https://docs.tenor.finance/get-started/borrow/liquidations#if-a-position-is-liquidated)
* [Post-maturity liquidations](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
* [Delayed liquidations](https://docs.tenor.finance/get-started/borrow/liquidations#delayed-liquidations)
---
# Maturity | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/maturity#__docusaurus_skipToContent_fallback)
On this page
Every position on Tenor has an explicit end date called its maturity. Maturity is the timestamp at which the outstanding debt becomes due. It is set when the position is initiated and does not change.
On Morpho Midnight (fixed-rate, fixed-term markets), every position in a given market shares the same maturity timestamp.
Fixed terms give both sides of the market certainty: borrowers know exactly when repayment is due, and lenders know exactly when they will be able to redeem their lend positions.
warning
A borrow position that reaches maturity without being repaid or renewed will be liquidated, even if the LTV is below the LLTV. Configure [auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
to avoid this.
What happens at maturity[](https://docs.tenor.finance/get-started/borrow/maturity#what-happens-at-maturity "Direct link to What happens at maturity")
-------------------------------------------------------------------------------------------------------------------------------------------------------
At maturity, the outstanding debt is due in full. One of three things happens:
* **Repayment:** The borrower settles the outstanding debt, the lender redeems their position, and the borrower can then withdraw their collateral as they see fit.
* **Renewal:** If the borrower has [auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
configured, the position is renewed into a new fixed-term position or into a variable-rate open-term position, and the borrower continues under the new terms. The lender redeems their position as in a normal repayment. Renewal is permitted within a window before maturity and remains permitted after maturity, until the position is repaid, renewed, or liquidated.
* **Liquidation:** If the borrower does not settle the debt and the position is not rolled into a new term via renewal, the Morpho protocol allows third-party liquidators to seize part of the borrower's collateral to repay the outstanding debt. See [post-maturity liquidations](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
.
Repaying before maturity[](https://docs.tenor.finance/get-started/borrow/maturity#repaying-before-maturity "Direct link to Repaying before maturity")
-------------------------------------------------------------------------------------------------------------------------------------------------------
A borrower can close their position at any time before maturity by repaying the borrowed amount plus accrued interest. See [repay early](https://docs.tenor.finance/get-started/borrow/repay-early)
for details and examples.
* [What happens at maturity](https://docs.tenor.finance/get-started/borrow/maturity#what-happens-at-maturity)
* [Repaying before maturity](https://docs.tenor.finance/get-started/borrow/maturity#repaying-before-maturity)
---
# Borrow offer types | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/offer-types#__docusaurus_skipToContent_fallback)
On this page
There are two ways to initiate a [fixed-rate borrow position](https://docs.tenor.finance/get-started/borrow/fixed-rate)
: **take at the market rate** or post a **limit offer**. Taking at the market rate fills immediately at the best available rate; a limit offer posts a target rate and waits for a match.
Borrow at the market rate[](https://docs.tenor.finance/get-started/borrow/offer-types#borrow-at-the-market-rate "Direct link to Borrow at the market rate")
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Taking at the market rate fills immediately at the best rate currently available. The borrower accepts the rate counterparties are offering at that moment, and the position is initiated right away.
If there is not enough liquidity to fill the full amount, the take fills up to the available liquidity and the remainder is left as a resting sell (borrow) offer.
Borrow limit offers[](https://docs.tenor.finance/get-started/borrow/offer-types#borrow-limit-offers "Direct link to Borrow limit offers")
-------------------------------------------------------------------------------------------------------------------------------------------
A limit offer is a sell (borrow) offer that specifies the rate and term the borrower wants to borrow at. The offer rests until a counterparty (a lender) is willing to take it at that rate or better. A limit offer can be cancelled at any time before it is filled.
Limit offers are useful when a borrower has a specific rate in mind and is comfortable waiting for a match. There is no guarantee of execution if no counterparty takes the offer.
### Collateral supplied on fill[](https://docs.tenor.finance/get-started/borrow/offer-types#collateral-supplied-on-fill "Direct link to Collateral supplied on fill")
When you post a limit offer, your [collateral](https://docs.tenor.finance/get-started/borrow/liquidations)
is not locked upfront. It is supplied onchain only when a counterparty takes the offer, with pro-rata amounts on partial fills.
* Your collateral remains free to use elsewhere while the offer waits.
* Collateral is supplied automatically when the offer is taken; no separate transaction is required.
See the [supply collateral callback](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral)
for the contract-level flow.
### Example[](https://docs.tenor.finance/get-started/borrow/offer-types#example "Direct link to Example")
Suppose you post a limit offer to borrow 50,000 USDC at 4% fixed for 30 days against 1 cbBTC of collateral:
* The sell (borrow) offer is posted onchain at 4%. The cbBTC stays in your wallet while the offer waits.
* When a lender takes the offer at 4% or better, the collateral is supplied and the 50,000 USDC position initiates at the agreed rate and term.
* If only part of the offer fills, a pro-rata share of collateral is supplied and the remainder stays posted until filled or cancelled.
* [Borrow at the market rate](https://docs.tenor.finance/get-started/borrow/offer-types#borrow-at-the-market-rate)
* [Borrow limit offers](https://docs.tenor.finance/get-started/borrow/offer-types#borrow-limit-offers)
* [Collateral supplied on fill](https://docs.tenor.finance/get-started/borrow/offer-types#collateral-supplied-on-fill)
* [Example](https://docs.tenor.finance/get-started/borrow/offer-types#example)
---
# Fees | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/fees#__docusaurus_skipToContent_fallback)
**Tenor does not charge any fees** for actions like initiating a position, renewing a position, or using the interface.
The Tenor smart contracts include configurable fee parameters. For example, the [Tenor Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
supports a per-user renewal fee config, and [callbacks](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview)
can carry a fee model. Fees may be introduced in the future for specific functionalities or integrations, and any such changes will be communicated in advance.
---
# Exit Early | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/borrow/repay-early#__docusaurus_skipToContent_fallback)
On this page
A borrower can exit their position early before maturity. There are three options:
* **Repay at par** by paying the full maturity value (principal plus full-term interest) directly. This is always available and does not depend on market liquidity.
* **Exit at the current market rate** by lending against the position at the prevailing rate, matching an existing sell (borrow) offer. This requires sufficient market liquidity and executes immediately.
* **Relist at a preferred rate** by creating a buy (lend) limit offer. The position remains open until another user fills the offer at that rate, or until the borrower cancels it.
The market-rate and relist options work by taking an offsetting position that cancels the original one, so they depend on a matching counterparty being available.
Exiting through the market costs less than par
Exiting through the market costs less than repaying at par. You lend against the position at the prevailing rate, so you need less principal today than the full maturity value to offset the debt. The higher the rate, the larger the saving.
Example[](https://docs.tenor.finance/get-started/borrow/repay-early#example "Direct link to Example")
-------------------------------------------------------------------------------------------------------
A user borrows **1,000 USDC** at **5% APR** for **30 days**. The interest owed over the term is:
1,000×5%×30365\=4.11 USDC1{,}000 \\times 5\\% \\times \\frac{30}{365} = 4.11 \\text{ USDC}1,000×5%×36530\=4.11 USDC
To repay at par, the user pays the full maturity value of **1,004.11 USDC** (principal plus interest) to net off the debt. This is always available and does not depend on market liquidity.
To exit through the market instead, the user lends against the position, transferring it to another borrower. If there is an outstanding sell (borrow) offer at 5% APR for 30 days, the user can lend 1,000 USDC against it to exit immediately, without paying interest upfront. Note that if the rate at which the user exits differs from the rate at which they initially entered, they will have a gain or a loss due to the difference in interest rates.
Impact of rates on the value of a position[](https://docs.tenor.finance/get-started/borrow/repay-early#impact-of-rates-on-the-value-of-a-position "Direct link to Impact of rates on the value of a position")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
When closing a borrow position before maturity, the user may realize a mark-to-market gain or loss depending on how interest rates have moved since the position was initiated.
* If rates have risen and the user entered at a lower rate than the rate at which they exit, they will realize a gain.
* If rates have fallen and the user entered at a higher rate than the rate at which they exit, they will realize a loss.
### Example: rates increase from 5% to 7%[](https://docs.tenor.finance/get-started/borrow/repay-early#example-rates-increase-from-5-to-7 "Direct link to Example: rates increase from 5% to 7%")
A user borrowed **1,000 USDC** at **5% APR** for **30 days**, owing **1,004.11 USDC** at maturity.
To exit early at the current market rate of **7%**, the user needs to lend enough USDC to cover the 1,004.11 USDC debt at maturity. At 7% APR, the amount needed to lend today is:
1,004.111+30365×7%\=998.37 USDC\\frac{1{,}004.11}{1 + \\frac{30}{365} \\times 7\\%} = 998.37 \\text{ USDC}1+36530×7%1,004.11\=998.37 USDC
The user initially borrowed 1,000 USDC and only needs to lend **998.37 USDC** to exit, resulting in a **gain of 1.63 USDC** (1,000 - 998.37). This gain is due to exiting at a higher rate (7%) than the rate at which they entered (5%).
### Example: rates decrease from 5% to 3%[](https://docs.tenor.finance/get-started/borrow/repay-early#example-rates-decrease-from-5-to-3 "Direct link to Example: rates decrease from 5% to 3%")
A user borrowed **1,000 USDC** at **5% APR** for **30 days**, owing **1,004.11 USDC** at maturity.
To exit early at the current market rate of **3%**, the user needs to lend enough USDC to cover the 1,004.11 USDC debt at maturity. At 3% APR, the amount needed to lend today is:
1,004.111+30365×3%\=1,001.64 USDC\\frac{1{,}004.11}{1 + \\frac{30}{365} \\times 3\\%} = 1{,}001.64 \\text{ USDC}1+36530×3%1,004.11\=1,001.64 USDC
The user initially borrowed 1,000 USDC but needs to lend **1,001.64 USDC** to exit, resulting in a **loss of 1.64 USDC** (1,001.64 - 1,000). This loss is due to exiting at a lower rate (3%) than the rate at which they entered (5%).
* [Example](https://docs.tenor.finance/get-started/borrow/repay-early#example)
* [Impact of rates on the value of a position](https://docs.tenor.finance/get-started/borrow/repay-early#impact-of-rates-on-the-value-of-a-position)
* [Example: rates increase from 5% to 7%](https://docs.tenor.finance/get-started/borrow/repay-early#example-rates-increase-from-5-to-7)
* [Example: rates decrease from 5% to 3%](https://docs.tenor.finance/get-started/borrow/repay-early#example-rates-decrease-from-5-to-3)
---
# Create OTC offers | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/otc/create-otc-offers#__docusaurus_skipToContent_fallback)
On this page
Tenor OTC lets counterparties express offers outside of standardized markets. Counterparties can specify custom durations, any loan token, any set of collateral assets, and any rate. Each offer also specifies the LLTV and oracle to use for each collateral asset.
OTC covers cases that standardized markets do not serve well: long-tail collateral assets that are not listed in public markets, large offers that would not fill at a reasonable rate against standard market depth, and offers with custom terms, such as an allowlist gate that restricts participation to a specific set of counterparties.
Create offer[](https://docs.tenor.finance/get-started/otc/create-otc-offers#create-offer "Direct link to Create offer")
-------------------------------------------------------------------------------------------------------------------------
Create an offer by specifying:
* Loan token and side: sell (borrow) offer or buy (lend) offer
* Collateral asset(s) and amount (for sell (borrow) offers)
* LLTV per collateral asset
* Oracle per collateral asset
* Term
* Rate
* Counterparties (optional): see [Gated offers](https://docs.tenor.finance/get-started/otc/gated-markets)
* Liquidation grace period (optional): see [Liquidation grace periods](https://docs.tenor.finance/get-started/otc/delayed-liquidations)
Offers are settled onchain. Once an offer is published, anyone (or any allowlisted counterparty, on a gated market) can accept it.
Take offer[](https://docs.tenor.finance/get-started/otc/create-otc-offers#take-offer "Direct link to Take offer")
-------------------------------------------------------------------------------------------------------------------
Any counterparty (or any allowlisted counterparty, on a gated market) can take an offer that has been published onchain. Taking an offer settles the position immediately at the offer's terms.
Verify offer terms before taking
Because OTC offers can specify any oracle, any collateral asset, and any LLTV, deceptive actors may publish offers with misleading parameters: oracles that report inflated prices, unfamiliar or unsafe collateral tokens, or LLTVs set aggressively high. Before taking an OTC offer, verify the oracle source and address, confirm the collateral token contract, and review the LLTV against the volatility of the collateral. The same checks apply when creating an offer that targets a counterparty-supplied parameter set.
### Exit early[](https://docs.tenor.finance/get-started/otc/create-otc-offers#exit-early "Direct link to Exit early")
As a borrower, you can [repay early](https://docs.tenor.finance/get-started/borrow/repay-early)
: repay at par, create a limit offer to exit, or exit at market rate if the lender has already published an exit offer.
### OTC renewal offers[](https://docs.tenor.finance/get-started/otc/create-otc-offers#otc-renewal-offers "Direct link to OTC renewal offers")
OTC offers do not support auto-renewal. When a position approaches maturity, one side has to publish a renewal offer that the other side then takes onchain:
* The borrower can publish a sell (borrow) offer for the renewed terms; the lender takes it to roll the position forward.
* The lender can publish a buy (lend) offer for the renewed terms; the borrower takes it to roll the position forward.
If neither side publishes and takes a renewal offer before maturity, the position matures and settles normally.
* [Create offer](https://docs.tenor.finance/get-started/otc/create-otc-offers#create-offer)
* [Take offer](https://docs.tenor.finance/get-started/otc/create-otc-offers#take-offer)
* [Exit early](https://docs.tenor.finance/get-started/otc/create-otc-offers#exit-early)
* [OTC renewal offers](https://docs.tenor.finance/get-started/otc/create-otc-offers#otc-renewal-offers)
---
# Fixed-rate lending | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/lend/fixed-rate#__docusaurus_skipToContent_fallback)
On this page
A fixed-rate position locks in both the interest rate and the term length when it is initiated. Both inputs are set once, at the moment of the match, so both sides know the terms upfront. Every position has an explicit end date called its [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
— the timestamp at which the outstanding debt becomes due, set when the position is initiated and unchanged thereafter. The term is agreed upfront and defines exactly when repayment is due.
On Tenor[](https://docs.tenor.finance/get-started/lend/fixed-rate#on-tenor "Direct link to On Tenor")
-------------------------------------------------------------------------------------------------------
When you initiate a fixed-rate position on Tenor, you either take at the market rate or post a limit offer:
* **Take at the market rate:** fill right away at the best rate currently available.
* **Limit offer:** set the rate you want and wait for a counterparty to match it.
See [offer types](https://docs.tenor.finance/get-started/lend/offer-types)
for how each one works and what happens to your capital while a limit offer waits.
Every match is peer-to-peer. The borrower posts collateral and the lender supplies funds at the agreed rate and term, and the position is initiated and settled onchain through [Morpho Midnight (fixed-rate, fixed-term markets)](https://docs.tenor.finance/technical-docs/architecture#midnight-markets)
smart contracts.
Each market sets its terms in advance: the loan token, the accepted collateral tokens (each with its own oracle and [LLTV](https://docs.tenor.finance/get-started/borrow/liquidations#ltv-based-liquidation)
), and the maturity are fixed and shared by everyone in the market. Only the rate and the filled amount vary from one match to the next.
### Example[](https://docs.tenor.finance/get-started/lend/fixed-rate#example "Direct link to Example")
Consider a USDC market collateralized by cbBTC, priced by a Chainlink BTC/USD oracle, with an 86% LLTV and a maturity 30 days out. A lender supplies **1,000 USDC** at a **5% fixed rate** until that maturity. From the moment the position is matched:
* The lender's 1,000 USDC is locked in the market for the full 30 days at 5%.
* The borrower posts cbBTC collateral and owes the outstanding debt on the agreed schedule.
* At maturity, the lender is owed **1,004.11 USDC**:
1,000×(1+30365×5%)≈1,004.11 USDC1{,}000 \\times \\left(1 + \\tfrac{30}{365} \\times 5\\%\\right) \\approx 1{,}004.11 \\text{ USDC}1,000×(1+36530×5%)≈1,004.11 USDC
### At maturity[](https://docs.tenor.finance/get-started/lend/fixed-rate#at-maturity "Direct link to At maturity")
Borrowers can repay and close their position at any time before maturity. At [maturity](https://docs.tenor.finance/get-started/borrow/maturity)
, the outstanding debt is due. Borrowers can settle the debt, or opt in to Tenor's [auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
smart contracts to roll into a new term automatically. Once the debt is repaid or renewed, the lender withdraws their principal plus the accrued interest.
If the debt is neither repaid nor renewed, Morpho Midnight enables [post-maturity liquidation](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
: a third-party liquidator repays a portion of the outstanding debt and receives an equivalent amount of the borrower's collateral plus the liquidation bonus. The lender is repaid from that repayment; any collateral left after the seizure stays with the borrower.
warning
If a borrower is liquidated and their debt can't be fully repaid by the liquidation (for example, when collateral value falls faster than liquidators can act), the shortfall becomes **bad debt**. On Morpho Midnight, bad debt is socialized across all lenders in the market: each lender's position is slashed proportionally, reducing the amount they can withdraw.
* [On Tenor](https://docs.tenor.finance/get-started/lend/fixed-rate#on-tenor)
* [Example](https://docs.tenor.finance/get-started/lend/fixed-rate#example)
* [At maturity](https://docs.tenor.finance/get-started/lend/fixed-rate#at-maturity)
---
# Exit Early | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/lend/resell-early#__docusaurus_skipToContent_fallback)
On this page
A lender can exit their position early by reselling it before maturity. There are two ways to resell a lend position:
* **Resell at the current market rate** by matching against an existing buy (lend) offer, transferring the lending position to another lender. This requires sufficient market liquidity and executes immediately at the prevailing rate.
* **Relist at a preferred rate** by creating a sell (borrow) limit offer. The position remains open until another user fills the offer at that rate, or until the lender cancels it.
Resell option for lenders
A lender can always relist their lend position at a lower rate than the entry rate to capture part or all of the accrued interest early. Relisting at par (0%) captures the full interest accrued so far.
Example[](https://docs.tenor.finance/get-started/lend/resell-early#example "Direct link to Example")
------------------------------------------------------------------------------------------------------
A user lent **1,000 USDC** at **5% APR** for **30 days** and will receive **1,004.11 USDC** at maturity.
The user can borrow USDC to close their lend position. If there is an outstanding buy (lend) offer at 5% APR for 30 days, the user can borrow 1,000 USDC at 5% APR for 30 days against that offer to exit early. Note that if the rate at which the user borrows to close their position differs from the rate at which they initially entered, they will have a gain or a loss due to the difference in interest rates.
Impact of rates on the value of a position[](https://docs.tenor.finance/get-started/lend/resell-early#impact-of-rates-on-the-value-of-a-position "Direct link to Impact of rates on the value of a position")
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Reselling a lend position before maturity exposes the user to mark-to-market gains or losses. If market rates have moved since the position was opened, the cost to unwind it will differ from the original principal:
* If rates have risen and the user entered at a lower rate than the rate at which they exit, they will realize a loss.
* If rates have fallen and the user entered at a higher rate than the rate at which they exit, they will realize a gain.
### Example: rates increase from 5% to 7%[](https://docs.tenor.finance/get-started/lend/resell-early#example-rates-increase-from-5-to-7 "Direct link to Example: rates increase from 5% to 7%")
A user lent **1,000 USDC** at **5% APR** for **30 days**, earning **1,004.11 USDC** at maturity.
To exit early at the current market rate of **7%**, the user needs to borrow against their lend position to create an offsetting obligation of 1,004.11 USDC at maturity. At 7% APR, the amount they receive today from borrowing is:
1,004.111+30365×7%\=998.37 USDC\\frac{1{,}004.11}{1 + \\frac{30}{365} \\times 7\\%} = 998.37 \\text{ USDC}1+36530×7%1,004.11\=998.37 USDC
The user initially lent 1,000 USDC but only receives **998.37 USDC** when exiting, resulting in a **loss of 1.63 USDC** (1,000 - 998.37). This loss is due to exiting at a higher rate (7%) than the rate at which they entered (5%).
### Example: rates decrease from 5% to 3%[](https://docs.tenor.finance/get-started/lend/resell-early#example-rates-decrease-from-5-to-3 "Direct link to Example: rates decrease from 5% to 3%")
A user lent **1,000 USDC** at **5% APR** for **30 days**, earning **1,004.11 USDC** at maturity.
To exit early at the current market rate of **3%**, the user needs to borrow against their lend position to create an offsetting obligation of 1,004.11 USDC at maturity. At 3% APR, the amount they receive today from borrowing is:
1,004.111+30365×3%\=1,001.64 USDC\\frac{1{,}004.11}{1 + \\frac{30}{365} \\times 3\\%} = 1{,}001.64 \\text{ USDC}1+36530×3%1,004.11\=1,001.64 USDC
The user initially lent 1,000 USDC and receives **1,001.64 USDC** when exiting, resulting in a **gain of 1.64 USDC** (1,001.64 - 1,000). This gain is due to exiting at a lower rate (3%) than the rate at which they entered (5%).
* [Example](https://docs.tenor.finance/get-started/lend/resell-early#example)
* [Impact of rates on the value of a position](https://docs.tenor.finance/get-started/lend/resell-early#impact-of-rates-on-the-value-of-a-position)
* [Example: rates increase from 5% to 7%](https://docs.tenor.finance/get-started/lend/resell-early#example-rates-increase-from-5-to-7)
* [Example: rates decrease from 5% to 3%](https://docs.tenor.finance/get-started/lend/resell-early#example-rates-decrease-from-5-to-3)
---
# Lend offer types | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/lend/offer-types#__docusaurus_skipToContent_fallback)
On this page
There are two ways to initiate a lend position: **take at the market rate** or post a **limit offer**. Taking at the market rate fills immediately at the best available rate; a limit offer posts a target rate and waits for a match.
Lend at the market rate[](https://docs.tenor.finance/get-started/lend/offer-types#lend-at-the-market-rate "Direct link to Lend at the market rate")
-----------------------------------------------------------------------------------------------------------------------------------------------------
Taking at the market rate fills immediately at the best rate currently available. The lender accepts the rate counterparties are offering at that moment, and the position is initiated right away.
If there is not enough liquidity to fill the full amount, the take fills up to the available liquidity and the remainder is left as a resting buy (lend) offer.
Lend limit offers[](https://docs.tenor.finance/get-started/lend/offer-types#lend-limit-offers "Direct link to Lend limit offers")
-----------------------------------------------------------------------------------------------------------------------------------
A limit offer is a buy (lend) offer that specifies the rate and term the lender wants to lend at. The offer rests until a counterparty is willing to take it at that rate or better. A limit offer can be cancelled at any time before it is filled.
Limit offers are useful when a lender has a specific rate in mind and is comfortable waiting for a match. There is no guarantee of execution if no counterparty takes the offer. While the offer is unfilled, you choose what happens to your capital: it can earn the variable rate in a Morpho vault, or it can sit idle in your wallet. Both paths leave the offer itself unchanged; only the location of the funds differs.
### Earn while you wait[](https://docs.tenor.finance/get-started/lend/offer-types#earn-while-you-wait "Direct link to Earn while you wait")
When enabled, your funds are deposited into a [Morpho Vault-V2](https://docs.morpho.org/learn/concepts/vault/)
that allocates to a Morpho Blue variable-rate market with the same collateral asset as the fixed-rate market. The vault is unmanaged, immutable, and allocates into a single variable-rate market.
When a borrower takes the offer, funds are programmatically withdrawn from the vault and the fixed-rate lend position is initiated. If the offer fills partially, the remaining amount stays in the vault and keeps earning until it is filled or the offer expires. If the offer expires or is cancelled, the funds are withdrawn from the vault back to your wallet. You can withdraw from the vault at any time before the offer fills, provided there is sufficient liquidity.
#### Example[](https://docs.tenor.finance/get-started/lend/offer-types#example "Direct link to Example")
Suppose you post a limit offer to lend 10,000 USDC at 8% fixed for 1 month using cbBTC as collateral, with Earn while you wait enabled:
* Immediately, the 10,000 USDC is deposited into the Morpho Vault-V2 that allocates entirely to the cbBTC/USDC variable-rate market on Morpho Blue, and you begin earning the variable rate.
* When a fixed-rate borrower takes the offer, the funds are withdrawn from the vault and the lend position initiates at 8% for 1 month.

### Sit idle[](https://docs.tenor.finance/get-started/lend/offer-types#sit-idle "Direct link to Sit idle")
The lend asset (e.g., USDC) stays in your wallet while the offer is open. When a borrower takes the offer, the funds are pulled from the wallet to fund the fixed-rate position. If the offer expires or is cancelled, nothing moves — the funds were never deposited anywhere.
You earn nothing while the offer waits. The tradeoff is simplicity: no vault interaction, no withdrawal step, and no exposure to vault risk.
Advanced controls[](https://docs.tenor.finance/get-started/lend/offer-types#advanced-controls "Direct link to Advanced controls")
-----------------------------------------------------------------------------------------------------------------------------------
### Lend ladder offers[](https://docs.tenor.finance/get-started/lend/offer-types#lend-ladder-offers "Direct link to Lend ladder offers")
A ladder is a set of linked limit offers placed across multiple maturities at the same time, all backed by the same pool of capital. For example, a lender can post four offers of 100 USDC across the 1-week, 2-week, 1-month, and 3-month markets. The same 100 USDC backs every offer: once one fills, the rest are cancelled automatically.
Ladders let a lender target several (rate, maturity) combinations using the same funds. They are useful when the lender wants to take whichever rate fills first, or when they don't yet know which maturity will attract a borrower. Under the hood, a ladder is implemented as multiple grouped signed offers that share the same capital.
### Multi-collateral offers[](https://docs.tenor.finance/get-started/lend/offer-types#multi-collateral-offers "Direct link to Multi-collateral offers")
A multi-collateral offer is a single limit offer exposed to multiple markets that share the same loan token but use different collateral. For example, a 1,000 USDC offer can be made available simultaneously to the USDC/cbBTC, USDC/WETH, and USDC/wstETH markets. The same 1,000 USDC backs every market: whichever offer fills first claims the funds, and the offer is removed from the others.
This exposes the same capital to multiple markets without splitting it. Borrowers across multiple collateral types can fill against the same liquidity, and the lender only manages one offer. Like ladders, multi-collateral offers are implemented as multiple linked signed offers backed by a single capital allocation.
### Market making[](https://docs.tenor.finance/get-started/lend/offer-types#market-making "Direct link to Market making")
Market makers can programmatically allocate to lend at a specific rate, exit at a lower rate, and sit in the variable-rate market while not holding an open fixed-rate position.
The [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
handles this for lend-side flows: entering Morpho Midnight from a vault allocating to a Morpho Blue market, and exiting back to the vault. Setup is a one-time authorization plus per-leg parameters; from then on, takers can settle on the market maker's behalf inside the limits set in the configuration.
* [Lend at the market rate](https://docs.tenor.finance/get-started/lend/offer-types#lend-at-the-market-rate)
* [Lend limit offers](https://docs.tenor.finance/get-started/lend/offer-types#lend-limit-offers)
* [Earn while you wait](https://docs.tenor.finance/get-started/lend/offer-types#earn-while-you-wait)
* [Sit idle](https://docs.tenor.finance/get-started/lend/offer-types#sit-idle)
* [Advanced controls](https://docs.tenor.finance/get-started/lend/offer-types#advanced-controls)
* [Lend ladder offers](https://docs.tenor.finance/get-started/lend/offer-types#lend-ladder-offers)
* [Multi-collateral offers](https://docs.tenor.finance/get-started/lend/offer-types#multi-collateral-offers)
* [Market making](https://docs.tenor.finance/get-started/lend/offer-types#market-making)
---
# Liquidation grace periods | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/otc/delayed-liquidations#__docusaurus_skipToContent_fallback)
On this page
OTC agreements can be deployed with a delayed-liquidation gate. The gate gives borrowers a grace period after a position becomes unhealthy, during which they can repay debt or add collateral before any liquidator can act.
Each delayed-liquidation gate is deployed with its own immutable grace and liquidation periods (between 60 seconds and 72 hours). Lenders may require a lower LLTV or higher rate to offset the additional price risk.
How it works[](https://docs.tenor.finance/get-started/otc/delayed-liquidations#how-it-works "Direct link to How it works")
----------------------------------------------------------------------------------------------------------------------------
When a position becomes unhealthy, any keeper can call the gate to start the grace period. The borrower then has the configured duration to add collateral or repay debt before any liquidator can seize collateral. After the grace period ends, a liquidation period opens during which liquidators can act. Only one grace + liquidation window can be active at a time per borrower; once the full window elapses, a fresh call is required to restart.
If a priority liquidator was specified when the grace period started, they have exclusive access for up to one minute at the start of the liquidation period before it opens to other callers.
After maturity, liquidations on the agreement proceed immediately regardless of grace or liquidation period state. See [post-maturity liquidations](https://docs.tenor.finance/get-started/borrow/liquidations#post-maturity-liquidations)
.
### Restoring health during the grace period[](https://docs.tenor.finance/get-started/otc/delayed-liquidations#restoring-health-during-the-grace-period "Direct link to Restoring health during the grace period")
Position health is re-checked at the moment of liquidation, not when the grace period was started. If the borrower restores health during the grace period and the position stays healthy through the liquidation period, no liquidation occurs, even though the gate's timing window is still open. A fresh keeper call is required to restart the grace period if the position becomes unhealthy again later.
Risk for lenders
Filling offers on an agreement with a delayed-liquidation gate exposes lenders to additional price risk. If collateral value falls sharply during the grace period, bad debt becomes more likely. In addition, the grace period must be started onchain by a keeper. Until that call lands, no liquidation can happen even if the position is already unhealthy.
For a technical breakdown of the delayed liquidation mechanism, see [DelayedLiquidationGate](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation)
.
* [How it works](https://docs.tenor.finance/get-started/otc/delayed-liquidations#how-it-works)
* [Restoring health during the grace period](https://docs.tenor.finance/get-started/otc/delayed-liquidations#restoring-health-during-the-grace-period)
---
# Gated markets | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/otc/gated-markets#__docusaurus_skipToContent_fallback)
On this page
Gated markets restrict participation to an allowlist of approved counterparties, giving access to permissioned collateral types and structures that are not available in permissionless markets.
How it works[](https://docs.tenor.finance/get-started/otc/gated-markets#how-it-works "Direct link to How it works")
---------------------------------------------------------------------------------------------------------------------
A gated market has an allowlist of approved addresses. The gate has independent permissions for lending, borrowing, and liquidating. The market owner can permit an address to lend but not borrow, or authorize a separate set of liquidators. The owner manages the allowlist and can add or remove counterparties as compliance requirements change. If the owner permanently gives up ownership, the allowlist becomes immutable and can no longer change.
As a lender, you get controlled exposure to specific counterparties or KYC'd participants. As a borrower, you get access to collateral types and structures that are not available in open markets.
Gated markets are typically used for:
* Tokenized real-world assets (RWAs) and other permissioned collateral
* Compliance-restricted lending where participants must satisfy KYC requirements
* Institutional and OTC venues where the market owner needs to control who participates
For the underlying allowlist mechanism, see the technical documentation on the [Midnight Allowlist Gate](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist)
.
* [How it works](https://docs.tenor.finance/get-started/otc/gated-markets#how-it-works)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/#__docusaurus_skipToContent_fallback)
On this page

When you interact with the Tenor platform, you are interacting with the [Morpho protocol](https://docs.tenor.finance/security/morpho-smart-contracts)
and the [Tenor smart contracts](https://docs.tenor.finance/security/tenor-smart-contracts)
. The Morpho smart contracts hold collateral, enable liquidations, and handle settlement between lenders and borrowers. The Tenor smart contracts are opt-in modular extensions that add gates, callbacks, and ratifiers to Morpho Midnight. Tenor takes a multidisciplinary approach to security, combining:
### External Security Reviews
The Morpho and Tenor smart contracts have been reviewed by independent security researchers across multiple engagements.
[More →](https://docs.tenor.finance/security/security-reviews)
### Formal Verification
Key properties of the Tenor and Morpho smart contracts are formally verified using the Certora Prover.
[More →](https://docs.tenor.finance/security/security-reviews)
### Fuzzing
The Tenor smart contracts have been tested using stateless and stateful fuzzing to validate that key invariants hold across a large combination of states.
[More →](https://github.com/crytic/medusa)
### Bug Bounty
The Tenor and Morpho smart contracts are covered by bug bounties for responsible disclosures: up to $200,000 for critical findings on Tenor, up to $2,500,000 on Morpho.
[More →](https://docs.tenor.finance/security/bug-bounty)
### Onchain Monitoring
Tenor uses Blockaid to run continuous monitors on smart contract state and onchain activity to surface abnormal behavior.
[More →](https://docs.tenor.finance/security/monitoring)
### Non-custodial, Immutable Contracts
Tenor smart contracts are non-custodial and immutable (non-upgradable). Pause functionality is limited in scope and used only for cybersecurity purposes.
[More →](https://docs.tenor.finance/security/pausable-functions)
No process eliminates risk entirely. Onchain protocols can carry inherent smart contract and economic risks. For a full overview of the risks associated with using the platform, see the [Tenor Risk Disclosures](https://cdn.tenor.finance/legal/tenor-risk-disclosures.pdf)
.
Reporting a Vulnerability[](https://docs.tenor.finance/security/#reporting-a-vulnerability "Direct link to Reporting a Vulnerability")
----------------------------------------------------------------------------------------------------------------------------------------
If you believe you have found a vulnerability in the Tenor contracts, submit it through the [Tenor bug bounty program on Sherlock](https://audits.sherlock.xyz/)
. For issues in the underlying Morpho contracts, use the [Morpho bug bounty program](https://cantina.xyz/bounties/35a5f0a1-2ffd-432c-8f3b-77d169add8c3)
.
* [Reporting a Vulnerability](https://docs.tenor.finance/security/#reporting-a-vulnerability)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/bug-bounty#__docusaurus_skipToContent_fallback)
Two separate bug bounty programs cover the contracts used by the Tenor platform. Both incentivize responsible disclosure of bugs by external security researchers.
* **Tenor smart contracts:** Up to $200,000 for critical findings, run by [Sherlock](https://audits.sherlock.xyz/)
.
* **Morpho smart contracts:** Up to [$2,500,000](https://cantina.xyz/bounties/35a5f0a1-2ffd-432c-8f3b-77d169add8c3)
for critical findings in the underlying Morpho protocol.
If you believe you have found a vulnerability in the Tenor contracts, submit it through the [Tenor bug bounty program on Sherlock](https://audits.sherlock.xyz/)
. For issues in the underlying Morpho contracts, use the [Morpho bug bounty program](https://cantina.xyz/bounties/35a5f0a1-2ffd-432c-8f3b-77d169add8c3)
.
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/morpho-smart-contracts#__docusaurus_skipToContent_fallback)
On this page
Morpho is a permissionless, immutable lending protocol that provides the foundation for Tenor. Its contracts are minimal and non-upgradable, with isolated markets and configurable risk parameters. Because Tenor sits directly on top of Morpho, every Tenor market inherits the security properties of the Morpho contracts.
Its contracts have undergone multiple [security reviews](https://docs.morpho.org/overview/resources/audits/)
from leading security firms, and are covered by an active bug bounty offering up to $2,500,000 for critical findings. See [Bug Bounty](https://docs.tenor.finance/security/bug-bounty)
for details.
Isolated Markets[](https://docs.tenor.finance/security/morpho-smart-contracts#isolated-markets "Direct link to Isolated Markets")
-----------------------------------------------------------------------------------------------------------------------------------
Each Tenor market is deployed as an isolated Morpho market with immutable parameters. Once a market is created, its LLTV, oracle address, loan token, and collateral token cannot be changed. This mitigates governance risk: no party can retroactively alter the terms of an existing market, and the parameters lenders and borrowers agree to at creation are the parameters they keep for the lifetime of the market.
Isolation also means risk in one market cannot propagate to another, and lets users configure markets to match their preferences by choosing the collateral, oracle, and LLTV that fit their risk profile rather than being limited to a single shared pool.
* [Isolated Markets](https://docs.tenor.finance/security/morpho-smart-contracts#isolated-markets)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/price-oracles#__docusaurus_skipToContent_fallback)
On this page
To mitigate the risks of relying on a single oracle provider, Tenor built a new Morpho-compatible oracle smart contract, the [Oracle With Validation](https://docs.tenor.finance/technical-docs/oracle-with-validation)
, that validates the primary oracle price against a secondary feed.
Before any price-dependent action proceeds, the Oracle With Validation compares the primary oracle (e.g., Chainlink) against the validation feed (e.g., Chronicle or Redstone). If the two diverge beyond a configured deviation threshold, price calls revert and dependent operations (borrow, withdraw collateral, and liquidations) are blocked until the discrepancy resolves.
Not every market on Tenor uses the Oracle With Validation. Some markets are deployed against a single primary oracle without a cross-validation feed. Whether a given market uses the validated oracle, and which feeds it relies on, is visible in the market details on the app.
The deviation threshold is typically set below `(1 - LLTV)`. This is wide enough that normal oracle drift does not trip the check, but tight enough to catch a large divergence. Normal drift comes from feed update cadence, decimal rounding, sequencer lag, or TWAP smoothing. By contrast, a large divergence usually means one feed has been manipulated or is stale. Once deployed, the primary and validation oracle addresses, the deviation threshold, and the validation-failure behavior are immutable.
The main risk of this design is that liquidations are also blocked while the validation check is tripped. If the validation feed misbehaves for a sustained period, healthy markets can be frozen along with the unhealthy ones, which is why the pause described below exists.
Pausing the Validation Check[](https://docs.tenor.finance/security/price-oracles#pausing-the-validation-check "Direct link to Pausing the Validation Check")
--------------------------------------------------------------------------------------------------------------------------------------------------------------
The Oracle With Validation can expose an owner-controlled pause on the validation step, documented under [Pausable Functions](https://docs.tenor.finance/security/pausable-functions#oracle-validation-pause)
. It can also be deployed without an active owner (for example, by renouncing ownership), in which case no pause is available. While paused, the contract returns the primary oracle's price without comparing it against the secondary feed. The primary oracle is always used to source the price; the pause only disables the validation check.
When available, the pause is intended for situations where the validation feed itself is blocking legitimate price calls, such as when a secondary source has been deprecated, is reporting stale data, or is diverging from market price for a sustained period. In that case, the owner can remove the validation check so that the market continues to function on the primary oracle alone.
* [Pausing the Validation Check](https://docs.tenor.finance/security/price-oracles#pausing-the-validation-check)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/tenor-sdk#__docusaurus_skipToContent_fallback)
On this page
The [Tenor SDK](https://docs.tenor.finance/technical-docs/sdk)
is a TypeScript and React toolkit for executing fixed-rate lending and borrowing flows on Tenor and Morpho Midnight stacks. It covers lend, borrow, repay, withdraw, collateral management, offers, renewal, and auto-renewal, exposed as a high-level `ActionsBuilder` and per-operation React hooks.
It handles transaction assembly, approval routing across EOA, Safe, and smart-account wallets, signature collection, and simulation, so integrators only supply markets, offers, and amounts.
Security Reviews[](https://docs.tenor.finance/security/tenor-sdk#security-reviews "Direct link to Security Reviews")
----------------------------------------------------------------------------------------------------------------------
The SDK has been reviewed through a combination of manual and AI-assisted reviews by the following security firms:
* **[Obsidian Audits](https://www.obsidianaudits.com/about)
**: Best-effort manual review of the SDK, covering authorization flows, approval routing, signature handling, and bundle construction.
* **[Octane Security](https://www.octane.security/)
**: AI-driven code review run against the SDK codebase to surface vulnerability patterns, unsafe approval flows, and integration-layer mistakes.
* **[Cantina Apex](https://cantina.xyz/solutions/code-analyzer/enterprise)
**: AI-driven security review run against the SDK codebase to surface logic bugs, edge cases, and integration risks in the SDK code paths.
These reviews complement the security reviews of the [Tenor smart contracts](https://docs.tenor.finance/security/tenor-smart-contracts)
, focusing on the risks specific to the integration SDK: transaction assembly, approval routing, and wallet handling.
* [Security Reviews](https://docs.tenor.finance/security/tenor-sdk#security-reviews)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/pausable-functions#__docusaurus_skipToContent_fallback)
On this page
Only select Tenor contracts have pause functions. These functions are used only for cybersecurity reasons, such as responding to a critical bug bounty report or to suspicious onchain activity.
A pause can only remove a check or halt a renewal pathway. It cannot move user funds, transfer collateral, alter market parameters, or change the price returned by an oracle.
Pausable Interest Rate Policy[](https://docs.tenor.finance/security/pausable-functions#pausable-interest-rate-policy "Direct link to Pausable Interest Rate Policy")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
A designated pauser can halt the pausable variant of the [Static Rate Policy](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy)
(`PausableStaticRatePolicy`). While paused, `getRate()` reverts and any renewal that points at this policy is blocked at the take step.
### Benefit[](https://docs.tenor.finance/security/pausable-functions#benefit "Direct link to Benefit")
Pausing one policy only blocks the renewals that depend on it. Other renewals, markets, and Morpho-level operations keep running, so a vulnerability or suspicious activity tied to a single policy can be isolated without halting the rest of the protocol.
### Scope of the pause[](https://docs.tenor.finance/security/pausable-functions#scope-of-the-pause "Direct link to Scope of the pause")
The pause only blocks new renewal takes that go through this policy. It does not affect collateral, lender balances, borrower positions, or any other Morpho-level operation. Users retain full control of their assets while the policy is paused.
Oracle Validation Pause[](https://docs.tenor.finance/security/pausable-functions#oracle-validation-pause "Direct link to Oracle Validation Pause")
----------------------------------------------------------------------------------------------------------------------------------------------------
The [Oracle With Validation](https://docs.tenor.finance/technical-docs/oracle-with-validation)
can have its validation step paused by its owner. While paused, the contract returns the primary oracle's price without comparing it to the secondary feed.
### Benefit[](https://docs.tenor.finance/security/pausable-functions#benefit-1 "Direct link to Benefit")
Pausing the validation step keeps a market operational when the secondary feed becomes unreliable or is deprecated. Without the pause, a stale or removed validation feed would cause valid price calls to revert and block legitimate actions (borrow, withdraw collateral, liquidate).
### Scope of the pause[](https://docs.tenor.finance/security/pausable-functions#scope-of-the-pause-1 "Direct link to Scope of the pause")
The pause only removes the deviation check between the primary and validation feeds. The primary oracle is always used to source the price. The deviation threshold, oracle addresses, and validation-failure behavior remain immutable, and at no point can the pauser influence the price returned to the protocol.
* [Pausable Interest Rate Policy](https://docs.tenor.finance/security/pausable-functions#pausable-interest-rate-policy)
* [Benefit](https://docs.tenor.finance/security/pausable-functions#benefit)
* [Scope of the pause](https://docs.tenor.finance/security/pausable-functions#scope-of-the-pause)
* [Oracle Validation Pause](https://docs.tenor.finance/security/pausable-functions#oracle-validation-pause)
* [Benefit](https://docs.tenor.finance/security/pausable-functions#benefit-1)
* [Scope of the pause](https://docs.tenor.finance/security/pausable-functions#scope-of-the-pause-1)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/security-reviews#__docusaurus_skipToContent_fallback)
Status
Audits are in progress. Reports are expected to be published throughout Q2 2026 and will be linked in the table below as they become available.
| Auditor | Type | Dates | Report |
| --- | --- | --- | --- |
| [Blackthorn](https://www.blackthorn.xyz/)
(Sherlock) | Security Review | Ongoing | Coming Soon |
| [Cantina](https://cantina.xyz/) | Security Review | Ongoing | Coming Soon |
| [Obsidian Audits](https://www.obsidianaudits.com/about) | Security Review | April 2026 | [Report](https://docs.tenor.finance/assets/files/Obsidian-audits-04-2026-review-4b568434f49923915c85a1cfacba4b64.pdf) |
| [Guardian Audits](https://guardianaudits.com/) | Security Review | May 2026 | [Report](https://docs.tenor.finance/assets/files/Guardian-audits-05-2026-review-4516ff0e21bb53baa8bd3e87e883f223.pdf) |
| [Alex Zoid](https://alexzoid.com/) | Formal Verification (Certora Prover) | Ongoing | Coming Soon |
---
# Request quotes | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/otc/request-offers#__docusaurus_skipToContent_fallback)
On this page
Tenor lets borrowers post requests **offchain** to inform lenders they are looking to borrow against a specific collateral asset. Lenders (curators, asset managers) can then respond with onchain buy (lend) offers, letting borrowers compare them and assess if they want to take one onchain.
The request itself is **non-binding**: no collateral is deposited and nothing settles onchain when the request is sent. Settlement happens only when the borrower takes one of the resulting onchain offers.
Benefits of the request system[](https://docs.tenor.finance/get-started/otc/request-offers#benefits-of-the-request-system "Direct link to Benefits of the request system")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
A borrower may not want to choose the oracle, LLTV, and other advanced parameters up front. Using Tenor's request feature, borrowers can specify only a subset of the parameters needed to create an onchain offer on Morpho Midnight:
* Loan token and amount
* Collateral asset
Counterparties propose the remaining terms (LLTV, oracle, term, rate) in their onchain offers, and the borrower compares quotes from multiple lenders before accepting one.
When to use a request
Requests suit borrowers who do not want to set the oracle, LLTV, term, and rate themselves. The borrower specifies only the loan and collateral, and specialist lenders compete to propose the rest.
How a request works[](https://docs.tenor.finance/get-started/otc/request-offers#how-a-request-works "Direct link to How a request works")
-------------------------------------------------------------------------------------------------------------------------------------------
1. **Borrower request (non-binding):** The borrower specifies the loan token, amount, and collateral asset and broadcasts the request, either to all counterparties or to a chosen set. No collateral is deposited; the request is non-binding.
2. **Lender receives request:** Lenders see the request in their inbox on the Tenor interface, and can opt in to notifications for incoming requests.
3. **Lender creates offer (onchain):** Interested lenders respond by publishing onchain buy (lend) offers, each setting LLTV, oracle, term, and rate.
4. **Borrower accepts offer (onchain):** The borrower compares the returned offers and takes the one that matches their needs. Taking the offer settles the position onchain through Morpho.
In the example below, a borrower sends a request to three counterparties. Two return onchain offers, and the borrower takes the one from Counterparty 3.

[Tenor Prime](https://docs.tenor.finance/get-started/tenor-prime)
users (e.g. curators, lending firms, asset managers) can receive requests from borrowers and send onchain offers.
* [Benefits of the request system](https://docs.tenor.finance/get-started/otc/request-offers#benefits-of-the-request-system)
* [How a request works](https://docs.tenor.finance/get-started/otc/request-offers#how-a-request-works)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-prime#__docusaurus_skipToContent_fallback)
Tenor Prime is a layer on top of Tenor for organizations (curators, asset managers, funds, and trading firms) that operate across multiple wallets and vaults. It groups those resources under a single organization account, adds role-based access for team members, and adds organization-level surfaces for OTC offers, gated vaults, and monitoring.
* [Organization Accounts](https://docs.tenor.finance/get-started/tenor-prime/organization-accounts)
: Unify wallets, delegate roles, run vaults, and manage gated vaults from a single account.
* [OTC](https://docs.tenor.finance/get-started/tenor-prime/otc)
: Bilateral offers, customizable terms, and gated markets.
* [Gated vaults](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults)
: Permissioned Vault V2 with an allowlist controlling deposits, withdrawals, and who can hold shares.
* [Monitoring, SDK & API](https://docs.tenor.finance/get-started/tenor-prime/monitoring)
: Real-time monitoring, reporting, alerts, and programmatic access.
---
# Risks | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/risks#__docusaurus_skipToContent_fallback)
On this page
The summary below highlights the risks most relevant to users of the Tenor interface; it is not exhaustive. The full legal disclosure is available in the [Platform Risks document](https://cdn.tenor.finance/legal/tenor-risk-disclosures.pdf)
.
Market and oracle risks[](https://docs.tenor.finance/get-started/risks#market-and-oracle-risks "Direct link to Market and oracle risks")
------------------------------------------------------------------------------------------------------------------------------------------
### Price volatility and depegs[](https://docs.tenor.finance/get-started/risks#price-volatility-and-depegs "Direct link to Price volatility and depegs")
The value of any token used as collateral or as a loan asset can move sharply. Stablecoins can lose their peg. Sudden price moves can push a borrow position into liquidation and reduce the realizable value of collateral.
### Oracle inaccuracy or manipulation[](https://docs.tenor.finance/get-started/risks#oracle-inaccuracy-or-manipulation "Direct link to Oracle inaccuracy or manipulation")
Each market relies on a price oracle for collateral valuation. If the oracle returns a stale, incorrect, or manipulated price, a position may be liquidated improperly, or a borrower's position may remain open while undercollateralized. Cross-check oracle prices against external sources before time-sensitive actions.
### Onchain illiquidity[](https://docs.tenor.finance/get-started/risks#onchain-illiquidity "Direct link to Onchain illiquidity")
Reselling a lend position, repaying a borrow early, or exiting before maturity depends on counterparties being present onchain. In thin markets you may be unable to exit at the price you expect, or at all, until maturity.
Borrowing and liquidation risks[](https://docs.tenor.finance/get-started/risks#borrowing-and-liquidation-risks "Direct link to Borrowing and liquidation risks")
------------------------------------------------------------------------------------------------------------------------------------------------------------------
### Liquidation[](https://docs.tenor.finance/get-started/risks#liquidation "Direct link to Liquidation")
A borrow position can be liquidated without warning whenever its LTV crosses the market's LLTV, or when debt remains unpaid past maturity. Liquidations may be partial or total, may apply a penalty, and during congestion or oracle delays may execute at prices significantly worse than expected. Attempts to add collateral may fail in the same conditions. Cascading liquidations across positions are possible.
### Grace periods are not a guarantee[](https://docs.tenor.finance/get-started/risks#grace-periods-are-not-a-guarantee "Direct link to Grace periods are not a guarantee")
Where a market offers a grace period before liquidation: the grace period depends on a third party submitting a trigger transaction; adding collateral or repaying during the grace period does not automatically prevent liquidation; and the grace period is a one-time buffer per initiation, not continuous protection.
### Bad debt and loss socialization[](https://docs.tenor.finance/get-started/risks#bad-debt-and-loss-socialization "Direct link to Bad debt and loss socialization")
If a borrower's position becomes insolvent (debt exceeds collateral value even after liquidation), the shortfall is socialized across all lenders in the same Morpho market. Your lend position can lose value because of another participant's default, not because of any action of your own.
### Collateral composition[](https://docs.tenor.finance/get-started/risks#collateral-composition "Direct link to Collateral composition")
A market may accept or be affected by multiple collateral types (vault shares, wrapped or derivative assets), even when the interface highlights only one. Where vault shares are accepted as collateral, losses in the underlying vault can propagate to the market, including to lenders whose borrower counterparties posted a different collateral asset. Review every collateral exposure in a market, not only the asset you see in the interface.
Renewal, rollover, and contingent orders[](https://docs.tenor.finance/get-started/risks#renewal-rollover-and-contingent-orders "Direct link to Renewal, rollover, and contingent orders")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
### Auto-renewal may not execute[](https://docs.tenor.finance/get-started/risks#auto-renewal-may-not-execute "Direct link to Auto-renewal may not execute")
Auto-renewal depends on a third party (keeper or solver) submitting the onchain transaction to roll your position. If no third party finds the renewal economically viable (due to market conditions, network fees, or other reasons), the position will not be renewed and may mature, expire, or become liquidatable. Do not assume a rollover will execute simply because you configured it.
### Renewal gates[](https://docs.tenor.finance/get-started/risks#renewal-gates "Direct link to Renewal gates")
If you opt in to a renewal gate restricting which addresses may roll your position, no authorized party may be available when needed. A gate may also be activated as a pause mechanism in response to a security event, which equally prevents your position from rolling during the suspension.
### Contingent orders[](https://docs.tenor.finance/get-started/risks#contingent-orders "Direct link to Contingent orders")
Limit and other contingent orders may not execute at all, or may execute differently from a centralized exchange-style order, because they depend on onchain conditions and third-party execution. Multiple contingent orders with overlapping conditions can fire simultaneously and produce a position different from any one of them individually.
Smart contract and protocol risks[](https://docs.tenor.finance/get-started/risks#smart-contract-and-protocol-risks "Direct link to Smart contract and protocol risks")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
### Smart contract bugs[](https://docs.tenor.finance/get-started/risks#smart-contract-bugs "Direct link to Smart contract bugs")
Tenor, the Morpho Protocol, and the surrounding contracts (bundlers, adapters, ratifiers, callback contracts) are software and may contain bugs, novel exploits, or interactions that cause partial or total loss of funds. Onchain failures are often irreversible.
### Morpho Protocol dependency[](https://docs.tenor.finance/get-started/risks#morpho-protocol-dependency "Direct link to Morpho Protocol dependency")
Tenor is built on the Morpho Protocol, developed by a third party. Insufficient liquidity, repositioning limits, market unavailability, composability failures, or protocol changes within Morpho can cause expected transactions (rollovers, auto-close, liquidations) to fail and can result in losses.
### Token approvals and protocol authorizations[](https://docs.tenor.finance/get-started/risks#token-approvals-and-protocol-authorizations "Direct link to Token approvals and protocol authorizations")
Using Tenor requires token approvals (including unlimited allowances or permit signatures) and protocol-level authorizations that let contracts such as the Tenor Bundler, adapters, ratifiers, and callback contracts act on your behalf (supplying or withdrawing collateral, repaying debt, executing renewals). These authorizations persist until you explicitly revoke them. Review and revoke authorizations you no longer need.
### Bundle execution[](https://docs.tenor.finance/get-started/risks#bundle-execution "Direct link to Bundle execution")
When the Tenor Bundler executes multiple operations in a single transaction, verify the contents of the bundle before signing. A malicious or compromised interface could construct a bundle that grants authorizations, transfers assets, or modifies positions in ways you did not intend. Hardware wallets and transaction simulation help.
### MEV and frontrunning[](https://docs.tenor.finance/get-started/risks#mev-and-frontrunning "Direct link to MEV and frontrunning")
Some onchain transactions can be exposed to MEV bots or frontrunning that may affect execution price.
Custom configurations[](https://docs.tenor.finance/get-started/risks#custom-configurations "Direct link to Custom configurations")
------------------------------------------------------------------------------------------------------------------------------------
### Custom orders and OTC parameters[](https://docs.tenor.finance/get-started/risks#custom-orders-and-otc-parameters "Direct link to Custom orders and OTC parameters")
Custom orders, custom OTC offers, custom vault rules, signer permissions, allowlists, adapters, and other user-controlled settings can be easier to misconfigure than standard markets. Always double-check parameters and consider a test transaction first. Interface guardrails may not catch your error.
### Non-custodial vaults and multisignature wallets[](https://docs.tenor.finance/get-started/risks#non-custodial-vaults-and-multisignature-wallets "Direct link to Non-custodial vaults and multisignature wallets")
Morpho non-custodial vaults and Safe multisignature wallets created or managed through Tenor are controlled entirely by their signers and configurations: signers, thresholds, allowlists, permissions, adapters, modules, allocation instructions, and risk settings. Misconfiguration can cause permanent loss of access, blocked withdrawals, or forced liquidations, and cannot be reversed by Tenor.
### Market and vault gates[](https://docs.tenor.finance/get-started/risks#market-and-vault-gates "Direct link to Market and vault gates")
Markets and vaults may use smart contract gates restricting which addresses can lend, borrow, liquidate, deposit, withdraw, or receive shares. A gate may be misconfigured, changed by an owner, or made immutable. If a gate prevents you from withdrawing or exiting, you may be unable to take time-sensitive action, up to the total loss of your assets. Verify gate configuration before depositing.
Wallet, device, and recovery risks[](https://docs.tenor.finance/get-started/risks#wallet-device-and-recovery-risks "Direct link to Wallet, device, and recovery risks")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
### Device and key security[](https://docs.tenor.finance/get-started/risks#device-and-key-security "Direct link to Device and key security")
Tenor cannot recover the keys of an external wallet you connect. If you lose access to recovery options for an embedded wallet (e.g., Privy authentication via email or phone), access to that wallet and any assets it controls may be permanently lost. Offline backups of private keys and seed phrases mitigate this.
### Social engineering and impersonation[](https://docs.tenor.finance/get-started/risks#social-engineering-and-impersonation "Direct link to Social engineering and impersonation")
Be cautious of messages, emails, or social posts impersonating Tenor, integrated third parties, or counterparties. Official channels can also be compromised. Verify contract addresses and instructions through multiple independent sources before transacting. Tenor support will never ask for private keys or seed phrases.
### Cybersecurity attacks on Tenor itself[](https://docs.tenor.finance/get-started/risks#cybersecurity-attacks-on-tenor-itself "Direct link to Cybersecurity attacks on Tenor itself")
Frontends, SDKs, APIs, and supporting infrastructure may be targeted by attacks (DNS hijacking, supply chain compromise, malicious transaction substitution, credential theft). No software is fully secure. Verify URLs and package sources, use hardware wallets where appropriate, and review every transaction before signing.
Network and operational risks[](https://docs.tenor.finance/get-started/risks#network-and-operational-risks "Direct link to Network and operational risks")
------------------------------------------------------------------------------------------------------------------------------------------------------------
### Network, node, and sequencer failures[](https://docs.tenor.finance/get-started/risks#network-node-and-sequencer-failures "Direct link to Network, node, and sequencer failures")
Tenor relies on public blockchain networks. Node delays, chain reorganizations, hard forks, and sequencer downtime on L2s can delay or prevent transactions, including time-sensitive actions like adding collateral or closing a position to avoid liquidation.
### Network congestion and gas[](https://docs.tenor.finance/get-started/risks#network-congestion-and-gas "Direct link to Network congestion and gas")
Critical actions such as adding collateral may have to be executed at high gas prices during congestion. Gas estimates shown in the interface may be inaccurate, and a transaction can fail even where the interface predicted success.
### Service interruption[](https://docs.tenor.finance/get-started/risks#service-interruption "Direct link to Service interruption")
The Tenor interface may be temporarily unavailable due to maintenance, upgrades, or attacks. In such cases you may need to interact with the Morpho Protocol directly, or may miss an alert from Tenor.
### Monitoring tools are not guarantees[](https://docs.tenor.finance/get-started/risks#monitoring-tools-are-not-guarantees "Direct link to Monitoring tools are not guarantees")
Monitoring features in Tenor are provided as a convenience and may not function correctly. Tracking the state of your positions and responding to onchain events ultimately depends on you.
### Estimates and displayed numbers[](https://docs.tenor.finance/get-started/risks#estimates-and-displayed-numbers "Direct link to Estimates and displayed numbers")
Rates, slippage estimates, projected outcomes, and other numbers in the interface are estimates and may differ from actual results. Displayed numbers are not suitable for tax reporting.
Third-party and counterparty risks[](https://docs.tenor.finance/get-started/risks#third-party-and-counterparty-risks "Direct link to Third-party and counterparty risks")
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
### Bridges, DEXs, and integrated onchain services[](https://docs.tenor.finance/get-started/risks#bridges-dexs-and-integrated-onchain-services "Direct link to Bridges, DEXs, and integrated onchain services")
Tenor may integrate with bridges, decentralized exchanges, and other onchain utilities operated by third parties. These have their own security models and prior history of failures.
### Third-party services and infrastructure[](https://docs.tenor.finance/get-started/risks#third-party-services-and-infrastructure "Direct link to Third-party services and infrastructure")
Tenor depends on third parties including the Morpho Protocol, Privy, Safe, oracle providers, bridges, RPC providers, and other infrastructure. A security failure or compromise of any of these can cause or amplify losses, even if Tenor itself is functioning correctly.
### Messaging and counterparties[](https://docs.tenor.finance/get-started/risks#messaging-and-counterparties "Direct link to Messaging and counterparties")
If you use Tenor messaging tools (for example, for OTC quotes or indications of interest), counterparties may be unresponsive, dishonest, or technically defective. Tenor does not verify counterparty identity, solvency, or compliance. Wallet addresses, signatures, and links shared through messaging may be inaccurate or malicious; verify independently before acting.
### Customer support[](https://docs.tenor.finance/get-started/risks#customer-support "Direct link to Customer support")
Customer support communications, including from AI systems, can be erroneous. Verify any guidance against official documentation. Real Tenor support will never ask for private keys, seed phrases, or assets.
Mitigation[](https://docs.tenor.finance/get-started/risks#mitigation "Direct link to Mitigation")
---------------------------------------------------------------------------------------------------
Common steps that reduce exposure to the risks above: monitor your positions, maintain sufficient collateral, verify URLs and contract addresses, review transaction details before signing, use hardware wallets where appropriate, keep secure backups of keys and recovery credentials, revoke token approvals and protocol authorizations you no longer need, and verify the authenticity of any communication that claims to come from Tenor.
For the full risk disclosure, see [Platform Risks](https://cdn.tenor.finance/legal/tenor-risk-disclosures.pdf)
.
* [Market and oracle risks](https://docs.tenor.finance/get-started/risks#market-and-oracle-risks)
* [Price volatility and depegs](https://docs.tenor.finance/get-started/risks#price-volatility-and-depegs)
* [Oracle inaccuracy or manipulation](https://docs.tenor.finance/get-started/risks#oracle-inaccuracy-or-manipulation)
* [Onchain illiquidity](https://docs.tenor.finance/get-started/risks#onchain-illiquidity)
* [Borrowing and liquidation risks](https://docs.tenor.finance/get-started/risks#borrowing-and-liquidation-risks)
* [Liquidation](https://docs.tenor.finance/get-started/risks#liquidation)
* [Grace periods are not a guarantee](https://docs.tenor.finance/get-started/risks#grace-periods-are-not-a-guarantee)
* [Bad debt and loss socialization](https://docs.tenor.finance/get-started/risks#bad-debt-and-loss-socialization)
* [Collateral composition](https://docs.tenor.finance/get-started/risks#collateral-composition)
* [Renewal, rollover, and contingent orders](https://docs.tenor.finance/get-started/risks#renewal-rollover-and-contingent-orders)
* [Auto-renewal may not execute](https://docs.tenor.finance/get-started/risks#auto-renewal-may-not-execute)
* [Renewal gates](https://docs.tenor.finance/get-started/risks#renewal-gates)
* [Contingent orders](https://docs.tenor.finance/get-started/risks#contingent-orders)
* [Smart contract and protocol risks](https://docs.tenor.finance/get-started/risks#smart-contract-and-protocol-risks)
* [Smart contract bugs](https://docs.tenor.finance/get-started/risks#smart-contract-bugs)
* [Morpho Protocol dependency](https://docs.tenor.finance/get-started/risks#morpho-protocol-dependency)
* [Token approvals and protocol authorizations](https://docs.tenor.finance/get-started/risks#token-approvals-and-protocol-authorizations)
* [Bundle execution](https://docs.tenor.finance/get-started/risks#bundle-execution)
* [MEV and frontrunning](https://docs.tenor.finance/get-started/risks#mev-and-frontrunning)
* [Custom configurations](https://docs.tenor.finance/get-started/risks#custom-configurations)
* [Custom orders and OTC parameters](https://docs.tenor.finance/get-started/risks#custom-orders-and-otc-parameters)
* [Non-custodial vaults and multisignature wallets](https://docs.tenor.finance/get-started/risks#non-custodial-vaults-and-multisignature-wallets)
* [Market and vault gates](https://docs.tenor.finance/get-started/risks#market-and-vault-gates)
* [Wallet, device, and recovery risks](https://docs.tenor.finance/get-started/risks#wallet-device-and-recovery-risks)
* [Device and key security](https://docs.tenor.finance/get-started/risks#device-and-key-security)
* [Social engineering and impersonation](https://docs.tenor.finance/get-started/risks#social-engineering-and-impersonation)
* [Cybersecurity attacks on Tenor itself](https://docs.tenor.finance/get-started/risks#cybersecurity-attacks-on-tenor-itself)
* [Network and operational risks](https://docs.tenor.finance/get-started/risks#network-and-operational-risks)
* [Network, node, and sequencer failures](https://docs.tenor.finance/get-started/risks#network-node-and-sequencer-failures)
* [Network congestion and gas](https://docs.tenor.finance/get-started/risks#network-congestion-and-gas)
* [Service interruption](https://docs.tenor.finance/get-started/risks#service-interruption)
* [Monitoring tools are not guarantees](https://docs.tenor.finance/get-started/risks#monitoring-tools-are-not-guarantees)
* [Estimates and displayed numbers](https://docs.tenor.finance/get-started/risks#estimates-and-displayed-numbers)
* [Third-party and counterparty risks](https://docs.tenor.finance/get-started/risks#third-party-and-counterparty-risks)
* [Bridges, DEXs, and integrated onchain services](https://docs.tenor.finance/get-started/risks#bridges-dexs-and-integrated-onchain-services)
* [Third-party services and infrastructure](https://docs.tenor.finance/get-started/risks#third-party-services-and-infrastructure)
* [Messaging and counterparties](https://docs.tenor.finance/get-started/risks#messaging-and-counterparties)
* [Customer support](https://docs.tenor.finance/get-started/risks#customer-support)
* [Mitigation](https://docs.tenor.finance/get-started/risks#mitigation)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-prime/organization-accounts#__docusaurus_skipToContent_fallback)
On this page
An organization account in Tenor Prime groups the wallets, vaults, and team members behind a single organization, such as a curator, fund, lending firm, or asset manager. It lets these organizations operate on Tenor through multiple wallets while simplifying views and access control, so members can create transactions and coordinate across separate EOAs, multisigs, and vaults.
Prime account features[](https://docs.tenor.finance/get-started/tenor-prime/organization-accounts#prime-account-features "Direct link to Prime account features")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
* **Unify assets in one view**: Link any number of EOAs, multisigs, and vaults to the organization. Holdings, positions, and analytics aggregate across all of them in a single portfolio view.
* **Delegate by role**: Tenor reflects the onchain roles assigned to each wallet, so members can act on behalf of vaults and multisigs they have permissions for. Roles can be added, removed, or updated from the interface, with configurable timelocks on role changes.
* **Create custom vaults**: Create one or more [gated vaults](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults)
, set their risk parameters and allowlist, and assign team members to roles (allocator, curator, sentinel).
* **Receive OTC borrower requests**: Incoming [OTC borrower requests](https://docs.tenor.finance/get-started/otc/request-offers)
land in the organization's inbox. Authorized members can review them and respond with an onchain offer on behalf of the organization via an EOA, multisig, or vault.
* **Generate consolidated reports**: Pull transaction history, PnL, and other reports across all wallets and vaults under the organization.
To create a Tenor Prime organization account, [book an onboarding meeting](https://calendar.app.google/LETVU1kBiRkuoXy57)
or contact [contact@tenor.finance](mailto:contact@tenor.finance)
.
* [Prime account features](https://docs.tenor.finance/get-started/tenor-prime/organization-accounts#prime-account-features)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-prime/otc#__docusaurus_skipToContent_fallback)
On this page
An offer request is a private, offchain message a borrower sends to a specific Tenor Prime organization to quote terms for a position. It lands in the organization's inbox, where authorized members can reject it or respond with an onchain lend offer from the organization account.
A request specifies only the loan token, amount, and collateral asset. LLTV, oracle, term, and rate are set by the responding lender.
Handling a request[](https://docs.tenor.finance/get-started/tenor-prime/otc#handling-a-request "Direct link to Handling a request")
-------------------------------------------------------------------------------------------------------------------------------------
From the inbox, you have three options for each incoming request:
* **Publish a quote**: respond with an onchain buy (lend) offer from the organization account, setting LLTV, oracle, term, and rate. The offer is linked to the originating request, so the borrower sees it as a response in their inbox.
* **Reject**: dismiss the request from the inbox.
* **Leave it**: take no action. Requests that fall outside the organization's mandate can be ignored.
Only publishing a quote is an onchain action; rejecting and leaving are both offchain.
After you quote[](https://docs.tenor.finance/get-started/tenor-prime/otc#after-you-quote "Direct link to After you quote")
----------------------------------------------------------------------------------------------------------------------------
Your offer is live onchain from the moment you publish it. The borrower may have requested quotes from several organizations:
* If the borrower takes your offer, the position settles onchain through Morpho.
* If the borrower takes a different lender's offer, yours remains live onchain and can be cancelled or left available for other counterparties.
For the end-to-end flow from the borrower's side, see [Request offers](https://docs.tenor.finance/get-started/otc/request-offers)
.
* [Handling a request](https://docs.tenor.finance/get-started/tenor-prime/otc#handling-a-request)
* [After you quote](https://docs.tenor.finance/get-started/tenor-prime/otc#after-you-quote)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#__docusaurus_skipToContent_fallback)
On this page
Gated vaults are customizable vaults with an onchain allowlist that controls who can deposit, withdraw, and hold shares. They support compliance-restricted strategies, institutional mandates, and asset managers running their own onchain funds, where participation must be limited to a defined set of depositors.
What gated vaults enable[](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#what-gated-vaults-enable "Direct link to What gated vaults enable")
----------------------------------------------------------------------------------------------------------------------------------------------------------------
* **Institutional and compliance use cases:** Curators can deploy a vault that only accepts deposits from a curated set of wallets, with the allowlist enforced onchain at every transfer.
* **Asset managers running their own funds:** A manager can deploy a gated vault as the onchain vehicle for their own capital, using the allowlist as a role-based access control layer for their organization.
* **Whitelist-restricted deposits:** When a strategy is only open to specific counterparties (partners, an internal allocation, or a private mandate), the allowlist defines exactly who can deposit and who can receive shares.
Vaults as a role-based access control mechanism[](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#vaults-as-a-role-based-access-control-mechanism "Direct link to Vaults as a role-based access control mechanism")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
A vault separates its operational authority into distinct roles enforced at the smart contract level. Each role can be assigned to a different signer (a single wallet or a multisig), so a curator can delegate operations across a team and constrain what each signer is able to do:
* **Owner:** Holds top-level administrative authority over the vault, with capabilities restricted to fee management (vault fee, fee recipient, skim recipient), submitting new sentinels, and accepting ownership transfers. The Owner does not control risk parameters or allocator authority, which remain the Curator's responsibility.
* **Curator:** Sets the vault's risk parameters and the limits it operates within. The role can be held by one signer in the organization or by a multisig.
* **Allocators:** A subset of wallets authorized to allocate vault liquidity across fixed-term and open-term markets, within the limits set by the curator. Allocators cannot change those limits.
* **Sentinels:** Wallets whose only role is to revoke a pending decision during its timelock window, for example, to cancel a parameter change for cybersecurity reasons before it takes effect.
How it works[](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#how-it-works "Direct link to How it works")
----------------------------------------------------------------------------------------------------------------------------
The vault curator deploys an allowlist gate alongside the vault and configures the set of approved wallets. Each address on the allowlist carries independent permission flags for depositing, withdrawing, receiving shares, and sending shares, so the curator can grant different roles to different participants. The vault's fee recipients are allowed automatically so management and performance fees continue to accrue.
The allowlist is updatable by the gate owner. To make the vault permanently immutable, the owner can renounce ownership of the gate, after which no address can be added or removed.
* [What gated vaults enable](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#what-gated-vaults-enable)
* [Vaults as a role-based access control mechanism](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#vaults-as-a-role-based-access-control-mechanism)
* [How it works](https://docs.tenor.finance/get-started/tenor-prime/gated-vaults#how-it-works)
---
# Rewards | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/rewards#__docusaurus_skipToContent_fallback)
Tenor distributes rewards through [Merkl](https://merkl.xyz/)
, a reward distribution protocol. Rewards may be issued by Tenor or by third parties as part of liquidity incentive programs.
Rewards are added weekly rather than in real time, so your claimable balance may not immediately reflect your most recent activity.
You can claim rewards directly from the Tenor interface or through Merkl. For step-by-step instructions, see [Claim Rewards](https://docs.tenor.finance/tutorials/guide-rewards)
.
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#__docusaurus_skipToContent_fallback)
On this page
Tenor markets accept the variable-rate lend position as collateral against a fixed-rate borrow, on otherwise identical market parameters. This creates continuity between Morpho Blue markets (variable rate) and Morpho Midnight markets (fixed rate).
This is done by listing vault shares as collateral on Morpho Midnight, where the underlying vault is an unmanaged, immutable Vault-V2 allocating entirely to a Morpho Blue variable-rate market.
Example[](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#example "Direct link to Example")
----------------------------------------------------------------------------------------------------------------------
Take a variable-rate market where USDC is borrowed against cbBTC. A Vault-V2 is created that lends only to that market, and its shares are listed on the Midnight market alongside cbBTC. Users can then borrow USDC at a fixed rate against either cbBTC or the variable-rate USDC vault shares.

* **Vault shares:** Vault-V2 shares are listed as collateral on the Midnight market. Because the shares are denominated in the same asset as the borrow currency (e.g., USDC vault shares used to borrow USDC at a fixed rate), they are listed at 98% LLTV. The 2% buffer accounts for rare bad-debt events in the variable-rate market.
* **Matching parameters:** The vault's underlying Morpho Blue market shares the Midnight market's loan token, main collateral token, LLTV, and oracle, so the share value tracks the same risk parameters the fixed-rate market is priced against.
Use cases[](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#use-cases "Direct link to Use cases")
----------------------------------------------------------------------------------------------------------------------------
### Expressing a view on rates[](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#expressing-a-view-on-rates "Direct link to Expressing a view on rates")
This market structure allows more advanced users to borrow at a fixed rate and use the proceeds to lend at the variable rate. The position's net rate is the spread between the realized variable rate over the term and the fixed borrow rate, so users can take a position on whether the variable rate will trade above the fixed rate.
### Improving liquidity for borrower exits[](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#improving-liquidity-for-borrower-exits "Direct link to Improving liquidity for borrower exits")
A borrower can post a resting sell (borrow) offer at a target rate (e.g., 3%). The offer is paired with a supply callback (described below) that wraps the proceeds into vault-share collateral when filled, so the offer rests without parking idle capital.
Resting borrow offers of this kind add sell-side depth on the Midnight market. Other borrowers exiting early — who must take buy (lend) offers to close their debt — can match against these offers instead of repaying at par (paying back the full face value of the debt), reducing the mark-to-market cost of an early exit. More advanced users can thus create liquidity for normal borrowers (e.g., borrowing USDC against cbBTC) to exit early.
Creating the position via callbacks[](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#creating-the-position-via-callbacks "Direct link to Creating the position via callbacks")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
As part of a borrow offer, the borrower can attach a callback that wraps and unwraps the position atomically. When the offer is taken, the callback is invoked: the borrowed loan tokens are deposited into the Vault-V2 and the resulting shares are supplied as collateral on the Midnight market.
Conversely, another callback can be used to exit the position — it withdraws the required shares from collateral, redeems them for loan tokens, and uses the proceeds to lend on the fixed-rate market (closing the borrow position).
For the technical implementation, see the technical documentation on the [supply vault shares](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares)
and [withdraw vault shares](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares)
callbacks.
* [Example](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#example)
* [Use cases](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#use-cases)
* [Expressing a view on rates](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#expressing-a-view-on-rates)
* [Improving liquidity for borrower exits](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#improving-liquidity-for-borrower-exits)
* [Creating the position via callbacks](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral#creating-the-position-via-callbacks)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/tenor-prime/monitoring#__docusaurus_skipToContent_fallback)
On this page
Tenor Prime accounts get programmatic access to the same data and actions that power the dashboard, through the [Tenor GraphQL API](https://docs.tenor.finance/technical-docs/api)
for queries and the [Tenor SDK](https://docs.tenor.finance/technical-docs/sdk)
for transactions.
Monitoring[](https://docs.tenor.finance/get-started/tenor-prime/monitoring#monitoring "Direct link to Monitoring")
--------------------------------------------------------------------------------------------------------------------
The GraphQL API aggregates onchain data from the Tenor indexer into a single query interface, so you can fetch positions, the current yield curve across maturities, and historical PnL.
You can monitor:
* **Positions:** active borrow and lend positions across every wallet and vault in the organization, including size, collateral, maturity, and renewal status.
* **PnL:** realized and unrealized profit and loss across positions.
* **Transactions:** historical transaction activity.
* **Activity:** actions taken by each role within the organization.
* **Markets:** supported markets and available liquidity.
* **Rates:** current and historical rates across maturities.
* **Offers:** available offers in the RFQ, with the current validity state of each one covering authorization, funding, and approval.
Requesting access
API access is whitelisted to partners. Reach out to [contact@tenor.finance](mailto:contact@tenor.finance)
to request access.
SDK[](https://docs.tenor.finance/get-started/tenor-prime/monitoring#sdk "Direct link to SDK")
-----------------------------------------------------------------------------------------------
The Tenor SDK is a TypeScript library for building and executing transactions on Tenor. It handles transaction bundling, token approvals, signature management, and wallet compatibility across EOA, Safe, and smart-account wallets.
The SDK covers:
* **Borrow and lend:** take offers at the market rate or post limit offers.
* **Repay and withdraw:** close positions and reclaim funds.
* **Manage collateral:** add or remove collateral on existing positions.
* **Configure auto-renewal:** enable fixed-to-fixed or fixed-to-variable rolling.
See the [SDK reference](https://docs.tenor.finance/technical-docs/sdk)
for hooks, the ActionsBuilder, and the lower-level bundler.
* [Monitoring](https://docs.tenor.finance/get-started/tenor-prime/monitoring#monitoring)
* [SDK](https://docs.tenor.finance/get-started/tenor-prime/monitoring#sdk)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/get-started/what-is-tenor#__docusaurus_skipToContent_fallback)
On this page
Tenor is a **non-custodial** platform for borrowing and lending onchain. It builds on the [Morpho protocol](https://docs.tenor.finance/technical-docs/architecture)
(Morpho Midnight for fixed-rate, fixed-term markets, Morpho Blue for variable-rate, open-term markets, and Vaults V2) and extends it with modular smart contract features (auto-renewal, gated markets, liquidation with grace periods, etc.). Tenor also offers an interface and additional tools such as notifications.
The Tenor platform is non-custodial
Tenor is fully non-custodial: all logic executes onchain and is controlled by users from their own wallets. Collateral and lent funds remain in the [Morpho smart contracts](https://docs.tenor.finance/security/morpho-smart-contracts)
, never in Tenor. The Tenor platform provides the tools and interface for accessing this functionality; it never takes custody of user assets.
How is Tenor expanding on the Morpho protocol?[](https://docs.tenor.finance/get-started/what-is-tenor#how-is-tenor-expanding-on-the-morpho-protocol "Direct link to How is Tenor expanding on the Morpho protocol?")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The Morpho protocol supports new functionality without changes to its core: callbacks, ratifiers, oracles, and gates to build features on top. Tenor builds on these extension points to deliver a platform and interface for fixed-rate, fixed-term borrowing and lending, along with advanced functionality:
* **[Auto-renewal](https://docs.tenor.finance/get-started/borrow/auto-renewal)
** of positions at maturity, including programmatic borrower debt repayment using an onchain auction system and collateral migration
* **Composability** between fixed-rate markets and [variable-rate markets](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral)
* **Advanced execution**, including [ladders](https://docs.tenor.finance/get-started/lend/offer-types#lend-ladder-offers)
, multi-market offers, [earn-while-you-wait](https://docs.tenor.finance/get-started/lend/offer-types#earn-while-you-wait)
lend offers, and deposit-collateral-only-on-fill borrow offers
* **Programmatic strategy tools** such as [market making](https://docs.tenor.finance/technical-docs/contracts/market-making-policy)
* **Creation of custom markets**, including [gated markets](https://docs.tenor.finance/get-started/otc/gated-markets)
and markets with [liquidation grace periods](https://docs.tenor.finance/get-started/otc/delayed-liquidations)
* **Offchain tools** that improve UX for institutional asset managers, including the creation of gated [Vault V2](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist)
as a role-based access control system to manage onchain actions, notifications, [SDK](https://docs.tenor.finance/technical-docs/sdk)
, and [API](https://docs.tenor.finance/technical-docs/api)
Security[](https://docs.tenor.finance/get-started/what-is-tenor#security "Direct link to Security")
-----------------------------------------------------------------------------------------------------
Users interact directly with the [Morpho](https://docs.tenor.finance/security/morpho-smart-contracts)
and [Tenor](https://docs.tenor.finance/security/tenor-smart-contracts)
smart contracts from their self-custodial wallets. Both sets of contracts are immutable: the Morpho contracts hold collateral, enable liquidations, and keep the accounting between lenders and borrowers, while the Tenor contracts add opt-in gates, callbacks, and ratifiers on top of Morpho Midnight.
Tenor uses a multidisciplinary approach to security:
* **External security reviews** by independent researchers across multiple engagements. See [Security Reviews](https://docs.tenor.finance/security/security-reviews)
.
* **Formal verification** of key properties of the Tenor and Morpho contracts using the Certora Prover.
* **Stateless and stateful fuzzing** of the Tenor contracts to validate that key invariants hold across a wide range of states.
* **Bug bounties** of up to $200,000 on the Tenor contracts and up to $2,500,000 on the Morpho contracts for responsible disclosures. See [Bug Bounty](https://docs.tenor.finance/security/bug-bounty)
.
* **Continuous monitoring** of onchain activity through [Blockaid](https://www.blockaid.io/)
, with the ability to pause select functions in response to abnormal behavior. See [Pausable Functions](https://docs.tenor.finance/security/pausable-functions)
.
See the [Security](https://docs.tenor.finance/security/)
section for the full overview.
* [How is Tenor expanding on the Morpho protocol?](https://docs.tenor.finance/get-started/what-is-tenor#how-is-tenor-expanding-on-the-morpho-protocol)
* [Security](https://docs.tenor.finance/get-started/what-is-tenor#security)
---
# Unknown
Te n o r S e c u r i t y R e v i e w Auditors 0xjuaan 0xSpearmint April 16, 2026 RepoFiles in scope tenor-morpho-v2- contracts-2 Commit hash: 1f75249414b61828c561e 61f927fc0b365e41da8 src/\*\*/\*.sol (excluding src/external, src/periphery/clamps, src/adapter/MidnightAdapterBase.sol) Introduction Obsidian Audits Obsidian audits is a team of top-ranked security researchers, with a publicly proven track record, specialising in DeFi protocols across EVM chains and Solana. The team has achieved 10+ top-2 placements in audit competitions, placing 1st in competitions for Wormhole, Pump.fun, Yearn Finance, and many more. Find out more: obsidianaudits.com Audit Overview Te n o r o f fe r s a p owe r f u l i n t e r fa c e fo r b o r rowe r s a n d l e n d e r s t o a c c e s s M o r p h o Midnight's fixed-rate, fixed-term markets. With Tenor, users can opt in to features like auto-renewal at maturity, conditional execution of orders, offers with bespoke terms, and more. Te n o r e n g a g e d O b s i d i a n Au d i t s t o p e r fo r m a s e c u r i t y re v i e w o f t h e s m a r t contracts in the tenor-morpho-v2-contracts-2 repo. The review was conducted from April 7, 2026 to April 16, 2026. Scope Files in scope IDTitleSeverityStatus H-01 Incorrect effective price formulas undercharge protocol fees HighFixed M-01 \`\_validatePrice\` does not account for Morpho V2's continous fee MediumFixed M-02 \`\_capTakeUnits\` does not account for the fee charged in Tenor callbacks MediumFixed M-03 A borrower can enter a liquidation grace period even when their position is healthy MediumFixed M-04 Caller controlled \`onBehalf\` parameter in \`midnightSupplyCollateral\` enables collateral bitmap griefing MediumFixed M-05 Lenders can be renewed into obligations which will realize bad debt MediumAcknowledged Summary of Findings Severity Breakdown A total of 10 issues were identified and categorized based on severity: 1 High severity 5 Medium severity 2 Low severity 2 Informational Findings Overview L-01 VaultV2 rounding mismatch leaves 1 wei of loan token stuck in callback LowFixed L-02 Borrower migration to lower-LTV market can leave position near liquidation LowFixed I-01 Priority liquidator inaction during grace period can lead to bad debt InformationalFixed I-02 Inconsistent zero-amount fee check allows transfer revert InformationalFixed Severity Impact: High Impact: Medium Impact: Low Likelihood: HighCriticalHighMedium Likelihood: Medium HighMediumLow Likelihood: LowMediumLowLow Severity Classification Impact High - leads to a significant material loss of assets in the protocol or significantly harms a group of users. Medium - leads to a moderate material loss of assets in the protocol or moderately harms a group of users. Alternatively, breaking a core aspect of the protocol's intended functionality Low - leads to a minor material loss of assets in the protocol or harms a small group of users. Likelihood High - attack path is possible with reasonable assumptions that mimic on-chain conditions, and the cost of the attack is relatively low compared to the amount of funds that can be stolen or lost. Medium - the attack/vulnerability requires minimal or no preconditions, but there is limited or no incentive to exploit it in practice Low - requires highly unlikely precondition states, or requires a significant attacker capital with little or no incentive. Findings \[H-01\] Incorrect effective price formulas undercharge protocol fees Description The Tenor protocol charges a fee as a percentage of the interest on position renewals. Interest per unit is (WAD - price), and the fee per unit is feeRate \* (WAD - price) / WAD, denoted x in the code. The total fee should therefore be: fee = obligationUnits \* feePerUnit / WAD fee = obligationUnits \* feeRate \* (WAD - price) / WAD / WAD Instead of computing the fee directly, CallbackLib derives it through a "buyer effective price", the all-in price per unit the buyer pays including fees. The fee is then extracted as a residual: fee = obligationUnits \* (buyerEffectivePrice / WAD) - buyerAssets Since the buyer pays price per unit for the bond plus x per unit in fees, the correct effective price is price + x. The current implementation instead computes price \* (WAD + 2x) / (WAD + x), which is not algebraically equivalent and produces a smaller fee for any price. The same issue exists in sellerEffectivePrice, which computes price \* WAD / (WAD + x) instead of the correct price - x. Proof of Concept Add the following logging to test\_onBuy\_happyPath\_withFee in test/unit/MorphoV2ToV2LendWithdrawable.t.sol: Console output: With a feeRate of 50%, the fee should be exactly half of the total interest. The fee received (243310213058145716) is ~1.46% less than the expected value (246914827008139605). The discrepancy grows significantly for lower prices. Recommendation Change effPrice in buyerEffectivePrice from price.mulDivUp(WAD + 2 \* x, WAD + x) to price + x, and effPrice in sellerEffectivePrice from price.mulDivDown(WAD, WAD + x) to price - x. Since x is the fee per unit, the all-in price is simply the base price plus (or minus for the seller) the fee component: // 2. Fee paid to recipient (exact amount) uint256 feeReceived = loanToken.balanceOf(feeRecipient) - feeRecipientBefore; + uint256 totalInterest = result.obligationUnits - result.buyerAssets; + console.log("obligationUnits:", result.obligationUnits); + console.log("buyerAssets: ", result.buyerAssets); + console.log("total interest: ", totalInterest); + console.log("interest / 2: ", totalInterest / 2); + console.log("fee received: ", feeReceived); assertEq(feeReceived, expectedFee, "Fee recipient should receive exact fee"); Ran 1 test for test/unit/MorphoV2ToV2LendWithdrawable.t.sol:MorphoV2ToV2Len dWithdrawableTest \[PASS\] test\_onBuy\_happyPath\_withFee() (gas: 382267) Logs: obligationUnits: 50493829654016279211 buyerAssets: 50000000000000000000 total interest: 493829654016279211 interest / 2: 246914827008139605 fee received: 243310213058145716 Additionally, consider updating buyerFeeFromTick and sellerFeeFromTick to directly calculate: fee = obligationUnits \* feeRate \* (WAD - price) / WAD / WAD fee = obligationUnits \* x / WAD , rather than deriving the fee through the effective price residual. function buyerEffectivePrice(uint256 price, uint256 feeRate) internal pure returns (uint256 effPrice) { if (feeRate == 0) return price; uint256 x = \_interestFeeComponent(price, feeRate); - effPrice = price.mulDivUp(WAD + 2 \* x, WAD + x); + effPrice = price + x; } function sellerEffectivePrice(uint256 price, uint256 feeRate) internal pure returns (uint256 effPrice) { if (feeRate == 0) return price; uint256 x = \_interestFeeComponent(price, feeRate); - effPrice = price.mulDivDown(WAD, WAD + x); + effPrice = price - x; } function sellerFeeFromTick(uint256 tick, uint256 feeRate, uint256 units, uint256 assets) internal pure returns (uint256) { if (feeRate == 0) return 0; uint256 price = TickLib.tickToPrice(tick); - uint256 effPrice = sellerEffectivePrice(price, feeRate); - return sellerFee(units, assets, effPrice); + uint256 x = \_interestFeeComponent(price, feeRate); + return units.mulDivUp(x, WAD); } function buyerFeeFromTick(uint256 tick, uint256 feeRate, uint256 units, uint256 assets) internal Remediation: Fixed in commit ef231eb Note: the original finding was referring to the fact that the feeRate was not treated as the portion of interest paid (since it's treated as a portion of the APY instead), while the comments made it seem otherwise. Based on this message, we see that it's intended for the feeRate to represent the percentage diff in interest rate, rather than percentage diff in absolute interest. Though we do confirm that the buyerEffectivePrice() calculation was incorrect for a different reason, and has been fixed here. pure returns (uint256) { if (feeRate == 0) return 0; uint256 price = TickLib.tickToPrice(tick); - uint256 effPrice = buyerEffectivePrice(price, feeRate); - return buyerFee(units, assets, effPrice); + uint256 x = \_interestFeeComponent(price, feeRate); + return units.mulDivUp(x, WAD); } \[M-01\] \`\_validatePrice\` does not account for Morpho V2's continous fee Description RenewalOrchestrator.\_validatePrice enforces the user's limitRatePerSecond, their minimum acceptable yield, but does not account for Midnight's per-obligation continuousFee. A lender can accept a renewal offer that satisfies Tenor's gross-rate check, but yields less than their stated minimum once the continuous fee accrues. Midnight stamps a fresh pendingFee on a lender's position every time they gain credit via take(): The lender's true face value at maturity is credit - pendingFee. The continuousFee is per-obligation, capped at MAX\_CONTINUOUS\_FEE ≈ 1%, and can be adjusted at any time by the feeSetter. Te n o r ' s r a t e va l i d a t i o n o n l y c h e c ks t h e g ro s s b o n d y i e l d a g a i n s t t h e u s e r ' s limit. The fee parameter is Tenor's own callback fee, not Midnight's continuous fee. Example scenario: A lender sets limitRatePerSecond = 5% APR on a takeLendV2ToV2Withdrawable callback. The target obligation has continuousFee = MAX\_CONTINUOUS\_FEE = 1% APR. A keeper presents an offer at exactly 5% APR gross. Tenor's \_validatePrice passes. The lender accepts. At target maturity, the lender's net yield is ~4% APR — below their configured 5% minimum. Recommendation uint128 buyerPendingFeeIncrease = UtilsLib.toUint128(buyerCreditIncrease.mulDivDown( \_obligationState.continuousFee \* timeToMaturity, WAD)); buyerPos.pendingFee += buyerPendingFeeIncrease; In takeLendV2ToV2Withdrawable and takeLendV1ToV2, including the obligation's continuousFee during validation and decrement obligationUnits by the expected continuous-fee accrual. This aligns obligationUnits with what the lender actually receives at maturity (face value = credit - pendingFee). Remediation: Fixed in PR #362 uint32 targetContinuousFee = MORPHO\_MIDNIGHT.continuousFee(targetObligationId); if (targetContinuousFee > 0) { uint256 fullTimeToTargetMaturity = targetMaturity - block.timestamp; uint256 continuousFeeAmount = obligationUnits.mulDivUp( uint256(targetContinuousFee) \* fullTimeToTargetMaturity, WAD ); obligationUnits -= continuousFeeAmount; } \_validatePrice( isBuy, obligationUnits, buyerAssets, fee, renewalParams.limitRatePerSecond, policyRate, secondsToMaturity ); fillIndex Fee effect on tracked total Result FILL\_BUYER\_ASSETS buyerAssets += fee Overshoot → FillOvershoot revert FILL\_SELLER\_ASSETS sellerAssets - = fee Silent underfill (batch completes below maxFill) FILL\_OBLIGATION\_UNITSno fee on unitsno issue \[M-02\] \`\_capTakeUnits\` does not account for the fee charged in Tenor callbacks Description In the TakeRouter, the \_capTakeUnits function is used to limit the obligation units of a take() on Midnight. The takeUnits is capped based on the remaining assets that can be filled (based on maxFill), but \_remainingToUnits does not account for the Te n o r fe e . T h e o rc h e s t r a t o r / outputResolver then adjusts the returned amounts by the fee after the cap, so the tracked totals\[fillIndex\] drifts away from the raw Midnight amount that was sized for. Impact The direction of the fee adjustment depends on the fillIndex: There are two distinct impacts: 1. DoS on multi-action batches when the fee inflates the taker's tracked side, the last action pushes totals\[fillIndex\] past maxFill and the if (remaining < type(uint256).max / WAD) { takeUnits = UtilsLib.min( takeUnits, \_remainingToUnits(taker, action.offer, fillIndex, remaining, obligationId) ); } batch reverts with FillOvershoot 2. Silent underfill when the fee deflates the tracked side — the batch fills strictly less than the caller requested. Both \_dispatchOrchestrator and \_dispatchMidnightTake (via the outputResolver) are affected, since the cap happens before the fee adjustment in both paths. Recommendation Deflate (or inflate) remaining by the Tenor fee before passing it to \_remainingToUnits when the fill dimension an asset rather than obligation units. Remediation: Fixed in PR #379 \[M-03\] A borrower can enter a liquidation grace period even when their position is healthy Description DelayedLiquidationGate.startGracePeriod() requires that the borrower's position is healthy, using the following check: The issue is that a borrower can make their position temporarily unhealthy (by calling take() to borrow loan tokens without supplying enough collateral). During this transient unhealthy state, they can call DelayedLiquidationGate.startGracePeriod() during the onSell callback (which will succeed since the position is temporarily unhealthy), before repaying the under-collateralised loan, to ensure the health check at the end of take() succeeds. This breaks the invariant GATE-3, which states that only unhealthy positions can enter a grace period. Recommendation One (restrictive) solution would be to enforce that the caller of startGracePeriod() is an EOA (without EIP-7702 delegations): Remediation: Fixed in PR #372 if (MIDNIGHT.isHealthy(obligation, obligationId, borrower)) { revert PositionIsHealthy(); } require(tx.origin == msg.sender && msg.sender.code.length == 0 \[M-04\] Caller controlled \`onBehalf\` parameter in \`midnightSupplyCollateral\` enables collateral bitmap griefing Description Morpho Midnight limits each borrower to 10 active collateral types per obligation (MAX\_COLLATERALS\_PER\_BORROWER). To prevent griefing, supplyCollateral requires authorization: require(onBehalf == msg.sender || isAuthorized\[onBehalf\]\[msg.sender\]). This stops anyone from activating unwanted bitmap slots on a victim's position. MidnightAdapterBase.midnightSupplyCollateral accepts onBehalf as a parameter rather than pinning it to initiator(). Every user who wants to use the bundler for withdrawals or collateral operations must authorize the Te n o rAd a p t e r o n M i d n i g h t . As a result an attacker can exploit this by calling Bundler3.multicall with a bundle that transfers 1 wei of a collateral token to the adapter and then calls midnightSupplyCollateral(obligation, collateralIndex, 1, victim). Midnight sees msg.sender = TenorAdapter, which the victim authorized, and accepts the supply. Repeating across collateral indices fills all 10 bitmap slots with dust, blocking the victim from adding the collateral they actually need. This is a griefing attack with no direct fund loss. The victim can withdraw the dust collateral to free collateral slots. Recommendation Pin onBehalf to initiator() in midnightSupplyCollateral, matching how midnightWithdrawCollateral and midnightWithdraw already handle it. Remove the onBehalf function parameter. Remediation: Fixed in PR #382 function midnightSupplyCollateral( Obligation calldata obligation, uint256 collateralIndex, uint256 assets, - address onBehalf ) external onlyBundler3 { - require(onBehalf != address(0), ErrorsLib.ZeroAddress()); - require(onBehalf != address(this), ErrorsLib.AdapterAddress()); + address onBehalf = initiator(); \[M-05\] Lenders can be renewed into obligations which will realize bad debt Description Any lender who has configured RenewalParams with a targetMarketId whose obligation includes a particular collateral token (e.g. USR) can be forced into that market by an attacker after the collateral token loses its value. The attacker exploits the auto-renewal mechanism to move the lender's funds into an obligation backed by worthless collateral, causing the lender to lose their principal. Using an OracleWithValidationCheck (which can be used to revert when oracle and DEX prices diverge) does not fully mitigate this, because the oracle price is only queried when debt > 0. A lender-to-lender renewal (takeLendV2ToV2Withdrawable) where the counterparty (seller) already holds credit bypasses the oracle check entirely. In a lender-to-lender transfer, the counterparty's \_position.credit decreases but their \_position.debt does not increase, so the price query in isHealthy is skipped and the OracleWithValidationCheck never gets a chance to revert. Example scenario: 1. An obligation exists with loanToken = USDC and collateral = USR. A lender authorizes renewals into this targetMarketId with takeGate = address(0). 2. USR depegs, and its collateral is now worthless. 3. An attacker who already has lending credit on the USR-collateralised obligation signs a sell offer to offload that credit. 4. The attacker calls takeLendV2ToV2Withdrawable. The lender's USDC is withdrawn from their source obligation and sent to the attacker, while the lender receives credit backed by worthless USR collateral. Recommendation Consider creating a TakeGate implementation that validates the collateral quality of the target obligation at renewal time. The gate should: 1. Compare oracle price to a DEX/spot price for each collateral token in the target obligation (by querying the oracle price) 2. Verify the target obligation's oracle is a trusted OracleWithValidationCheck, rejecting renewals into obligations using oracles that lack depeg protection Using such a TakeGate in the user's renewal params will prevent this issue. Remediation: Issue acknowledged \[L-01\] VaultV2 rounding mismatch leaves 1 wei of loan token stuck in callback Description The (CB-DUST-1) property states: "After callback completes, the callback contract holds no token balances." However MorphoV2WithdrawVaultSharesCallback violates this when used with a VaultV2 vault. The callback computes shares via sharesToWithdraw = previewWithdraw(buyerAssets), then calls redeem(sharesToWithdraw). VaultV2's previewWithdraw uses: and redeem internally calls previewRedeem which computes assets returned via: previewWithdraw rounds up to guarantee the shares redeem to at least buyerAssets. This means sharesToWithdraw can be a share count that actually redeems for more than buyerAssets. The callback only approves exactly buyerAssets for Midnight to pull, so the excess (1 wei) remains stuck in the callback. Recommendation Replace redeem(sharesToWithdraw) with withdraw(buyerAssets) so the vault transfers exactly buyerAssets to the callback and no dust remains. Remediation: Fixed in PR #383 assets.mulDivUp(newTotalSupply + virtualShares, newTotalAssets + 1) shares.mulDivDown(newTotalAssets + 1, newTotalSupply + virtualShares) \[L-02\] Borrower migration to lower-LTV market can leave position near liquidation Description When a borrower renewal executes across any migration path (V1-to-V2, V2- to-V1, V2-to-V2), collateral is migrated pro-rata from the source to the target. If the target market has a lower LLTV than the source, the borrower's position can land just above the liquidation threshold on the target, making them liquidatable shortly after from minor price movement or interest accrual. The borrower called setUserRenewalParams for this source/target pair, so they are aware of the target market's LLTV. However, a borrower approving a renewal offer likely does not intend for the migration itself to place them at the edge of liquidation. Recommendation Document this risk clearly for borrowers setting up renewal offers across markets with different LLTVs. The borrower can configure a takeGate in their UserRenewalParams, but no health-factor gate exists today. Consider implementing one that reverts the migration if the resulting target position LTV exceeds a borrower-specified threshold. Remediation: Fixed in PR #384 \[I-01\] Priority liquidator inaction during grace period can lead to bad debt Description The DelayedLiquidationGate gives a priority liquidator exclusive access for PRIORITY\_PERIOD after the grace period ends. During this window, no other liquidator can act. If the priority liquidator fails to liquidate (offline, or malicious), the position remains unliquidated for the combined duration of GRACE\_PERIOD + PRIORITY\_PERIOD before other liquidators can step in. With volatile collateral, this delay can be enough for the collateral value to drop below the outstanding debt, resulting in bad debt. Recommendation Consider allowing any liquidator to act immediately after the grace period ends, removing exclusive priority access. If priority exclusivity is necessary, consider clearly documenting the bad debt risk and conveying it to lenders. Remediation: Fixed in PR #378 \[I-02\] Inconsistent zero-amount fee check allows transfer revert Description MorphoV2ToV1LendCallback.onSell calculates a flat percentage fee on sellerAssets using CallbackLib.percentageFee, which rounds down via mulDivDown. The transfer is guarded by if (callbackData.feeRate > 0), which checks whether a fee rate is configured, not whether the computed fee is non-zero. If sellerAssets \* feeRate < 1e18, the fee rounds to zero and safeTransfer is called with amount zero. Some ERC20 tokens (e.g. BNB) revert on zero-amount transfers, preventing the lender's V2-to-V1 migration from occuring. All five other callbacks in the codebase guard the transfer with if (fee > 0), checking the computed value. Recommendation Compute fee unconditionally and wrap the safeTransfer in onSell with if (fee > 0), matching the pattern used in the other callbacks. Remediation: Fixed in commit 05c4e4b uint256 fee; if (callbackData.feeRate > 0) { fee = CallbackLib.percentageFee(sellerAssets, callbackData.feeRate); - SafeTransferLib.safeTransfer(obligation.loanToken, callbackData.feeRecipient, fee); + } + if (fee > 0) { + SafeTransferLib.safeTransfer(obligation.loanToken, callbackData.feeRecipient, fee); }
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/monitoring#__docusaurus_skipToContent_fallback)
Tenor uses [Blockaid](https://www.blockaid.io/)
to run continuous monitors on smart contract state and onchain activity to surface abnormal behavior.
If abnormal behavior is detected, Tenor can respond through established incident response procedures. Depending on the situation, this may include pausing the [Pausable Interest Rate Policy](https://docs.tenor.finance/security/pausable-functions#pausable-interest-rate-policy)
while the situation is investigated.
---
# Tenor
[Skip to main content](https://docs.tenor.finance/security/tenor-smart-contracts#__docusaurus_skipToContent_fallback)
On this page
The Tenor smart contracts extend Morpho Midnight with additional capabilities, grouped into callbacks, ratifiers, and gates. See the [developer documentation](https://docs.tenor.finance/technical-docs/architecture)
for a full breakdown.
The Tenor smart contracts are open source under a Business Source License (BSL).
The contracts have undergone [security reviews](https://docs.tenor.finance/security/security-reviews)
from leading security firms and independent researchers, and are covered by an active bug bounty offering up to $200,000 for critical findings. See [Bug Bounty](https://docs.tenor.finance/security/bug-bounty)
for details.
Design Principles[](https://docs.tenor.finance/security/tenor-smart-contracts#design-principles "Direct link to Design Principles")
-------------------------------------------------------------------------------------------------------------------------------------
Tenor follows the same design philosophy as Morpho: keep the core minimal and unopinionated, and push opinionated logic — rate policies, renewal rules, gating, callbacks — into periphery contracts that users opt into. The core Tenor contracts are immutable and cannot be upgraded once deployed, which keeps the trusted surface small and removes governance risk from the protocol itself. Periphery contracts can be pausable, where pause functions apply only to the users who opted into them. See [Pausable Functions](https://docs.tenor.finance/security/pausable-functions)
for the full list.
The same opt-in model bounds the impact of any periphery contract. Gates apply only to the markets and vaults that set them, and callbacks and ratifiers act only on the positions that invoke them. Behavior added by one contract cannot affect markets or positions that don't use it.
* [Design Principles](https://docs.tenor.finance/security/tenor-smart-contracts#design-principles)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/api#__docusaurus_skipToContent_fallback)
On this page
The Tenor GraphQL API exposes markets, positions, offers, and portfolio analytics in a single flexible query interface, so a client can fetch related data across markets and maturities in one request. It aggregates onchain data from the Tenor indexer, so integrators do not need to run a separate indexer.
Requesting access
API access is whitelisted to partners. Reach out to [contact@tenor.finance](mailto:contact@tenor.finance)
to request access.
What You Can Query[](https://docs.tenor.finance/technical-docs/api#what-you-can-query "Direct link to What You Can Query")
----------------------------------------------------------------------------------------------------------------------------
| Category | Description | Example Queries |
| --- | --- | --- |
| **Markets** | Browse available markets with rates, TVL, and supported tokens | `morphoMarkets`, `morphoMarketById` |
| **Obligations** | Query fixed-rate terms with specific maturities and their current state | `morphoObligations`, `morphoObligationByKey` |
| **Positions** | Fetch borrow and lend positions for any wallet, including balances, rates, and renewal status | `obligationPositions`, `obligationPosition` |
| **Offers** | View active offers, filterable by rate, maturity, and direction | `offersList` |
| **Yield Curve** | Retrieve yield curves across maturities | `yieldCurve` |
| **Portfolio** | Get account summaries and historical portfolio performance | `accountSummary`, `historicalSummaryForAccount` |
| **Market Analytics** | Platform-wide TVL, volume, and historical trends | `marketSummary`, `historicalSummaryForAllMarkets` |
| **Tokens** | Search and filter tokens by symbol, type, or whitelist status | `tokens`, `loanTokens`, `collateralTokens` |
| **Transactions** | Query transaction history for any wallet | `transactions` |
* [What You Can Query](https://docs.tenor.finance/technical-docs/api#what-you-can-query)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#__docusaurus_skipToContent_fallback)
On this page
`BorrowMidnightRenewalCallback` migrates a borrow position from a source Morpho Midnight market (for example, one maturing in 2 days) to a target Morpho Midnight market with a later maturity (for example, 30 days), in a single transaction.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#description "Direct link to Description")
-----------------------------------------------------------------------------------------------------------------------------------------------
Both markets sit on Morpho Midnight. The borrower posts a sell (borrow) offer in the target market (the one maturing in 30 days in the example above) with the callback set as the proceeds receiver. When that sell (borrow) offer is filled, the callback uses the loan-token proceeds to repay the source debt (the position maturing in 2 days) and moves shared collateral from the source market to the target market.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Before any take, the borrower posts a renewal sell (borrow) offer on the **target** market with `receiverIfMakerIsSeller` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's renewal sell (borrow) offer on the target market by calling `MORPHO_MIDNIGHT.take()`. The loan-token proceeds (`sellerAssets`) land on the callback because the offer's `receiverIfMakerIsSeller` points at it, and Morpho Midnight then invokes `onSell` on the callback.
2. **Validation:** The callback checks that source and target markets share the same `loanToken`. Collaterals listed in both markets are auto-detected and will migrate; collaterals listed only on the source are skipped and stay on the source.
3. **Fee:** A fee is derived from the offer tick (an effective-price fee applied to the interest portion only). When the callback is routed through the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent)
, the ratifier sets the fee rate. Used directly, the offer creator encodes the fee themselves.
4. **Repay source debt:** The callback repays the source obligation with `sellerAssets - fee`. It reads the borrower's current source debt first and reverts with `ExcessRepayment` if the encoded units would overpay. It also reverts with `ZeroAmount` if the source debt is already zero.
5. **Move collateral:** For each shared collateral token, the callback withdraws pro-rata from the source market (scaled by `repaidUnits / sourceDebtBefore`) and supplies it on the target market. On the final fill (when the repay closes the source debt entirely) it moves the full remaining balance to avoid leaving dust.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#prerequisites "Direct link to Prerequisites")
-----------------------------------------------------------------------------------------------------------------------------------------------------
Before the take can succeed, the borrower must:
* Authorize the callback. A single authorization covers both source and target markets, since they live on the same Midnight instance.
* Post a sell (borrow) offer on the target market with `receiverIfMakerIsSeller` set to this callback's address.
Source-only collaterals stay behind
Collaterals listed on the source but not on the target will not be migrated. After the renewal, the borrower can call `withdrawCollateral` on the source market to recover collateral assets in the source market that were not accepted in the target market.
Routing[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#routing "Direct link to Routing")
-----------------------------------------------------------------------------------------------------------------------------------
The natural taker of a borrower's target sell (borrow) renewal offer is a **lender**, since taking that offer means providing loan tokens at the offered rate. A **keeper** can also match the offer against an existing target lend offer in the RFQ if that results in a positive spread (MEV profit).
When posting a renewal offer, the borrower prices it as the implied combined rate of closing the source position at par early plus borrowing in the target market, not as a standalone target borrow rate. The `X%` on the target sell (borrow) offer reflects that combined effective rate.
### Taker net rate (lender)[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#taker-net-rate-lender "Direct link to Taker net rate (lender)")
When a lender takes the borrower's target sell (borrow) renewal offer, they lend at the offered rate `X%`. If the lender already holds a position in the source market, or can enter a source-side lend position at a rate above par (0%) in the same transaction, they can capture an extra source-side return: the callback repays the borrower's source debt at par, letting the lender exit the source position atomically and pocket the source-side rate on top.
**Example.** Borrower's target sell (borrow) offer at `X% = 6%`. A lender takes the offer and lends at `6%` on the target. In the same transaction, the lender also exits a source-side lend position at `5%`, atomically unlocked when the callback repays the source debt at par. The lender's net effective rate is `6%` on the target plus the source-side profit on top.
### Keeper[](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#keeper "Direct link to Keeper")
A third-party keeper doesn't want to hold lend exposure; they match existing offers when a positive net spread exists.
1. Read the borrower's target sell (borrow) offer at `X%`.
2. Find a target buy (lend) offer at `Y%` and compute the target spread `X − Y`.
3. Optionally take a source-side sell (borrow) offer at a positive rate `S%` in the same transaction: the keeper enters a source-side lend position, and when the callback repays the borrower's source debt at par, that position settles instantly at its full term value, locking in `S%` of source-side interest for the keeper.
4. Match the target offers via `MORPHO_MIDNIGHT.take()` whenever the target spread plus any source-side profit covers gas.
Even when the target leg alone is unprofitable (`X − Y ≤ 0` or below gas), a positive source-side leg can cover the shortfall and make the trade profitable overall.
In the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent)
context, the keeper can take on behalf of the borrower against a buy (lend) offer in the RFQ, executing as soon as the combined spread exceeds gas.
**Example.** Borrower's target sell (borrow) offer at `X% = 6%` and a matching target buy (lend) offer at `Y% = 2%`. The `4%` gross spread is the keeper's profit headroom. The keeper matches both offers atomically: the borrower rolls into the `6%` target borrow, the lender lends at `2%`, and the keeper pockets whatever remains of the `4%` spread after gas. If the target legs alone don't clear gas, the keeper can also take a source-side sell (borrow) offer at e.g. `S% = 3%`; the callback repays the source debt at par, settling the keeper's source position at full term value and adding `3%` of interest to the bottom line.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#prerequisites)
* [Routing](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#routing)
* [Taker net rate (lender)](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#taker-net-rate-lender)
* [Keeper](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal#keeper)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#__docusaurus_skipToContent_fallback)
On this page
`BorrowBlueToMidnightCallback` migrates a borrower from a variable-rate position on Morpho Blue into a fixed-rate position on Morpho Midnight in a single take.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#description "Direct link to Description")
----------------------------------------------------------------------------------------------------------------------------------------
The borrower has a variable-rate position on Blue and posts a sell (borrow) offer on Midnight with this callback as the receiver. When the sell (borrow) offer is taken, the Midnight proceeds (`sellerAssets`) land on the callback. The callback uses them to repay the Blue debt, withdraws the Blue collateral, and supplies it to Midnight on the borrower's behalf, so the borrower exits the take with a fixed-rate borrow position on Midnight.
Blue has no maturity, so the renewal window is anchored to the user's `renewalCadence` (for example, [FourWeekCadence](https://docs.tenor.finance/technical-docs/contracts/callbacks/four-week-cadence)
) rather than a source maturity timestamp.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts a sell (borrow) offer on the **Midnight target** market with `receiverIfMakerIsSeller` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's sell (borrow) offer on the Midnight target market by calling `MORPHO_MIDNIGHT.take()`. The Midnight proceeds (`sellerAssets`) land on the callback because the offer's `receiverIfMakerIsSeller` points at it, and Morpho Midnight invokes `onSell` on the callback.
2. **Validate:** The Blue source market and the Midnight target market must share the same `loanToken`. The Blue market's single collateral token must appear somewhere in the Midnight market's collateral set; the matching slot is resolved automatically by the callback.
3. **Calculate and transfer fees:** An effective-price fee is derived from the offer's tick and applied to the interest portion only, then transferred to the protocol fee recipient.
4. **Repay Blue:** The callback repays the borrower's variable-rate debt with `sellerAssets - fee`. On the final fill the position is closed in shares (exactly zero); on partial fills the debt is repaid pro-rata in assets.
5. **Withdraw collateral:** The matching amount of collateral is withdrawn from Blue (pro-rata on partial fills, in full on the final fill).
6. **Supply collateral:** The withdrawn collateral is supplied to the matching slot on the Midnight market on the borrower's behalf.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#prerequisites "Direct link to Prerequisites")
----------------------------------------------------------------------------------------------------------------------------------------------
* The borrower must authorize the callback on **Morpho Blue**. Blue's authorization surface is distinct from Midnight's, so granting Midnight authorization is not sufficient.
* The offer's `receiverIfMakerIsSeller` must point at this callback so `sellerAssets` lands here before `onSell` runs.
Single-collateral migration
Only the **Blue market's single collateral token** is migrated. If the Midnight target lists additional collateral slots, the borrower must supply those separately (for example, via [Supply Collateral](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight/supply-collateral)
). They are not pulled by this callback.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight#prerequisites)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#__docusaurus_skipToContent_fallback)
On this page
Rolls a lender's fixed-rate lend position from one Morpho Midnight market into a longer-dated Midnight fixed-rate market.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#description "Direct link to Description")
---------------------------------------------------------------------------------------------------------------------------------------------
The lender posts a buy (lend) offer on the target market with this callback set as the receiver. When the offer fills, Midnight invokes `onBuy` on the callback. The callback withdraws principal from the lender's source position and uses it to fund the new lend on the target market.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The lender posts a buy (lend) offer on the **target** market with `receiverIfMakerIsBuyer` set to this callback. The callback will withdraw the lender's funds from the source market and fund the offer in the target market. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the lender's renewal buy (lend) offer on the target market by calling `MORPHO_MIDNIGHT.take()`. Morpho Midnight invokes `onBuy` on the callback. Only Morpho Midnight may call; `buyerAssets` and `obligationUnits` must both be non-zero.
2. **Validation:** The source and target obligations must share the same `loanToken`, otherwise `TokenMismatch`. The callback also reads the source position with slashing and continuous accrual applied and reverts with `ZeroAmount` if the post-update credit is zero.
3. **Fee:** A fee is derived from the offer tick using the buyer-side effective price and applies to the interest portion of the offer only. When routed through the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent)
, the ratifier sets the fee rate; used directly, the offer creator encodes it themselves. If `fee > 0`, it is sent to the fee recipient encoded in the callback data.
4. **Withdraw principal:** The callback withdraws `buyerAssets + fee` from the source market on the lender's behalf.
5. **Transfer principal:** The withdrawn `buyerAssets` funds the new lend on the target market.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#prerequisites "Direct link to Prerequisites")
---------------------------------------------------------------------------------------------------------------------------------------------------
* The lender must authorize the callback. The callback withdraws from the source position on the lender's behalf.
* The target buy (lend) offer's receiver field must point at this callback.
* The source position must be withdrawable at execution time. See [Routing](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#routing)
for the two cases.
Source must be withdrawable at take time
The source market must already permit withdrawal at execution time. This withdrawable balance can be created just in time by the keeper rolling the lender's position forward.
Routing[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#routing "Direct link to Routing")
---------------------------------------------------------------------------------------------------------------------------------
The natural taker of a lender's target buy (lend) renewal offer is a **borrower**, since taking that offer means borrowing at the offered rate. A **keeper** can also match the offer against an existing target borrow offer in the RFQ if that results in a positive spread (MEV profit).
The callback needs the lender's source position to be withdrawable when the take fires:
* **Source already withdrawable** (matured, or a prior borrower repay at par). The take fills with no source-side action.
* **Source not yet withdrawable:** Someone (the taker or a keeper) must bundle a source-side borrow + repay in the same transaction to make the source position withdrawable.
### Taker net rate (borrower)[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#taker-net-rate-borrower "Direct link to Taker net rate (borrower)")
When a borrower takes the lender's target buy (lend) renewal offer, they borrow at the offered rate `X%`. If the lender's source position isn't already withdrawable, the borrower also bears the cost of the source-side bundle (borrow + repay) needed to unlock it. That source-side interest accrues to the lender, so it raises the borrower's effective cost above `X%`.
**Example.** Lender's target buy (lend) offer at `X% = 2%`. A borrower takes the offer and borrows at `2%` on the target. If the borrower also bundles a source-side borrow at the source market rate and repays at par to unlock the lender, the accrued source interest is added on top of their `2%` target borrow.
### Keeper[](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#keeper "Direct link to Keeper")
A third-party keeper doesn't want to hold borrow exposure; they match existing offers when a positive spread exists.
1. Read the lender's target buy (lend) offer at `X%`.
2. Find a target sell (borrow) offer at `Y%` with `Y − X > 0`.
3. If the source isn't already withdrawable, bundle a source-side borrow + repay to unlock the lender's position.
4. Match the two target offers whenever the `Y − X` spread covers the source-side cost plus the keeper's transaction cost (gas).
In the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent)
context, the keeper can take on behalf of the lender against a sell (borrow) offer in the RFQ, executing as soon as the spread exceeds the source-side cost and gas.
**Example.** Lender's target buy (lend) offer at `X% = 2%` and a matching target sell (borrow) offer at `Y% = 6%`. The `4%` gross spread is the keeper's profit headroom. The keeper bundles a source-side borrow + repay to unlock the lender; the source-side interest accrues to the lender. The renewal goes through as long as the `4%` target spread exceeds the source-side cost plus gas. The lender closes the source position early, rolls into the `2%` target lend, and collects the keeper's source-side interest on top, netting a return above `2%`.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#prerequisites)
* [Routing](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#routing)
* [Taker net rate (borrower)](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#taker-net-rate-borrower)
* [Keeper](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal#keeper)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#__docusaurus_skipToContent_fallback)
On this page
`BorrowMidnightToBlueCallback` moves a borrower from a fixed-rate Midnight position into a variable-rate Blue position.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#description "Direct link to Description")
----------------------------------------------------------------------------------------------------------------------------------------
A borrower holds a fixed-rate position on Morpho Midnight and wants to move into a variable-rate position on Blue, either before or after maturity. They post a buy (lend) offer on Midnight to repay their existing borrow position with this callback attached. When the buy (lend) offer is taken, the callback moves the matching collateral to a Morpho Blue market, borrows variable-rate debt on Blue to settle the Midnight exit, and leaves the borrower with a variable-rate position on Blue.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts an exit buy (lend) offer on the **Midnight source** market with `receiverIfMakerIsBuyer` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's exit buy (lend) offer on Midnight by calling `MORPHO_MIDNIGHT.take()`. Morpho Midnight invokes `onBuy` on the callback.
2. **Validate:** The Midnight source and the Blue target must share the same `loanToken`. The Blue market's single collateral token must appear in the Midnight market's collateral set.
3. **Move collateral:** Withdraw the matching collateral from Midnight pro-rata to the debt being closed (or in full if the Midnight debt drops to zero), then supply it to the Blue market.
4. **Borrow on Blue:** Borrow `buyerAssets + fee` from the Blue market on the borrower's behalf. The proceeds land on the callback.
5. **Pay the fee and settle:** Transfer the fee to the recipient (zero in practice when orchestrated by the ratifier) and approve `buyerAssets` back to Morpho Midnight to settle the exit.
6. **Crossing guard:** Read the borrower's Midnight position after the exit. If any credit balance remains, revert with `PositionCrossing`. The borrower must stay a pure borrower throughout; they cannot flip into a lender mid-exit.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#prerequisites "Direct link to Prerequisites")
----------------------------------------------------------------------------------------------------------------------------------------------
* The borrower must authorize the callback on **Morpho Blue**. The callback supplies collateral and borrows on the borrower's behalf there.
* The offer must route the Blue borrow proceeds to the callback, so the `receiverIfMakerIsTaker`/`receiverIfTakerIsSeller` field points at the callback contract.
* The destination Blue market must have enough lender supply to fund the `buyerAssets + fee` borrow.
Other Midnight collaterals stay on Midnight
Only the collateral matching the Blue target's collateral token migrates. Other collaterals the borrower had on Midnight remain there. After the exit the Midnight position has no debt, so the borrower can withdraw them separately.
This direction uses a flat percentage fee on raw `buyerAssets` rather than an interest-based fee, because Midnight's rate check happens pre-fee. If the destination Blue market lacks enough spare supply to fund `buyerAssets + fee`, the `MORPHO_BLUE.borrow` call reverts and the entire take rolls back.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue#prerequisites)
---
# Unknown
Summary Audit Firm Guardian Prepared By Robert Reigada, Wafflemakr, Nicholas Chew Client Firm Tenor Final Report Date June 1st, 2026 Audit Summary Tenor engaged Guardian to perform a review of its Morpho integration contracts. From April 20 through May 4, 2026, Guardian reviewed the code in scope and recorded the vulnerabilities found in the following report. ✅ Verify the authenticity of this report on Guardian’s GitHub: https://github.com/guardianaudits Confidence Ranking Given the lack of High and Critical issues detected during the main review, Guardian assigns a Confidence Ranking of 4 to the protocol. Guardian advises the protocol to address issues thoroughly and consider a targeted follow-up audit depending on code changes. For detailed understanding of the Guardian Confidence Ranking, please see the rubric on the following page. 2 Confidence RankingDefinition and RecommendationRisk Profile 5: Very High ConfidenceCodebase is mature, clean, and secure. No High or Critical vulnerabilities were found. Follows modern best practices with high test coverage and thoughtful design. Recommendation: Code is highly secure at time of audit. Low risk of latent critical issues. 0 High/Critical findings and few Low/Medium severity findings. 4: High Confidence Code is clean, well-structured, and adheres to best practices. Only 1 Significant issue was uncovered per week. Design patterns are sound, and test coverage is strong. Recommendation: Suitable for deployment after remediations; consider periodic review with changes. 0-1 High/Critical findings per engagement week and little to no Medium severity issues. Varied Low severity findings. 3: Moderate Confidence Medium-severity and occasional High-severity issues found. Code is functional, but there are concerning areas (e.g., weak modularity, risky patterns). No critical design flaws, though some patterns could lead to issues in edge cases. Recommendation: Address issues thoroughly and consider a targeted follow-up audit depending on code changes. 1-2 High/Critical findings per engagement week. 2: Low ConfidenceCode shows frequent emergence of Critical/High vulnerabilities. Audit revealed recurring anti-patterns, weak test coverage, or unclear logic. These characteristics suggest a high likelihood of latent issues. Recommendation: Post-audit development and a second audit cycle are strongly advised. 2-4 High/Critical findings per engagement week. Or additional High/Critical findings uncovered in remediation review which have not been resolved and confirmed by Guardian. 1: Very Low ConfidenceCode has systemic issues. Multiple High/Critical findings (≥5/week), poor security posture, and design flaws that introduce compounding risks. Safety cannot be assured. Recommendation: Halt deployment and seek a comprehensive re-audit after substantial refactoring. ≥5 High/Critical findings and overall systemic flaws. Guardian Confidence Ranking 3 Table of Contents Project Information Project Overview ..................................................................................... 5 Audit Scope & Methodology ...................................................................... 6 Smart Contract Risk Assessment Findings & Resolutions ............................................................................ 9 Addendum Disclaimer .............................................................................................. 93 About Guardian ....................................................................................... 94 Project Overview Project Summary Audit Summary Vulnerability Summary Project NameTenor Codebasehttps://github.com/Shippooor-Labs/tenor-morpho-v2-contracts-2 Commit(s) Main Review: 75ee03e8502b955522445acb8142047c3e0ad652 Remediation Review: 13b94b0acd7597f3fffc6ac9ebdfb2517e960480 Delivery DateJune 1, 2026 Audit MethodologyStatic Analysis, Manual Review Vulnerability LevelTotalPendingDeclinedAcknowledgedPartially ResolvedResolved ● Critical000000 ● High100001 ● Medium25001708 ● Low22001606 ● Info290018011 5 Audit Scope & Methodology RenewalIntentRatifier.sol RenewalIntentRegistry.sol RenewalIntentTakeOnBehalf.sol MidnightAuthorizationAdapter.sol RenewalIntentTakeOnBehalfAdapterBase.sol TakeRouterAdapterBase.sol TenorAdapter.sol MorphoV1ToV2BorrowCallback.sol MorphoV1ToV2LendCallback.sol MorphoV2SupplyCollateralCallback.sol MorphoV2SupplyVaultSharesCallback.sol MorphoV2ToV1BorrowCallback.sol MorphoV2ToV1LendCallback.sol MorphoV2ToV2BorrowRepay.sol MorphoV2ToV2LendWithdrawable.sol MorphoV2WithdrawVaultSharesCallback.sol DelayedLiquidationGateFactory.sol MidnightAllowlistGateFactory.sol OracleWithValidationCheckFactory.sol VaultV2AllowlistGateFactory.sol DelayedLiquidationGate.sol MidnightAllowlistGate.sol PauseTakeGate.sol VaultV2AllowlistGate.sol CallbackLib.sol CollateralTransferLib.sol Constants.sol KeyLib.sol PriceLib.sol RatePointLib.sol ToIdLib.sol OracleWithValidationCheck.sol MidnightVaultExecutor.sol TakeRouter.sol MidnightLib.sol RouterLib.sol ClampLib.sol SupplyCollateralCallbackClamp.sol V1ToV2LendClamp.sol V2ToV2BorrowRepayClamp.sol V2ToV2LendWithdrawableClamp.sol VaultSupplyClamp.sol VaultWithdrawClamp.sol FourWeekCadence.sol PausableStaticRatePolicy.sol StaticRatePolicy.sol 6 Vulnerability Classifications Audit Scope & Methodology SeverityImpact: HighImpact: MediumImpact: Low Likelihood: High ● Critical● High● Medium Likelihood: Medium ● High● Medium● Low Likelihood: Low ● Medium● Low● Low Impact High Significant loss of assets in the protocol, significant harm to a group of users, or a core . functionality of the protocol is disrupted. Medium A small amount of funds can be lost or ancillary functionality of the protocol is affected. . The user or protocol may experience reduced or delayed receipt of intended funds. Low Can lead to any unexpected behavior with some of the protocol's functionalities that is . notable but does not meet the criteria for a higher severity. Likelihood High The attack is possible with reasonable assumptions that mimic on-chain conditions, . and the cost of the attack is relatively low compared to the amount gained or the . disruption to the protocol. Medium An attack vector that is only possible in uncommon cases or requires a large amount of . capital to exercise relative to the amount gained or the disruption to the protocol. Low Unlikely to ever occur in production. 7 Audit Scope & Methodology Methodology Guardian is the ultimate standard for Smart Contract security. An engagement with Guardian entails the following: ●Two competing teams of Guardian security researchers performing an independent review. ●A dedicated fuzzing engineer to construct a comprehensive stateful fuzzing suite for the project. ●An engagement lead security researcher coordinating the 2 teams, performing their own analysis, relaying findings to the client, and orchestrating the testing/verification efforts. The auditing process pays special attention to the following considerations: ●Testing the smart contracts against both common and uncommon attack vectors. ●Assessing the codebase to ensure compliance with current best practices and industry standards. ●Ensuring contract logic meets the specifications and intentions of the client. ●Cross-referencing contract structure and implementation against similar smart contracts produced by industry leaders. ●Thorough line-by-line manual review of the entire codebase by industry experts. Comprehensive written tests as a part of a code coverage testing suite. ●Contract fuzzing for increased attack resilience. 8 Round One Findings & Resolutions IDTitleCategory SeverityStatus H-01 Overdue V2-V2 Borrow Renewals Bypass Max Rate Logical Error ● High Resolved M-01 Executor Lets Callers Sweep Shared Balance Unexpected Behavior ● Medium Acknowledged M-02 Primary-based Deviation Can Erase Buffer Logical Error ● Medium Acknowledged M-03 V2-to-V2 Lend Can Buy Bad Credit Unexpected Behavior ● Medium Acknowledged M-04 Callback Spends Shared Loan Balance Unexpected Behavior ● Medium Acknowledged M-05Ratifiers Miss Seller Receiver Unexpected Behavior ● Medium Acknowledged M-06 TakeRouter Breaks Caller-sensitive Policies Logical Error ● Medium Resolved M-07 onFlashLoan Missing Approval Breaks Bundled Ops Logical Error ● Medium Resolved M-08 Incorrect Math Inversion For SELL + Fee Rounding ● Medium Resolved M-09 Executor Hides Real Liquidator From Gate Validation ● Medium Resolved M-10 Zero Oracle Price Gives Free Collateral Unexpected Behavior ● Medium Acknowledged M-11 Keeper Params Do Not Guarantee Profitability Logical Error ● Medium Acknowledged M-12 Temporary Grants Bypass Router Limits Validation ● Medium Acknowledged 9 Round One Findings & Resolutions IDTitleCategory SeverityStatus M-13 Flash Callback Spends Pending Reentry Validation ● Medium Acknowledged M-14 Source Unlocked During Borrow Renewal Reentrancy ● Medium Acknowledged M-15 executeAndConsume Burns Keeper consumeGroup Logical Error ● Medium Resolved M-16 Keeper Adapter Callbacks Reject Delegation Logical Error ● Medium Resolved M-17 Adapter Callbacks Cannot Batch Reentries Compatibility ● Medium Resolved M-18 executeAndConsume Accepts Intervening Fills Unexpected Behavior ● Medium Acknowledged M-19 Clamps Ignore Revoked Callback Auth / Allowances DoS ● Medium Acknowledged M-20 Tenor Prioritizes Midnight's Trading Fees Logical Error ● Medium Acknowledged M-21 Keeper Gets Full Midnight Control Access Control ● Medium Acknowledged M-22 TenorAdapter Bypasses Midnight's Repay Auth Validation ● Medium Resolved M-23 Arbitrary Policies Can Gas Grief Keepers DoS ● Medium Acknowledged M-24 Permissionless First Touch Freezes Default Fees Unexpected Behavior ● Medium Acknowledged M-25 Fresh Credit Drains Old Repayments Validation ● Medium Acknowledged 10 Round One Findings & Resolutions IDTitleCategory SeverityStatus L-01 allowRevert Misses Pre-dispatch Reverts Unexpected Behavior ● Low Acknowledged L-02 Frozen Gate Trusts Fee Recipients Trust Assumptions ● Low Acknowledged L-03 Vault-share Exit Ignores Vault Withdrawal Limits Validation ● Low Acknowledged L-04 Zero-price Asset Caps Act Unbounded Unexpected Behavior ● Low Acknowledged L-05 isFinalFill Never True With Non-Zero Fee Rounding ● Low Acknowledged L-06 Fee Recipients Can Get Stuck Shares Unexpected Behavior ● Low Resolved L-07 Clamps Ignore Active Collateral Cap Validation ● Low Resolved L-08 Fragmented Fills Skip Discrete Collateral Rounding ● Low Acknowledged L-09 Target-side Netting Changes Renewal Semantics Unexpected Behavior ● Low Acknowledged L-10 Oracle Validation Can Fail Open Or Freeze Oracle ● Low Acknowledged L-11 Vault Executor Redeems Without Asset Floors Validation ● Low Resolved L-12Fee Adjuster Overshoots maxFillRounding ● Low Resolved L-13Griefing V1=>V2 MigrationGas Griefing ● Low Acknowledged 11 Round One Findings & Resolutions IDTitleCategory SeverityStatus L-14 Router Fee Metadata Under-reports Fees Unexpected Behavior ● Low Acknowledged L-15 Bidirectional Intents Enable Fee-draining Churn Gaming ● Low Acknowledged I-01 V1->V2 Borrow Renewal Can Spend Target Credit Validation ● Info Acknowledged I-02 Stale Grace Timer Survives Healthy Recovery Unexpected Behavior ● Info Acknowledged I-03 V2-to-V1 Fees Bypass Rate Limits Validation ● Info Acknowledged I-04 Sell Takes Can Spend Router-held Tokens Warning ● Info Acknowledged I-05 Supply Collateral Clamp Quotes Unhealthy Fills Warning ● Info Acknowledged I-06 TENOR\_VAULT\_V2 Clamp Reads Wrong Data Unexpected Behavior ● Info Resolved I-07 Vault Supply Clamp Uses An Unbound Tick Unexpected Behavior ● Info Resolved I-08 Vault Supply Clamp Omits Solvency Bound Validation ● Info Resolved I-09 Grace Period Skipped Once Obligation Matures Validation ● Info Resolved I-10 Factory Allows Near-total Oracle Deviation Validation ● Info Acknowledged I-11 Zero-owner Oracle Docs Cannot Deploy Documentation ● Info Resolved 12 Round One Findings & Resolutions IDTitleCategory SeverityStatus I-12 Lender Rate Floors Bypassed After Early Repay Logical Error ● Info Acknowledged I-13 Withdrawals Escape Lazy Bad Debt Validation ● Info Acknowledged I-14 Renewal Skips Collateral Missing From Target Unexpected Behavior ● Info Resolved I-15 TakeRouter Returns Adjusted Totals Only Unexpected Behavior ● Info Acknowledged I-16 V2-to-V2 Lend Callback Self-funds Fresh Credit Unexpected Behavior ● Info Acknowledged I-17 Renewal Grants Survive Midnight Revocation Trust Assumptions ● Info Acknowledged I-18 V2-to-V1 Borrow Exits Lack Target Health Buffer Validation ● Info Acknowledged I-19Stale DocumentationDocumentation ● Info Resolved I-20Sentinel Silently No-ops TakesLogical Error ● Info Resolved I-21Stable Max Sentinel Nets Gas Unexpected Behavior ● Info Resolved I-22 Delayed Gate Blocks Gated Vault Liquidations DoS ● Info Acknowledged 13 H-01 | Overdue V2-V2 Borrow Renewals Bypass Max Rate Description BaseRenewalRatifier intentionally allows V2-source renewals after the source obligation has matured. BaseRenewalRatifier.\_ratifyWindow() only rejects executions that are too early, so once the clock passes sourceMaturity the renewal remains permanently executable. That behavior is documented and tested. The problem is that the downstream rate check does not switch to a post-maturity duration model when that overdue state is reached. For V2-to-V2 renewals, BaseRenewalRatifier.\_computeDuration() always returns targetMaturity - sourceMaturity. That is correct only while the source obligation is still alive. After maturity, the new debt does not begin at the expired sourceMaturity; it begins when the keeper executes the renewal. The economically correct duration for the borrow-side rate check is therefore targetMaturity - block.timestamp, or equivalently targetMaturity - max(block.timestamp, sourceMaturity). That distinction matters because PriceLib.computePrice() is inversely proportional to duration. A longer duration produces a lower accepted price. On the borrower path, PriceLib.satisfiesRateLimit() treats a lower accepted price as a looser rate ceiling because the borrower only needs (assets - fee) \* WAD to stay above units \* price. In other words, once the ratifier uses an overdue-inclusive duration, it accepts prices that imply a materially higher realized rate over the actual remaining term of the renewed debt. The effect scales directly with overdue time. If a borrower set limitRatePerSecond to cap a renewal at rate r, the post-maturity V2-to-V2 check accepts the price for duration T + D, where D is time overdue and T is the actual time from execution until the target maturity. The debt that is actually created lasts only T, so the same accepted discount corresponds to an effective ceiling of r \* (T + D) / T. A renewal that is overdue by sixty days and rolls into a new thirty-day target therefore weakens a ten percent ceiling into roughly a thirty percent realized ceiling. The ratifier explicitly accepts the overdue state, then computes borrower protection from the wrong time origin. The issue is reachable on live accounting because the source debt is already matured, the target maturity must still satisfy the user's minDuration and maxDuration relative to the current timestamp, and the callback opens fresh target debt immediately after ratification. The lender side does not have the same risk direction. Reusing targetMaturity - sourceMaturity on a lend renewal makes the floor stricter, not looser. The unsafe manifestation is specifically the V2-to-V2 borrow path because borrower ceilings are enforced with the seller-side inequality. Recommendation If overdue rolling is not intended, close the V2 renewal window at sourceMaturity and revert once the source obligation is overdue. That is the simplest fix because it restores the original interpretation of sourceMaturity as the start of the renewed term. If overdue rolling is intended, keep the window semantics explicit but compute duration from the actual renewal start time: function \_computeDuration(address callback, uint256 sourceMaturity, uint256 targetMaturity) internal view ... ... } Resolution Tenor Team: The issue was resolved in PR#427. CategorySeverityLocationStatus Logical Error ● High BaseRenewalRatifier.solResolved 14 M-01 | Executor Lets Callers Sweep Shared Balance Description repayAndWithdrawCollateral and liquidateAndRedeem both treat the executor's entire loanToken balance as if it belonged to the current caller. In repayAndWithdrawCollateral, the function calls Midnight.repay, then transfers IERC20(obligation.loanToken).balanceOf(address(this)) to onBehalf. It never tracks how much of that balance came from the current repayment. The function also accepts repayUnits == 0 and sharesToWithdraw == 0, so any account can call it with onBehalf = msg.sender and withdraw every loan token already sitting on the executor. liquidateAndRedeem has the same accounting problem from the funding side. It grants Midnight a max allowance and lets Midnight.liquidate pull repaidUnits from whatever balance the executor already holds. The integration tests model an EOA liquidator by pre-funding the executor before calling liquidateAndRedeem (test/integration/MidnightVaultExecutorIntegration.t.sol:823-832). When Midnight computes actualRepaidUnits below the prefunded amount, the helper never refunds the surplus. That residue stays on the public executor and becomes claimable by the next caller through repayAndWithdrawCollateral, or usable as free repayment capital in a later liquidation. This directly contradicts the documented EXEC-3 invariant in PROPERTIES.md:316. Consequently any loan tokens stranded on the executor are not isolated to the account that supplied them. They are globally spendable. This is a direct loss of funds for liquidators or users who overfund the executor and the contract already acknowledges one source of residual dust in the mint path. The bug does not directly drain the normal protocol fee sinks: Tenor callback fees are sent to the configured feeRecipient and Midnight trading fees accrue inside Midnight as claimableTradingFee. The practical theft target is loan-token residue that actually lands on the executor, including unused liquidation prefunds, accidental transfers and executor-held dust. Recommendation Stop using the executor's aggregate token balance as the payment and refund source. Snapshot the loan-token balance at function entry and only refund the positive delta created by the current call. Reject repayAndWithdrawCollateral when both repayUnits and sharesToWithdraw are zero. For liquidateAndRedeem, either require exact callback-based funding, or refund postBalance - preBalance to msg.sender and revert if the executor starts with a nonzero loan-token balance. If unexpected balances must remain recoverable, add a dedicated privileged rescue mechanism instead of a public sweep. Resolution Tenor Team: The issue was resolved in PR#449. CategorySeverityLocationStatus Unexpected Behavior ● Medium MidnightVaultExecutor.solAcknowledged 15 M-02 | Primary-based Deviation Can Erase Buffer Description price() scales MAX\_ORACLE\_DEVIATION by primaryPrice, then returns that same primaryPrice if the check passes. When the primary oracle is the high side, this accepts primaryPrice <= validationPrice / (1 - d) rather than primaryPrice <= validationPrice \* (1 + d). Consequently a configured 5% threshold actually allows about 5.263% overpricing against the validation oracle. This matters because the README recommends sizing MAX\_ORACLE\_DEVIATION below (1 - Liquidation LTV) for lending markets. With a 95% LLTV market and a 5% threshold, this wrapper can still return a price high enough to let the protocol lend 100% of the validation-based collateral value. Even at 94.5% LLTV, the remaining buffer is only about 0.53%. A borrower who can push the primary oracle up within this accepted band can open positions that are effectively at or near full LTV on the validation price, so negligible market movement or routine interest accrual can leave bad debt. Recommendation Measure the allowed deviation from validationPrice, or more conservatively from min(primaryPrice, validationPrice), instead of primaryPrice. If the current formula must remain, do not size MAX\_ORACLE\_DEVIATION directly from (1 - LLTV). Document and enforce the stricter bound MAX\_ORACLE\_DEVIATION <= (1 - LLTV) / (2 - LLTV) for markets that rely on this wrapper to preserve liquidation margin. Resolution Tenor Team: The issue was resolved in commit 14660ca. CategorySeverityLocationStatus Logical Error ● Medium OracleWithValidationCheck.solAcknowledged 16 M-03 | V2-to-V2 Lend Can Buy Bad Credit Description MorphoV2ToV2LendWithdrawable.onBuy only checks that the source and target obligations use the same loan token before withdrawing the lender's source credit. It does not prove that the target fill created fresh borrower debt backed by currently healthy collateral. That matters when the seller already has credit on the target obligation. Midnight.take() updates positions before the callback runs. For a sell offer, it first decreases the seller's existing credit and only creates seller debt for any excess units: uint256 sellerCreditDecrease = UtilsLib.min(units, sellerPos.credit); uint256 sellerDebtIncrease = units - sellerCreditDecrease; If sellerDebtIncrease == 0, the seller exits an existing lending position instead of opening a new borrow. Midnight's final isLiquidatable check then returns immediately because the seller has no debt, so no collateral oracle is queried. Consequently, a lender's renewal can spend withdrawable source credit to buy target credit that is already backed by stale, worthless, or otherwise bad collateral. This can happen after the lender has configured the target market and authorized the callback, because any keeper can execute a matching renewal while the callback only enforces loan-token equality and source-credit availability. Recommendation Require explicit opt-in before a V2-to-V2 lend renewal buys existing target credit. The safer default is to reject fills where the target seller has enough credit to avoid a debt increase, or to require the router or ratifier to verify a fresh target health/oracle check for the underlying target obligation before the source withdrawal is allowed. If buying existing target credit is intended, expose it as a separate operation with distinct user consent and event semantics. Do not treat it as the same renewal as creating new target lending exposure against fresh borrower debt. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Medium MorphoV2ToV2LendWithdrawable.solAcknowledged 17 M-04 | Callback Spends Shared Loan Balance Description onSell assumes the callback's current loanToken balance came from the current Midnight fill. The function never checks that receiverIfMakerIsSeller sent sellerAssets to the callback, and it never snapshots the callback's balance before using totalDeposit. If the callback already holds loanToken, a seller can sign a sell offer that uses this callback but routes receiverIfMakerIsSeller to another address. Midnight will send the borrowed sellerAssets to that receiver, then onSell will pull only amountFromSeller from the seller. The later deposit(totalDeposit, address(this)) can consume the pre-existing callback balance for the missing sellerAssets, mint vault shares, and supply those shares as the seller's collateral. Consequently any loan tokens accidentally sent to this public callback, or otherwise stranded there, are not isolated. A later seller can convert that shared balance into their own vault-share collateral and unwind the position to recover the value. Recommendation Do not use the callback's aggregate token balance as implicit funding. At minimum, snapshot IERC20(loanToken).balanceOf(address(this)) at entry and require that the balance equals sellerAssets before any seller top-up is pulled. After pulling amountFromSeller, require that the balance equals totalDeposit. This makes wrong receiver configuration and pre-existing balances fail before the vault deposit. If stranded balances need recovery, add an explicit rescue function instead of letting future fills consume them. Resolution Tenor Team: The issue was resolved in PR#451. CategorySeverityLocationStatus Unexpected Behavior ● Medium MorphoV2SupplyVaultSharesCallback.s ol Acknowledged 18 M-05 | Ratifiers Miss Seller Receiver Description RenewalIntentTakeOnBehalf.take forwards a caller-supplied receiver into MORPHO\_MIDNIGHT.take, but it does not pass that receiver to the user-side ratifier. The ratifier only sees caller, takerCallback, intent, offer, takerCallbackData and ratifierData: IIntentRatifier(intent.ratifier) .onIntentRatify(msg.sender, takerCallback, intent, offerData.offer, takerCallbackData, ratifierData); MORPHO\_MIDNIGHT.take( ... offerData.proof ); This is a dangerous blind spot for renewal routes where intent.user is the seller side of the Midnight trade. In those routes, Midnight sends sellerAssets to receiverIfTakerIsSeller before executing the seller callback. The in-repo seller-side renewal callbacks explicitly rely on that receiver being the callback contract so the callback can spend the fresh fill proceeds. For example, MorphoV2ToV2BorrowRepay states that the offer receiver must be the callback, then transfers fee, approves repayBudget, repays the source obligation and moves collateral without proving that the current fill actually funded the callback. An arbitrary keeper can choose a different receiver that the ratifier cannot inspect. If the callback already holds enough loan-token balance, the fill can still complete: the keeper diverts the current sellerAssets to the chosen receiver, while the callback spends its old balance to pay the fee and repay or deposit on behalf of the user. The old balance can be accidental token residue, or it can be created by the callbacks themselves when feeRecipient == address(this), because the fee transfer becomes a no-op and only sellerAssets - fee is spent afterward. The same aggregate-balance pattern appears in src/callbacks/MorphoV1ToV2BorrowCallback.sol and src/callbacks/MorphoV2ToV1LendCallback.sol. The root cause is that the user-side ratifier is supposed to police renewal shape, but one of the most important value-routing fields is omitted from the ratification payload. Recommendation Include receiver in IIntentRatifier.onIntentRatify and require the relevant ratifiers to validate it for each renewal route. Seller-side renewal routes should reject any receiver other than the expected callback contract. The callbacks should also enforce fresh funding instead of relying on their aggregate token balance. Consider asserting that the callback's loan-token balance increased by exactly the expected current-fill amount before spending, and that no unexpected loan-token balance remains afterward. Also reject feeRecipient == address(this) or add an explicit controlled sweep path for accidental balances. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Medium RenewalIntentTakeOnBehalf.solAcknowledged 19 M-06 | TakeRouter Breaks Caller-sensitive Policies Description The renewal flow is documented and typed so ratifiers can evaluate the original orchestrator caller, not just the user being renewed. IIntentRatifier.onIntentRatify explicitly defines its caller argument as the address that called the orchestrator boundary, and RenewalIntentRatifier.\_ratifyRate forwards that value into IInterestRatePolicy.getRate(...) so policies can implement caller-sensitive authorization or pricing. Vulnerability: the ORCHESTRATOR\_TAKE branch in TakeRouter breaks that contract. Instead of preserving the external caller it calls RenewalIntentTakeOnBehalf.take, and RenewalIntentTakeOnBehalf then forwards its own msg.sender to the ratifier. For routed renewals, the ratifier therefore sees the router or adapter contract address instead of the real keeper / bundler initiator. Impact: any deployment that relies on caller-sensitive interestRatePolicy logic can have that policy silently bypassed or unexpectedly DOSed when execution is routed through TakeRouter. A policy that is supposed to whitelist specific keepers, deny specific operators, or return different rate curves per caller instead receives the router address for every routed renewal, collapsing distinct operators into one shared identity. The currently included StaticRatePolicy and PausableStaticRatePolicy ignore caller, which limits immediate impact for those implementations, but the bug defeats a security property the ratifier interface and comments explicitly advertise. Recommendation Clarify and enforce a single caller model across renewal execution. If caller-sensitive policies are not intended, document that interestRatePolicy must be caller-insensitive and do not treat caller as a security boundary. If caller-sensitive policies are intended, implement that behavior in a separate router-specific entrypoint rather than changing the existing take flow in place. Resolution Tenor Team: The issue was resolved in PR#407. CategorySeverityLocationStatus Logical Error ● Medium TakeRouter.sol:240-242Resolved 20 M-07 | onFlashLoan Missing Approval Breaks Bundled Ops Description midnightFlashLoan approves assets to Midnight before calling MORPHO\_MIDNIGHT.flashLoan, then relies on that allowance surviving the callback so Midnight can pull the repayment on return: // Midnight.flashLoan SafeTransferLib.safeTransfer(token, msg.sender, assets); // send to adapter IFlashLoanCallback(callback).onFlashLoan(token, assets, data); // run nested bundle SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), assets); // pull back via allowance The adapter's callback forwards into the nested bundle and returns without restoring the allowance: function onFlashLoan(address, uint256, bytes memory data) external { require(msg.sender == address(MORPHO\_MIDNIGHT), ErrorsLib.UnauthorizedSender()); \_midnightCallback(data); // no re-approval — Midnight will pull \`assets\` on return } Any adapter action dispatched inside the nested bundle that forceApproves Midnight for the same token will overwrite the outer allowance, and Midnight will consume whatever is left on its next pull. Same-token culprits: midnightRepay, midnightSupplyCollateral (when collateral token equals the flashLoan token), a nested midnightFlashLoan, and an inner take that routes onBuy through this adapter. In every case the outer safeTransferFrom sees allowance 0 and reverts, unwinding the bundle. onBuy in the same file demonstrates the correct pattern — it re-approves Midnight for the exact pull amount after the nested bundle runs. onFlashLoan has identical post-callback pull semantics but does not mirror it. Impact: the canonical flash-loan flows (flashLoan → repay debt → withdraw collateral → swap → repay, or flashLoan → take an offer → repay) are unreachable whenever the flashLoan token is also touched inside the nested bundle. Recommendation Restore the outer allowance at the end of onFlashLoan, mirroring onBuy. The callback already receives token and assets: function onFlashLoan(address token, uint256 assets, bytes memory data) external { require(msg.sender == address(MORPHO\_MIDNIGHT), ErrorsLib.UnauthorizedSender()); \_midnightCallback(data); SafeERC20.forceApprove(IERC20(token), address(MORPHO\_MIDNIGHT), assets); } Resolution Tenor Team: The issue was resolved in PR#406. CategorySeverityLocationStatus Logical Error ● Medium MidnightAdapterBase.solResolved 21 M-08 | Incorrect Math Inversion For SELL + Fee Description ClampLib.maxUnitsForSellerBudget() uses mulDivDownInverse (floor inverse) to compute the maximum units for SELL offers with feeRate > 0. However, the actual repay budget uses ceiling arithmetic (mulDivUp via sellerFeeFromTick). When units × effPrice % WAD ≠ 0, the floor inverse returns a unit count one too high — causing repayBudget = sourceDebt + 1, which triggers an ExcessRepayment revert in Midnight. This DoS affects fee-enabled full renewals via TakeRouter for SELL offers in the vast majority of real-world cases; the only safe inputs are those where units × effPrice is exactly divisible by WAD (1e18), which is coincidental in practice. Recommendation Replace mulDivDownInverse with a plain floor division in the SELL+feeRate>0 branch of maxUnitsForSellerBudget: // Before (wrong): return mulDivDownInverse(maxBudget, WAD, effPrice); // After (correct): return maxBudget.mulDivDown(WAD, effPrice); Resolution Tenor Team: The issue was resolved in PR#421. CategorySeverityLocationStatus Rounding ● Medium src/periphery/libraries/ClampLib.sol:182Resolved 22 M-09 | Executor Hides Real Liquidator From Gate Description MidnightVaultExecutor.liquidateAndRedeem() calls Midnight.liquidate() as the executor contract, so the obligation's liquidatorGate only sees address(MidnightVaultExecutor). The function is public. It does not check whether the external caller would pass the same gate. This matters for caller-aware gates such as MidnightAllowlistGate. If the gate allowlists the executor so vault-share liquidations can use liquidateAndRedeem(), any external account can call the executor and inherit that permission. Midnight.liquidate() accepts the call because canLiquidate(msg.sender) is evaluated against the executor. The executor then forwards the seized value to the original caller by encoding msg.sender as the liquidator and redeeming seized shares to that address in onLiquidate(). The caller still has to fund repaidUnits, either by pre-funding the executor or by returning the repayment amount in its callback, but the caller-level liquidation restriction is gone. This is an authorization bypass in a documented executor-based liquidation setup. The project documentation explicitly describes liquidateAndRedeem() as the liquidation mechanism where only the executor needs to be allowlisted and liquidators receive assets directly. If a market also relies on liquidatorGate to restrict which end liquidators may act, that policy is no longer enforced once the public executor is admitted. An unapproved liquidator can take the liquidation opportunity and spread even though the gate would reject that same account on a direct Midnight call. Recommendation Do not treat MidnightVaultExecutor as a safe public router for obligations that rely on caller identity at liquidatorGate. Consider mirroring the gate check on the external caller before forwarding the liquidation. If obligation.liquidatorGate != address(0), liquidateAndRedeem() should require ILiquidatorGate(obligation.liquidatorGate).canLiquidate(msg.sender) and revert otherwise. Resolution Tenor Team: The issue was resolved in PR#408. CategorySeverityLocationStatus Validation ● Medium MidnightVaultExecutor.solResolved 23 M-10 | Zero Oracle Price Gives Free Collateral Description Midnight.liquidate() allows the liquidator to specify seizedAssets instead of repaidUnits. In that mode, the function computes the repayment from the selected collateral oracle price: repaidUnits = seizedAssets.mulDivUp(liquidatedCollatPrice, ORACLE\_PRICE\_SCALE).mulDivUp(WAD, lif); If the selected collateral oracle returns zero, repaidUnits becomes zero for any positive seizedAssets. The function still subtracts the collateral from the borrower, adds zero to withdrawable, transfers the seized collateral to the liquidator, and finally pulls zero loan tokens from the liquidator. The same liquidation can also mark the borrower's debt as bad debt and update lossIndex, so lenders can be slashed while the liquidator receives collateral for no repayment. Tenor's OracleWithValidationCheck rejects a zero primary price, but Midnight obligations accept arbitrary oracle addresses. Any direct obligation or non-wrapper oracle that can transiently return zero exposes this path. Recommendation Reject zero selected-collateral prices whenever collateral can be seized. For example, in liquidate(): if (seizedAssets > 0 && liquidatedCollatPrice == 0) revert ZeroCollateralPrice(); More conservatively, require every active collateral oracle used in liquidation to return a nonzero price. Also document that a zero price is not just a liveness failure; in seized-asset liquidation mode it can become a value-extraction path. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Medium Midnight.solAcknowledged 24 M-11 | Keeper Params Do Not Guarantee Profitability Description Tenor collects a callback fee on every renewal, sent to a protocol-controlled feeRecipient. There is no on-chain payment to the keeper. Tenor must run and subsidise the keeper itself. The fee scales with position size and interest discount (fee = feeRate × (1 − price) × units), while keeper gas is fixed per renewal. For short renewal durations or small positions the fee falls below gas cost (around 415k gas spent on renewal). Adding more collaterals make it worse — each adds roughly 93k ($0.47 at 2 gwei) with no effect on the fee. Measured gas (2 gwei, $2,500/ETH): $2.08 for 1 collateral, $3.94 for 5 collaterals. At current market rates (4–5% APR), a $1,000 position renewed weekly earns $0.50 in fees per renewal against $2.08–$3.94 in gas — a $1.58–$3.44 loss per execution. Even a monthly renewal at 5% APR on a $1,000 position earns only $2.11, leaving no margin for gas spikes or additional collaterals. No minimum position size or renewal duration is enforced. Recommendation Document the break-even threshold explicitly (position size, renewal duration, collateral count, target chain) so keeper can make an informed decision. If self-sustaining keeper economics are required, enforce limits so fee ≥ gas\_cost before executing. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Logical Error ● Medium src/BaseRenewalRatifier.solAcknowledged 25 M-12 | Temporary Grants Bypass Router Limits Description RenewalIntentTakeOnBehalf.take() has no action-scoped authorization and no reentrancy guard. It only checks that the user's ratifier is currently authorized, calls that ratifier, then forwards the take to Midnight. This is unsafe when users rely on Bundler3 to grant renewal permissions temporarily. AuthorizationAdapter can grant RenewalIntentTakeOnBehalf and the user's renewal ratifier at the start of a multicall, run TenorAdapter.execute(), then revoke those permissions at the end. During the execution, those permissions are global. TakeRouter only accounts for the amounts returned by the outer \_TAKE\_ON\_BEHALF.take() call. If a malicious maker ratifier is called inside Midnight.take(), it can call RenewalIntentTakeOnBehalf.take() again for the same user while the temporary grants are still active. The nested renewal can pass the user's ratifier and Midnight checks, but it is invisible to the outer router. Therefore it is not included in maxFill, minFill, slippage checks, BatchExecuted, or executeAndConsume accounting. For example, a lender submits a bundled renewal capped at 100 units and grants the needed permissions only for that bundle. The maker ratifier reenters during the first take and executes another valid renewal for 100 units before the outer router receives a result. The outer router then completes the original 100 unit fill and reports that amount. The user's position changed by 200 units even though the transaction-level router limit was 100. Recommendation Do not use global boolean authorization as the only control for bundled renewals. Bind renewal authorization to a concrete execution scope that includes the expected caller, callback, source market, target market, expiry or nonce and maximum units. Consume that authorization before calling any untrusted ratifier or callback. As an immediate hardening step, add a reentrancy guard to RenewalIntentTakeOnBehalf.take() so a maker ratifier, callback, token hook, or other synchronous external call cannot start another renewal before the first one returns. The router should also reject or explicitly account for nested orchestrator execution if nested renewals are intended to be supported. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Medium RenewalIntentTakeOnBehalf.solAcknowledged 26 M-13 | Flash Callback Spends Pending Reentry Description MidnightAdapterBase.onFlashLoan() forwards any Midnight flash-loan callback data to Bundler3.reenter() after only checking that msg.sender is MORPHO\_MIDNIGHT. It does not record that TenorAdapter started the flash loan through midnightFlashLoan(). It also does not check the expected token, amount, callback data hash, offer, or router action before forwarding the reentry data. Midnight.flashLoan() lets the caller choose both the callback address and the callback data. It also accepts assets == 0. Therefore, while Bundler3 is executing TenorAdapter.execute() with a pending callbackHash = keccak256(reentryData), a malicious maker ratifier can call: MORPHO\_MIDNIGHT.flashLoan(token, 0, address(TenorAdapter), reentryData); Midnight then calls TenorAdapter.onFlashLoan(token, 0, reentryData). The adapter forwards reentryData to Bundler3. Bundler3 accepts the reentry because TenorAdapter is the caller and the reentry bytes match the pending hash. This consumes the user's precommitted reentry before the intended callback uses it. The ratifier must know the exact reentry bytes. This can happen if the route passes those bytes in ratifierData, embeds them in maker-controlled offer data, or uses predictable callback data. The attacker is not granted arbitrary Bundler execution. Bundler3 still executes the user's precommitted calls, but it executes them at the wrong time. This matters because TenorAdapter is used as a temporary payer inside a bundled transaction. For a direct MIDNIGHT\_TAKE of a sell offer with no taker callback, TakeRouter.\_dispatchMidnightTake() gives Midnight a temporary allowance from TenorAdapter. Midnight also treats TenorAdapter as the payer. If the premature reentry withdraws the victim's source credit into the adapter before settlement, the resumed Midnight.take() can pull those tokens from the adapter and pay them to the maker. The impact is stronger when the direct MIDNIGHT\_TAKE targets an untrusted Midnight obligation. Obligations are created permissionlessly and touchObligation() only checks structural fields such as collateral ordering, allowed LLTV and maxLif. It does not whitelist collateral tokens or oracles. A malicious maker can publish a sell offer whose loan token is the real loan token, but whose collateral is a worthless attacker token and whose oracle is controlled by the attacker. In that scenario, the premature reentry withdraws the victim's source credit to TenorAdapter before the attacker offer settles. Midnight then pulls the newly withdrawn loan tokens from the adapter and pays the maker. The victim receives credit in the attacker's obligation instead of keeping the original source credit. The attacker-controlled oracle can report a high price during settlement so the seller health check passes. After settlement, the oracle can report zero. A zero-repay liquidation then realizes the attacker's debt as bad debt, clears the attacker debt and slashes the victim's toxic credit through the obligation loss index. This requires a direct MIDNIGHT\_TAKE sourced from an untrusted maker or obligation, exposed or predictable reentry bytes, withdrawable source credit and passing route price checks. It does not affect passive users. It also does not affect users routed only through canonical ORCHESTRATOR\_TAKE execution with ratifier market-id binding. If no adapter reentry hash is pending, the malicious flash-loan callback cannot satisfy Bundler3's reentry check and the take reverts. Recommendation Only allow onFlashLoan() during a flash loan started by midnightFlashLoan(). Set a transient guard before calling MORPHO\_MIDNIGHT.flashLoan(token, assets, address(this), data). Store the expected token, amount and callback data hash. Require those values in onFlashLoan() before calling \_midnightCallback(data), then clear the guard after the flash loan returns. Also bind adapter reentry data to the callback that is allowed to use it. The adapter should verify an expected callback selector, obligation id, taker role, subject address and offer or action hash before forwarding the inner Call\[\] to Bundler3. For router-level defense in depth, raw MIDNIGHT\_TAKE execution should reject offers whose obligation id or market id is not explicitly expected. The check should bind offer.obligation to an allowlisted target market or to the same market id authorized by the user's renewal intent. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Medium MidnightAdapterBase.solAcknowledged 27 M-14 | Source Unlocked During Borrow Renewal Description Midnight.take() only applies the transient liquidation lock to the seller on the obligation being traded. In a V2-to-V2 borrow renewal, that traded obligation is the target obligation. The source obligation is different, but MorphoV2ToV2BorrowRepay.onSell() later repays that source debt and moves source collateral. When the matched offer has offer.buy == true, the maker is the buyer and offer.callback runs as the buyer callback before the user's seller callback. The canonical renewal ratifier validates the user's renewal callback data, but it does not reject a nonzero maker-side buyer callback. Consequently, a maker can sign a BUY offer whose callback touches the user's source obligation before the renewal callback repays it. If the source position is already unhealthy, past maturity, or otherwise eligible for liquidation-related actions, the maker callback can call DelayedLiquidationGate.startGracePeriod() on the source obligation or partially liquidate the source before the renewal repairs it. A full source liquidation usually makes the later repayment revert because repayBudget can exceed the remaining source debt. The practical profitable case is a partial liquidation that leaves enough source debt for MorphoV2ToV2BorrowRepay.onSell() to continue, letting the attacker capture liquidation premium inside the same renewal transaction. The grace-period case can also leave a source timer armed even if the renewal later makes the source healthy. Recommendation Lock every Midnight obligation that a borrow renewal will mutate, not only the traded target obligation. For V2-to-V2 borrow renewals, the source obligation should be locked for the borrower before Midnight.take() executes any buyer callback. If Midnight cannot support multi-obligation locks, the renewal ratifier or wrapper should reject nonzero maker-side buyer callbacks for borrow renewals where the user is the seller. An allowlist is also acceptable if only audited inert callbacks are permitted. Do not rely on the token reentrancy assumption as the mitigation, because this vector uses the normal buyer callback order. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Reentrancy ● Medium RenewalIntentRatifier.sol, Midnight.sol,Acknowledged 28 M-15 | executeAndConsume Burns Keeper consumeGroup Description executeAndConsume is supposed to couple a routed fill with a consumed update for the user whose position is being filled. The batch itself is executed on behalf of params.taker: TakeRouter authorizes the caller as params.taker or a Midnight-approved delegate, and the downstream takes use params.taker as the taker identity. However, the final setConsumed call uses initiator() instead of params.taker: function executeAndConsume(ExecuteParams calldata params, Action\[\] calldata actions, bytes32 consumeGroup) external onlyBundler3 ... return (totals\[0\], totals\[1\], totals\[2\]); } • A user authorizes a keeper on Midnight and relies on executeAndConsume to fill a self-limiting order group. • The keeper calls the adapter with params.taker = user. • The batch moves the user's position and returns non-zero rawTotals. • The adapter updates consumed for the keeper's namespace, leaving consumed\[user\]\[consumeGroup\] unchanged. As a result, the user's order/group remains live even though the routed fill already happened. Any flow that relies on consumeGroup to cap or one-shot a user-owned order can therefore be overfilled or replayed through delegated execution. Recommendation Replace initiator() with params.taker in the final setConsumed path. Resolution Tenor Team: The issue was resolved in PR#418. CategorySeverityLocationStatus Logical Error ● Medium TakeRouterAdapterBase.solResolved 29 M-16 | Keeper Adapter Callbacks Reject Delegation Description TakeRouterAdapterBase explicitly supports delegated execution: the bundle submitter can differ from params.taker as long as the taker has authorized that caller on Midnight. This means a keeper can submit a Bundler3 bundle on behalf of a user and still pass the router's access control. However, the adapter's own Midnight callbacks reject that same delegated shape. onBuy and onSell require the Midnight buyer or seller to equal initiator(): function onBuy(...) external returns (bytes32) { require(msg.sender == address(MORPHO\_MIDNIGHT), ErrorsLib.UnauthorizedSender()); require(buyer == initiator(), ErrorsLib.UnauthorizedSender()); // bundle submitter must be buyer ... } For \`MIDNIGHT\_TAKE\`, Midnight passes the actual take ... into those callback roles, not the delegated caller: // sell offer: taker is buyer ( taker, takerCallback, takerCallbackData, offer.maker, offer.callback, offer.callbackData, offer.receiverIfMakerIsSeller ) As a result, any route that uses \`TenorAdapter\` itse ... r == params.taker\`, and the adapter callback reverts. Recommendation buyer/seller should be validated against the taker authorization model, not strict equality with initiator. Mirror authorization in TakeRouter: address caller = initiator(); require( buyer == caller || MORPHO\_MIDNIGHT.isAuthorized(buyer, caller), ErrorsLib.UnauthorizedSender() ); Resolution Tenor Team: The issue was resolved in PR#419. CategorySeverityLocationStatus Logical Error ● Medium MidnightAdapterBase.solResolved 30 M-17 | Adapter Callbacks Cannot Batch Reentries Description TakeRouter is designed to fill a user's position across multiple actions in one call. TenorAdapter exposes that router through Bundler3, and its Midnight callbacks reenter Bundler3 through \_midnightCallback. The problem is that Bundler3 only supports one expected reentry per top-level call. Before each call in multicall, it stores a single reenterHash derived from that call's callbackHash: for (uint256 i; i < bundle.length; ++i) { address to = bundle\[i\].to; bytes32 callbackHash = bundle\[i\].callbackHash; ... require(reenterHash == bytes32(0), ErrorsLib.MissingExpectedReenter()); } That is incompatible with a single adapter.execute call that dispatches multiple MIDNIGHT\_TAKE actions where more than one action uses TenorAdapter as its taker callback. TakeRouter loops through all actions inside the same top-level call and the first adapter callback consumes the only pending reentry: function onSell(...) external returns (bytes32) { require(msg.sender == address(MORPHO\_MIDNIGHT), ErrorsLib.UnauthorizedSender()); require(seller == initiator(), ErrorsLib.UnauthorizedSender()); \_midnightCallback(data); // consumes the only pending reenterHash return CALLBACK\_SUCCESS; } After that callback returns, reenterHash is cleared. If a later action in the same adapter.execute call reaches another adapter callback, \_midnightCallback calls Bundler3.reenter again with no matching pending hash. Bundler3 reverts with IncorrectReenterHash, causing the action or the whole batch to fail. A concrete example: • Alice submits one adapter.execute batch with two actions, filling Bob's offer and Carol's offer. • Both actions set takerCallback = address(TenorAdapter) because each fill needs a reentrant Bundler3 funding step. • Bundler3 stores one reenterHash for the outer adapter.execute call. • Bob's fill reaches TenorAdapter.onSell, calls Bundler3.reenter, and consumes that hash. • Carol's fill later reaches TenorAdapter.onSell in the same router loop, calls Bundler3.reenter again, and reverts because reenterHash is already zero. Recommendation Consider a redesign of Tenor adapter operations around Bundler3's one-reentry-per-top-level-call model. Resolution Tenor Team: The issue was resolved in PR#420. CategorySeverityLocationStatus Compatibility ● Medium Bundler3.sol, TakeRouter.sol, MidnightAdapterBase.sol Resolved 31 M-18 | executeAndConsume Accepts Intervening Fills Description executeAndConsume() executes the whole router batch before it reads the user's current consumed value for consumeGroup. It then writes currentConsumed + rawTotals\[params.fillIndex\]. (uint256\[3\] memory totals, uint256\[3\] memory rawTotals) = \_executeResolvingSentinels(params, actions); address caller = initiator(); \_MIDNIGHT.setConsumed( consumeGroup, \_MIDNIGHT.consumed(caller, consumeGroup) + rawTotals\[params.fillIndex\], caller ); This is unsafe when executeAndConsume() is used to implement an OCO-style action, where a user executes one route and wants the same transaction to consume or cancel a standing offer group. \_executeResolvingSentinels() can call untrusted Midnight ratifiers, maker callbacks, taker callbacks, token transfers, and fee adjusters before the final setConsumed() call. Any of those external components can fill the user's live offer in consumeGroup before executeAndConsume() performs its final write. Midnight's own take() checks the offer cap while it fills the offer. For a unit-capped offer, it increments consumed\[offer.maker\]\[offer.group\] and requires the result to be at most offer.maxUnits. However, setConsumed() is a separate monotonic setter. It only requires the new value to be greater than or equal to the current value. It does not know the max value of any signed offer in that group, and it intentionally accepts values above the offer cap. Consequently, a group can be filled once through the live offer and then advanced again by executeAndConsume() in the same transaction. For example, Alice has a standing sell offer in group G with maxUnits = 100. Alice submits executeAndConsume(..., consumeGroup = G) to sell 100 units elsewhere and mark G as consumed. During the router execution, a malicious maker callback fills Alice's standing offer for 100 units. The standing offer's own fill is valid, so Midnight sets consumed\[Alice\]\[G\] = 100. The router then completes Alice's other 100 unit fill. Finally, executeAndConsume() reads the now-current consumed value and sets consumed\[Alice\]\[G\] = 200. The transaction does not revert even though Alice's standing offer was capped at 100 units. Alice experiences both fills. The impact is not arbitrary theft because the intervening fill must use an offer Alice already made available, and the router fill must also pass Alice's submitted route constraints. The broken property is the OCO guarantee: one fill is supposed to consume the shared capacity before the other can execute. A malicious counterparty, ratifier, or callback reached by the route can force both sides to execute under realistic conditions. The same race exists across transactions when the live offer is filled in the mempool before Alice's executeAndConsume() transaction lands, because the adapter has no expectedConsumed input. Recommendation Make executeAndConsume() bind the final write to an expected starting consumed value. The caller should provide expectedConsumed, and the adapter should check it before any external execution. After the router returns, the adapter should require that the same consumeGroup is still unchanged before writing the final value. function executeAndConsume( ExecuteParams calldata params, Action\[\] calldata actions, ... return (totals\[0\], totals\[1\], totals\[2\]); } If the intended behavior is hard reservation rather than exact OCO accounting, pre-consume expectedConsumed + maxFill before external calls and then document that unused capacity is burned. If delegated execution remains supported, apply the same snapshot to the account whose group is being consumed. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Medium TakeRouterAdapterBase.solAcknowledged 32 M-19 | Clamps Ignore Revoked Callback Auth / Allowances Description Several withdrawal-backed clamps size actions from balances, debt, or withdrawable liquidity only, but they do not verify whether the callback still has the authorization or token allowance required to execute the action. Examples: • VaultWithdrawClamp assumes the callback can withdraw that collateral from Midnight on the user's behalf. • V2ToV2LendWithdrawableClamp assumes the callback is still authorized to call Midnight.withdraw(...). • V1ToV2LendClamp does not check whether the vault-share allowance to MorphoV1ToV2LendCallback is still in place. Those assumptions are revocable. A user can: • revoke Midnight authorization for the callback contract • revoke the callback's ERC20 / ERC4626 allowance • leave an old offer signed and otherwise valid After that, the clamps still report the offer as fillable because they only inspect economic state. But the real callback path reverts when it attempts the privileged operation. The clamp output a likely offchain executability signal for production keepers, especially on callback-heavy routes where the callback-specific clamp is the intended way to avoid reverts. Because of that, the issue creates a real griefing vector: 1. A user signs a callback-backed offer and leaves it visible to the keeper network. 2. The same user later revokes the callback authorization or allowance needed by the callback path. 3. The keeper still sees a positive clamp result, because the clamp only checks economic state. 4. The keeper routes the action onchain. 5. The callback reverts on the missing auth / allowance. 6. If the action is batched with allowRevert = false, the entire batch aborts, including unrelated fills for other users. The attacker cost is low: they do not need to break pricing or capital constraints, only to publish a seemingly valid signed offer and then revoke the callback permission that the clamp forgot to model. The impact is an actionable DoS / gas-griefing vector against keeper infrastructure: • takers and keepers can be lured into reverted fills • routed batches can be deterministically reverted when allowRevert is not set • unrelated users in the same keeper batch can be denied execution • off-chain systems that treat clamp output as a reliable executability signal will overestimate available liquidity Recommendation For each clamp, include the callback's required authority in the view check when that authority is cheap to read on-chain. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus DoS ● Medium VaultWithdrawClamp.sol, V2ToV2LendWithdrawableClamp.sol, V1ToV2LendClamp.sol, Acknowledged 33 M-20 | Tenor Prioritizes Midnight's Trading Fees Description Tenor callback fees are computed from the actual asset amount passed by Midnight into the callback, but that amount may already include Midnight's target obligation trading-fee adjustment. As a result, Midnight trading fees can consume the Tenor fee spread and cause a configured nonzero Tenor callback fee to settle as zero. This was originally observed in MorphoV2ToV2BorrowRepay.onSell(). For BUY offers, Midnight deducts the target obligation trading fee from the borrower's sellerAssets before calling the borrower-side seller callback. The callback then compares that already-reduced sellerAssets against a seller effective price derived only from offer.tick and the Tenor fee rate. When the target Midnight trading fee is larger than the Tenor fee spread, CallbackLib.sellerFeeFromTick() zero-floors the fee to 0. The source debt is still repaid and collateral is migrated, so the renewal succeeds while the configured Tenor fee recipient receives nothing. The same issue also affects buyer-side lender flows. In MorphoV2ToV2LendWithdrawable.onBuy() and MorphoV1ToV2LendCallback.onBuy(), Midnight passes buyerAssets after adding the target obligation trading fee. Both callbacks compute the Tenor fee with CallbackLib.buyerFeeFromTick(..., buyerAssets), which compares the Tenor buyer budget against the already-inflated buyerAssets and zero-floors the result. If the Midnight trading fee exceeds the Tenor buyer-side fee spread, the lender renewal or migration still succeeds and the lender receives target credit, but the Tenor fee recipient receives no fee. Recommendation Define the intended Tenor fee base explicitly and make callback fee calculation trading-fee-aware. If Tenor fees should apply independently of Midnight trading fees, compute Tenor fees from the tick-priced raw asset amount before Midnight target trading fees are applied, or pass both raw tick assets and trading-fee-adjusted settlement assets into the callback. Do not compute Tenor fees solely from sellerAssets or buyerAssets after Midnight trading-fee adjustment. If Midnight trading fees intentionally take priority, document that nonzero Tenor fee configs can settle with zero callback fee across both borrower-side and lender-side renewal flows. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Logical Error ● Medium src/callbacks/MorphoV2ToV2BorrowRe pay.sol Acknowledged 34 M-21 | Keeper Gets Full Midnight Control Description Normal renewal execution through RenewalIntentTakeOnBehalf.take() does not require the user to authorize a keeper on Midnight. The renewal orchestrator is permissionless at the caller layer: any caller can submit a valid renewal, while the user-controlled protections come from user -> takeOnBehalf authorization on Midnight and user -> ratifier authorization on the renewal orchestrator. The ratifier then enforces the source/target markets, maturity window, rate policy, callback schema, and fee config. The delegated Router path adds a different requirement. TakeRouter.\_execute() allows a non-user caller only when Midnight.isAuthorized(params.taker, caller) is true. For direct Router execution this means the user must authorize the keeper address. For TenorAdapter, \_caller() resolves to the Bundler initiator, so the same condition applies to the keeper/initiator. The Router or adapter must also be authorized on Midnight so it can call Midnight.take(... taker = user ...). That keeper authorization is not scoped to Router or renewal execution. It is full Midnight operator authority. Once granted, the keeper does not need Router at all: it can call Midnight directly as the user, bypassing RenewalIntentTakeOnBehalf and all renewal-ratifier checks. A malicious or compromised keeper can accept arbitrary valid offers as the user, choose receiverIfTakerIsSeller, withdraw the user's credit to any receiver, withdraw collateral while health checks allow, cancel offers via setConsumed or shuffleSession, and grant further authorization with setIsAuthorized. This is a privilege-scope mismatch. Users may reasonably understand a keeper approval as limited automation for renewal fills, but the approval needed by the Router keeper path gives the keeper direct control over the user's Midnight account. Recommendation Do not require users to grant keepers direct Midnight authorization for Router-based renewal automation. Route delegated execution through a scoped contract authorization instead: the user should authorize a contract that enforces renewal-only action types, ratifier checks, receiver constraints, and callback schemas before calling Midnight. Alternatively, add a scoped authorization layer in Midnight or the Router that distinguishes "can call Router renewal flow" from "can operate all Midnight account functions." If the intended model is that keepers are fully trusted Midnight operators, document this explicitly in user-facing integration docs and avoid presenting keeper approval as limited renewal automation. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Access Control ● Medium src/periphery/TakeRouter.solAcknowledged 35 M-22 | TenorAdapter Bypasses Midnight's Repay Auth Description Midnight's account authorization check trusts msg.sender: a caller can act for onBehalf only if onBehalf == msg.sender or isAuthorized\[onBehalf\]\[msg.sender\] is true. Once a user authorizes TenorAdapter, Midnight correctly trusts calls from that adapter. The adapter must therefore bind every forwarded account action to the real Bundler3 initiator(). However, midnightRepay() fails to do this and accepts arbitrary onBehalf, so any unrelated Bundler3 initiator can call TenorAdapter.midnightRepay(sourceObligation, 1, 0, victim). Midnight sees msg.sender == TenorAdapter, so the victim's adapter authorization is enough for the repay to pass even though the actual bundle initiator is not authorized by the victim. Impacts: • exact V2-to-V2 borrow renewals can be reverted. A 1 wei adapter repay lowers live source debt while the signed renewal still uses the old exact repayBudget; MorphoV2ToV2BorrowRepay.onSell() then reverts with ExcessRepayment. • exact V2-to-V1 borrow exits can be reverted. The same dust repay makes an old exact BUY fill cross the borrower from debt into tiny credit, causing MorphoV2ToV1BorrowCallback to revert on PositionCrossing. • reduce-only BUY offers can be invalidated. If a previously exact debt-reducing fill now exceeds live debt by 1 wei, Midnight rejects it because the maker would increase credit despite reduceOnly. • exact repayment liquidations can be invalidated. A liquidator transaction quoted with repaidUnits == borrowerDebt becomes stale after a 1 wei repay and reverts, so liquidation bots must re-read and requote before retrying. Recommendation Pin midnightRepay() to the current Bundler3 initiator, matching the rest of the account-mutating adapter surface Resolution Tenor Team: The issue was resolved in PR#432. CategorySeverityLocationStatus Validation ● Medium src/bundler/MidnightAdapterBase.solResolved 36 M-23 | Arbitrary Policies Can Gas Grief Keepers Description A malicious user can install an arbitrary interestRatePolicy, and BaseRenewalRatifier will call it on every renewal through getRate. The policy could perform arbitrarily expensive read-only work before returning or reverting. Users fully control the policy address, and the docs describe StaticRatePolicy and PausableStaticRatePolicy as examples rather than an allowlisted set. A malicious policy can: • burn substantial gas through expensive read-only logic before reverting, • selectively reverts based on caller, block.timestamp, or other execution context, • passes one offchain simulation and fails during real inclusion when the environment changes slightly. This is more impactful than ordinary revoked auth / allowance grief. Those failures are typically cheap; here the router only catches the failure after the policy has already consumed most of the gas forwarded (EIP-150 63/64 rule) into \_TAKE\_ON\_BEHALF.take. Hence, allowRevert = trueis ineffective here as most of the gas budget is lost, preventing subsequent actions from continuing. Recommendation Consider capping the gas forwarded per takeOnBehalf action in TakeRouter(or alternatively at the getRate step) so that a single bad policy does not drain the keeper's remaining gas budget and allowRevert remains effective in practice. More defensively, consider allowlisting approved interestRatePolicy implementations, or requiring policies to come from a trusted factory. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus DoS ● Medium src/RenewalIntentTakeOnBehalf.solAcknowledged 37 M-24 | Permissionless First Touch Freezes Default Fees Description touchObligation() can be called by anyone and the first caller copies the current default trading and continuous fees into the obligation. Later calls to setDefaultTradingFee() or setDefaultContinuousFee() do not affect obligations that were already touched. The fee setter can repair a known touched obligation with setObligationTradingFee() and setObligationContinuousFee(), but default fee updates will not repair it automatically. Consequently, a searcher can pre-create predictable future obligations before the fee setter raises defaults. Those obligations keep the old fee schedule even after governance expects new markets for the same loan token to use higher fees. Users and keepers can then trade against the pre-created obligations and avoid both the trading fee and the continuous fee until each obligation is discovered and patched. For a concrete Tenor case, assume USDC renewals are expected to use a known collateral set, oracle set, LLTV, gate configuration, and four-week maturity cadence. During launch, the default Midnight fees for USDC are still zero or temporarily lower. A searcher can enumerate the next several Tenor target obligations from those public parameters and call touchObligation() for each future maturity. When Morpho later raises the USDC default trading and continuous fees, those pre-created target obligations keep the old schedule. A borrower or lender can then route renewals through RenewalIntentTakeOnBehalf into the pre-created obligations and settle at the intended Tenor market and maturity, but Midnight charges the stale fees stored on first touch. The same renewal into an otherwise identical target obligation created after the fee update would accrue protocol fees. The obligations can be created by anyone, the future Tenor obligation ids are predictable from public market parameters and skipped fees are not recoverable. Any trading fee skipped before the obligation-specific repair is permanently lost. Continuous-fee liability for credit opened while the stale fee was active is also fixed from the old continuousFee. A later repair only applies to new credit opened after the patch. It does not retroactively charge the correct liability to already-opened credit, so existing credit keeps the stale fee value through maturity. Recommendation Do not let arbitrary callers lock in protocol defaults for future obligations. Either require an authorized fee setter or market creator to initialize fee-bearing obligations or make untouched obligations read current defaults until the first real position-changing action. If immutable per-obligation fees are required, Tenor should treat fee initialization as an explicit market-activation step. Before allowing renewals into a known obligation, compute its id, call touchObligation() if needed and have Midnight's feeSetter call setObligationTradingFee() for all seven trading-fee breakpoints plus setObligationContinuousFee() for that obligation. The ratifier can also reject target obligations whose stored tradingFees(id) or continuousFee(id) do not match the expected schedule. This recovery only protects future trades after the obligation-specific fee update. It cannot recover trading fees already skipped. It also cannot retroactively assign the correct continuous-fee liability to credit that was opened while the stale fee was active. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Medium Midnight.solAcknowledged 38 M-25 | Fresh Credit Drains Old Repayments Description Midnight tracks repaid cash in one obligation-level withdrawable bucket. repay() adds units to that bucket, and withdraw() lets any current credit holder burn credit and pull from the bucket. There is no accounting that ties a repayment to the lenders whose credit existed when that repayment happened. That lets a new buyer take a discounted SELL offer, receive fresh credit, and immediately redeem that fresh credit against old repayments at par. For example, if an obligation already has 100e18 withdrawable units and a borrower sells 100e18 new units at a discounted price, the buyer can pay less than 100e18, receive 100e18 credit, then call withdraw() for 100e18. The buyer captures the discount while the old lenders' repaid liquidity is drained and replaced with exposure to the new borrower's debt. This also explains the callback self-funding variants. Midnight.take() updates buyer credit before callbacks and before final payment settlement. If a callback path can call withdraw() on the same obligation, the fresh credit created by the in-flight take is treated as source credit for old withdrawable liquidity: • MorphoV2ToV2LendWithdrawable.onBuy() can be called with sourceObligation == obligation, so direct callback data can withdraw buyerAssets + fee from the same obligation immediately after the buyer credit is minted. • MidnightAdapterBase.onBuy() can reenter Bundler3 before approving Midnight to collect buyerAssets, and the reentered bundle can call midnightWithdraw() against the same obligation. Those callback routes remove the buyer's upfront capital requirement, but they are not a separate economic root cause. They are same-transaction manifestations of the broader issue: newly created credit can claim older repayments from the global withdrawable pool. Even without a callback, the buyer can fund the discounted purchase with their own balance or external flash liquidity and withdraw old repayments after the take settles. If the offer uses a zero-price tick, the same entitlement bug becomes a zero-principal version of the attack. The existing zero-price cap issue covers the cap-accounting footgun, but the loss here is the ability of newly created credit to claim older repayments. The root cause is that take() creates transferable/redeemable buyer credit immediately: uint256 buyerCreditIncrease = UtilsLib.zeroFloorSub(units, buyerPos.debt); uint256 sellerCreditDecrease = UtilsLib.min(units, sellerPos.credit); uint256 sellerDebtIncrease = units - sellerCreditDecrease; ... \_obligationState.totalUnits = UtilsLib.toUint128(\_obligationState.totalUnits + buyerCreditIncrease - sellerCreditDecrease); Then withdraw() only checks the caller's current credit and the obligation's global withdrawable balance. It never checks whether that credit existed when the repaid liquidity entered the bucket: \_updatePosition(obligation, id, onBehalf); Position storage \_position = position\[id\]\[onBehalf\]; uint128 pendingFeeDecrease; ... \_obligationState.totalUnits -= UtilsLib.toUint128(units); SafeTransferLib.safeTransfer(obligation.loanToken, receiver, units); Recommendation Make repayment entitlement position-indexed instead of first-come-first-served. A typical fix is to maintain a global withdrawal index that increases on repayment, track each position's index and initialize newly created credit at the current index so it cannot claim older repayments. As a blunt mitigation, reject debt-increasing takes while an obligation has nonzero withdrawable, but that is likely too restrictive. The durable fix is to separate old repayment claims from newly minted credit. Callback-specific guards are still useful defense in depth. MorphoV2ToV2LendWithdrawable.onBuy() should reject sourceObligationId == targetObligationId, and generic adapter buy callbacks should not be allowed to withdraw newly created same-obligation credit during the in-flight take. Those local guards reduce the no-capital self-funding paths, but they do not fix the post-settlement version where a buyer funds the discounted purchase and then withdraws old repayments at par. Related follow-up issue: I-16. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Medium Midnight.sol, MorphoV2ToV2LendWithdrawable.sol Acknowledged 39 L-01 | allowRevert Misses Pre-dispatch Reverts Description allowRevert only catches failures from \_TAKE\_ON\_BEHALF.take() and \_MIDNIGHT.take(). Both dispatch helpers do meaningful work before reaching those try/catch blocks. They decode action data, call \_MIDNIGHT.touchObligation() and call \_capTakeUnits(). \_capTakeUnits() then reaches RouterLib.budgetToUnits(), ClampLib.getOfferRemaining() and any caller-supplied fee adjuster or clamp. Those operations can revert before the router has entered the soft-fail boundary. For example, ClampLib.getOfferRemaining() computes offerPrice - tradingFee for buy offers without a zero floor. A low-tick buy offer can therefore underflow before \_TAKE\_ON\_BEHALF.take() or \_MIDNIGHT.take() is attempted. Similarly, asset-denominated sizing can revert inside TakeAmountsLib before the action reaches the catch block. Consequently a single malformed or adversarial offer can abort the entire batch even when its Action.allowRevert flag is set. This contradicts the documented soft-failure behavior and creates a griefing target for keepers that aggregate untrusted actions. Recommendation Move all maker-controlled pre-dispatch work inside the same failure-isolation boundary as the downstream take. A practical fix is to wrap obligation touching and unit capping in an internal call that can be caught, then convert any failure into (false, 0, 0, 0, reason) when allowRevert is enabled. Also normalize known sizing edge cases, such as zero prices and tradingFee > offerPrice, so they return a safe zero cap where possible instead of panicking before the action can be skipped. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Low TakeRouter.solAcknowledged 40 L-02 | Frozen Gate Trusts Fee Recipients Description canReceiveShares does not rely only on the gate's own allowlist. It also returns true for whatever addresses msg.sender currently exposes as managementFeeRecipient() and performanceFeeRecipient(). That means renounceOwnership() does not freeze the effective receive-share whitelist. VaultV2 keeps setManagementFeeRecipient and setPerformanceFeeRecipient as live curator actions, so the curator can nominate any non-zero address after the gate owner has burned control. The new recipient becomes immediately eligible to receive shares even if it was never present in allowlist. This also works when both fee rates are zero, because the gate checks only the recipient fields. This matters because the gate is documented as the mechanism that keeps vault shares away from secondary markets and lending venues. A curator can later reopen that surface for an arbitrary EOA or protocol without touching the gate mapping and without re-enabling setReceiveSharesGate. Consequently, the "immutable allowlist" guarantee is weaker than it appears, and the share-receive restriction can be relaxed after the setup is supposedly frozen. Recommendation Do not derive share-receive permission from live vault fee-recipient fields. Require fee recipients to be explicitly allowlisted, then freeze only the gate's own storage. If the auto-whitelist must remain, the deployment process should also abdicate both fee-recipient setters before the gate is treated as frozen and that requirement should be documented clearly. Resolution Tenor Team: The issue was resolved in PR#452. CategorySeverityLocationStatus Trust Assumptions ● Low VaultV2AllowlistGate.solAcknowledged 41 L-03 | Vault-share Exit Ignores Vault Withdrawal Limits Description onBuy sizes the collateral pull with previewWithdraw(buyerAssets) and then immediately calls withdraw(buyerAssets, ...). That only proves how many shares are needed at the current exchange rate. It does not prove that buyerAssets is actually withdrawable from the vault. ERC4626 explicitly separates previewWithdraw from withdrawal limits. previewWithdraw must ignore limits such as queues, cooldowns and liquidity ceilings, while withdraw must revert if the requested assets cannot actually be withdrawn. This callback never checks any executable withdrawal bound after it receives the buyer's shares, so a position can hold enough vault-share value on Midnight while the vault still refuses to redeem the requested assets. In that state the callback reverts inside withdraw, which makes the BUY offer unfillable. This is a liveness issue on its own. It becomes a taker-griefing issue because VaultWithdrawClamp bounds fills with convertToAssets(userVaultShares) instead of redeemable liquidity. Routers and keepers can therefore derive a non-zero fill size for a callback execution that is guaranteed to revert once the vault enforces its withdrawal ceiling. The intended VaultV2 integration has the same problem, but the right bound is not maxWithdraw: VaultV2.maxWithdraw() deliberately returns 0 because the vault cannot make a revert-free promise when gates may be installed. VaultV2.withdraw() can still revert after the clamp returns a positive value because exit() checks canSendShares(onBehalf) and canReceiveAssets(receiver), then may call deallocateInternal() through the liquidity adapter. The Morpho Blue adapter withdraws exact assets from Morpho Blue and the MetaMorpho adapter calls the child vault's withdraw(). Both can fail when liquidity, queues, adapter state, or the underlying vault make the requested assets unavailable. The current clamp does not inspect any of those conditions. Recommendation Do not treat share balance as redeemable liquidity. For generic ERC4626 vaults, the callback should check the actual withdrawal bound, for example maxWithdraw(address(this)), after the shares are transferred in and before calling withdraw. Clamps should only advertise capacity when they have a reliable pre-transfer bound for the supported vault. For VaultV2, do not rely on maxWithdraw(). The clamp should check that the vault exit can actually go through: the right gate permissions must be in place, and the liquidity adapter must be able to provide the requested assets. If the clamp cannot reliably tell how much is withdrawable, it should return zero for that vault type or clearly document that these fills may revert. It should not use convertToAssets() as if share value always means withdrawable liquidity. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Low MorphoV2WithdrawVaultSharesCallback .sol Acknowledged 42 L-04 | Zero-price Asset Caps Act Unbounded Description Midnight.take() lets makers cap an offer by maxUnits, maxSellerAssets, or maxBuyerAssets. Unit-capped offers advance consumed\[maker\]\[group\] by the filled units. Asset-capped offers instead advance consumed\[maker\]\[group\] by the computed asset amount. That distinction breaks down when the selected asset amount is zero for a nonzero unit fill. TickLib.tickToPrice(0) returns 0. For a SELL offer at that tick, sellerPrice is zero, so sellerAssets is also zero for any positive units. uint256 offerPrice = TickLib.tickToPrice(offer.tick); uint256 sellerPrice = offer.buy ? offerPrice - \_tradingFee : offerPrice; uint256 buyerPrice = sellerPrice + \_tradingFee; ... require(newConsumed <= offer.maxSellerAssets, ConsumedSellerAssets()); } For a SELL offer capped by maxSellerAssets, every nonzero fill transfers obligation units while adding zero to the cap counter. The signed asset cap never becomes exhausted in unit terms. If the seller already owns credit and marks the offer reduceOnly, takers can buy that existing credit for zero seller proceeds until the seller's credit is gone. The practical bound is the seller's remaining credit, not the finite asset cap. This should be treated as a maker or integration footgun rather than arbitrary theft. The maker must authorize a zero-price asset-denominated offer, or an integration must create one on the maker's behalf. A maker who manually signs this offer has effectively agreed to a zero price. The unsafe part is that a finite asset cap may look like an exposure cap even though it cannot bound units when the selected asset leg is always zero. Zero-price trading is also not impossible input in this codebase. Midnight's tests cover zero-price unit fills, and periphery helpers explicitly treat zero price as unbounded unit capacity. For example, ClampLib.getOfferRemaining() returns type(uint128).max when a finite seller-asset cap has a zero seller price. Consequently, router sizing can treat the same finite asset-capped offer as having effectively unbounded remaining unit capacity. The cleanest case is a SELL offer with tick == 0, maxSellerAssets > 0, and no unit cap. This case does not depend on buyer-side trading fee for the cap itself because SELL sellerAssets uses offerPrice directly. If trading fee is nonzero, the taker may still pay the protocol fee, but the seller receives zero assets and consumed remains unchanged. In the no-fee case, both buyer and seller asset amounts are zero. For example, Alice owns 100e18 units of credit on a Midnight obligation. She signs a reduce-only SELL offer with tick = 0, maxSellerAssets = 1, maxUnits = 0, and a fresh group. Bob fills 60e18 units. Midnight.take() emits buyerAssets = 0, sellerAssets = 0, and units = 60e18. Alice's credit falls by 60e18, Bob's credit rises by 60e18, and consumed\[Alice\]\[group\] is still zero. Bob can then fill the same offer for another 40e18 units because the group cap still appears unused. Recommendation Document that zero-price asset-denominated caps do not bound unit exposure. Maker-facing integrations should reject this configuration unless the user explicitly opts into a free unit transfer. Reject asset-denominated offers when the selected asset leg can be zero for a positive unit fill. For the main SELL case, require offerPrice > 0 whenever offer.buy == false and maxSellerAssets > 0. For BUY offers, require the relevant buyer or seller price to be positive before allowing the corresponding asset cap. The periphery should also avoid reporting unbounded unit capacity for finite zero-priced asset caps unless that behavior is deliberately part of the public API. ClampLib.getOfferRemaining() can return 0 for such offers, or expose a distinct result that callers must handle explicitly. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Low Midnight.solAcknowledged 43 L-05 | isFinalFill Never True With Non-Zero Fee Description CollateralTransferLib checks isFinalFill = (sourceDebtBefore == repaidUnits) to decide whether to sweep all source collateral. But repaidUnits is set to sellerAssets - fee in the callback, which is always strictly less than sourceDebt when fee > 0 — sellerFeeFromTick uses ceiling arithmetic, so fee ≥ 1 whenever interest rate > 0. The equality can never hold, and isFinalFill is permanently false for all fee-enabled renewals. As a result, every fee-enabled renewal leaves a residual debt (≈ fee in loan token units) and a proportional share of source collateral stranded in the source obligation. At standard parameters on a 1000 ETH position this amounts to ~4.67 ETH of residual debt and ~24.8 ETH of stranded collateral. The zero-fee path is unaffected. Recommendation Replace the static budget comparison with a post-repay on-chain state check: bool isFinalFill = MORPHO\_MIDNIGHT.debtOf(sourceObligationId, seller) == 0; Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Rounding ● Low src/callbacks/MorphoV2ToV2BorrowRe pay.sol:53 Acknowledged 44 L-06 | Fee Recipients Can Get Stuck Shares Description The contract special-cases fee recipients only in canReceiveShares. canSendShares and canReceiveAssets still read the static allowlist with no equivalent exemption. When the same gate is installed as sendSharesGate, VaultV2.accrueInterest() can mint fee shares to a management or performance fee recipient that was never manually allowlisted. Those shares arrive successfully because canReceiveShares returns true, but any later redeem or withdraw from that recipient reverts because VaultV2.exit() requires canSendShares(onBehalf). If receiveAssetsGate is also set, the recipient may fail that check as well. If ownership has already been renounced, there is no recovery path inside the gate. Fees can keep accruing and diluting other holders, while the corresponding claim stays stranded behind the exit gates. This turns a convenience exemption for fee minting into a permanent liveness failure for fee realization whenever sendSharesGate is active and fee recipients were not explicitly granted exit permissions. Recommendation Fee recipients need an exit permission set as well as a mint permission set. Either require operators to add explicit canSendShares and, when relevant, canReceiveAssets permissions for fee recipients before freezing the gate, or make the fee-recipient exemption configurable for the directions needed to redeem fees. If the intended deployment only supports receiveSharesGate, document that using this contract as sendSharesGate requires manual fee-recipient allowlisting. Resolution Tenor Team: The issue was resolved in PR#413. CategorySeverityLocationStatus Unexpected Behavior ● Low VaultV2AllowlistGate.solResolved 45 L-07 | Clamps Ignore Active Collateral Cap Description SupplyCollateralCallbackClamp.clamp() sizes a MorphoV2SupplyCollateralCallback fill from the caller-supplied collateral amounts, the seller's token balances and allowances, current debt, and the projected health or raw max-LTV bound. It never checks whether the callback schedule would activate more collateral slots than Midnight allows for one borrower. Midnight permits obligations with up to 128 collateral parameters, but supplyCollateral() reverts with TooManyActivatedCollaterals() once a borrower's activated bitmap would exceed MAX\_COLLATERALS\_PER\_BORROWER (10). The supply callback calls supplyCollateral() once per nonzero scheduled slot. If the seller already has 10 active collateral slots, or if the callback schedule activates more than 10 fresh slots during a fill, the clamp can still return a positive max fill because it treats every nonzero collateralAmounts\[i\] as valid projected collateral. Execution then reverts part way through onSell() when the later slot is supplied. The same mismatch exists for V2-to-V2 borrow renewals. MorphoV2ToV2BorrowRepay.onSell() calls CollateralTransferLib.transferCollaterals(), which withdraws matching source collateral and re-supplies it into the target obligation. V2ToV2BorrowRepayClamp.clamp() only caps by source debt and reduce-only target accounting. It does not model the union of the borrower's already active target collateral slots and the source collateral slots that would become newly active during migration. A borrower can therefore have a clamp-reported fill where the target already has 10 active slots and the migrated source collateral would activate an 11th target slot, causing supplyCollateral() to revert. This does not let a taker bypass Midnight health or steal funds. The revert happens inside the same Midnight.take() call after the seller callback, so the prior debt updates, token transfers, approvals, and earlier collateral supplies roll back. The impact is liveness and keeper griefing: router users can receive a positive clamp quote for an offer that cannot execute under Midnight's real collateral-activation rule, and repeated allowRevert batches can burn gas on these doomed actions. Recommendation Make the clamps enforce the same activation-cap invariant before returning a nonzero fill. For SupplyCollateralCallbackClamp, read MIDNIGHT.activatedCollaterals(data.obligationId, offer.maker), count the already active bits, then add each callback slot where the current collateral is zero and the pro-rata supply for the candidate fill would be nonzero. For V2ToV2BorrowRepayClamp, either require source and target active-collateral compatibility before advertising a fill, or extend the clamp data so the clamp can compare the target active bitmap with the source collateral slots that CollateralTransferLib would migrate for the proposed fill. Return zero, or cap below the first nonzero activation, when the resulting target active slot count would exceed MAX\_COLLATERALS\_PER\_BORROWER. At minimum, offer builders should reject supply-collateral schedules and V2-to-V2 migration tuples that can activate more than 10 target slots for the borrower. The tighter fix is to put this check in the clamp layer so router sizing matches Midnight.supplyCollateral(). Resolution Tenor Team: The issue was resolved in PR#410. CategorySeverityLocationStatus Validation ● Low SupplyCollateralCallbackClamp.sol, V2ToV2BorrowRepayClamp.sol Resolved 46 L-08 | Fragmented Fills Skip Discrete Collateral Description onSell computes each collateral deposit independently for each fill with mulDivDown(). If a nonzero configured collateral amount is small relative to offerSellerAssets, a taker can split the offer into fills where every individual supplyAmount rounds down to zero. The callback then skips the transfer and supplyCollateral() call for that slot, even though the same total fill executed in one transaction would have supplied collateral. This affects seller-asset-denominated offers, not only unsupported maxUnits offers. For example, a callback schedule can require one raw unit of a 0-decimal collateral token for a full 100e18 seller-asset fill. If the taker instead fills the offer as one hundred 1e18 fills, each fill computes 1 \* 1e18 / 100e18 = 0 raw collateral units. The taker creates the same aggregate debt while none of that configured callback collateral is supplied. This can remain healthy when the seller already has enough collateral, but the final collateralization differs from the signed callback schedule. The final impact is configuration-dependent. It is most relevant for low-decimal collateral, small configured collateral amounts, loose or disabled maxLtv and accounts with enough existing collateral for dust fills to pass Midnight's final health check. It does not directly steal funds, but it lets a taker consume an offer while leaving the maker with less callback-supplied collateral than the maker would get from an equivalent full fill. Recommendation Do not rely on per-fill floor rounding for exact collateral schedules. Enforce a minimum fill size for every nonzero collateral slot so amounts\[i\] \* sellerAssets / offerSellerAssets is nonzero whenever that slot is expected to contribute collateral. If exact aggregate collateral is required across arbitrary partial fills, add stateful residual accounting keyed by the offer or group so rounding dust carries into later fills. As a simpler operational mitigation, require meaningful maxLtv caps and reject low-decimal callback configurations where the configured amount is too small relative to the allowed minimum fill. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Rounding ● Low MorphoV2SupplyCollateralCallback.solAcknowledged 47 L-09 | Target-side Netting Changes Renewal Semantics Description Midnight nets a user's existing opposite-side position on the target obligation before creating new credit or debt. Tenor's V2-to-V2 renewal callbacks assume the target fill opens or increases the expected target position, but Midnight.take() first consumes any existing opposite-side position. For borrow renewals, the user is the seller on the target obligation. If the borrower already has credit on the target obligation, the fill consumes that credit before creating new target debt: uint256 sellerCreditDecrease = UtilsLib.min(units, sellerPos.credit); uint256 sellerDebtIncrease = units - sellerCreditDecrease; sellerPos.credit -= UtilsLib.toUint128(sellerCreditDecrease); sellerPos.debt += UtilsLib.toUint128(sellerDebtIncrease); MorphoV2ToV2BorrowRepay.onSell() then repays source debt and migrates collateral as if the fill were a normal borrow renewal. Economically, the user's existing target lend was sold/netted to fund the migration. For lend renewals, the user is the buyer on the target obligation. If the lender already has target debt, the fill reduces that debt before creating new target credit. MorphoV2ToV2LendWithdrawable.onBuy() still withdraws source credit to fund the BUY offer, so the route performs debt buyback rather than opening new target lending credit. The ratifier checks market IDs, fee config, rate, maturity, cadence, and callback data, but it does not reject nonzero target opposite-side positions. reduceOnly does not protect the user here because it constrains the maker, not the taker-side renewal subject. Consequently, a validated renewal can succeed while producing a different user-visible operation from the one configured: • borrow renewal can spend existing target credit instead of creating target debt; • lend renewal can spend source credit to pay down target debt instead of creating target credit; • callback events and route labels can describe a renewal while the economic result is netting/buyback. This is not necessarily unsafe when the user explicitly wants netting, but it should not be implicit in ordinary renewal routes. Recommendation For standard renewals, reject execution when the user has an opposite-side position on the target obligation: • V2-to-V2 borrow renewal: reject if updatePositionView(targetObligation, targetId, borrower).credit != 0. • V2-to-V2 lend renewal: reject if debtOf(targetId, lender) != 0. If netting is intended, expose it as a separate route with explicit naming, separate user opt-in, and events that report how much target credit/debt was netted versus newly opened. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Low Midnight.sol, MorphoV2ToV2BorrowRepay.sol, BaseRenewalRatifier.sol, MorphoV2ToV2LendWithdrawable.sol Acknowledged 48 L-10 | Oracle Validation Can Fail Open Or Freeze Description OracleWithValidationCheck can lose its secondary-validation property in several operational modes. When REVERT\_ON\_VALIDATION\_ORACLE\_PRICE\_FAILURE == false, any revert from the validation oracle is caught and the primary price is returned without cross-checking. This includes deliberate validation-oracle circuit breakers. The wrapper can therefore become primary-only exactly when the validation oracle is signaling failure. Graceful mode is also incomplete: Solidity try ... returns (uint256) catches call reverts, but malformed successful return data can revert during ABI decoding and bypass the intended graceful fallback. The owner can pause validation, and only the owner can unpause. If ownership is renounced while paused, the wrapper is permanently primary-only. Conversely, in strict mode, renouncing ownership removes the only built-in recovery control for future validation-oracle failure. Consequently, depending on deployment mode, the oracle can silently run without validation, remain permanently primary-only, or become permanently unusable when the validation oracle fails. Recommendation Use strict mode for safety-critical markets unless primary-only fallback is explicitly accepted. If graceful mode is kept, use low-level staticcall and validate returndata length before decoding. Override renounceOwnership() so it cannot be called while validation is paused and document that strict immutable deployments intentionally give up emergency pause recovery. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Oracle ● Low OracleWithValidationCheck.solAcknowledged 49 L-11 | Vault Executor Redeems Without Asset Floors Description MidnightVaultExecutor exposes redeem operations without caller-supplied minimum asset amounts. withdrawCollateralAndRedeem() withdraws an exact number of vault shares from Midnight and accepts whatever redeem() returns for the receiver. onRepay() does the same while trying to fund a Midnight repayment. onLiquidate() redeems the seized shares and forwards whatever assets the vault returns to the liquidator. For a trusted stable vault this may be acceptable. As a public executor, though, the caller cannot protect against vault losses, share-price movement between transaction construction and execution, or supported vaults whose redeemable value changes because of queues, fees, donations, or adapter state. A withdrawal or liquidation can execute with materially fewer assets than the user or liquidator expected and the transaction has no local bound that can stop it. Recommendation Add explicit minimum asset parameters to redeeming entrypoints. withdrawCollateralAndRedeem() should require assets >= minAssetsOut. repayAndWithdrawCollateral() should require the redeemed amount to cover the requested repayment plus any user-specified surplus floor. liquidateAndRedeem() should let the liquidator provide a minimum redeemed amount or minimum liquidation surplus and revert when the actual redeem output is below that value. Resolution Tenor Team: The issue was resolved in PR#424. CategorySeverityLocationStatus Validation ● Low MidnightVaultExecutor.solResolved 50 L-12 | Fee Adjuster Overshoots maxFill Description CallbackFeeAdjuster.beforeDispatch() is intended to cap takeUnits so the router's effective fill in fillIndex stays within the remaining budget. For SELL offers using percentage-fee adjustment with fillIndex == FILL\_BUYER\_ASSETS, the closed-form inverse can return a units cap that is one wei too large. The router dispatches Midnight.take() with that cap, then CallbackFeeAdjuster.afterDispatch() adds the percentage fee to buyerAssets. On affected rounding boundaries, the adjusted total exceeds maxFill by one wei and TakeRouter reverts with FillOvershoot. This does not create a value-stealing primitive because the revert unwinds the transaction. The impact is deterministic DoS of otherwise valid exact-budget routed fills, forcing keepers or offchain routers to add slack or avoid exact FILL\_BUYER\_ASSETS percentage-fee routes. Recommendation Add a forward-check correction after the inverse calculation for percentage-fee FILL\_BUYER\_ASSETS sizing: compute the actual raw buyer assets and fee for the candidate units, then decrement units until rawBuyerAssets + fee <= remainingBudget. Resolution Tenor Team: The issue was resolved in PR#433. CategorySeverityLocationStatus Rounding ● Low src/periphery/resolvers/CallbackFeeAdj uster.sol Resolved 51 L-13 | Griefing V1=>V2 Migration Description MorphoV1ToV2BorrowCallback.onSell() migrates a borrower from Morpho Blue V1 to Midnight V2 by using sellerAssets - fee as the repayment budget for the V1 debt. During callback execution it reads the live V1 debt with MORPHO\_BLUE.expectedBorrowAssets(...), then reverts if the fixed repayment budget is greater than that live debt. Because Morpho Blue allows anyone to repay on behalf of a borrower, a third party can front-run a quoted migration with a dust repay. If the migration was quoted as an exact or near-exact full migration, the dust repay lowers the live debt while the signed offer still produces the original repayBudget. The callback then reverts with ExcessRepayment, forcing the borrower or keeper to re-quote and resubmit the migration. This is not a direct value-stealing issue: the attacker pays down the borrower's debt and the migration can be re-quoted. It is nevertheless a cheap liveness grief against time-sensitive V1->V2 borrow renewals. Morpho Blue explicitly documents the underlying behavior: a small repay can front-run a repay and make it revert. Recommendation Avoid reverting when repayBudget is slightly above the live V1 debt. If repayBudget >= v1Debt, treat the fill as a final migration: repay by pos.borrowShares, migrate all V1 collateral, and base accounting/events on the actual live debt repaid. If surplus loan tokens can remain in the callback after the exact share repay, refund them or ensure they cannot be stranded or reused across users. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Gas Griefing ● Low src/callbacks/MorphoV1ToV2BorrowCal lback.sol Acknowledged 52 L-14 | Router Fee Metadata Under-reports Fees Description TakeRouter delegates fee sizing and post-dispatch accounting to ICallbackFeeAdjuster using actions\[i\].feeAdjusterData. The canonical CallbackFeeAdjuster decodes that data as (feeRate, FeeFormula) and uses the selected formula in both beforeDispatch() and afterDispatch(). Neither the router nor the adjuster checks that the selected formula matches the callback encoded in the action. For callbacks that charge a flat percentage fee, such as V2-to-V1 borrow/lend paths, a caller can label the action as FeeFormula.INTEREST. Near par, the interest-based formula reports zero or near-zero fee, while the actual callback still charges percentageFee(...). The router can therefore report totals and enforce maxFill against a lower fee than the callback-side cost the taker experiences. This is not arbitrary third-party theft by itself: the transaction still must be submitted by the taker or by an operator that passes the router's authorization check. The protection failure is that router-level limits are only as correct as untrusted per-action metadata. Recommendation Do not accept the fee formula as arbitrary caller metadata. Derive the expected formula from the callback address, or validate (callback, FeeFormula) pairs before dispatch. Revert if a percentage-fee callback is routed with FeeFormula.INTEREST, or if an interest-fee callback is routed with FeeFormula.PERCENTAGE. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Low src/periphery/resolvers/CallbackFeeAdj uster.sol Acknowledged 53 L-15 | Bidirectional Intents Enable Fee-draining Churn Description RenewalIntentRatifier stores permissions independently per (user, callback, sourceMarketId, targetMarketId) and never consumes them on use. That lets opposite directions remain live at the same time. A realistic lifecycle is: 1. the user enables V1->V2 to move from V1 fixed-rate exposure into V2 variable-rate exposure 2. a keeper executes that migration 3. later, market conditions flip, so the user enables V2->V1 to move back into V1 4. a keeper executes that exit 5. the old V1->V2 intent is still live, so the keeper can immediately move the user back into V2 The protocol never enforces that: • only one direction may be active • executing one direction invalidates the reverse direction • a position cannot be renewed straight back into the market it just exited Each extra round-trip charges real fees: • V1->V2 lend withdraws buyerAssets + fee from the user's vault position • V2->V1 lend deposits only sellerAssets - fee back into the vault The problem is worse on V2->V1, where BaseRenewalRatifier excludes the flat callback fee from rate validation (reported I-03). In effect, the V2->V1 protocol fee is paid out of the user's ratified price budget. The keeper only needs a quote that passes the pre-fee check; the callback can then worsen the user's realised outcome by the omitted flat fee while still passing ratification. Assuming executable offer liquidity exists, including self-supplied or coordinated liquidity, stale bidirectional intents become a repeatable value-draining churn loop. V2->V1 becomes valid when block.timestamp == sourceMaturity - renewalWindow, and the stale V1->V2 intent back into that same market is still valid because targetMaturity == block.timestamp + minDuration. At that boundary a keeper can immediately alternate the two directions against the same V2 market and stack fees each time. Recommendation Invalidate stale directional intent when a migration executes. Consider: • consuming the executed intent so it cannot be replayed later • adding nonces or one-shot intent IDs • requiring explicit re-authorization before a position can migrate back into the market it just exited Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Gaming ● Low RenewalIntentRatifier.solAcknowledged 54 I-01 | V1->V2 Borrow Renewal Can Spend Target Credit Description MorphoV1ToV2BorrowCallback assumes the traded obligationUnits become new debt on the target Midnight obligation. That is not guaranteed in the actual renewal flow. RenewalIntentTakeOnBehalf.take() calls Midnight.take() with the borrower as the sell-side taker of a lender buy offer. In that branch, Midnight first applies units against sellerPos.credit before this callback executes: uint256 sellerCreditDecrease = UtilsLib.min(units, sellerPos.credit); uint256 sellerDebtIncrease = units - sellerCreditDecrease; uint128 buyerPendingFeeIncrease = ... sellerPos.credit -= UtilsLib.toUint128(sellerCreditDecrease); sellerPos.debt += UtilsLib.toUint128(sellerDebtIncrease); Therefore any credit the borrower already holds on the target obligation is sold first, and only the remainder becomes new debt. The callback does not guard against that condition. It still uses the received loan tokens to repay the V1 debt and migrates V1 collateral into the target obligation. A keeper can therefore turn a "borrow migration" into a discounted sale of the user's existing target credit whenever the user already lends in the chosen target market. The lender side receives that credit at the renewal price, while the user loses a position that was not covered by their borrow-renewal guardrails. reduceOnly does not help here, since the external offer maker is the lender and the borrower is the taker. Recommendation Reject V1-to-V2 borrow renewals when the user already has credit on the target obligation. This check needs to happen before Midnight.take() mutates the position, since the callback only sees post-trade state and cannot detect credit that was fully consumed in the same call. The ratifier or orchestrator should read updatePositionView or creditOf for the target obligation and revert if it is non-zero for this renewal type. If that behavior is intentional, document it explicitly and treat target-credit sales as a separate flow. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Info MorphoV1ToV2BorrowCallback.solAcknowledged 55 I-02 | Stale Grace Timer Survives Healthy Recovery Description startGracePeriod() stores a single timestamp per borrower and obligation and \_requireLiquidationAllowed() later checks only whether the current time still falls inside the window derived from that timestamp. The gate never invalidates gracePeriodInfo after the borrower returns healthy and it does not distinguish between a borrower who remained continuously unhealthy and a borrower who recovered before becoming unhealthy again. As a result, a borrower can become unhealthy, have anyone arm the grace timer, cure the position and then become unhealthy again before the original liquidation window expires. The second unhealthy episode inherits the old timer and can be liquidated immediately, even though the documentation describes a fresh unhealthy -> startGracePeriod -> grace period -> liquidate lifecycle. This does not let liquidators seize a healthy position, because Midnight still re-checks current health at liquidation time. The bug is narrower and more precise: a borrower who recovers loses the promised grace period on the next unhealthy episode. Recommendation Make grace-period validity track continuous unhealthy state, not just elapsed wall-clock time since some historical dip. Move the "unhealthy since" tracking into Midnight, where collateral and debt updates can invalidate stale timers whenever an account becomes healthy again. If the gate must keep the state, add an explicit invalidation path tied to every action that can restore health and require a fresh startGracePeriod() after recovery. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Info DelayedLiquidationGate.solAcknowledged 56 I-03 | V2-to-V1 Fees Bypass Rate Limits Description MAX\_FEE\_RATE\_V2\_V1 permits non-zero fees on V2->V1 renewals. That is unsafe because BaseRenewalRatifier.\_feeRateForRateCheck() returns zero for both V2->V1 callbacks before the ratifier builds the price checked by PriceLib.satisfiesRateLimit(). The callbacks still charge CallbackLib.percentageFee() on principal. Consequently, the fee is not included in the user's limitRatePerSecond protection. An offer that sits exactly on the user's rate boundary can pass ratification and then settle worse once the fee is added or deducted. This becomes severe near maturity. \_computeDuration() uses sourceMaturity - block.timestamp for V2->V1 exits, so the same 25 bps principal fee is roughly equivalent to 91% APR with 1 day remaining and about 131,400% APR with 1 minute remaining. After maturity, the rate model collapses to par because duration is zero, but the fee is still collected. Because setFeeConfig() is owner-controlled and takes effect immediately, a malicious or compromised fee admin can raise V2->V1 fees up to this cap and extract value from pending renewals without tripping InvalidOfferRate. Recommendation Do not include the V2-to-V1 flat percentage fee in the interest-rate check if the intended policy model is that V2-to-V1 exits are checked pre-fee. Instead, document that distinction directly in the ratifier code and deployment docs. The documentation should make clear that, for V2-to-V1 renewals, limitRatePerSecond constrains only the pre-fee exchange rate. The flat exit fee is controlled separately by MAX\_FEE\_RATE\_V2\_V1, market/action fee configuration and the ratifier owner trust model. Resolution Tenor Team: The issue was resolved in PR#441. CategorySeverityLocationStatus Validation ● Info Constants.solAcknowledged 57 I-04 | Sell Takes Can Spend Router-held Tokens Description \_dispatchMidnightTake() treats a sell offer with takerCallback == address(0) as a case where the router must approve Midnight to pull loan tokens. This matches Midnight.take()'s payer selection, but it means the payment source is the router contract itself, not params.taker. An attacker can self-authorize the router on Midnight, set themselves as params.taker, and submit a MIDNIGHT\_TAKE against an attacker-controlled sell offer. If the router holds the obligation's loanToken, Midnight pulls buyerAssets from the router, sends sellerAssets to the maker-side receiver, and credits the purchased position to the attacker-controlled taker. The router never checks that the spent loan tokens came from the taker who passed the authorization check. Consequently any loan-token balance stranded on the public router is globally spendable through this direct sell execution. The code and tests intentionally support router-funded settlement for this case, but the public TakeRouter is not documented as a custody component and has no per-user balance accounting or sweep function. The same inherited logic exists on TenorAdapter, but the adapter impact is weaker. TenorAdapter inherits CoreAdapter.erc20Transfer, so a user can append erc20Transfer(loanToken, user, type(uint256).max) to the same Bundler3 multicall and sweep the adapter's full residual token balance. If the user fails to do that, the stale adapter balance is already publicly sweepable through CoreAdapter.erc20Transfer, without needing this router sell execution. Therefore the adapter residual case does not justify high severity by itself. Recommendation Do not let the public router become the payer for direct sell takes. The smallest safe fix is to reject MIDNIGHT\_TAKE actions where !action.offer.buy && d.takerCallback == address(0). If router-funded settlement is intentionally needed, gate it behind a separate trusted mode and account for the exact balance supplied for the current action. Do not fund it from the router's aggregate token balance. Adapter integrations should also append an explicit final sweep when they transfer tokens into TenorAdapter. Resolution Tenor Team: The issue was resolved in PR#401. CategorySeverityLocationStatus Warning ● Info TakeRouter.solAcknowledged 58 I-05 | Supply Collateral Clamp Quotes Unhealthy Fills Description SupplyCollateralCallbackClamp.\_maxUnitsFromDebtLimit() computes an initial maximum fill from a linearized health formula, then simulates the callback collateral for that fill. If the simulated position is unhealthy, it performs one correction: if (forwardLimit < forwardDebt) { maxUnits = forwardLimit > currentDebt ? forwardLimit - currentDebt : 0; } That correction is not self-consistent. forwardLimit was computed using the old maxUnits, but the callback collateral is proportional to sellerAssets. When maxUnits is reduced, sellerAssets can also shrink. The callback then supplies less collateral than the value used to compute the corrected limit. Consequently the clamp can return a positive fill that still makes Midnight.take() revert with SellerIsLiquidatable, even though a smaller fill would succeed. Recommendation Make the health and max-LTV cap converge before returning a fill size. The simplest fix is to replace the single correction step with a monotone binary search over units, using the same seller-asset conversion and pro-rata collateral calculation that execution will use. Alternatively, repeat the forward simulation after each correction until the returned maxUnits satisfies forwardLimit >= currentDebt + maxUnits. The implementation should also keep the existing conservative rounding behavior so the clamp may return slightly less than the true maximum, but never more than an executable fill. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Warning ● Info SupplyCollateralCallbackClamp.solAcknowledged 59 I-06 | TENOR\_VAULT\_V2 Clamp Reads Wrong Data Description V1ToV2LendClamp.clamp always passes offer.callbackData into \_ownerWithdrawable. The TENOR\_VAULT\_V2 branch then reads morphoBlueMarketId from that byte array with assembly. That is wrong for the normal V1-to-V2 lend renewal shape. The lender is the taker and the lend callback is passed as takerCallback, so the real MorphoV1ToV2LendCallback.CallbackData lives in TakeOnBehalfData.takerCallbackData. offer.callbackData belongs to the maker's seller callback and is commonly empty because the seller offer does not need a callback. The result is that the TENOR\_VAULT\_V2 clamp cannot see the market ID that the callback will use. With empty offer.callbackData, it reads bytes32(0), queries the zero Morpho Blue market, and returns zero liquidity even when the lender has vault shares and the real Morpho Blue market is liquid. The audit test test\_tenorVaultV2ClampCannotReadTakerCallbackDataOnSellOffer() shows this directly: the clamp returns zero for the real SELL-offer shape, then returns a positive cap only after the same callback data is redundantly copied into the otherwise unused offer.callbackData. This blocks TENOR\_VAULT\_V2 clamped V1-to-V2 lend renewals unless the seller signs unused callback data solely for the clamp. If integrators fall back to the VAULT\_V2 or generic ERC4626 branch to avoid the zero cap, they lose the intended Morpho Blue liquidity bound and can produce revert-only fills when the underlying market is illiquid. Recommendation Do not read TENOR\_VAULT\_V2 liquidity data from offer.callbackData for taker-callback renewals. Move morphoBlueMarketId into V1ToV2LendClampData, or extend the router/clamp interface so the clamp receives the actual taker callback data used by Midnight.take(). The clamp should also ABI-decode the callback data or clamp data instead of using an unchecked assembly load. That makes malformed data fail clearly and allows the clamp to validate that the vault, fee rate, and market ID being capped match the callback execution. Resolution Tenor Team: The issue was resolved in PR#414. CategorySeverityLocationStatus Unexpected Behavior ● Info V1ToV2LendClamp.solResolved 60 I-07 | Vault Supply Clamp Uses An Unbound Tick Description VaultSupplyClamp.clamp computes the seller top-up cap from offer.tick, but MorphoV2SupplyVaultSharesCallback.onSell computes the actual top-up from callbackData.tick. The interface says those ticks must match, but neither the callback nor the clamp enforces that relationship. The clamp also reads only the third ABI word from offer.callbackData, so it does not reject malformed or inconsistent remaining callback data before returning a cap. For sell offers, a maker can sign an offer with a lower offer.tick and a higher callbackData.tick. The clamp then reports a fill size based on the smaller top-up. When the take executes, the callback pulls the larger top-up and can revert because the seller's balance or allowance is insufficient for the amount that the clamp did not size. Consequently, routed batch or keeper can select a fill that the clamp advertises as safe and then lose the action to a callback revert. Recommendation Bind the callback tick to the offer tick before returning a nonzero clamp value. The clamp should ABI-decode the full CallbackData, require the expected length and structure and require callbackData.tick == offer.tick. Resolution Tenor Team: The issue was resolved in PR#411. CategorySeverityLocationStatus Unexpected Behavior ● Info VaultSupplyClamp.solResolved 61 I-08 | Vault Supply Clamp Omits Solvency Bound Description VaultSupplyClamp.clamp returns a fill cap from the seller's top-up balance and allowance only. It does not check whether the shares minted by the callback will make the seller healthy under the obligation oracle and LLTV. The comment justifies this by assuming the vault-to-loan-token oracle price only increases. That assumption does not hold for the intended VaultV2 collateral. The project documentation states that underlying bad debt can reduce VaultV2 totalAssets and drop the share price. If the share price or oracle value decreases, or if additionalDepositPercent is below the current LLTV requirement, the clamp can return a positive unit amount that will always revert at Midnight's final SellerIsLiquidatable check. This does not create bad debt because Midnight still enforces health after onSell. The issue is liveness and routing reliability. A router or keeper using the clamp can select a fill that the clamp reports as available, only to revert after the callback deposits and supplies shares. Recommendation Make solvency part of the clamp's fill bound. For vault-share collateral, cap units by the seller's post-fill health using the current oracle price, LLTV, expected share output, existing debt, existing credit and existing collateral. If the clamp intentionally avoids oracle reads, document that it is only a balance/allowance clamp and must be paired with an independent health-aware bound before routed execution. Resolution Tenor Team: The issue was resolved in PR#415. CategorySeverityLocationStatus Validation ● Info VaultSupplyClamp.solResolved 62 I-09 | Grace Period Skipped Once Obligation Matures Description DelayedLiquidationGate enforces the grace period, liquidation window, and the exclusive priority-liquidator sub-window only while the obligation is pre-maturity. Once block.timestamp crosses obligation.maturity, the gate's checks are skipped entirely and any liquidator can proceed — regardless of whether the grace period has elapsed or whether a priority liquidator was assigned. There is no validation at startGracePeriod that the configured GRACE\_PERIOD + LIQUIDATION\_PERIOD fits inside the remaining time to maturity. If a position becomes unhealthy close to maturity, both the borrower's grace period (intended as a self-cure window) and the priority liquidator's exclusive sub-window can collapse — partially or entirely — the moment maturity is reached. Recommendation Either: 1. Validate at startGracePeriod that obligation.maturity > block.timestamp + GRACE\_PERIOD + LIQUIDATION\_PERIOD, reverting otherwise; or 2. Explicitly document that the grace period and priority window are best-effort and are truncated or skipped once the obligation matures. Resolution Tenor Team: The issue was resolved in PR#412. CategorySeverityLocationStatus Validation ● Info src/gates/DelayedLiquidationGate.solResolved 63 I-10 | Factory Allows Near-total Oracle Deviation Description createOracleWithValidationCheck() only rejects maxOracleDeviation >= 1e18. Therefore the factory accepts values arbitrarily close to 100%. That bound is enough to prevent an exact 100% deviation, but it is not a meaningful safety bound for a lending oracle. With maxOracleDeviation = 1e18 - 1, a primary price of 1e36 accepts a validation price of 1e18. The wrapper is still factory-deployed and isDeployedOracle is true, but the validation oracle can be almost zero relative to the primary price. This matters because Midnight computes collateral debt capacity from the returned primary price. A factory-valid wrapper can therefore be configured so the secondary oracle provides almost no protection before the price is used in borrowing-limit and liquidation checks. This is a configuration-boundary issue rather than a permission bypass. It requires a market or deployment process to accept an unsafe deviation value. Recommendation Do not treat maxOracleDeviation < 1e18 as a sufficient risk bound. Add deployment tooling or governance checks that enforce a market-specific maximum deviation before an oracle can be used as collateral. For lending markets, derive the maximum from the collateral's LLTV and liquidation assumptions. If the current primary-denominated formula remains, use the stricter bound documented in the existing deviation finding rather than comparing maxOracleDeviation directly to 1 - LLTV. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Info OracleWithValidationCheckFactory.solAcknowledged 64 I-11 | Zero-owner Oracle Docs Cannot Deploy Description The oracle README says to set owner to address(0) to make validation immutable. That deployment cannot succeed with the current dependency tree. OracleWithValidationCheck passes initialOwner into OpenZeppelin Ownable, and Ownable reverts when the initial owner is address(0). Consequently the documented immutable deployment method is not available through either the factory or a direct constructor call. The actual way to remove owner powers is to deploy with a nonzero owner and then call renounceOwnership(). That sequence has different safety requirements because the owner can pause validation before renouncing, which leaves the oracle permanently primary-only. Operators following the docs will either ship a failing transaction or use a renounce sequence without the explicit pre-renounce state checks that this contract needs. Recommendation Update the README to describe the real immutable deployment procedure. The safe runbook is to deploy with a nonzero owner, verify that validationCheckPaused == false, verify that both oracles return acceptable prices, and only then renounce ownership if immutability is desired. Resolution Tenor Team: The issue was resolved in PR#changes. CategorySeverityLocationStatus Documentation ● Info README.mdResolved 65 I-12 | Lender Rate Floors Bypassed After Early Repay Description BaseRenewalRatifier is supposed to stop a permissionless keeper from renewing a lender into a bond priced below the lender's configured floor rate. For LEND\_V2\_TO\_V2\_WITHDRAWABLE, that protection is computed with the wrong duration once the source bond has already been repaid early. The ratifier always prices V2->V2 renewals using targetMaturity - sourceMaturity. That assumption is valid for the borrower-side renew path, where the source debt remains live until it is repaid inside the callback. It is not valid for the lender-side withdrawable path. Once the borrower of the source obligation repays early, the lender's capital is no longer locked until sourceMaturity; it is immediately redeployable. The callback then pulls that cash out of the source obligation at execution time and uses it to fund the new target bond. So after an early repay, the economic tenor of the renewed lend is targetMaturity - block.timestamp, not targetMaturity - sourceMaturity. This direction matters because LEND\_V2\_TO\_V2\_WITHDRAWABLE is treated as userIsBuy = true, so the rate check enforces a lender floor. In PriceLib, a shorter duration produces a higher zero-coupon price. That makes the lender floor easier to satisfy, which lets the taker pay too much for the target bond and accept a lower yield than their configured minimum. In practice, the borrower on the other side of the target obligation receives financing at a better rate than the lender allowed, defeating the ratifier's primary purpose. This is exploitable under normal operation, for example: • lender originally lends 100 until t + 4w • source borrower fully repays at t + 1w • renewal executes immediately at t + 1w into a target maturing at t + 5w • ratifier duration: t+5w - t+4w = 1w • real deployed duration: t+5w - t+1w = 4w At a 5% floor, the 1-week price floor is materially higher than the 4-week price floor. A new borrower can therefore borrow against that lender's liquidity at a lower yield than the lender approved, simply by waiting for early repayment to create withdrawable source funds and then matching the renewal against a target offer priced at the inflated floor. Recommendation For LEND\_V2\_TO\_V2\_WITHDRAWABLE, compute duration from execution time, not from the old maturity: targetMaturity - block.timestamp Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Logical Error ● Info BaseRenewalRatifier.sol, MorphoV2ToV2LendWithdrawable.sol Acknowledged 66 I-13 | Withdrawals Escape Lazy Bad Debt Description Bad debt is socialized only when liquidate() is called and the bad-debt branch updates the obligation's lossIndex. Until that happens, withdraw() still lets any lender burn current credit and withdraw from the global withdrawable bucket at par. An informed lender can therefore exit before losses are realized. The lender withdraws their credit against existing withdrawable liquidity, then the same account or another liquidator calls liquidate() on an already-bad borrower. The subsequent lossIndex update slashes only the lenders who still hold credit. The exiting lender avoided their share of the loss because their credit was already burned at par. This creates a bank-run dynamic: repayments are paid out first-come-first-served, while losses are recognized lazily. DelayedLiquidationGate can make the timing worse because it may block liquidation during a grace or priority period while withdrawals from the same obligation remain open. Recommendation Do not allow par withdrawals to outrun known bad debt. Possible fixes include realizing pending bad debt before withdrawals, freezing or haircutting withdrawals while liquidation grace periods are active, or moving to indexed repayment/loss accounting where each credit holder's withdrawal entitlement and loss exposure are applied consistently over time. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Info Midnight.solAcknowledged 67 I-14 | Renewal Skips Collateral Missing From Target Description transferCollaterals walks every collateral token in the source obligation, but only transfers a token when targetObligation.findCollateral() returns found. If the target obligation does not list that source token, the helper leaves collateralAmounts\[i\] at zero and keeps going. This is safe for a generic helper only if the caller has already opted into partial collateral migration. MorphoV2ToV2BorrowRepay.onSell does not enforce that. It only checks that the source and target obligations use the same loanToken before it repays source debt and calls transferCollaterals. The ratifier binds the exact source and target market IDs, but it does not prove that the two obligations have the same collateral token set. The V2-to-V2 borrow clamp documents the opposite assumption: source and target obligations should be identical except for maturity. Consequently, a V2-to-V2 borrow renewal can repay some or all source debt, emit PositionRenewed and silently leave source collateral behind. On a final fill, the source debt can be fully closed while unmatched collateral remains in the old obligation. On a partial fill, the borrower can end with target debt plus residual source collateral state that the surrounding renewal machinery treats as migrated. Recommendation Enforce the renewal-specific compatibility requirement before calling transferCollaterals. For the documented V2-to-V2 borrow renewal, require the source and target obligations to have the same collateral token set and compatible collateral parameters, not just the same loanToken. If partial collateral migration is intentional, make it explicit. The callback should reject unapproved mismatches, emit clear residual-state information for skipped tokens and the README, clamp comments and source-closure properties should be updated to describe that behavior. Resolution Tenor Team: The issue was resolved in PR#417. CategorySeverityLocationStatus Unexpected Behavior ● Info CollateralTransferLib.solResolved 68 I-15 | TakeRouter Returns Adjusted Totals Only Description TakeRouter tracks two different aggregate result sets during batch execution: rawTotals, which match the raw fill amounts that Midnight writes into consumed\[maker\]\[group\], and totals, which are post-adjustment values after feeAdjuster.afterDispatch tilts the result against the taker. This distinction is documented inline in \_execute. However, the external execute entrypoint returns only totals, and BatchExecuted also emits only the adjusted totals. An integrator that assumes these returned values are equivalent to Midnight's raw fill accounting can drift from consumed when callback fee adjustment is enabled. function execute(ExecuteParams calldata params, Action\[\] calldata actions) external virtual ... rawTotals\[RouterLib.FILL\_BUYER\_ASSETS\] += buyerAssets; rawTotals\[RouterLib.FILL\_SELLER\_ASSETS\] += sellerAssets; Recommendation If external consumers are expected to reconcile against Midnight consumed accounting, expose rawTotals in the execute return values or emit them in a separate event. Otherwise, document more explicitly that execute and BatchExecuted expose taker-facing adjusted totals only and must not be used as a proxy for raw Midnight fill accounting. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Info TakeRouter.solAcknowledged 69 I-16 | V2-to-V2 Lend Callback Self-funds Fresh Credit Description MorphoV2ToV2LendWithdrawable.onBuy trusts callbackData.sourceObligation as the obligation from which the buyer already has withdrawable credit. It only checks that the source and target loan tokens match. It does not reject the case where the source obligation is the same obligation currently being bought into. This is a follow-up manifestation of H-03: Fresh credit drains old repayments. In Midnight.take, the buyer receives fresh credit before onBuy executes and before Midnight pulls payment from the buyer callback. Therefore a raw Midnight.take or a router MIDNIGHT\_TAKE action can pass sourceObligation == obligation to this public callback. In those paths, the taker supplies takerCallbackData, which is decoded by this callback as CallbackData; that makes sourceObligation, feeRate, feeRecipient and tick attacker-controlled for the exploit path. The callback then calls MORPHO\_MIDNIGHT.withdraw(callbackData.sourceObligation, neededAssets, buyer, address(this)) and burns the buyer's just-minted credit against the same obligation's old withdrawable pool. Because the raw-take path lets the attacker choose the callback data, the attacker can set feeRate == WAD and feeRecipient == attacker. With those values, the callback withdraws approximately the full obligation unit amount from old withdrawable liquidity. It sends the discount component to the attacker as a callback fee and approves only buyerAssets back to Midnight so settlement can complete. The attacker starts with no loan tokens, the borrower receives the discounted take price and old withdrawable liquidity is drained to fund the new borrower debt. The canonical renewal ratifier constrains feeRate and feeRecipient only when the sanctioned renewal-intent path is used and the V2ToV2LendWithdrawableClamp rejects or zeroes self-renewal configurations only when that optional clamp is used. Neither policy layer protects raw Midnight offers or TakeRouter actions that use ActionType.MIDNIGHT\_TAKE, because this callback is callable by Midnight directly with caller-provided callback data. This is repeatable but not unbounded. Each successful execution consumes the target obligation's existing withdrawable liquidity one-for-one, so the path stops for that obligation once the old withdrawable pool is empty unless later repayments refill it. The attacker needs both old withdrawable liquidity to drain and a way to mint fresh credit in the same obligation. There are two practical cases: 1. If honest borrower sell offers already exist, the attacker can take those offers with the public callback. The honest borrower receives the discounted loan proceeds, the attacker captures the discount as the callback fee, and pre-existing withdrawable lenders are left funding the new borrower debt. 2. If no suitable honest borrower offer exists, the attacker can use a second address as the borrower because Midnight.take only rejects exact same-address self-takes. In that case, the attacker-controlled borrower must still provide acceptable collateral and ends up with the new debt. The path can still drain old withdrawable liquidity, but profitability then depends on the borrower's collateral, liquidation/default assumptions, and the cost of creating that debt. Recommendation Reject same-obligation source and target use inside the callback itself. The guard should be local to MorphoV2ToV2LendWithdrawable.onBuy because external ratifiers, clamps and routers are optional policy layers. bytes32 targetObligationId = IdLib.toId(obligation, block.chainid, address(MORPHO\_MIDNIGHT)); bytes32 sourceObligationId = IdLib.toId(callbackData ... Obligation, block.chainid, address(MORPHO\_MIDNIGHT)); if (sourceObligationId == targetObligationId) revert SelfRenewal(); Also enforce any intended maturity and fee constraints in the callback when direct Midnight.take usage is supported. At minimum, reject callbackData.sourceObligation.maturity >= obligation.maturity, reject positive fees with a zero fee recipient, and keep the callback tick bound to the offer tick through the caller's data model. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Unexpected Behavior ● Info MorphoV2ToV2LendWithdrawable.solAcknowledged 70 I-17 | Renewal Grants Survive Midnight Revocation Description RenewalIntentTakeOnBehalf.setIsAuthorized() and RenewalIntentRatifier.setParams() accept calls from any account that is authorized by the user on Midnight at the time of the write. The resulting renewal-layer state is stored locally and is not linked to that Midnight authorization afterward. Consequently, a temporarily authorized delegate can plant a ratifier authorization and permissive renewal params, then keep that renewal surface alive after the user revokes the delegate on Midnight. The revoked delegate can no longer clean up the state it wrote, but the planted ratifier authorization still passes RenewalIntentTakeOnBehalf.take() because execution only checks isAuthorized\[intent.user\]\[intent.ratifier\]. Recommendation Do not let generic Midnight delegates create persistent renewal-layer authority unless that is explicitly part of the trust model. Require msg.sender == onBehalf for setParams() and setIsAuthorized(). Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Trust Assumptions ● Info RenewalIntentTakeOnBehalf.sol, RenewalIntentRatifier.sol Acknowledged 71 I-18 | V2-to-V1 Borrow Exits Lack Target Health Buffer Description MorphoV2ToV1BorrowCallback.onBuy() only checks that the target Morpho Blue market uses the same loan token as the source obligation and that the target collateral token appears somewhere in the source obligation. It does not require the target market to use the same oracle or LLTV as the source collateral. It also does not require any target-side health buffer before creating the Morpho Blue borrow. This matters because V2ToV1BorrowClamp assumes the source and target have the same loan token, collateral token, LLTV, and oracle. The clamp does not check target health; it only caps by source debt and target market liquidity. Therefore a configured migration can pass the local callback checks while entering a target market with stricter LLTV or different oracle assumptions. The migration may revert late at Morpho Blue's binary health check or succeed with little liquidation margin under the target market. The borrower chooses or authorizes the target tuple, so this is primarily a migration safety and product-correctness issue. The code and clamp comments promise a stronger compatibility model than the callback enforces. Recommendation Require the selected target market to match the source collateral's oracle and LLTV unless the borrower has explicitly opted into a different target risk profile. Add a target-side health-buffer check that accounts for fees, existing target debt and the actual collateral amount moved. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Info MorphoV2ToV1BorrowCallback.solAcknowledged 72 I-19 | Stale Documentation Description The repository documentation and contain several stale or contradictory statements relative to the current implementation. None of these are standalone runtime vulnerabilities, but they can mislead auditors, integrators, and offchain tooling that use the docs as the source of truth: • DIMENSIONAL\_UNITS.md:13 says higher tick means lower price / higher discount. In reality, TickLib.tickToPrice() is monotone increasing, so higher tick means higher price / lower discount. • DIMENSIONAL\_UNITS.md:17 states the price domain is (0, WAD\]. In reality, tickToPrice(0) == 0, so the implementation domain includes zero: \[0, WAD\]. • DIMENSIONAL\_UNITS.md:48 says tick prices round to 1e13 multiples. In reality, tickToPrice() rounds with .divHalfDownUnchecked(5e12) \* 5e12. • PROPERTIES.md:34 refers to MAX\_TICK (990). In reality, MAX\_TICK = 1046. • PROPERTIES.md:39 says tickToPrice(0) > 0. In reality, tests assert tickToPrice(0) == 0. • docs/audit-checklist.md:59 says tick 990 = near par. In reality, par is reached at tickToPrice(1046) == 1e18. • docs/audit-checklist.md:117 says the effective-price fee model uses all rounding down on both seller and buyer sides. In reality, CallbackLib uses mixed rounding: seller-side effective price / budget uses mulDivUp, while buyer-side effective price / budget uses mulDivDown. • CallbackData.feeRate in V2 to V1 interfaces: Fee rate on sellerAssets in WAD (max 1%, e.g., 0.01e18 = 1%) but declared constant MAX\_FEE\_RATE\_V2\_V1 = 0.0025e18 • Several callback interfaces say feeRecipient address(0) = no fee; runtime callbacks do not use feeRecipient as the no-fee sentinel. Fees are disabled by feeRate == 0 or by a computed zero fee. • IMorphoV2SupplyVaultSharesCallback first states the vault top-up is ceil(sellerAssets \* additionalDepositPercent / WAD), but the implementation computes ceil(obligationUnits ceil(tickToPrice(tick) additionalDepositPercent / WAD) / WAD) Recommendation Fix the documentation contradictions. Resolution Tenor Team: The issue was resolved in PR#437. CategorySeverityLocationStatus Documentation ● Info GLOBALResolved 73 I-20 | Sentinel Silently No-ops Takes Description TakeRouterAdapterBase.\_executeResolvingSentinels overloads type(uint256).max on ExecuteParams.maxFill / minFill as a request to derive the cap from on-chain state. For fillIndex == FILL\_OBLIGATION\_UNITS, \_resolveSentinel returns debtOf(id, taker), falling through to creditAfterUpdate if debt is zero, and returning 0 if neither exists. The intended semantic is "fill up to my existing position size" — meaningful only for renewal or close-out flows where the taker already has a position in the obligation. The type(uint256).max value, however, is the canonical ERC-20 idiom for "unlimited / no cap." An integrator unfamiliar with the renewal-only semantic — or wired by a frontend reusing the ERC-20 convention — passes maxFill = type(uint256).max for an opening take (no prior debt or credit). \_resolveSentinel returns 0, and \_execute's loop guard totals\[fillIndex\] >= maxFill fires 0 >= 0 on the first iteration, breaking before any action runs. With the default minFill = 0, the post-loop floor check totals\[fillIndex\] < minFill does not revert, so BatchExecuted is emitted with all-zero amounts. The caller observes a successful transaction that filled nothing, with valid signatures, valid approvals, and no error to diagnose. Recommendation Either (a) revert with a named error (e.g. SentinelResolvedToZero / SentinelRequiresExistingPosition) when \_resolveSentinel returns 0 and the caller passed type(uint256).max for the corresponding bound — opening takes that misuse the sentinel then fail loudly instead of silently; or (b) document at the ExecuteParams site that maxFill = type(uint256).max is a renewal/close-out helper, not an "unlimited" sentinel, and direct opening flows to pass concrete values. Option (a) also subsumes the FILL\_BUYER\_ASSETS BUY-offer path in Guardian 69eb590a with a single guard. Resolution Tenor Team: The issue was resolved in PR#436. CategorySeverityLocationStatus Logical Error ● Info src/bundler/TakeRouterAdapterBase.sol :77-83 Resolved 74 I-21 | Stable Max Sentinel Nets Gas Description On Stable, USDT0 is both the native gas token and an ERC20 token. Gas is precharged from the transaction sender before EVM execution, then refunded after execution. User-called Bundler3 routes that use type(uint256).max as a "use my full balance" sentinel therefore resolve against the user's post-gas-reservation USDT0 balance, not the pre-submit wallet balance. This affects TenorAdapter routes that first transfer the user's max available USDT0 into the adapter, then rely on adapter-balance sentinels or max-balance actions such as FILL\_BUYER\_ASSETS, midnightRepay(..., assets = type(uint256).max), or midnightSupplyCollateral(..., assets = type(uint256).max). Depending on the route's minimum-fill and exact-amount requirements, the transaction can either succeed with a smaller fill than the user's pre-submit full-balance expectation, or revert because the post-precharge balance is insufficient for the exact downstream action. Recommendation For Stable deployments, document that max-balance sentinels mean "post-gas-reservation balance." Frontends and keepers should leave USDT0 gas headroom or use explicit fixed maxFill / minFill values when exact fills are required. Resolution Tenor Team: The issue was resolved in PR#434. CategorySeverityLocationStatus Unexpected Behavior ● Info src/bundler/TakeRouterAdapterBase.solResolved 75 I-22 | Delayed Gate Blocks Gated Vault Liquidations Description MidnightVaultExecutor.liquidateAndRedeem() cannot liquidate an obligation that uses DelayedLiquidationGate as its liquidatorGate. This is not just a missing deployment allowlist entry. The executor and the delayed gate use incompatible liquidation call shapes. The executor calls MORPHO\_MIDNIGHT.liquidate() directly, so Midnight checks DelayedLiquidationGate.canLiquidate(address(executor)). The delayed gate only returns true for address(this), so the executor reverts with LiquidatorGatedFromLiquidating. The alternative is also broken for share-gated VaultV2 collateral. Liquidators are expected to call DelayedLiquidationGate.liquidate() directly, but Midnight transfers seized collateral to msg.sender, which is then the delayed gate. If the collateral token is a VaultV2 share with receiveSharesGate set, that transfer requires DelayedLiquidationGate to be allowed by canReceiveShares. The documented share-gated setup allowlists MidnightVaultExecutor, Midnight and the vault-share callbacks, but not DelayedLiquidationGate. Consequently, the executor cannot pass the delayed liquidation gate, while the delayed gate cannot receive the seized shares. Simply allowlisting DelayedLiquidationGate only fixes the second revert. It does not make MidnightVaultExecutor.liquidateAndRedeem() usable with delayed-gated obligations and it leaves the delayed gate holding raw VaultV2 shares that its current onLiquidate() logic tries to forward to the external liquidator. If that liquidator is not allowlisted to receive shares, the forward transfer can still revert. If arbitrary liquidators are allowlisted, the share-gating design is weakened because protected shares can leave the curated set. This affects an intended protocol configuration: VaultV2 share collateral is documented as supported behind VaultV2AllowlistGate, liquidateAndRedeem() is documented as the liquidation mechanism that gives liquidators underlying assets instead of shares and DelayedLiquidationGate is documented as a general liquidatorGate and liquidation router for Midnight obligations. If these components are wired together, underwater or post-maturity vault-share positions can become unliquidatable and bad debt can remain unresolved. Recommendation Make the delayed liquidation and vault-share redemption components compatible instead of relying on deployment guidance alone. One safe design is to route vault-share liquidations through the delayed gate with the executor as the gate-level liquidator. The executor should pass callback data that lets DelayedLiquidationGate.onLiquidate() forward seized shares to the executor, then MidnightVaultExecutor.onLiquidate() can redeem those shares and collect repayment from the original liquidator. This still requires DelayedLiquidationGate and MidnightVaultExecutor to be allowed by the VaultV2 receiveSharesGate, because Midnight first transfers seized shares to the gate and the gate then transfers them to the executor. Alternatively, fold vault-share redemption directly into DelayedLiquidationGate or deploy a dedicated delayed vault liquidation router. In either design, the gate should not forward raw gated vault shares to arbitrary liquidators. It should redeem shares into underlying assets before delivery or forward shares only to an allowlisted redemption contract. Resolution Tenor Team: The issue was resolved in PR#455. CategorySeverityLocationStatus DoS ● Info MidnightVaultExecutor.solAcknowledged 76 Remediation Findings & Resolutions IDTitleCategory SeverityStatus L-01 executeAndConsume Does Not Bind Group Dimension Validation ● Low Acknowledged L-02 Unbound Collateral Supply Denominator Validation ● Low Acknowledged L-03 Renewal Window Can Underflow Before Validation Validation ● Low Resolved L-04Optional Router Fee AdjustmentValidation ● Low Acknowledged L-05 Router Limits Assume Homogeneous Actions Validation ● Low Resolved L-06 Clamp Data Not Bound To Callback Data Validation ● Low Acknowledged L-07 Renewal Can Rescue A Liquidatable Obligation Trust Assumptions ● Low Acknowledged I-01 Effective Units Panic On Long Maturities Validation ● Info Resolved I-02 Open Liquidator Choice Delays Liquidations Warning ● Info Acknowledged I-03 Rate Limits Ignore Settlement Rounding Warning ● Info Acknowledged I-04 Standing Renewals Lack Amount Bounds Warning ● Info Acknowledged I-05 Vault Withdraw Callback Allows Position Crossing Warning ● Info Acknowledged I-06 Stale MAX\_FEE\_RATE\_V2\_V1 In Docs Documentation ● Info Resolved 77 Remediation Findings & Resolutions IDTitleCategory SeverityStatus I-07allowRevert Scope Is IncompleteDocumentation ● Info Acknowledged 78 L-01 | executeAndConsume Does Not Bind Group Dimension Description executeAndConsume() advances the caller's consumeGroup by rawTotals\[params.fillIndex\]. Midnight offer groups do not carry denomination metadata. The same consumed\[user\]\[group\] counter can represent units, seller assets or buyer assets depending on whether the signed offer used maxUnits, maxSellerAssets, or maxBuyerAssets. Midnight's take() path enforces this distinction from the offer fields, but executeAndConsume() only receives a bytes32 consumeGroup and the router's fillIndex. As a result, the adapter cannot verify that the amount it writes into consumeGroup is denominated the same way as the standing offer the caller meant to consume or cancel. If an integration uses executeAndConsume() with a unit-capped standing offer but chooses fillIndex = FILL\_BUYER\_ASSETS for the routed fill, the adapter writes buyer assets into a counter that the standing offer later compares against units. The reverse mismatch can also over-consume the group and cancel more capacity than intended. This does not let an arbitrary caller modify another user's group because setConsumed() still requires the adapter to be authorized for the initiator. The risk is at the adapter API boundary: a frontend, solver, or SDK can produce a valid authorized bundle that leaves phantom standing-offer capacity or burns too much capacity because the route limit dimension and the standing-offer group dimension are conflated. Recommendation Bind the group denomination explicitly. For example, add an expected consume dimension to executeAndConsume() and require it to match params.fillIndex, or split the API into denomination-specific helpers so a unit group, seller-asset group and buyer-asset group cannot be advanced with the wrong total. If the adapter intentionally leaves this responsibility to offchain tooling, document that consumeGroup must only be used with a fillIndex matching the active max field of every standing offer in that group and add SDK/front-end validation for that invariant. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Low TakeRouterAdapterBase.solAcknowledged 79 L-02 | Unbound Collateral Supply Denominator Description MorphoV2SupplyCollateralCallback.onSell decodes offerSellerAssets from arbitrary callback data and uses it as the denominator for every pro-rata collateral pull: CallbackData memory callbackData = abi.decode(data, (CallbackData)); ... uint256 supplyAmount = configAmount.mulDivDown(sellerAssets, callbackData.offerSellerAssets); The interface documents that offerSellerAssets must equal offer.maxSellerAssets, but this callback never receives the offer and cannot enforce that relationship. If the offer builder, router or integration signs callback data with a denominator that does not match the offer's actual seller-asset capacity, the callback supplies collateral according to the mismatched denominator instead of the amount implied by the offer. For example, a unit-denominated or price-discounted borrow offer can settle sellerAssets below obligationUnits. If the callback data is populated from the unit capacity rather than the seller-asset capacity, each fill supplies less collateral than a full-offer simulation using amounts\[\] would suggest. Midnight's final seller health check still prevents immediately liquidatable debt and a nonzero maxLtv adds a stricter post-fill guard. The remaining risk is an integration hazard: users or routers can rely on an unenforced denominator and produce fills with less posted collateral, less liquidation margin or revert-only execution paths than expected. Recommendation Only use this callback with seller-asset-denominated offers where offer.maxSellerAssets is nonzero and exactly equals callbackData.offerSellerAssets. Enforce that invariant in the router, clamp, ratifier or offer-construction layer before a fill is submitted. If the callback is intended to support other offer denominations, include an explicit denomination mode or expected offer hash in the signed callback data and reject mismatches before using the pro-rata amount. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Low MorphoV2SupplyCollateralCallback.solAcknowledged 80 L-03 | Renewal Window Can Underflow Before Validation Description BaseRenewalRatifier.\_ratifyWindow() subtracts p.renewalWindow from sourceMaturity before checking that the renewal window is not larger than the source maturity. renewalPeriodStart = sourceMaturity - p.renewalWindow; If a user configures renewalWindow > sourceMaturity, ratification reverts with Solidity panic 0x11 instead of the ratifier's domain-specific InvalidRenewalParams() error. This is reachable because RenewalIntentRatifier.setParams() stores user renewal params without validating this relationship. Recommendation Validate the configured renewal window before subtracting it from sourceMaturity. if (p.renewalWindow > sourceMaturity) revert InvalidRenewalParams(); renewalPeriodStart = sourceMaturity - p.renewalWindow; Resolution Tenor Team: The issue was resolved in PR#489. CategorySeverityLocationStatus Validation ● Low BaseRenewalRatifier.solResolved 81 L-04 | Optional Router Fee Adjustment Description TakeRouter.\_execute only adjusts buyerAssets and sellerAssets for callback-level fees when the caller supplies a nonzero feeAdjuster. Without that resolver, the router records the raw amounts returned by Midnight and then enforces maxFill, minFill and price bounds against those raw amounts. Fee-charging renewal callbacks can still transfer part of those assets to feeRecipient. Consequently, a route can satisfy minFill on the router's gross accounting while the taker's net amount after the callback fee is lower. The same mismatch can make maxFill and aggregate price checks weaker than intended for callbacks with nonzero fees. This does not bypass the renewal ratifier's rate check and the canonical CallbackFeeAdjuster can make the accounting conservative when it is configured correctly. The risk is an integration failure: frontends, keepers or direct callers that omit the matching adjuster receive weaker router-level fill protection than the API shape suggests. Recommendation Require a matching fee adjuster for action types and callbacks that can charge nonzero fees or derive the expected fee configuration from callback data and apply the adjustment inside the router. If the router intentionally accepts raw accounting, rename or document minFill, maxFill and price bounds as raw-Midnight limits rather than net-taker limits. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Low TakeRouter.solAcknowledged 82 L-05 | Router Limits Assume Homogeneous Actions Description TakeRouter.\_execute sums buyerAssets, sellerAssets and obligationUnits across every successful action in the batch. It then enforces one maxFill, one minFill and one average price check against those aggregate totals. The router does not require the actions to use the same loan token, obligation, maturity, offer side, callback or renewal user. Therefore the batch-level limits are only meaningful when the caller or offchain solver keeps the batch homogeneous. If unrelated actions are mixed, a favorable fill can offset an unfavorable fill and make the aggregate price pass even though one action would not satisfy the same price bound on its own. This is mainly an integration and user-protection issue. TAKE\_ON\_BEHALF actions still pass through each user's ratifier and MIDNIGHT\_TAKE acts as the initiating taker. However, if a frontend or solver presents the router's aggregate maxFill, minFill or price bounds as per-action protection, users can receive weaker protection than expected. TakeRouterAdapterBase.\_resolveSentinel has the same assumption. It resolves type(uint256).max from actions\[0\] only, then applies the resolved cap to the whole batch. Recommendation Either enforce homogeneous batches whenever aggregate limits are used or expose per-action bounds so each action can be checked independently. At minimum, compare every action against the first action's loan token, obligation id and offer side before applying a shared fill cap or shared price bound. If mixed batches are intended, document that router totals are batch-level accounting only and must not be treated as per-action slippage protection. Resolution Tenor Team: The issue was resolved in PR#485. CategorySeverityLocationStatus Validation ● Low TakeRouter.solResolved 83 L-06 | Clamp Data Not Bound To Callback Data Description SupplyCollateralCallbackClamp documents (line 29) that clampData.collateralAmounts must equal offer.callbackData.amounts, but never enforces it on-chain. TakeRouter.\_capTakeUnits passes both blobs independently to the clamp and the callback. If the keeper passes a clampData with inflated amounts, clamp() returns an inflated maxUnits, the take proceeds, and the callback supplies the lower real amount — leaving the seller with debt exceeding the supplied collateral. The only on-chain catch is the callback's optional maxLtv check (MorphoV2SupplyCollateralCallback.onSell), which defaults to 0 (unconstrained) when callbackData.length < 160. The harmed party is the taker (lender) who ends up holding undercollateralized debt — exposing them to the keeper they authorized. Recommendation Document that callers MUST pass clampData.collateralAmounts identical to the amounts field encoded in offer.callbackData, and that with maxLtv = 0 the only safety net is keeper honesty. Optionally: have the clamp abi.decode offer.callbackData and assert array equality before computing bounds. This eliminates the trust assumption entirely. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Validation ● Low src/periphery/clamps/SupplyCollateralC allbackClamp.sol#L29 Acknowledged 84 L-07 | Renewal Can Rescue A Liquidatable Obligation Description Midnight's take() does not check source-obligation health. A renewal whose source is already liquidatable will still execute, atomically repaying source debt and migrating collateral to the target — effectively dodging liquidation. This is inherent to Midnight (the same is achievable via flash loans or hand-rolled callbacks); Tenor's renewal paths just expose it as a packaged flow. Impact: borrowers (or anyone they authorize) can use renewal to avoid the liquidation incentive penalty. Risk migrates to whichever target obligation the renewal intent points at. Recommendation Document explicitly that renewal paths do not block liquidatable sources — they are not "rescue-safe" from the protocol's perspective, and borrowers configuring a more lenient target market can use renewal to bypass liquidation. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Trust Assumptions ● Low src/callbacks/MorphoV2ToV2BorrowRe pay.sol Acknowledged 85 I-01 | Effective Units Panic On Long Maturities Description \_effectiveUnitsPerWad() subtracts the target obligation's continuous-fee discount from WAD without checking that the product is below WAD. uint256 cf = MORPHO\_MIDNIGHT.continuousFee(obligationId); if (cf == 0) return WAD; uint256 t = UtilsLib.zeroFloorSub(offer.obligation.maturity, block.timestamp); return WAD - cf \* t; Midnight bounds continuousFee, but t comes from the target obligation maturity. At the maximum Midnight continuous fee, cf \* t reaches WAD at roughly 100 years. UserRenewalParams.maxDuration is a uint32, so a user can authorize a target duration above that threshold. The ratifier's target-maturity check only compares the maturity against \[block.timestamp + minDuration, block.timestamp + maxDuration\]. It does not reject maturities where continuousFee \* timeToMaturity > WAD. Consequently, a lend renewal into a very long target maturity with a nonzero continuous fee can revert with Solidity arithmetic panic 0x11 during ratification. Midnight would later reject credit creation when pending fees exceed credit, but the ratifier should fail with an intentional validation error instead of an unchecked arithmetic panic. Recommendation Reject target maturities whose continuous-fee product is too large before subtracting from WAD. uint256 fee = cf \* t; if (fee > WAD) revert InvalidTargetMaturity(); return WAD - fee; Use >= WAD instead if zero effective units should also be treated as invalid. This is preferable to silently saturating to zero because the underlying Midnight trade is not useful once continuous fees consume all target credit. Resolution Tenor Team: https://github.com/Shippooor-Labs/tenor-morpho-v2-contracts-2/pull/490/. CategorySeverityLocationStatus Validation ● Info BaseRenewalRatifier.solResolved 86 I-02 | Open Liquidator Choice Delays Liquidations Description startGracePeriod() can be called by any account once a position is unhealthy and not liquidation-locked. The caller supplies \_priorityLiquidator and the gate stores that address without checking that it is authorized, borrower-approved, or derived from a deterministic rule. \_requireLiquidationAllowed() later enforces the stored address during the first PRIORITY\_PERIOD after the grace period. While that exclusive window is active, every other liquidator reverts with LiquidationNotAllowed. Consequently, a bot watching unhealthy positions can front-run the first grace-period transaction and assign itself as the priority liquidator. If it does not liquidate, other liquidators must wait until the priority period expires. Factory-created gates currently limit this exclusive window to one minute, so the impact is limited in the intended factory setup. Direct DelayedLiquidationGate deployments can choose a longer PRIORITY\_PERIOD, making the delay materially worse. Recommendation Do not let arbitrary callers choose the priority liquidator for a permissionless grace-period start. Either remove priority assignment from startGracePeriod(), require an authorized market role to set a nonzero priority liquidator, or derive the priority liquidator from a deterministic market configuration. Also enforce the intended maximum PRIORITY\_PERIOD in the DelayedLiquidationGate constructor, not only in the factory, so direct deployments cannot bypass the deployment-time cap. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Warning ● Info DelayedLiquidationGate.solAcknowledged 87 I-03 | Rate Limits Ignore Settlement Rounding Description BaseRenewalRatifier.\_ratifyRate checks the user's rate limit against a normalized per-WAD price. It passes effUnitsPerWad and effPrice into PriceLib.satisfiesRateLimit, so the check does not know the actual takeUnits value or the integer asset amounts that Midnight.take will settle. Midnight.take later computes the real buyerAssets and sellerAssets with mixed integer rounding. For small fills, the rounded settlement price can be materially worse than the normalized price approved by the ratifier. For example, if the theoretical buyer price is 0.5e18 and the fill is 1 unit, mulDivUp charges 1 raw asset unit, so the realized price is 1e18. This is usually dust for 18-decimal assets. The gap is more relevant for low-decimal loan tokens and repeated tiny fills. In those cases a keeper can execute fills that satisfy the continuous price check while the user receives a worse realized integer price. UserRenewalParams also has no minimum fill or minimum asset constraint to let the user reject these rounded executions at the ratifier level. Recommendation Add a user-configurable minimum fill size or minimum realized asset amount to the renewal policy, then reject takes below that threshold before calling Midnight.take. Alternatively, add a post-take validation step in RenewalIntentTakeOnBehalf.take that checks the returned buyerAssets, sellerAssets and obligationUnits against the user's realized price limits. The validation should use the same rounded values that were actually settled. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Warning ● Info BaseRenewalRatifier.solAcknowledged 88 I-04 | Standing Renewals Lack Amount Bounds Description UserRenewalParams records rate, timing, cadence and market constraints. It does not include a maximum asset amount, a maximum unit amount, a one-shot nonce or cumulative consumption for the renewal user. RenewalIntentRatifier then reuses those params for every take matching (user, callback, sourceMarketId, targetMarketId). BaseRenewalRatifier validates the callback tuple, fee config, renewal window, target maturity and rate policy, but it does not validate the size of the take. For V1 to V2 lend migrations, the take size directly controls how much MorphoV1ToV2LendCallback withdraws from the user's ERC4626 source vault: IERC4626(callbackData.vault).withdraw(buyerAssets + fee, address(this), buyer); After the user has approved this callback for vault shares, authorized RenewalIntentTakeOnBehalf, authorized the ratifier and set params for the V1 to V2 tuple, any keeper can execute as much migration as offer liquidity, vault withdrawability and the callback approval allow. The user still receives target V2 credit at a rate accepted by their policy, so this is not direct theft. It can still surprise users or integrations that treat standing renewal params as consent for a bounded migration amount. A keeper can force the user's whole approved V1 vault position into V2 and charge the configured fees. Recommendation Add user-controlled amount bounds to the renewal authorization. At minimum, include a max asset or max unit amount in the params used for each (user, callback, sourceMarketId, targetMarketId) tuple and enforce it during ratification. If standing automation is meant to be reusable, track cumulative consumption under intent.user and reset it by nonce, epoch or cadence period. If a renewal should be one-shot, include an expiry or nonce and consume it before external callbacks can run. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Warning ● Info IRenewalIntentRatifier.solAcknowledged 89 I-05 | Vault Withdraw Callback Allows Position Crossing Description MorphoV2WithdrawVaultSharesCallback.onBuy() does not check whether the BUY fill crosses the buyer from debt into credit. The callback withdraws vault-share collateral, redeems the shares for buyerAssets, approves Midnight and returns success without checking the buyer's post-fill position. Midnight allows this crossing when the signed offer is not reduceOnly. That means a stale or oversized vault-withdraw BUY offer can repay the borrower's remaining debt, redeem collateral and leave the borrower with a new credit position on the same obligation. MorphoV2ToV1BorrowCallback.onBuy() rejects the same condition with PositionCrossing, but this callback does not. This may be economically valid for some users, but it contradicts the borrower early-exit intent of this callback. It also makes stale offers more dangerous: if the borrower's live debt falls below the signed fill size before execution, the callback can still complete and convert the excess into credit instead of reverting. Recommendation Add the same post-fill crossing guard used by MorphoV2ToV1BorrowCallback. After approving Midnight, call updatePositionView() for the buyer and revert with CallbackLib.PositionCrossing() if the resulting credit is nonzero. Alternatively, require all integrations that use this callback to cap take units to the buyer's current debt before calling Midnight. The safer default is to enforce the invariant inside the callback so direct Midnight fills and unclamped router calls receive the same protection. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Warning ● Info MorphoV2WithdrawVaultSharesCallback .sol Acknowledged 90 I-06 | Stale MAX\_FEE\_RATE\_V2\_V1 In Docs Description DIMENSIONAL\_UNITS.md documents MAX\_FEE\_RATE\_V2\_V1 = 0.0025e18 (0.25% with units D18{loan-token/borrow-token/sec}). The actual constant in src/libraries/Constants.sol is 0, meaning no fee component is permitted on the V2→V1 renewal direction. Integrators reading the doc to size off-chain rate calculations will use a fee assumption the contract rejects, producing renewal attempts that always revert at BaseRenewalRatifier.\_ratifyRate. Recommendation Update DIMENSIONAL\_UNITS.md so MAX\_FEE\_RATE\_V2\_V1 = 0 matches the on-chain constant. Optionally add a one-line note explaining why V2→V1 has a hard-zero fee while V2→V2 and V1→V2 have non-zero ceilings. Resolution Tenor Team: The issue was resolved in PR#456. CategorySeverityLocationStatus Documentation ● Info src/libraries/Constants.sol#L8Resolved 91 I-07 | allowRevert Scope Is Incomplete Description Action.allowRevert = true only catches reverts inside the inner \_MIDNIGHT.take / \_TAKE\_ON\_BEHALF.take try/catch. The following per-action paths run outside the try/catch and abort the entire batch despite allowRevert: 1. abi.decode(action.data, ...) — malformed action data (L:264, 297). 2. \_MIDNIGHT.touchObligation(action.offer.obligation) — invalid obligation (L:266, 299). 3. ICallbackFeeAdjuster.beforeDispatch(...) — arbitrary external call (L:347). 4. RouterLib.budgetToUnits(...) — arithmetic (L:349). 5. ClampLib.getOfferRemaining(...) — arithmetic (L:355). 6. ITakeClamp.clamp(...) — arbitrary external call (L:358). 7. ICallbackFeeAdjuster.afterDispatch(...) — arbitrary external call, runs after a successful dispatch (L:206). Integrators relying on allowRevert = true for batch resiliency may not realize that these paths still abort the whole batch. Recommendation Update the Action.allowRevert natspec and the file-level comment block to enumerate the pre-dispatch and post-dispatch revert paths that bypass it. For the external calls and decode paths, consider wrapping them in their own try/catch so allowRevert actually covers them. Resolution Tenor Team: Acknowledged. CategorySeverityLocationStatus Documentation ● Info src/periphery/TakeRouter.solAcknowledged 92 Disclaimer This report is not, nor should be considered, an “endorsement” or “disapproval” of any particular project or team. This report is not, nor should be considered, an indication of the economics or value of any “product” or “asset” created by any team or project that contracts Guardian to perform a security assessment. This report does not provide any warranty or guarantee regarding the absolute bug-free nature of the technology analyzed, nor do they provide any indication of the technologies proprietors, business, business model or legal compliance. This report should not be used in any way to make decisions around investment or involvement with any particular project. This report in no way provides investment advice, nor should be leveraged as investment advice of any sort. This report represents an extensive assessing process intending to help our customers increase the quality of their code while reducing the high level of risk presented by cryptographic tokens and blockchain technology. Blockchain technology and cryptographic assets present a high level of ongoing risk. Guardian’s position is that each company and individual are responsible for their own due diligence and continuous security. Guardian’s goal is to help reduce the attack vectors and the high level of variance associated with utilizing new and consistently changing technologies, and in no way claims any guarantee of security or functionality of the technology we agree to analyze. The assessment services provided by Guardian is subject to dependencies and under continuing development. You agree that your access and/or use, including but not limited to any services, reports, and materials, will be at your sole risk on an as-is, where-is, and as-available basis. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. The assessment reports could include false positives, false negatives, and other unpredictable results. The services may access, and depend upon, multiple layers of third-parties. Notice that smart contracts deployed on the blockchain are not resistant from internal/external exploit. Notice that active smart contract owner privileges constitute an elevated impact to any smart contract’s safety and security. Therefore, Guardian does not guarantee the explicit security of the audited smart contract, regardless of the verdict. 93 About Guardian Founded in 2022 by DeFi experts, Guardian is a leading audit firm in the DeFi smart contract space. With every audit report, Guardian upholds best-in-class security while achieving our mission to relentlessly secure DeFi. To learn more, visit https://guardianaudits.com To view our audit portfolio, visit https://github.com/guardianaudits To book an audit, message https://t.me/guardianaudits 94
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#__docusaurus_skipToContent_fallback)
On this page
`LendMidnightToVaultCallback` moves a lender out of a fixed-rate Midnight position and into an ERC-4626 vault. This can happen before maturity or after maturity.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#description "Direct link to Description")
-----------------------------------------------------------------------------------------------------------------------------------------
A lender holds a fixed-rate position on Morpho Midnight and wants to exit into a variable-rate vault, either before or after maturity. They post a sell (borrow) offer on Midnight to exit their lending position with this callback set as `receiverIfMakerIsSeller`. When the sell (borrow) offer fills, the callback receives the taker's payment and deposits it into an ERC-4626 compliant vault (typically a Morpho Vault-V2) on the lender's behalf.
### Why an ERC-4626 vault, not Morpho Blue directly?[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#why-an-erc-4626-vault-not-morpho-blue-directly "Direct link to Why an ERC-4626 vault, not Morpho Blue directly?")
The simplest destination is a Morpho Vault-V2 that allocates 1:1 into a single Morpho Blue market: an unmanaged, immutable vault that mirrors one variable-rate market.
Targeting an ERC-4626 vault rather than a Morpho Blue market directly also keeps the callback generic: the same path supports the Money Market product, curated/managed vaults, and any third-party ERC-4626 strategy whose `asset()` matches the Midnight market's loan token.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Before any take, the lender posts an exit sell (borrow) offer on the **Midnight source** market with `receiverIfMakerIsSeller` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the lender's exit sell (borrow) offer on Midnight by calling `MORPHO_MIDNIGHT.take()`. The taker's payment (`sellerAssets`) lands on the callback because the offer's `receiverIfMakerIsSeller` points at it, and Morpho Midnight invokes `onSell` on the callback.
2. **Validate the target vault:** The vault's `asset()` must equal the Midnight market's `loanToken`. Otherwise the callback reverts with `TokenMismatch`.
3. **Check the lender is debt-free on the source market:** If the lender currently holds debt on the same Midnight market, the callback reverts with `PositionCrossing`. The lender must remain a pure lender throughout the exit.
4. **Calculate & transfer fee:** A flat percentage fee is taken from `sellerAssets`; if non-zero, it is transferred to `feeRecipient`. When routed through the ratifier, the fee is forced to zero and this step is skipped.
5. **Deposit into the vault:** Approve the vault and call `deposit(sellerAssets - fee, seller)`. The lender receives the vault shares directly.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#prerequisites "Direct link to Prerequisites")
-----------------------------------------------------------------------------------------------------------------------------------------------
* The sell (borrow) offer's `receiverIfMakerIsSeller` must point at this callback so `sellerAssets` lands here before `onSell` fires.
* The lender must not be carrying debt on the same Midnight market at execution time.
* The target vault's underlying `asset()` must equal the Midnight market's `loanToken`.
Fees are disabled by default through the ratifier
The Migration Intent Ratifier hard-disables fees on Midnight → Vault callbacks (`MAX_FEE_RATE_MIDNIGHT_TO_BLUE = 0`). Any non-zero `setFeeConfig` for these callbacks reverts at the ratifier.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#description)
* [Why an ERC-4626 vault, not Morpho Blue directly?](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#why-an-erc-4626-vault-not-morpho-blue-directly)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault#prerequisites)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture#__docusaurus_skipToContent_fallback)
On this page
The Tenor platform builds on the Morpho smart contracts stack ([Morpho Midnight](https://docs.morpho.org/get-started/)
for fixed-rate, fixed-term markets, [Morpho Blue](https://docs.morpho.org/learn/concepts/morpho-markets/)
for variable-rate, open-term markets, and [Vaults V2](https://docs.morpho.org/learn/concepts/vault-v2/)
) and extends it with modular contracts (callbacks, ratifiers, oracles, and gates) that add functionality without modifying core Morpho behavior.
Primer on Morpho Midnight[](https://docs.tenor.finance/technical-docs/architecture#primer-on-morpho-midnight "Direct link to Primer on Morpho Midnight")
----------------------------------------------------------------------------------------------------------------------------------------------------------
Morpho Midnight matches lenders and borrowers on signed offers that settle into fixed-rate positions with a shared maturity and set of collateral assets. Each market is isolated with a set of immutable parameters set at creation.
### Midnight Markets[](https://docs.tenor.finance/technical-docs/architecture#midnight-markets "Direct link to Midnight Markets")
A market is the core building block of Morpho Midnight. It defines the terms of a fixed-rate, fixed-term position and supports one or more collateral types, each with its own oracle and liquidation parameters.
Each market is defined by a set of parameters:
* **Loan token:** the asset being lent and borrowed
* **Collaterals:** one or more collateral assets, each with its own token, oracle, LLTV, and max liquidation incentive factor
* **Maturity:** the timestamp at which all positions in the market expire
* **Recovery close factor threshold:** governs when liquidations switch from partial to full close
* **Enter gate:** optional gate that restricts who can take offers in the market (used for permissioned markets)
* **Liquidator gate:** optional gate that restricts who can liquidate unhealthy positions
The market ID is derived from these parameters and is immutable once deployed. Trading fees and tick spacing are set separately by Morpho and can change over a market's lifetime.
### Midnight Offers[](https://docs.tenor.finance/technical-docs/architecture#midnight-offers "Direct link to Midnight Offers")
Lending and borrowing in Morpho Midnight works through an offer and take model:
* **Offers** are intents created by lenders or borrowers, specifying amount, rate, and terms
* **Takes** are onchain transactions that match against an offer to initiate a position
Offers come in two directions, named from the perspective of the debt instrument being traded:
* **Sell offers** are posted by borrowers offering to sell (issue) debt against collateral in exchange for loan tokens. A lender takes a sell offer to fund the position. The `onSell` callback fires on the borrower's side, letting them run logic such as supplying collateral atomically when the offer is filled.
* **Buy offers** are posted by lenders offering to buy a future repayment claim by providing loan tokens. A borrower takes a buy offer to draw the position. The `onBuy` callback fires on the lender's side.
Throughout the contracts documentation, `sellerAssets` and `buyerAssets` refer to the filled amounts on the offer being taken, as returned by the [Take Router](https://docs.tenor.finance/technical-docs/architecture/contracts/take-router)
. "Seller" always means the borrower side; "buyer" always means the lender side.
When a take executes, Morpho Midnight optionally invokes a callback. The callback is a smart contract hook that can perform additional logic atomically within the same transaction.
How Tenor Extends Morpho Midnight[](https://docs.tenor.finance/technical-docs/architecture#how-tenor-extends-morpho-midnight "Direct link to How Tenor Extends Morpho Midnight")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Tenor deploys a set of opt-in contracts on top of Morpho Midnight, each extending a specific capability without modifying core behavior. On top of Morpho Midnight, Tenor uses:
* **Callbacks:** Stateless contracts invoked by Morpho Midnight during a take. They perform multi-step transitions (migrations, supply-collateral on take, [deposit borrowed funds in a vault and add vault shares as collateral](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/supply-vault-shares)
) atomically in the same transaction.
* **Ratifier:** A contract that holds each user's intent (renewal parameters, fee config, rate limits) and validates every take against it. Routed through the shared `IntentSettler`, which calls into Morpho Midnight.
* **Router:** `TenorRouter` fills a position across multiple offers in a single atomic transaction, stepping through an ordered list of takes under a fill cap, price slippage bounds, and a deadline.
* **Bundler:** `TenorAdapter` exposes Morpho Midnight, the ratifiers, and `TenorRouter` as multicallable actions through Morpho's Bundler3, so user-facing transactions batch into one atomic call.
* **Midnight Gates:** Per-market gates set as the market's `enterGate` or `liquidatorGate`. They restrict who can take offers and who can liquidate, and add timing constraints around liquidation.

### Callbacks[](https://docs.tenor.finance/technical-docs/architecture#callbacks "Direct link to Callbacks")
Callbacks are stateless contracts invoked by Morpho Midnight during `take()`, before the take is finalized. Callbacks can be used to bundle arbitrary actions into the take transaction that execute atomically alongside settlement.
#### Migration Callbacks[](https://docs.tenor.finance/technical-docs/architecture#migration-callbacks "Direct link to Migration Callbacks")
Tenor offers a suite of six callbacks that handle migrations between fixed-rate markets (Morpho Midnight) and variable-rate instruments (Morpho Blue and Vaults V2), covering both the borrow and lend sides and all three directions (entry, exit, and same-product roll). Tenor migration callbacks also support Midnight to Midnight renewals. Borrow side migration callbacks ensure collateral is moved proportionally between the source market and target market. See the [Callbacks overview](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/overview)
for the full list.
| Direction | Borrow side | Lend side |
| --- | --- | --- |
| Variable → Fixed | Blue → Midnight ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/blue-to-midnight)
) | Vault → Midnight ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/vault-to-midnight)
) |
| Fixed → Fixed | Midnight → Midnight ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/borrow-midnight-renewal)
) | Midnight → Midnight ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/lend-midnight-renewal)
) |
| Fixed → Variable | Midnight → Blue ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/midnight-to-blue)
) | Midnight → Vault ([Read more](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/midnight-to-vault)
) |
#### Standalone Callbacks[](https://docs.tenor.finance/technical-docs/architecture#standalone-callbacks "Direct link to Standalone Callbacks")
Three additional callbacks support common patterns on take:
* **[Supply collateral](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/supply-collateral)
:** supply collateral only when a borrow offer is taken, with pro-rata amounts for partial fills
* **[Supply vault shares as collateral](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/supply-vault-shares)
:** wrap loan tokens into ERC-4626 vault shares and supply as collateral on a take
* **[Withdraw vault shares on early exit](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/withdraw-vault-shares)
:** withdraw vault shares from collateral and redeem them into loan tokens to repay an early-exit buy offer
Offers can attach a callback to guarantee its execution when the offer is taken. Ratifiers can also require the inclusion of a specific callback on every offer taken on a user's behalf.
### Ratifiers[](https://docs.tenor.finance/technical-docs/architecture#ratifiers "Direct link to Ratifiers")
A user can attach a migration callback to a single offer, but this requires the user to sign a new offer with the desired callback and parameters every time their position matures and needs to be rolled. The ratifier layer removes that requirement: a user stores their renewal preferences once, and the ratifier enforces onchain that any offer taken on their behalf matches those rules (specific callbacks, rate bounds, durations, fee config, and rate limits). Ratifiers are routed through the shared `IntentSettler`, which mediates both keeper-driven takes (`take()`) and maker-side ratification (`isRatified()`) gated by a single per-user authorization map.
* **[Tenor Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/architecture/contracts/ratifiers/migration-intent)
** stores each user's migration preferences and validates every take across the six supported callbacks (same-protocol renewals and cross-protocol migrations on both the borrow and lend side). The same contract covers two primary use cases: auto-renewal of borrow positions (Midnight ↔ Midnight, Blue ↔ Midnight) and lend-side market making, where a lender expresses a full 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. The lender quotes a fixed lending rate continuously while idle capital earns the variable rate.
### Router[](https://docs.tenor.finance/technical-docs/architecture#router "Direct link to Router")
[`TenorRouter`](https://docs.tenor.finance/technical-docs/architecture/contracts/take-router)
fills a position across multiple offers atomically. Each call takes an ordered list of actions: either direct Midnight takes for users running their own batches, or renewal takes on behalf of users with pre-committed intent parameters for trustless keeper automation. The batch enforces a fill cap along a chosen dimension (buyer assets, seller assets, or units), slippage bounds on price, and a deadline. Individual actions can be marked as revertible so a single failure no-ops instead of aborting the rest of the batch.
### Bundler[](https://docs.tenor.finance/technical-docs/architecture#bundler "Direct link to Bundler")
[`TenorAdapter`](https://docs.tenor.finance/technical-docs/architecture/contracts/tenor-adapter)
makes Morpho Midnight, the ratifiers, and `TenorRouter` operations available as multicallable actions via [Bundler3](https://docs.morpho.org/get-started/resources/contracts/bundlers#bundler3-structure)
, Morpho's generic multicall contract. User-facing transactions (supply collateral, take offer, approve tokens) are batched into a single atomic transaction through the adapter.
### Gates[](https://docs.tenor.finance/technical-docs/architecture#gates "Direct link to Gates")
Gates are per-market contracts that Morpho Midnight calls on specific actions: the `enterGate` is checked when an offer is taken, and the `liquidatorGate` is checked when a liquidation is attempted. They let a market restrict participation or constrain the conditions under which a position can be liquidated, without changing core Morpho behavior. A market sets its gates at deployment and the choice is immutable.
* **[DelayedLiquidationGate](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation)
:** Gives borrowers a grace period before a liquidation can be triggered, so a position that first becomes unhealthy is not liquidated immediately. Set as the market's `liquidatorGate`, which makes the gate the sole liquidator on the market, so liquidations only execute through the gate once the grace period has elapsed. Typically used on OTC and P2P offers where counterparties want a window to top up collateral before being liquidated.
* **[Midnight Allowlist Gate](https://docs.tenor.finance/technical-docs/architecture/contracts/gates/midnight-allowlist)
:** Restricts who can lend, borrow, and liquidate on a Morpho Midnight market. Used for permissioned markets with a restricted set of participants, such as bilateral or OTC agreements between known counterparties.
Vault V2[](https://docs.tenor.finance/technical-docs/architecture#vault-v2 "Direct link to Vault V2")
-------------------------------------------------------------------------------------------------------
[Vaults V2](https://docs.morpho.org/learn/concepts/vault-v2/)
are ERC-4626 compliant vaults with role-based access control and gates, allowing allocation into different underlying markets, such as Morpho Blue variable-rate markets.
### Vault V2 Gate[](https://docs.tenor.finance/technical-docs/architecture#vault-v2-gate "Direct link to Vault V2 Gate")
Tenor developed a simple [Vault V2 Allowlist Gate](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist)
: a gate contract that enforces a list of addresses authorized to hold vault shares. Any transfer or mint to a non-allowlisted address reverts, so the deployer of a vault can constrain who can ever end up holding shares without changing the vault's other behavior.
The gate's allowlist can be updated by its owner. To make the gate (and therefore the vault's allowlist) immutable, the owner can renounce ownership of the gate. After that, the allowlist is frozen and no address can be added or removed.
One use of the gate is to deploy a private vault (restricting shares to a defined set of depositors) while retaining Vault V2's role-based access control for risk decisions and allocations. This allows a vault to allocate across both open-term (variable-rate) and fixed-term markets under operator-defined policies, with deposits limited to the allowlisted participants.
### Vault V2 as Collateral[](https://docs.tenor.finance/technical-docs/architecture#vault-v2-as-collateral "Direct link to Vault V2 as Collateral")
Vault V2 shares can be used as collateral on Morpho Midnight markets via the [supply vault shares](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/supply-vault-shares)
and [withdraw vault shares](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/withdraw-vault-shares)
callbacks. A user can keep their lend exposure on a variable-rate market (open term) while borrowing against the same position at a fixed rate and term on Midnight.
For Tenor markets, only immutable, unmanaged vaults are used. Each vault allocates 100% of deposits to a single Morpho Blue market whose parameters (loan token, collateral token, LLTV, oracle, IRM) mirror the Midnight market where the shares are used as collateral.
When a Vault-V2 is used as collateral, its allowlist gate permits only Morpho Midnight and the vault-shares callbacks to hold shares. Outside of an active take, shares cannot land on any other address. The gate confines the vault to its collateral role and prevents the shares from being transferred or held by third parties.
Because all share movement is constrained to flow through Midnight and the callbacks, integrators interacting with the position (for example, to liquidate it) cannot move shares directly. Tenor exposes [`MidnightVaultExecutor`](https://github.com/Shippooor-Labs/tenor-morpho-v2-contracts-2/blob/main/src/periphery/MidnightVaultExecutor.sol)
, a periphery contract that bundles the required steps (withdraw collateral, redeem vault shares, settle on Midnight) into a single call so liquidators and other actors can operate against vault-collateralized positions without composing the sequence themselves.
Tenor Oracles[](https://docs.tenor.finance/technical-docs/architecture#tenor-oracles "Direct link to Tenor Oracles")
----------------------------------------------------------------------------------------------------------------------
Tenor builds Morpho-compatible oracles that wrap existing price feeds with additional logic such as sanity checks, cross-feed validation, or circuit breakers, so markets can use a hardened price source without changing how the oracle integrates with Morpho. Markets that don't need this extra check can use any other oracle that implements the Morpho oracle interface.
### Oracle with Validation[](https://docs.tenor.finance/technical-docs/architecture#oracle-with-validation "Direct link to Oracle with Validation")
[Oracle with Validation](https://docs.tenor.finance/technical-docs/architecture/oracle-with-validation)
sits in front of a primary Chainlink-compatible feed and a secondary feed, and reverts if the two deviate beyond a configured threshold. This is useful for markets that want a circuit breaker on stale or manipulated prices.
* [Primer on Morpho Midnight](https://docs.tenor.finance/technical-docs/architecture#primer-on-morpho-midnight)
* [Midnight Markets](https://docs.tenor.finance/technical-docs/architecture#midnight-markets)
* [Midnight Offers](https://docs.tenor.finance/technical-docs/architecture#midnight-offers)
* [How Tenor Extends Morpho Midnight](https://docs.tenor.finance/technical-docs/architecture#how-tenor-extends-morpho-midnight)
* [Callbacks](https://docs.tenor.finance/technical-docs/architecture#callbacks)
* [Ratifiers](https://docs.tenor.finance/technical-docs/architecture#ratifiers)
* [Router](https://docs.tenor.finance/technical-docs/architecture#router)
* [Bundler](https://docs.tenor.finance/technical-docs/architecture#bundler)
* [Gates](https://docs.tenor.finance/technical-docs/architecture#gates)
* [Vault V2](https://docs.tenor.finance/technical-docs/architecture#vault-v2)
* [Vault V2 Gate](https://docs.tenor.finance/technical-docs/architecture#vault-v2-gate)
* [Vault V2 as Collateral](https://docs.tenor.finance/technical-docs/architecture#vault-v2-as-collateral)
* [Tenor Oracles](https://docs.tenor.finance/technical-docs/architecture#tenor-oracles)
* [Oracle with Validation](https://docs.tenor.finance/technical-docs/architecture#oracle-with-validation)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses#__docusaurus_skipToContent_fallback)
On this page
Deployed contract addresses for Tenor and Morpho on supported networks.
Tenor Contracts[](https://docs.tenor.finance/technical-docs/addresses#tenor-contracts "Direct link to Tenor Contracts")
-------------------------------------------------------------------------------------------------------------------------
* Ethereum
| Contract | Address | GitHub Repository |
| --- | --- | --- |
| [`MigrationIntentRatifier`](https://docs.tenor.finance/technical-docs/addresses/contracts/ratifiers/migration-intent) | \- | \- |
| [`BorrowMidnightRenewalCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/borrow-midnight-renewal) | \- | \- |
| [`LendMidnightRenewalCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/lend-midnight-renewal) | \- | \- |
| [`BorrowBlueToMidnightCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/blue-to-midnight) | \- | \- |
| [`BorrowMidnightToBlueCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/midnight-to-blue) | \- | \- |
| [`LendVaultToMidnightCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/vault-to-midnight) | \- | \- |
| [`LendMidnightToVaultCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/midnight-to-vault) | \- | \- |
| [`MidnightSupplyCollateralCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/supply-collateral) | \- | \- |
| [`MidnightSupplyVaultSharesCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/supply-vault-shares) | \- | \- |
| [`MidnightWithdrawVaultSharesCallback`](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/withdraw-vault-shares) | \- | \- |
| [`StaticRatePolicy`](https://docs.tenor.finance/technical-docs/addresses/contracts/static-rate-policy) | \- | \- |
| [`TenorRouter`](https://docs.tenor.finance/technical-docs/addresses/contracts/take-router) | \- | \- |
| [`TenorAdapter`](https://docs.tenor.finance/technical-docs/addresses/contracts/tenor-adapter) | \- | \- |
| [`DelayedLiquidationGate`](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation) | \- | \- |
| [`MidnightAllowlistGate`](https://docs.tenor.finance/technical-docs/addresses/contracts/gates/midnight-allowlist) | \- | \- |
| [`VaultV2AllowlistGate`](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist) | \- | \- |
| [`OracleWithValidation`](https://docs.tenor.finance/technical-docs/addresses/oracle-with-validation) | \- | \- |
Morpho Contracts[](https://docs.tenor.finance/technical-docs/addresses#morpho-contracts "Direct link to Morpho Contracts")
----------------------------------------------------------------------------------------------------------------------------
* Ethereum
| Contract | Address | GitHub Repository |
| --- | --- | --- |
| Morpho Midnight | \- | N/A |
| Morpho Blue | 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb | [morpho-blue](https://github.com/morpho-org/morpho-blue/tree/v1.0.0) |
| Bundler3 | 0x6566194141eefa99Af43Bb5Aa71460Ca2Dc90245 | [morpho-org/bundler3](https://github.com/morpho-org/bundler3/tree/82e44ddebae998dc8b6e5cbd29ff69786135b1d3) |
* [Tenor Contracts](https://docs.tenor.finance/technical-docs/addresses#tenor-contracts)
* [Morpho Contracts](https://docs.tenor.finance/technical-docs/addresses#morpho-contracts)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#__docusaurus_skipToContent_fallback)
On this page
Callbacks are stateless, immutable contracts invoked by Morpho Midnight during `take()`. Each callback encodes a specific state transition, runs atomically within the take, and leaves no stored state between calls.
Throughout these pages, **Midnight** refers to Morpho Midnight (fixed-rate, fixed-term) and **Blue** refers to Morpho Blue (variable-rate). `sellerAssets` and `buyerAssets` always refer to the filled amounts on the offer being taken.
Callback Catalog[](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#callback-catalog "Direct link to Callback Catalog")
-----------------------------------------------------------------------------------------------------------------------------------------------
The callbacks divide into two groups: **renewal/migration callbacks**, which are referenced by the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent)
and run as part of the migration intent flow, and **standalone callbacks**, which are used directly through `MORPHO_MIDNIGHT.take()` without involving the ratifier.
### Renewal & Migration[](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#renewal--migration "Direct link to Renewal & Migration")
| Callback | Direction | Side | Page |
| --- | --- | --- | --- |
| `BorrowMidnightRenewalCallback` | Midnight → Midnight | Borrower | [Borrow Midnight Renewal](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/borrow-midnight-renewal) |
| `LendMidnightRenewalCallback` | Midnight → Midnight | Lender | [Lend Midnight Renewal](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/lend-midnight-renewal) |
| `BorrowBlueToMidnightCallback` | Blue → Midnight | Borrower | [Blue to Midnight](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/blue-to-midnight) |
| `LendVaultToMidnightCallback` | Blue → Midnight | Lender | [Vault to Midnight](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/vault-to-midnight) |
| `BorrowMidnightToBlueCallback` | Midnight → Blue | Borrower | [Midnight to Blue](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/midnight-to-blue) |
| `LendMidnightToVaultCallback` | Midnight → Blue | Lender | [Midnight to Vault](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/midnight-to-vault) |
### Standalone[](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#standalone "Direct link to Standalone")
| Callback | Purpose | Page |
| --- | --- | --- |
| `MidnightSupplyCollateralCallback` | Pull and supply collateral atomically on a take | [Supply Collateral](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/supply-collateral) |
| `MidnightSupplyVaultSharesCallback` | Wrap loan tokens into ERC-4626 vault shares and supply as collateral on a take | [Supply Vault Shares as Collateral](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/supply-vault-shares) |
| `MidnightWithdrawVaultSharesCallback` | Withdraw vault shares from collateral and redeem to loan tokens on an early exit | [Withdraw Vault Shares as collateral](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/withdraw-vault-shares) |
Common Guarantees[](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#common-guarantees "Direct link to Common Guarantees")
--------------------------------------------------------------------------------------------------------------------------------------------------
* **`msg.sender` check:** Every callback rejects any caller other than Morpho Midnight (`OnlyMidnight`).
* **Immutability:** All callbacks pin their integration addresses (Morpho Midnight, Morpho Blue) as constructor immutables. Behavior never changes after deployment.
* **No stored state:** Callbacks hold no balances and no per-user state. Anything left on a callback is recoverable only by a subsequent take that consumes it as input.
* **Health enforcement:** Morpho Midnight checks the resulting position's health after the callback returns. A callback that leaves the borrower undercollateralized causes the entire take to revert.
Do not send tokens directly to callback contracts
Tokens transferred directly to a callback contract outside an active take can be lost; the next compatible take can consume them as inputs. Always route token movement through `MORPHO_MIDNIGHT.take()` (or the [Tenor Adapter](https://docs.tenor.finance/technical-docs/contracts/callbacks/tenor-adapter)
). Never `transfer` directly to a callback address.
Fees[](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#fees "Direct link to Fees")
-----------------------------------------------------------------------------------------------------------
Renewal and migration callbacks accept an optional fee. The fee can be set by the user when creating an offer with the callback, or be forced by a ratifier. Two fee models are used:
* **Percentage of interest fee** (Midnight → Midnight and Blue → Midnight). Derived from the offer's tick and applied to the interest portion of the offer.
* **Flat percentage fee** (Midnight → Blue and Midnight → Vault). Percentage of the value moved to the variable-rate market or vault.
Standalone callbacks (`MidnightSupplyCollateralCallback`, vault-shares callbacks) have no ability to set fees.
* [Callback Catalog](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#callback-catalog)
* [Renewal & Migration](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#renewal--migration)
* [Standalone](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#standalone)
* [Common Guarantees](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#common-guarantees)
* [Fees](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview#fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/get-started/overview/#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Supply Collateral | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#__docusaurus_skipToContent_fallback)
On this page
`MidnightSupplyCollateralCallback` lets a borrower post a sell (borrow) offer on Morpho Midnight at a specific rate without locking collateral upfront. Collateral is only pulled and supplied to Midnight when the offer fills, in proportion to the fill amount; partial fills pull proportionally less collateral.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#description "Direct link to Description")
------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts a [sell (borrow) offer](https://docs.tenor.finance/technical-docs/architecture#midnight-offers)
on Morpho Midnight with this callback encoded in the offer data. When the offer is filled (fully or partially) the callback pulls collateral from the borrower in proportion to the fill and supplies it to Midnight in the same transaction. An optional `maxLtv` parameter reverts the take if the resulting position would exceed a borrower-specified LTV cap.
This pattern lets a borrower post a borrow offer that:
* executes only at the rate specified in the offer,
* commits no collateral while the offer is available, and
* bounds the LTV at execution, so the resulting position cannot exceed the user's preferred LTV if the collateral value changes while the offer is pending.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts a sell (borrow) offer on **Morpho Midnight** with this callback encoded in the offer's callback data. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's sell (borrow) offer on Morpho Midnight by calling `MORPHO_MIDNIGHT.take()`. Morpho Midnight invokes `onSell` on the callback.
2. **Decode:** Parse the callback data: `amounts[]` (per collateral slot), `offerSellerAssets` (the pro-rata denominator), and `maxLtv` (optional LTV cap).
3. **Validate:** `amounts.length` must equal `obligation.collateralParams.length`. `offerSellerAssets` must be non-zero.
4. **Pull and supply per slot:** For each slot `i` where `amounts[i] > 0`:
* Compute the pro-rata supply: `amounts[i] * sellerAssets / offerSellerAssets`.
* If it rounds to zero, skip the slot.
* Otherwise, `safeTransferFrom` the borrower for that amount and call `supplyCollateral` on Midnight for slot `i` on the borrower's behalf.
* Slots where `amounts[i] == 0` are skipped entirely.
5. **Optional LTV check:** If `maxLtv > 0`, compute the borrower's account LTV across all collateral slots (using each slot's oracle) and revert with `InvalidLtv` if it exceeds the cap.
Parameters[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#parameters "Direct link to Parameters")
---------------------------------------------------------------------------------------------------------------------------------------
* **`amounts[]`:** the full-fill collateral amount for each slot, indexed in the same order as `market.collateralParams`. Set an entry to `0` to skip a slot.
* **`offerSellerAssets`:** the denominator used to scale collateral on partial fills. Should equal the offer's full seller-assets capacity. The contract only enforces that it is non-zero.
* **`maxLtv`:** optional cap on the borrower's resulting LTV, in WAD precision. Set to `0` to skip the check.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#prerequisites "Direct link to Prerequisites")
------------------------------------------------------------------------------------------------------------------------------------------------
The borrower must approve this callback contract for **each** collateral token to be supplied. Missing approvals revert the take on `transferFrom`.
offerSellerAssets must match the offer's true capacity
If it is set lower than the offer's full capacity, each fill pulls **more** collateral than intended and the take can revert when the borrower's balance is exceeded. If it is set higher, each fill pulls **less** collateral than intended, likely reverting on the `InvalidLtv` check, or, if `maxLtv = 0`, completing with worse-than-expected health.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#step-by-step-callback-execution)
* [Parameters](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#parameters)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-collateral/#prerequisites)
---
# Lend Vault to Midnight | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#__docusaurus_skipToContent_fallback)
On this page
`LendVaultToMidnightCallback` moves a lender's position from an ERC-4626 compliant vault into a fixed-rate position on Morpho Midnight in a single take.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#description "Direct link to Description")
------------------------------------------------------------------------------------------------------------------------------------------
The lender holds shares in an ERC-4626 vault (typically a Morpho Vault-V2). They post a buy (lend) offer on Midnight with this callback as the receiver. When the offer is taken, the callback redeems enough vault shares to fund the fixed-rate lend offer on Midnight.
This is the onchain mechanism behind [Earn while you wait](https://docs.tenor.finance/get-started/lend/offer-types#earn-while-you-wait)
: the lender's idle capital sits in a variable-rate vault until a borrower fills their fixed-rate lend offer. For Earn while you wait on Tenor, the vault is typically an immutable, unmanaged Morpho Vault-V2 that allocates into a Morpho Blue market with the same parameters as the Morpho Midnight target market (same collateral, oracle, and LLTV).
### Why an ERC-4626 vault, not Morpho Blue directly?[](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#why-an-erc-4626-vault-not-morpho-blue-directly "Direct link to Why an ERC-4626 vault, not Morpho Blue directly?")
The simplest source is a Morpho Vault-V2 that allocates 1:1 into a single Morpho Blue market: an unmanaged, immutable vault that mirrors one variable-rate market. That is what Earn while you wait uses.
Sourcing from an ERC-4626 vault rather than a Morpho Blue market directly also keeps the callback generic: the same path supports the Money Market product, curated/managed vaults, and any third-party ERC-4626 strategy whose `asset()` matches the Midnight market's loan token.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Before any take, the lender posts a buy (lend) offer on the **Midnight target** market with `receiverIfMakerIsBuyer` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the lender's buy (lend) offer on the Midnight target market by calling `MORPHO_MIDNIGHT.take()`. Morpho Midnight invokes `onBuy` on the callback.
2. **Validate:** The vault's `asset()` must be the Midnight market's `loanToken`.
3. **Compute the fee:** The fee is derived from the offer tick using `CallbackLib.buyerFeeFromTick`. It is an effective-price fee applied to the interest portion only. Zero when no fee is configured.
4. **Withdraw from the vault:** The callback calls `IERC4626.withdraw(buyerAssets + fee, address(this), buyer)`, which pulls vault shares from the lender and produces loan tokens on the callback.
5. **Settle:** If the fee is non-zero, it is transferred to the recipient. `buyerAssets` is approved back to Morpho Midnight so it can settle the new lend.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#prerequisites "Direct link to Prerequisites")
------------------------------------------------------------------------------------------------------------------------------------------------
* The lender must grant an ERC-20 allowance on the **vault's share token** to this callback. The vault's `withdraw` call pulls shares from the lender via that allowance.
* The buy (lend) offer's receiver field must point at this callback so that `onBuy` lands here on fill.
Allowance is on vault shares
The required allowance is on the vault's share token, not on the underlying loan token. A loan-token allowance is not sufficient and the migration will revert at `withdraw`.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#description)
* [Why an ERC-4626 vault, not Morpho Blue directly?](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#why-an-erc-4626-vault-not-morpho-blue-directly)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight/#prerequisites)
---
# Supply Vault Shares as Collateral | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#__docusaurus_skipToContent_fallback)
On this page
`MidnightSupplyVaultSharesCallback` wraps the loan-token proceeds of a fixed-rate borrow into ERC-4626 vault shares and supplies them as collateral on Morpho Midnight, atomically in the same take.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#description "Direct link to Description")
--------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts a [sell (borrow) offer](https://docs.tenor.finance/technical-docs/architecture#midnight-offers)
on Morpho Midnight with this callback set as `receiverIfMakerIsSeller`. When the offer is filled, the loan tokens received from the lender are deposited into an ERC-4626 vault and the resulting shares are supplied as collateral on the borrower's behalf. The borrower never holds the shares directly between transactions.
This is the supply side of the [vault-as-collateral](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral)
pattern. The withdraw side, used on early exit, is the [`MidnightWithdrawVaultSharesCallback`](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares)
.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts a sell (borrow) offer on **Morpho Midnight** with `receiverIfMakerIsSeller` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's sell (borrow) offer on Morpho Midnight by calling `MORPHO_MIDNIGHT.take()`. The loan-token proceeds (`sellerAssets`) land on the callback because the offer's `receiverIfMakerIsSeller` points at it, and Morpho Midnight invokes `onSell` on the callback.
2. **Validate** that the vault is listed at `collateralIndex` and that `asset() == loanToken`.
3. **Optional top-up:** If `additionalDepositPercent > 0`, pull additional loan tokens from the borrower, priced from the offer's tick. The borrower can over-collateralize the position this way (for example, deposit 130% of the position's value into the vault even though only 100% came from the lender).
4. **Deposit** all loan tokens held by the callback (`sellerAssets + top-up`) into the vault. Shares are minted to the callback.
5. **Supply collateral:** Call `supplyCollateral` on Morpho Midnight to supply those shares to the market on the borrower's behalf at `collateralIndex`.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#prerequisites "Direct link to Prerequisites")
--------------------------------------------------------------------------------------------------------------------------------------------------
* The offer's `receiverIfMakerIsSeller` must be this callback so that `sellerAssets` lands here before `onSell` fires.
* If `additionalDepositPercent > 0`, the borrower must have approved the callback for the loan token.
Never transfer loan tokens directly to the callback
The callback holds no state and never refunds. Any loan tokens sent to it outside an active take get swept into the next supply call as if they were part of the fill, and end up in another borrower's position. Always route through Morpho Midnight's `take()`.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares/#prerequisites)
---
# Tutorials | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/#__docusaurus_skipToContent_fallback)

This section offers step-by-step walkthroughs for the most common flows on Tenor.
### Borrow
Borrow at the market rate, place a limit offer, or open an OTC borrow position.
[More →](https://docs.tenor.finance/tutorials/guide-borrow)
### Lend
Lend at the market rate, place a limit offer, or open an OTC lend position.
[More →](https://docs.tenor.finance/tutorials/guide-lend)
### Auto-Renewal
Enable, configure, or disable auto-renewal on a borrow position to roll over at maturity.
[More →](https://docs.tenor.finance/tutorials/guide-auto-renewal)
### Manage Position
Add collateral, borrow more, roll, or withdraw on an active borrow or lend position from the Portfolio page.
[More →](https://docs.tenor.finance/tutorials/guide-manage-position)
### Exit Early
Exit a borrow or lend position before maturity by repaying or reselling, from the Portfolio page or the trading component.
[More →](https://docs.tenor.finance/tutorials/guide-exit-early)
### Set Alerts
Subscribe to notifications for liquidation risk, offer execution, maturity, and renewal events.
[More →](https://docs.tenor.finance/tutorials/guide-set-alerts)
### OTC Offers
Create or accept bespoke fixed-rate offers with custom collateral, LLTV, oracle, rate, and term.
[More →](https://docs.tenor.finance/tutorials/guide-otc-offers)
### Claim Rewards
Claim Merkl rewards earned on lending and borrowing positions, directly from Tenor or via Merkl.
[More →](https://docs.tenor.finance/tutorials/guide-rewards)
---
# Withdraw Vault Shares as collateral | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#__docusaurus_skipToContent_fallback)
On this page
`MidnightWithdrawVaultSharesCallback` redeems ERC-4626 vault-share collateral back into loan tokens to settle a borrower's exit buy (lend) offer on Morpho Midnight, atomically in the same take.
Description[](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#description "Direct link to Description")
----------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts an exit [buy (lend) offer](https://docs.tenor.finance/technical-docs/architecture#midnight-offers)
on Morpho Midnight with this callback set as `receiverIfMakerIsBuyer`. When the offer is filled, the callback pulls vault-share collateral from the borrower's Midnight position, redeems it for loan tokens via the vault, and uses the proceeds to settle the exit. The borrower never holds the shares directly between transactions.
This is the early-exit side of the [vault-as-collateral](https://docs.tenor.finance/get-started/tenor-markets/vault-as-collateral)
pattern. The supply side, used when the position opens, is the [`MidnightSupplyVaultSharesCallback`](https://docs.tenor.finance/technical-docs/contracts/callbacks/supply-vault-shares)
.
Step-by-step callback execution[](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#step-by-step-callback-execution "Direct link to Step-by-step callback execution")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The borrower posts an exit buy (lend) offer on **Morpho Midnight** with `receiverIfMakerIsBuyer` set to this callback. The offer carries the callback wiring; the take triggers it.

1. **Take:** A taker fills the borrower's exit buy (lend) offer on Morpho Midnight by calling `MORPHO_MIDNIGHT.take()`. Morpho Midnight invokes `onBuy` on the callback.
2. **Validate** that the vault is listed at `collateralIndex` and that `asset() == loanToken`.
3. **Compute shares** needed to produce `buyerAssets` loan tokens: `previewWithdraw(buyerAssets)`.
4. **Withdraw collateral:** Call `withdrawCollateral` on Morpho Midnight to pull those shares from the borrower's collateral into the callback.
5. **Redeem** the shares via `IERC4626.withdraw`, producing `buyerAssets` loan tokens.
6. **Settle:** Approve `buyerAssets` back to Morpho Midnight to settle the exit.
7. **Remainder stays:** Any remaining collateral (and any other collateral the borrower had on the market) stays in place and can be withdrawn separately.
Prerequisites[](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#prerequisites "Direct link to Prerequisites")
----------------------------------------------------------------------------------------------------------------------------------------------------
The borrower must have authorized this callback on Morpho Midnight before the exit. The callback calls `withdrawCollateral` on the borrower's behalf, and the take reverts without that authorization.
Withdraw cap drift
Shares are sized by `previewWithdraw(buyerAssets)` at execution time. If the vault's exchange rate moves between submission and inclusion (a fee event, a yield distribution, etc.), the realized collateral cost shifts. Use the Tenor Router's slippage bounds or a pre-flight simulation on time-sensitive exits.
* [Description](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#description)
* [Step-by-step callback execution](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#step-by-step-callback-execution)
* [Prerequisites](https://docs.tenor.finance/technical-docs/contracts/callbacks/withdraw-vault-shares/#prerequisites)
---
# Borrow | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-borrow/#__docusaurus_skipToContent_fallback)
On this page
Borrowing on Tenor locks in a [fixed rate](https://docs.tenor.finance/get-started/borrow/fixed-rate)
for a fixed term. You can hold the position to maturity or [exit early](https://docs.tenor.finance/tutorials/guide-exit-early)
before then. You can borrow at the [market rate](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-at-market-rate)
, with a [limit offer](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-with-a-limit-offer)
, or through an [OTC agreement](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-through-an-otc-agreement)
.
Borrow at Market Rate[](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-at-market-rate "Direct link to Borrow at Market Rate")
------------------------------------------------------------------------------------------------------------------------------------------
Borrowing at the market rate fills immediately at the best available rate.
1
Open a market
Go to the **Markets** page and select the market you want to borrow in. Make sure the **Borrow** tab is selected.
2
Enter the amount
Enter the amount of collateral you want to deposit, or the amount you want to borrow.
3
Set your LTV and term
Set your loan-to-value ratio (LTV), select an initial term, then make sure the **Market** tab is selected.
4
Review and set renewal
Click **Review Order**, then select your renewal policy. If you choose fixed-rate auto-renewal, you can set a maximum renewal rate for the fixed-rate renewal. Click **Continue** to review your order.
5
Confirm
Review your order details, then confirm and sign the transaction in your wallet.
Once your position is initiated, you can monitor and manage it on the **Portfolio** page.
Borrow with a Limit Offer[](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-with-a-limit-offer "Direct link to Borrow with a Limit Offer")
------------------------------------------------------------------------------------------------------------------------------------------------------
A limit offer posts your target rate and waits for a counterparty to match it.
1
Open a market
Go to the **Markets** page and select the market you want to borrow in. Make sure the **Borrow** tab is selected.
2
Enter the amount
Enter the amount of collateral you want to deposit, or the amount you want to borrow.
3
Set your LTV and term
Set your loan-to-value ratio (LTV), select a term, then select the **Limit** tab. Choose your target rate by clicking a tick in the market depth or entering it in the **Fixed APR** field.
4
Review and set renewal
Click **Review Order**, then select your renewal policy. If you choose fixed-rate auto-renewal, you can set a maximum renewal rate for the fixed-rate renewal. Click **Continue** to review your order.
5
Confirm
Review your order details, then confirm and sign the transaction in your wallet.
Your collateral stays in your wallet and is only deposited when your limit offer is filled. Once you place your limit offer, you can monitor and manage it on the **Portfolio** page.
Cancel a Limit Offer[](https://docs.tenor.finance/tutorials/guide-borrow/#cancel-a-limit-offer "Direct link to Cancel a Limit Offer")
---------------------------------------------------------------------------------------------------------------------------------------
You can cancel a pending limit offer at any time before it is filled, directly from the **Portfolio** page.
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Expand the pending offer
If your pending offer is collapsed, click **View pending offer** to expand it. If it is already expanded, the cancel button is visible.
3
Cancel the offer
Click **Cancel** to cancel your limit offer.
Borrow through an OTC Agreement[](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-through-an-otc-agreement "Direct link to Borrow through an OTC Agreement")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
See the [OTC agreements guide](https://docs.tenor.finance/tutorials/guide-otc-offers)
for details.
* [Borrow at Market Rate](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-at-market-rate)
* [Borrow with a Limit Offer](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-with-a-limit-offer)
* [Cancel a Limit Offer](https://docs.tenor.finance/tutorials/guide-borrow/#cancel-a-limit-offer)
* [Borrow through an OTC Agreement](https://docs.tenor.finance/tutorials/guide-borrow/#borrow-through-an-otc-agreement)
---
# Auto-Renewal | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-auto-renewal/#__docusaurus_skipToContent_fallback)
On this page
Auto-renewal automatically rolls over your borrow position at maturity. Learn more about [how auto-renewal works](https://docs.tenor.finance/get-started/borrow/auto-renewal)
.
For a New Position[](https://docs.tenor.finance/tutorials/guide-auto-renewal/#for-a-new-position "Direct link to For a New Position")
---------------------------------------------------------------------------------------------------------------------------------------
You set the renewal policy when you initiate your borrow position.
1
Review your order
After entering your borrow transaction details, click **Review Order**.
2
Select a renewal policy
Select the renewal policy you want.
3
Set your maximum renewal rate
If you select fixed-rate auto-renewal, you can set your maximum renewal rate. Adjust it directly on the card, or open the **What happens at maturity?** section and click the maximum renewal rate.
**Note:** If the position cannot be renewed at a fixed rate, it defaults to a variable rate for an open term.
For an Existing Position[](https://docs.tenor.finance/tutorials/guide-auto-renewal/#for-an-existing-position "Direct link to For an Existing Position")
---------------------------------------------------------------------------------------------------------------------------------------------------------
1
Select your position
Go to the **Portfolio** page and select the position you want to update.
2
Open the renewal settings
On the position's page, click the **filter icon** on the **Maturity** line.
3
Select a renewal policy
Select your preferred renewal policy.
4
Set your maximum renewal rate
If you select fixed-rate auto-renewal, you can set your maximum renewal rate. Adjust it directly on the card, or open the **What happens at maturity?** section and click the maximum renewal rate.
* [For a New Position](https://docs.tenor.finance/tutorials/guide-auto-renewal/#for-a-new-position)
* [For an Existing Position](https://docs.tenor.finance/tutorials/guide-auto-renewal/#for-an-existing-position)
---
# Manage Position | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-manage-position/#__docusaurus_skipToContent_fallback)
On this page
Manage your active positions from three places:
* **Portfolio page:** Each position is listed in the positions table. Hover the row for its available actions, or click the position to open its detail page.
* **Position detail page:** The **Actions** tab groups every action for that position.
* **Trading component:** On the **Markets** page, select the market and term to act on a position you hold there.
Available actions depend on whether the position is a borrow or a lend position.
Borrow Positions[](https://docs.tenor.finance/tutorials/guide-manage-position/#borrow-positions "Direct link to Borrow Positions")
------------------------------------------------------------------------------------------------------------------------------------
### Add Collateral[](https://docs.tenor.finance/tutorials/guide-manage-position/#add-collateral "Direct link to Add Collateral")
You can add collateral at any time while a position is active.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Select Add collateral
In the positions table, hover over the position's row, then click the **three dots** and click **Add collateral**. You can also click the position to open its detail page and click **Add collateral** in the **Actions** tab. Both paths open the **Add collateral** window.
3
Enter the amount
Enter the amount of collateral you want to add.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Select the Add tab
In the **Collateral** section, make sure the **Add** tab is selected.
4
Enter the amount
Enter the amount of collateral you want to add.
5
Review and confirm
Click **Review Order**, review the details, then confirm and sign the transaction in your wallet.
### Withdraw Collateral[](https://docs.tenor.finance/tutorials/guide-manage-position/#withdraw-collateral "Direct link to Withdraw Collateral")
You can withdraw collateral at any time, as long as the withdrawal keeps the position within its maximum LTV.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Select Withdraw collateral
In the positions table, hover over the position's row, then click the **three dots** and click **Withdraw collateral**. You can also click the position to open its detail page, then in the **Actions** tab click the **three dots** in the **Collateral** section and click **Withdraw collateral**. Both paths open the **Withdraw collateral** window.
3
Enter the amount
Enter the amount of collateral you want to withdraw.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Select the Withdraw tab
In the **Collateral** section, select the **Withdraw** tab.
4
Enter the amount
Enter the amount of collateral you want to withdraw.
5
Review and confirm
Click **Review Order**, review the details, then confirm and sign the transaction in your wallet.
### Borrow More[](https://docs.tenor.finance/tutorials/guide-manage-position/#borrow-more "Direct link to Borrow More")
You can borrow more against an existing position. To borrow at a different term, follow the standard [borrow guide](https://docs.tenor.finance/tutorials/guide-borrow)
.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Select Borrow more
In the positions table, hover over the position's row, then click the **three dots** and click **Borrow more**. You can also click the position to open its detail page and click **Borrow more** in the **Borrow** section of the **Actions** tab. Both paths open the **Borrow more** window.
3
Enter the amount
Enter the additional amount you want to borrow.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Enter the amount
In the **Borrow more** section, enter the additional amount you want to borrow.
4
Review and confirm
Click **Review Order**, review the details, then confirm and sign the transaction in your wallet.
**Note:** If the new total exceeds the position's maximum LTV, you will need to add collateral.
### Roll[](https://docs.tenor.finance/tutorials/guide-manage-position/#roll "Direct link to Roll")
You can roll a borrow position to a new term before maturity.
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Start the roll
In the positions table, hover over the position's row, then click **Roll**. You can also click the position to open its detail page, then in the **Borrow** section of the **Actions** tab, click the **three dots** next to **Borrow more** and click **Roll**. Both paths open the **Roll position** window.
3
Choose a new term
The **Roll position** window shows your current term alongside the new term you are rolling into. If there is liquidity at a longer-dated term, you can roll at the market rate; select the term you want under **New Term**. If there is no liquidity, select the **Limit** tab to place a limit offer at your target rate. Review the new rate before continuing.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
Lend Positions[](https://docs.tenor.finance/tutorials/guide-manage-position/#lend-positions "Direct link to Lend Positions")
------------------------------------------------------------------------------------------------------------------------------
### Lend More[](https://docs.tenor.finance/tutorials/guide-manage-position/#lend-more "Direct link to Lend More")
You can add more funds to an existing lend position.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Select Lend more
In the positions table, hover over the position's row, then click **Lend more**. You can also click the position to open its detail page and click **Lend more** in the **Lend** section of the **Actions** tab. Both paths open the **Lend more** window.
3
Enter the amount
Enter the additional amount you want to lend.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Select the Lend tab
Select the **Lend** tab.
4
Enter the amount
In the **Lend more** section, enter the additional amount you want to lend.
5
Review and confirm
Click **Review Order**, review the details, then confirm and sign the transaction in your wallet.
### Withdraw[](https://docs.tenor.finance/tutorials/guide-manage-position/#withdraw "Direct link to Withdraw")
After the position reaches maturity, you can withdraw the amount you lent plus the interest earned.
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Select Withdraw
Once the position has reached maturity, hover over its row in the positions table, then click **Withdraw**. You can also click the position to open its detail page and click **Withdraw** in the **Actions** tab. Both paths open the **Withdraw** window.
3
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet.
* [Borrow Positions](https://docs.tenor.finance/tutorials/guide-manage-position/#borrow-positions)
* [Add Collateral](https://docs.tenor.finance/tutorials/guide-manage-position/#add-collateral)
* [Withdraw Collateral](https://docs.tenor.finance/tutorials/guide-manage-position/#withdraw-collateral)
* [Borrow More](https://docs.tenor.finance/tutorials/guide-manage-position/#borrow-more)
* [Roll](https://docs.tenor.finance/tutorials/guide-manage-position/#roll)
* [Lend Positions](https://docs.tenor.finance/tutorials/guide-manage-position/#lend-positions)
* [Lend More](https://docs.tenor.finance/tutorials/guide-manage-position/#lend-more)
* [Withdraw](https://docs.tenor.finance/tutorials/guide-manage-position/#withdraw)
---
# Set Alerts | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-set-alerts/#__docusaurus_skipToContent_fallback)
On this page
You can enable alerts to receive notifications about events, such as liquidations, maturities, or renewals.
Alert Types[](https://docs.tenor.finance/tutorials/guide-set-alerts/#alert-types "Direct link to Alert Types")
----------------------------------------------------------------------------------------------------------------
In-app notifications are enabled by default. Tenor supports four alert types:
| Alert | Description |
| --- | --- |
| **Liquidation** | Notifies you when a position is at risk of liquidation. You can configure the threshold. |
| **Maturity** | Reminds you before a position matures. You can configure the timing. |
| **Activity** | Updates you on renewals and limit offer executions. |
| **Inbox** | Notifies you when a counterparty responds to your quote request. |
View Your Notifications[](https://docs.tenor.finance/tutorials/guide-set-alerts/#view-your-notifications "Direct link to View Your Notifications")
----------------------------------------------------------------------------------------------------------------------------------------------------
Click the **bell icon** in the navbar to open the notification panel. Any notifications you have are listed there. To clear them, click the **trash icon** in the panel header.
Manage Your Alerts[](https://docs.tenor.finance/tutorials/guide-set-alerts/#manage-your-alerts "Direct link to Manage Your Alerts")
-------------------------------------------------------------------------------------------------------------------------------------
You can also enable alerts from the confirmation screen after initiating a position.
1
Open notification preferences
In the notification panel, click **Preferences** in the header.
2
Toggle the alerts you want
Each alert type can be turned on or off independently with its toggle.
3
Choose delivery channels
Beyond in-app, Tenor delivers alerts over email, SMS, and Telegram. Click the three dots beside an alert's toggle to enable or disable each channel for that alert.
4
Set thresholds and timing
For liquidation alerts, set the threshold at which the alert is triggered, expressed as a percentage below the liquidation LTV. For maturity alerts, set how long before maturity the alert is triggered.
You can edit or disable alerts at any time.
**Note:** Tenor uses [Privy](https://www.privy.io/)
to store your contact information (email, SMS, Telegram).
* [Alert Types](https://docs.tenor.finance/tutorials/guide-set-alerts/#alert-types)
* [View Your Notifications](https://docs.tenor.finance/tutorials/guide-set-alerts/#view-your-notifications)
* [Manage Your Alerts](https://docs.tenor.finance/tutorials/guide-set-alerts/#manage-your-alerts)
---
# Lend | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-lend/#__docusaurus_skipToContent_fallback)
On this page
Lending on Tenor locks in a [fixed rate](https://docs.tenor.finance/get-started/lend/fixed-rate)
for a fixed term. You hold the position to maturity, though you may be able to [exit early](https://docs.tenor.finance/tutorials/guide-exit-early)
before then, depending on available liquidity. You can lend at the [market rate](https://docs.tenor.finance/tutorials/guide-lend/#lend-at-market-rate)
, with a [limit offer](https://docs.tenor.finance/tutorials/guide-lend/#lend-with-a-limit-offer)
, or through an [OTC offer](https://docs.tenor.finance/tutorials/guide-lend/#lend-through-an-otc-offer)
.
Lend at Market Rate[](https://docs.tenor.finance/tutorials/guide-lend/#lend-at-market-rate "Direct link to Lend at Market Rate")
----------------------------------------------------------------------------------------------------------------------------------
Lending at the market rate fills immediately at the best available rate.
1
Open a market
Go to the **Markets** page and select the market you want to lend in. Make sure the **Lend** tab is selected.
2
Enter the amount
Enter the amount you want to lend.
3
Set your term
Select a term, then make sure the **Market** tab is selected.
4
Review your order
Click **Review Order**.
5
Confirm
Review your order details, then confirm and sign the transaction in your wallet.
Once your position is initiated, you can monitor and manage it on the **Portfolio** page.
Lend with a Limit Offer[](https://docs.tenor.finance/tutorials/guide-lend/#lend-with-a-limit-offer "Direct link to Lend with a Limit Offer")
----------------------------------------------------------------------------------------------------------------------------------------------
A limit offer posts your target rate and waits for a counterparty to match it.
1
Open a market
Go to the **Markets** page and select the market you want to lend in. Make sure the **Lend** tab is selected.
2
Enter the amount
Enter the amount you want to lend.
3
Set your term and rate
Select a term, then select the **Limit** tab. Choose your target rate by clicking a tick in the market depth or entering it in the **Fixed APR** field.
4
Review and choose Earn while you wait
Click **Review Order**, then choose whether to enable [Earn while you wait](https://docs.tenor.finance/get-started/lend/offer-types#earn-while-you-wait)
or leave your funds idle while your offer is pending. With Earn while you wait, your funds earn the variable rate until your offer is filled.
5
Confirm
Review your order details, then confirm and sign the transaction in your wallet.
Once you place your limit offer, you can monitor and manage it on the **Portfolio** page.
Cancel a Limit Offer[](https://docs.tenor.finance/tutorials/guide-lend/#cancel-a-limit-offer "Direct link to Cancel a Limit Offer")
-------------------------------------------------------------------------------------------------------------------------------------
You can cancel a pending limit offer at any time before it is filled, directly from the **Portfolio** page.
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed.
2
Expand the pending offer
If your pending offer is collapsed, click **View pending offer** to expand it. If it is already expanded, the cancel button is visible.
3
Cancel the offer
Click **Cancel** to cancel your limit offer.
Lend through an OTC Offer[](https://docs.tenor.finance/tutorials/guide-lend/#lend-through-an-otc-offer "Direct link to Lend through an OTC Offer")
----------------------------------------------------------------------------------------------------------------------------------------------------
See the [OTC offers guide](https://docs.tenor.finance/tutorials/guide-otc-offers)
for details.
* [Lend at Market Rate](https://docs.tenor.finance/tutorials/guide-lend/#lend-at-market-rate)
* [Lend with a Limit Offer](https://docs.tenor.finance/tutorials/guide-lend/#lend-with-a-limit-offer)
* [Cancel a Limit Offer](https://docs.tenor.finance/tutorials/guide-lend/#cancel-a-limit-offer)
* [Lend through an OTC Offer](https://docs.tenor.finance/tutorials/guide-lend/#lend-through-an-otc-offer)
---
# Claim Rewards | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-rewards/#__docusaurus_skipToContent_fallback)
Tenor distributes rewards through [Merkl](https://merkl.xyz/)
, a decentralized reward distribution protocol. Rewards may come from Tenor or from third-party liquidity incentive programs. You can claim them directly from the Tenor interface or through the Merkl website.
Learn more about [how rewards work](https://docs.tenor.finance/get-started/rewards)
.
1
Open the rewards menu
Click the **gift icon** to the left of the wallet connect button.
2
Select your rewards
Select the rewards you want to claim.
3
Claim
Click **Claim**.
**Note:** Rewards are not calculated in real time. Your claimable balance may not reflect your most recent activity immediately.
---
# Exit Early | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-exit-early/#__docusaurus_skipToContent_fallback)
On this page
Borrow Positions[](https://docs.tenor.finance/tutorials/guide-exit-early/#borrow-positions "Direct link to Borrow Positions")
-------------------------------------------------------------------------------------------------------------------------------
A borrow position is exited by closing its debt before maturity. Because a full repayment is always available, you can exit a borrow position at any time.
You close your debt through the market, with two ways to exit:
* **Market repayment:** You repay at the current market rate. When there is liquidity, the app matches existing offers, which can cost less than repaying at par; otherwise it repays the remaining interest in full.
* **Limit offer:** From the trading component, you set a target rate and relist your position as a limit offer, then wait for a counterparty.
If no counterparty fills your offer, the position stays open and you can still repay in full at any time. See [Exit Early](https://docs.tenor.finance/get-started/borrow/repay-early)
for how rates affect the cost of exiting.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed. You can exit directly from the positions table without opening the position's detail page.
2
Select Repay
In the positions table, hover over the position's row, then click the **three dots** and click **Repay**. You can also click the position to open its detail page, then in the **Borrow** section of the **Actions** tab, click the **three dots** and click **Repay**. Both paths open the **Repay** window.
3
Enter the amount to repay
In the **Repay** window, enter the amount you want to repay, up to the **Max repay** shown. The app takes the most efficient path for that amount: it matches existing offers at the current market rate when there is liquidity, otherwise it repays at par. Review the cost to close.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet. To exit through the trading component instead, click **Trading view** in the window.
You can also exit from the trading component, where you can repay at the current market rate or relist your position as a limit offer at a target rate and wait for a counterparty.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Open the Repay prompt
Select the **Lend** tab. Because you already hold a borrow position at that term, the app shows an **Existing borrow position** prompt instead of letting you initiate a new one. Click **Repay** in that prompt to open a **Repay** tab.
4
Set market or limit
In the **Repay** tab, enter the amount you want to repay, then choose **Market** to repay against an existing offer at the current rate, or **Limit** to set a target rate and relist your position as a limit offer.
5
Confirm
Click **Review Order** for a market repayment or **Review Limit Order** for a limit offer, then confirm and sign the transaction in your wallet.
Lend Positions[](https://docs.tenor.finance/tutorials/guide-exit-early/#lend-positions "Direct link to Lend Positions")
-------------------------------------------------------------------------------------------------------------------------
A lend position is exited by reselling it to another lender before maturity. Unlike a borrow position, which can always be repaid at par, exiting a lend position depends on liquidity: reselling needs a counterparty willing to buy the position.
You resell your position through the market, with two ways to exit:
* **Market resale:** You resell at the current market rate. When liquidity covers only part of your position, the resale is partial: you resell up to the amount available liquidity can absorb and hold the rest to maturity.
* **Limit offer:** From the trading component, you set a target rate and wait for a counterparty, whether to hold out for a better rate than the market offers or because there is no liquidity to resell against right now.
If no counterparty fills your offer, you hold the position to maturity and withdraw your principal and interest then. See [Exit Early](https://docs.tenor.finance/get-started/lend/resell-early)
for how rates affect the value of your position when exiting.
* Portfolio page
* Trading component
1
Go to the Portfolio page
Go to the **Portfolio** page, where your positions are listed. You can exit directly from the positions table without opening the position's detail page.
2
Select Resell
In the positions table, hover over the position's row, then click the **three dots** and click **Resell**. You can also click the position to open its detail page, then in the **Lend** section of the **Actions** tab, click the **three dots** and click **Resell**. Both paths open the **Resell** window.
3
Enter the amount to resell
In the **Resell** window, enter the amount you want to resell, up to the **Max resell** shown. The position is resold against existing offers at the current market rate, so the **Max resell** reflects the liquidity available to buy it. Review the resale value and any gain or loss before continuing.
4
Confirm
Click **Continue**, then confirm and sign the transaction in your wallet. To exit through the trading component instead, click **Trading view** in the window.
You can also exit from the trading component, where you can resell at the current market rate or relist your position as a limit offer at a target rate and wait for a counterparty.
1
Open a market
Go to the **Markets** page and select the market where you hold the position.
2
Select the term
Select the term your position is in so it displays in the component.
3
Open the Resell prompt
Select the **Borrow** tab. Because you already hold a lend position at that term, the app shows an **Existing lend position** prompt instead of letting you initiate a new one. Click **Resell** in that prompt to open a **Resell** tab. The prompt appears only when the selected term matches your position's term.
4
Set market or limit
In the **Resell** tab, enter the amount you want to resell, then choose **Market** to resell against an existing offer at the current rate, or **Limit** to set a target rate and relist your position as a limit offer.
5
Confirm
Click **Review Order** for a market resale or **Review Limit Order** for a limit offer, then confirm and sign the transaction in your wallet.
**Note:** If market rates have moved since your position was initiated, you may realize a gain or a loss when reselling. Learn more about [how rates affect early exit value](https://docs.tenor.finance/get-started/lend/resell-early)
.
* [Borrow Positions](https://docs.tenor.finance/tutorials/guide-exit-early/#borrow-positions)
* [Lend Positions](https://docs.tenor.finance/tutorials/guide-exit-early/#lend-positions)
---
# OTC Agreements | Tenor
[Skip to main content](https://docs.tenor.finance/tutorials/guide-otc-offers/#__docusaurus_skipToContent_fallback)
On this page
OTC lets you reach [bespoke fixed-rate agreements](https://docs.tenor.finance/get-started/otc/create-otc-offers)
with custom collateral, LLTV, oracle, rate, and term, settled onchain through the Morpho protocol. You can [browse offers](https://docs.tenor.finance/tutorials/guide-otc-offers/#browse-offers)
, [request a quote](https://docs.tenor.finance/tutorials/guide-otc-offers/#request-a-quote)
from counterparties, or [create an offer](https://docs.tenor.finance/tutorials/guide-otc-offers/#create-an-offer)
.
Browse Offers[](https://docs.tenor.finance/tutorials/guide-otc-offers/#browse-offers "Direct link to Browse Offers")
----------------------------------------------------------------------------------------------------------------------
The **Available Offers** section lists the OTC offers you can act on.
1
Open the OTC page
Go to the **OTC** tab in the navbar.
2
Select Borrow or Lend
Select the **Borrow** tab to find offers to borrow against, or the **Lend** tab to find offers to lend into.
3
Filter the offers
Filter by the borrow or lend currency and by collateral. On either tab, click **Add filter** to also filter by rate, counterparty, and maturity.
4
Review an offer
Select an offer to see its full terms, including the rate, collateral, LLTV, oracle, and maturity.
5
Accept the offer
Click **Accept**, then confirm and sign the transaction in your wallet. The position settles onchain and appears on your **Portfolio** page.
Request a Quote[](https://docs.tenor.finance/tutorials/guide-otc-offers/#request-a-quote "Direct link to Request a Quote")
----------------------------------------------------------------------------------------------------------------------------
When borrowing, you can [request a quote](https://docs.tenor.finance/get-started/otc/request-offers)
instead of setting every term yourself. A request is a non-binding, offchain message sent to the counterparties you choose. It requires no signature and deposits no collateral.
1
Open the request form
On the **OTC** page, make sure the **Borrow** tab is selected, then click **Request Quote**.
2
Enter the borrow and collateral details
Enter the amount and currency you want to borrow, and select the collateral you will post.
3
Set the maturity
Select the maturity for the position.
4
Choose counterparties
Select which counterparties to send the request to. You can target a specific set or send it to all of them.
5
Send the request
Optionally set the oracle, liquidation LTV, and request expiry, then send the request.
Counterparties respond with onchain offers that fill in the remaining terms, such as the rate. See [Accept Offers](https://docs.tenor.finance/tutorials/guide-otc-offers/#accept-offers)
to review and accept them.
Create an Offer[](https://docs.tenor.finance/tutorials/guide-otc-offers/#create-an-offer "Direct link to Create an Offer")
----------------------------------------------------------------------------------------------------------------------------
Creating an offer posts your exact terms onchain for a counterparty to accept. You can create both borrow and lend offers, so select the correct side before you set your terms.
1
Open the form and select Borrow or Lend
Go to the **OTC** page and click **Create Offer**. Make sure the **Create Offers** tab is selected, then choose **Borrow** or **Lend**.
2
Enter the amount and currency
Enter the amount and currency you want to borrow or lend.
3
Set the collateral
* **Borrow offer:** enter the collateral amount and asset you will post.
* **Lend offer:** select the collateral assets you accept.
4
Set the terms
Select the price oracle, set the interest rate (Fixed APR), and select the maturity.
5
Review and publish
Optionally adjust the offer expiry or restrict the offer to specific [counterparties](https://docs.tenor.finance/get-started/otc/gated-markets)
. Click **Review Offer**, then confirm and sign the transaction in your wallet to publish the offer onchain.
Your offer is now visible to counterparties, and any eligible counterparty can accept it. You can cancel it at any time before it is accepted.
Track Your Requests[](https://docs.tenor.finance/tutorials/guide-otc-offers/#track-your-requests "Direct link to Track Your Requests")
----------------------------------------------------------------------------------------------------------------------------------------
The quote requests you have sent appear in a banner above the **Available Offers** section on the OTC page, and in the **My requests** view on the Portfolio page.
To manage them, click **My requests** to open the **Quote Requests** table, which lists each request with its borrow amount, collateral, max term, expiry, and recipients. From there, click **View request** for the full details, or **Cancel** to withdraw a request before it expires.
Accept Offers[](https://docs.tenor.finance/tutorials/guide-otc-offers/#accept-offers "Direct link to Accept Offers")
----------------------------------------------------------------------------------------------------------------------
Offers you receive land in your inbox, shown in the **Requests** panel on the Portfolio page. This includes counterparties' responses to your quote requests and direct offers sent to you.
1
Open the offer
In the **Requests** panel, review the offer preview, including the counterparty, rate, and collateral. Click the action button (for example, **Borrow**) to open the full position details. The offer is also available under **My requests**.
2
Confirm
Review the position details, then confirm and sign the transaction in your wallet.
Your collateral is deposited into the Morpho Midnight smart contracts and the position settles onchain. Once the position is initiated, the funds you borrow are available to you and you can manage the position on the **Portfolio** page.
Click **Mark as read** to clear an offer from the preview. It stays available under **My requests**.
* [Browse Offers](https://docs.tenor.finance/tutorials/guide-otc-offers/#browse-offers)
* [Request a Quote](https://docs.tenor.finance/tutorials/guide-otc-offers/#request-a-quote)
* [Create an Offer](https://docs.tenor.finance/tutorials/guide-otc-offers/#create-an-offer)
* [Track Your Requests](https://docs.tenor.finance/tutorials/guide-otc-offers/#track-your-requests)
* [Accept Offers](https://docs.tenor.finance/tutorials/guide-otc-offers/#accept-offers)
---
# Four Week Cadence | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/four-week-cadence/#__docusaurus_skipToContent_fallback)
On this page
The Four Week Cadence contract enforces that renewal target maturities fall on 28-day boundaries measured from the Unix epoch at 00:00:00 UTC. When configured as a user's `renewalCadence` in the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
, it rejects any renewal whose target maturity is not a clean multiple of 28 days from epoch.
When every fixed-term market in the protocol uses this cadence, renewals concentrate on the same four-week maturity dates, which deepens liquidity at each point instead of fragmenting it across arbitrary timestamps.
Boundary Function[](https://docs.tenor.finance/technical-docs/contracts/four-week-cadence/#boundary-function "Direct link to Boundary Function")
--------------------------------------------------------------------------------------------------------------------------------------------------
The contract exposes a single function from `IRenewalCadence`:
nearestBoundary(timestamp) = (timestamp / 28 days) * 28 days
`nearestBoundary` returns the largest cadence point less than or equal to the given timestamp. The Migration Intent Ratifier uses it in two places.
* **Target maturity validation:** During Midnight renewals and Midnight-to-Blue exits, the ratifier calls `nearestBoundary(targetMaturity)` and reverts if the returned value is not exactly `targetMaturity`. This guarantees the new maturity lands on a four-week boundary.
* **Blue-to-Midnight renewal anchor:** Blue sources have no maturity, so the ratifier uses `nearestBoundary(block.timestamp)` as the renewal period start. Rate interpolation and elapsed-time calculations are anchored to the most recent cadence boundary.
Cadence is optional, except for Blue-to-Midnight
If `renewalCadence` is set to `address(0)`, cadence validation is skipped. For Blue-to-Midnight migrations, a cadence is required and the ratifier reverts if one is not set.
* [Boundary Function](https://docs.tenor.finance/technical-docs/contracts/four-week-cadence/#boundary-function)
---
# Morpho Midnight Allowlist Gate | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#__docusaurus_skipToContent_fallback)
On this page
The Morpho Midnight Allowlist Gate gives a market owner full control over who can lend into, borrow from, and liquidate a Morpho Midnight market. It combines `IEnterGate` (controlling who can take offers) and `ILiquidatorGate` (controlling who can liquidate) in a single contract, with three independent permission flags per address.
For example, an institution or fund can deploy a market with this gate set as both the `enterGate` and the `liquidatorGate`, then allowlist a curated set of verified lenders and borrowers while authorizing only a small set of trusted liquidators. Anyone outside the allowlist is rejected at take time and at liquidation time.
Setup[](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#setup "Direct link to Setup")
---------------------------------------------------------------------------------------------------------------------
Midnight Allowlist Gates are deployed via the `MidnightAllowlistGateFactory` using CREATE2 for deterministic addresses. After deployment, the gate blocks all takes and liquidations by default because every permission flag defaults to `false`.
Setup takes two steps:
1. Deploy the gate via the factory, passing the owner address and a salt.
2. The owner calls `setAllowlist(roles[])` to configure permissions for participating addresses.
The owner can later call `renounceOwnership()` to make the allowlist permanently immutable.
Role-Based Permissions[](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#role-based-permissions "Direct link to Role-Based Permissions")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Each allowlisted address has three independent permission flags.
* **canIncreaseCredit:** Whether the address can lend into the market (taking a sell (borrow) offer or otherwise increasing credit).
* **canIncreaseDebt:** Whether the address can borrow from the market (taking a buy (lend) offer or otherwise increasing debt).
* **canLiquidate:** Whether the address can call `liquidate()` on the market.
Flags are independent: an address can be permitted to lend but not borrow, or to liquidate but not lend. The owner can set multiple roles in a single `setAllowlist` call.
Gate Interface[](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#gate-interface "Direct link to Gate Interface")
------------------------------------------------------------------------------------------------------------------------------------------------
The gate implements three view functions that Morpho Midnight queries:
* `canIncreaseCredit(address) → bool`: checked before any take that increases credit (lend side).
* `canIncreaseDebt(address) → bool`: checked before any take that increases debt (borrow side).
* `canLiquidate(address) → bool`: checked before any liquidation.
Each returns the corresponding flag from the `allowlist` mapping. There is no fee-recipient exemption: unlike the [Vault V2 Allowlist Gate](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist)
, the Midnight allowlist gate does not auto-permit any address outside the explicit allowlist.
Ownership[](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#ownership "Direct link to Ownership")
---------------------------------------------------------------------------------------------------------------------------------
The gate uses OpenZeppelin's `Ownable2Step` for safe two-step ownership transfers.
* The owner is the only address that can call `setAllowlist` to update permissions.
* The owner can call `renounceOwnership()` to permanently lock the allowlist, making it immutable.
Use Cases[](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#use-cases "Direct link to Use Cases")
---------------------------------------------------------------------------------------------------------------------------------
* Permissioned markets for institutional users.
* Compliance-controlled markets that restrict participation to KYC-verified addresses.
* Specialist liquidator setups where only pre-approved keepers can seize collateral.
* [Setup](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#setup)
* [Role-Based Permissions](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#role-based-permissions)
* [Gate Interface](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#gate-interface)
* [Ownership](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#ownership)
* [Use Cases](https://docs.tenor.finance/technical-docs/contracts/gates/midnight-allowlist/#use-cases)
---
# Delayed Liquidation Gate | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#__docusaurus_skipToContent_fallback)
On this page
The Delayed Liquidation Gate gives borrowers a grace period before a liquidation can be triggered onchain, reducing the risk of immediate liquidation the moment a position first becomes unhealthy. Lenders and borrowers can create a custom market that uses this gate by setting it as the `liquidatorGate`, after which Morpho Midnight only permits liquidations that originate from this contract.
The grace period gives borrowers a window to add collateral or repay debt before any liquidator can seize their position. For example, a borrower with a 30-day USDC position briefly crosses their liquidation LTV during a 5% ETH flash crash. Under a gate configured with a 1-hour grace period, the borrower has 60 minutes to restore position health before liquidation becomes possible.
Risk for lenders
Lenders filling offers configured with a delayed liquidation gate take on additional price risk. If collateral value falls sharply during the grace period, bad debt becomes more likely. The grace period must also be started onchain: until someone calls `startGracePeriod`, no liquidation can happen, even if the position is already unhealthy. Lenders can require lower LLTVs or higher rates to compensate for these risks.
Delayed liquidation steps[](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#delayed-liquidation-steps "Direct link to Delayed liquidation steps")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Morpho Midnight calls `canLiquidate(caller)` before allowing any liquidation; the gate's implementation returns `true` only when `caller == address(this)`, which forces liquidators to call `liquidate()` on the gate. The gate enforces the grace and liquidation windows, then forwards the call to Morpho Midnight and handles token flows in the resulting `onLiquidate` callback.

1. **Start grace period:** Once a borrower's debt exceeds the maximum allowed by collateral value and LLTV, any keeper can call `startGracePeriod(id, borrower, priorityLiquidator)` to begin the countdown. The call reverts if a grace or liquidation period is already active for this borrower-market pair; a new one can only start once the previous `GRACE_PERIOD + LIQUIDATION_PERIOD` window has fully elapsed. The `priorityLiquidator` argument is optional and is explained in the priority window step below.
2. **Grace period:** For `GRACE_PERIOD` seconds after the start, no caller, including the `priorityLiquidator`, can liquidate the position; any `liquidate()` call reverts with `LiquidationNotAllowed`. This window gives the borrower time to add collateral, repay debt, or otherwise restore position health before liquidation becomes possible.
3. **Priority window:** When the grace period ends, the liquidation period opens. The optional `priorityLiquidator` supplied to `startGracePeriod` exists to incentivize keepers to make that call: if one was set and the borrower is still unhealthy, the first `PRIORITY_PERIOD` seconds (capped at 60 seconds) are reserved for that address, and any other caller is rejected with `LiquidationNotAllowed`. Leaving the argument empty opens liquidation to any caller immediately when the grace period ends.
4. **Open liquidation:** After `PRIORITY_PERIOD` elapses (or immediately, if no priority liquidator was set), the rest of the `LIQUIDATION_PERIOD` is open to any caller. Liquidators call `liquidate(market, collateralIndex, seizedAssets, repaidUnits, borrower, data)` on the gate.
When `liquidate()` is called, the gate validates timing, then calls `MORPHO_MIDNIGHT.liquidate()`. Inside the resulting `onLiquidate` callback, the gate transfers seized collateral to the liquidator, optionally invokes the liquidator's own `onLiquidate` callback (when `data` is non-empty) so they can sell collateral for loan tokens, then pulls loan tokens from the liquidator and approves Morpho Midnight to reclaim them.
Position health is re-checked at the moment `liquidate()` is called, not when the grace period was started. Two consequences follow:
* If the borrower restores health during the grace period and the position remains healthy through the liquidation window, no liquidation can occur, even though the gate's timing window is open. The liquidation call reverts on Morpho Midnight's health check, and a fresh `startGracePeriod` is required if the position becomes unhealthy again later.
* If the borrower adds collateral during the grace period but the position falls back below the LLTV during the liquidation window, the position can be liquidated immediately, with no additional grace period.
Post-maturity liquidations
After the market's maturity timestamp, `liquidate()` on the gate always proceeds regardless of grace or liquidation period state. This ensures lenders can recover funds from matured, unpaid positions.
Key Parameters[](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#key-parameters "Direct link to Key Parameters")
----------------------------------------------------------------------------------------------------------------------------------------------------------
Parameters are set at deployment from the factory and are immutable.
* **GRACE\_PERIOD:** Duration in seconds that borrowers have to restore position health before liquidation becomes possible.
* **LIQUIDATION\_PERIOD:** Duration in seconds during which liquidation is allowed after the grace period expires.
* **PRIORITY\_PERIOD:** Duration at the start of the liquidation window during which only the `priorityLiquidator` supplied to `startGracePeriod` may call `liquidate()`.
Factory Deployment[](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#factory-deployment "Direct link to Factory Deployment")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Delayed Liquidation Gates are deployed via the `DelayedLiquidationGateFactory` using CREATE2 for deterministic addresses.
Factory constraints:
* Minimum period: 60 seconds (`MIN_PERIOD`).
* Maximum period: 72 hours (`MAX_PERIOD`).
* Both `GRACE_PERIOD` and `LIQUIDATION_PERIOD` must be within `[MIN_PERIOD, MAX_PERIOD]`.
* `PRIORITY_PERIOD` is capped at `MIN_PERIOD` (at most 60 seconds).
* `LIQUIDATION_PERIOD` must be at least `PRIORITY_PERIOD + MIN_PERIOD`, guaranteeing at least one minute of open-liquidation time after any priority window.
* [Delayed liquidation steps](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#delayed-liquidation-steps)
* [Key Parameters](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#key-parameters)
* [Factory Deployment](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation/#factory-deployment)
---
# Market Making Policy | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#__docusaurus_skipToContent_fallback)
On this page
The Market Making Policy lets a market maker quote a two-sided rate curve (lend and resell) to the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
.
Use Case[](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#use-case "Direct link to Use Case")
--------------------------------------------------------------------------------------------------------------------------
The policy is built for lend-side market making, round-tripping between a variable-rate market on Morpho Blue and a fixed-rate market on Midnight:
1. **Deposit** funds into a Morpho Vault allocating to a Morpho Blue market, earning the variable rate.
2. **Lend fixed** by migrating into a Midnight position when a borrower matches with the lend rate offered by the market maker.
3. **Resell the fixed position** back into the variable-rate market when a third party wants to lend (or exit their borrow position) at the maker's exit rate, locking in the spread between entry and exit.
The maker quotes both legs continuously through one curve per market. The buy offer side prices lending into Midnight; the sell offer side prices exits out of lend positions.
The policy enforces that the buy (lend) rate is always above the sell (exit) rate at every maturity, such that every round trip carries a positive spread.
Yield Curve[](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#yield-curve "Direct link to Yield Curve")
-----------------------------------------------------------------------------------------------------------------------------------
Users can specify up to 8 points on their curve. For example, a curve might quote a lend rate of 4% for 1-day maturities, 5% for 1 week, 6% for 2 weeks, and so on.
Each point has three fields:
| Field | Meaning |
| --- | --- |
| `ttm` | Time to maturity, in seconds (the curve's x-axis) |
| `sellRate` | Exit rate quoted by the maker to exit a fixed-rate lend position |
| `buyRate` | Lend rate quoted by the maker to enter a fixed-rate position |
The curve enforces that the lend rate is strictly higher than the exit rate at every point.
The curve is managed via two functions:
* `setCurve(onBehalf, tenorMarketId, points)` overwrites the entire curve.
* `clearCurve(onBehalf, tenorMarketId)` removes it, after which quotes for that slot revert. This is the per-user kill switch.
Either function can be called by the curve owner directly, or by any address the owner has authorized on Morpho Midnight (the same authorization used for bundlers, no separate ACL).
Example: Three-Point Term Structure[](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#example-three-point-term-structure "Direct link to Example: Three-Point Term Structure")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
A market maker quoting entries above exits at 7d / 30d / 90d on a Midnight market `M`:
| Time to maturity | Sell rate (exit) | Buy rate (entry) |
| --- | --- | --- |
| 7 days | ≈ 4% APR | ≈ 5% APR |
| 30 days | ≈ 5% APR | ≈ 6% APR |
| 90 days | ≈ 6% APR | ≈ 7% APR |
8%7%6%5%4%3%7d30d90dTime to maturityRate (APR)5%6%7%4%5%6%Buy (entry, lend fixed)Sell (exit, resell)
The shaded area between the two lines is the spread the maker captures on each round trip. The policy enforces this gap stays positive at every maturity.
All three points are set in a single `setCurve` call. With this curve in place:
* A lend exit on `M` maturing in 14 days reads `sellRate` interpolated between the 7-day and 30-day points.
* A lend entry on `M` targeting a 60-day maturity reads `buyRate` interpolated between the 30-day and 90-day points.
* A take further out than 90 days reads the 90-day rate flat.
Pausable Variant[](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#pausable-variant "Direct link to Pausable Variant")
--------------------------------------------------------------------------------------------------------------------------------------------------
`PausableMarketMakingPolicy` is a pausable subclass shipped alongside the base policy. When paused, `getRate()` reverts and blocks every take routed through any user's slot on that policy. The kill switch is global to the deployment, not per user.
* Any designated pauser can call `pause()`.
* Only the owner can `unpause()`.
* [Use Case](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#use-case)
* [Yield Curve](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#yield-curve)
* [Example: Three-Point Term Structure](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#example-three-point-term-structure)
* [Pausable Variant](https://docs.tenor.finance/technical-docs/contracts/market-making-policy/#pausable-variant)
---
# Tenor Migration Intent Ratifier | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#__docusaurus_skipToContent_fallback)
On this page
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`](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#execution-via-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 decodes `intent.data`.
* **`BaseMigrationRatifier`:** abstract base. Owns fee config, callback discrimination, and the window/cadence/maturity/rate checks.
How a Renewal Flows[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#how-a-renewal-flows "Direct link to How a Renewal Flows")
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
A renewal moves through three stages:
1. **Configure:** The user sets renewal preferences for one or more `(callback, sourceMarket, targetMarket)` slots and authorizes the ratifier on `IntentSettler`.
2. **Validate:** When a take comes in, `IntentSettler` loads the matching slot and asks the ratifier whether the take matches the user's parameters.
3. **Execute:** If validation passes, `IntentSettler` forwards the take to Morpho Midnight, which runs the callback to perform the position transition atomically.
Supported Callbacks[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#supported-callbacks "Direct link to 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](https://docs.tenor.finance/technical-docs/contracts/callbacks/borrow-midnight-renewal) |
| `LEND_MIDNIGHT_RENEWAL_CALLBACK` | Midnight → Midnight | [Lend Midnight Renewal](https://docs.tenor.finance/technical-docs/contracts/callbacks/lend-midnight-renewal) |
| `BORROW_BLUE_TO_MIDNIGHT_CALLBACK` | Blue → Midnight | [Blue to Midnight](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight) |
| `LEND_VAULT_TO_MIDNIGHT_CALLBACK` | Blue → Midnight | [Vault to Midnight](https://docs.tenor.finance/technical-docs/contracts/callbacks/vault-to-midnight) |
| `BORROW_MIDNIGHT_TO_BLUE_CALLBACK` | Midnight → Blue | [Midnight to Blue](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-blue) |
| `LEND_MIDNIGHT_TO_VAULT_CALLBACK` | Midnight → Blue | [Midnight to Vault](https://docs.tenor.finance/technical-docs/contracts/callbacks/midnight-to-vault) |
User Setup[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#user-setup "Direct link to User Setup")
--------------------------------------------------------------------------------------------------------------------------------------
Setting up a renewal takes three calls:
1. **Authorize the ratifier on `IntentSettler`:** `setIsAuthorized(onBehalf, ratifier, true)` lets the settler route takes through this ratifier.
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.
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[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#user-parameters "Direct link to 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](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#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`](https://docs.tenor.finance/technical-docs/contracts/four-week-cadence)
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. |
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[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#validation "Direct link to Validation")
--------------------------------------------------------------------------------------------------------------------------------------
`onIntentRatify` 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. **Intent matches callback:** `intent.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).
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[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#execution-via-intentsettler "Direct link to 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)` with `intent.ratifier = MigrationIntentRatifier`. The settler checks `isAuthorized[intent.user][intent.ratifier]`, validates the intent on this ratifier, then calls `MORPHO_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 into `IntentSettler.isRatified`, which validates the intent against this ratifier and returns `CALLBACK_SUCCESS`.
A single `isAuthorized[user][ratifier]` map gates both flows. Users opt in once per ratifier.
Interest Rate Policies[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#interest-rate-policies "Direct link to 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](https://docs.tenor.finance/technical-docs/contracts/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[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#market-making-policy "Direct link to Market Making Policy")
The [Static Rate Policy](https://docs.tenor.finance/technical-docs/contracts/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](https://docs.tenor.finance/technical-docs/contracts/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)[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#pausable-variant-emergency-pause "Direct link to 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[](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#fees "Direct link to 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:** 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.
* [How a Renewal Flows](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#how-a-renewal-flows)
* [Supported Callbacks](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#supported-callbacks)
* [User Setup](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#user-setup)
* [User Parameters](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#user-parameters)
* [Validation](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#validation)
* [Execution via IntentSettler](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#execution-via-intentsettler)
* [Interest Rate Policies](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#interest-rate-policies)
* [Market Making Policy](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#market-making-policy)
* [Pausable Variant (Emergency Pause)](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#pausable-variant-emergency-pause)
* [Fees](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent/#fees)
---
# Static Rate Policy | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#__docusaurus_skipToContent_fallback)
On this page
The Static Rate Policy defines a fixed rate curve that controls how renewal auction pricing changes over time. It stores up to 8 immutable rate points with linear interpolation between them, giving you precise control over the rate a position renews at through the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
.
For example, a borrower can configure a renewal to start bidding at 3% APR the moment the renewal window opens, ramp up to 6% APR over the next 24 hours, and stay at 6% until a lender matches. The rate curve is flat, then linear, then flat, and the policy interpolates the current rate at every take attempt.
Rate Curve[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#rate-curve "Direct link to Rate Curve")
------------------------------------------------------------------------------------------------------------------------------
The rate curve is defined at deployment as an array of `(rate, duration)` points. Between points, the rate is linearly interpolated. The curve is stored as immutable packed values in bytecode for gas efficiency.
Rate behavior by elapsed time:
* **At or before the first point:** Returns the first point's rate.
* **Between two points:** Linear interpolation between the two rates.
* **Past the last point:** Returns the last point's rate.
Constraints:
* Maximum 8 rate points.
* Durations must be strictly increasing.
* Rates are specified per second in WAD precision.
### Rate Calculation[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#rate-calculation "Direct link to Rate Calculation")
When the ratifier calls `getRate`, the policy:
1. Computes `elapsed = max(block.timestamp - renewalPeriodStart, 0)` from the renewal period start provided by the ratifier.
2. Finds the segment of the rate curve containing `elapsed`.
3. Returns the linearly interpolated rate at that point.
### Example Rate Curves[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#example-rate-curves "Direct link to Example Rate Curves")
**Simple linear ramp** (2 points):
* Point 0: rate = 0, duration = 0s (start at 0%)
* Point 1: rate = 3.17e8 (1% APR), duration = 86400s (1 day)
* Result: rate increases linearly from 0% to 1% APR over the first day, then stays at 1%.
**Step function with plateau** (3 points):
* Point 0: rate = 1.58e8 (0.5% APR), duration = 0s
* Point 1: rate = 1.58e8 (0.5% APR), duration = 43200s (12 hours)
* Point 2: rate = 6.34e8 (2% APR), duration = 86400s (1 day)
* Result: flat at 0.5% for 12 hours, then ramps to 2% over the next 12 hours.
Pausable Variant[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#pausable-variant "Direct link to Pausable Variant")
------------------------------------------------------------------------------------------------------------------------------------------------
`PausableStaticRatePolicy` is a pausable subclass shipped alongside the static policy. When paused, `getRate()` reverts with `IsPaused()`, which causes any ratifier's rate check to revert and blocks every renewal take that points at this 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 designated pauser address can call `pause()`.
* Only the owner can `unpause()`.
Canonical Tenor Policies[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#canonical-tenor-policies "Direct link to Canonical Tenor Policies")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
In production, Tenor renewals do not use bespoke per-position rate curves. Every renewal route points at one of two canonical, immutable `PausableStaticRatePolicy` instances deployed by the protocol.
### Borrow renewals (ascending, 8h steps)[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#borrow-renewals-ascending-8h-steps "Direct link to Borrow renewals (ascending, 8h steps)")
Used by `v2v2Borrow` and `v1v2Borrow` renewals. The rate starts at 0% APR and climbs over 24 hours: the longer a borrow renewal sits unfilled, the more attractive it becomes to a counterparty.
20%15%10%5%0%0h8h16h24hElapsed since renewal window openedRate (APR)0%3%10%20%
| Elapsed | Rate (APR) |
| --- | --- |
| 0 | 0% |
| 8h | 3% |
| 16h | 10% |
| 24h | 20% |
### Lend renewals (descending, 4h steps)[](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#lend-renewals-descending-4h-steps "Direct link to Lend renewals (descending, 4h steps)")
Used by `v2v1Borrow`, `v1v2Lend`, and `v2withdrawableLend` renewals. The rate starts at 20% APR and falls over 12 hours: the longer a lend renewal sits unfilled, the more attractive it becomes to a counterparty.
20%15%10%5%0%0h4h8h12hElapsed since renewal window openedRate (APR)20%10%3%0%
| Elapsed | Rate (APR) |
| --- | --- |
| 0 | 20% |
| 4h | 10% |
| 8h | 3% |
| 12h | 0% |
The rate curves above are immutable once deployed; only the pause state of either policy can be flipped, by its designated pauser. The `v2→v1` Lend renewal flow is not in launch scope, so no dedicated policy is deployed for it.
* [Rate Curve](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#rate-curve)
* [Rate Calculation](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#rate-calculation)
* [Example Rate Curves](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#example-rate-curves)
* [Pausable Variant](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#pausable-variant)
* [Canonical Tenor Policies](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#canonical-tenor-policies)
* [Borrow renewals (ascending, 8h steps)](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#borrow-renewals-ascending-8h-steps)
* [Lend renewals (descending, 4h steps)](https://docs.tenor.finance/technical-docs/contracts/static-rate-policy/#lend-renewals-descending-4h-steps)
---
# Tenor Adapter | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#__docusaurus_skipToContent_fallback)
On this page
The Tenor Adapter enables batching Tenor and Morpho Midnight operations into a single atomic transaction through Morpho's Bundler3. It wraps Morpho Midnight operations, ratifier configuration, and Tenor Router batch execution into passthrough functions that maintain correct callback routing.
For example, a borrower initiating a new position can approve tokens, supply collateral, take a [sell offer](https://docs.tenor.finance/technical-docs/architecture#midnight-offers)
(a borrower's offer to borrow), and configure auto-renewal in a single transaction through the adapter. If any step fails, the entire bundle reverts and the borrower is back where they started.
Use the Tenor SDK
The recommended way to construct these batches is via the [Tenor SDK](https://github.com/Shippooor-Labs/tenor-app/tree/main/src/lib/tenor-sdk)
, which handles ABI encoding, authorization routing, and call ordering. Hand-rolling a Bundler3 multicall is supported but error-prone.
Bundle Composition[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#bundle-composition "Direct link to Bundle Composition")
-------------------------------------------------------------------------------------------------------------------------------------------------
A Tenor bundle is a `Bundler3.multicall` composing calls across several adapters:
* **`TenorAdapter`**: Tenor and Morpho Midnight operations (this page).
* **`AuthorizationAdapter`**: protocol authorizations and ratifier configuration. See [Authorization Adapter](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#authorization-adapter)
below.
* **`GeneralAdapter1`**: Morpho's standard adapter. Used for ERC-20 `transferFrom` (with Permit2), native ETH wrap/unwrap, ERC-4626 vault deposits/withdrawals, and Morpho Blue operations.
* **`CoreAdapter`**: Morpho's base adapter that `GeneralAdapter1` extends. Used for native ETH transfers and ERC-20 transfers from adapter balance.
Token approvals and `setIsAuthorized` grants are scoped to these adapters (plus renewal callback contracts). They are never granted to `Bundler3` itself: `Bundler3` is a stateless dispatcher, and any approval it held would become available to every adapter routed through it.
Morpho Blue
Direct Morpho Blue `supply` / `borrow` / `repay` / `withdraw` are not exposed through the Tenor SDK. The Blue surface is touched indirectly: Blue↔Midnight migrations bridge via a `GeneralAdapter1` flash loan, with the renewal callback contracts handling the Blue side internally.
Benefits of the Tenor Adapter[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#benefits-of-the-tenor-adapter "Direct link to Benefits of the Tenor Adapter")
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
All Tenor operations go through the adapter rather than calling contracts directly, for two reasons.
* **Callback routing.** Bundler3 callbacks must come from the same address that was called. Passthrough functions ensure the adapter is `msg.sender` to Tenor contracts, so callbacks flow back correctly.
* **Atomic execution.** Multiple operations can be batched into a single transaction, with all-or-nothing execution.
Morpho Midnight Operations[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#morpho-midnight-operations "Direct link to Morpho Midnight Operations")
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The adapter exposes passthrough functions for core Morpho Midnight operations.
* **midnightRepay:** Repay debt. Pass `type(uint256).max` as `assets` to repay using the adapter's full loan-token balance, or as `debt` to repay the initiator's entire current debt.
* **midnightSupplyCollateral:** Supply collateral from the adapter's balance. Supports `type(uint256).max` to use the adapter's full balance.
* **midnightWithdrawCollateral:** Withdraw collateral on behalf of the initiator. Supports `type(uint256).max` to withdraw the initiator's full collateral.
* **midnightWithdraw:** Withdraw loan assets from a market on behalf of the initiator. Supports `type(uint256).max` to withdraw the initiator's full credit after position update (slashing + continuous fee accrual).
* **midnightSetConsumed:** Set the consumed amount for an offer group (used for atomic cancel + downstream action). Passing `type(uint256).max` cancels all offers in the group.
* **midnightFlashLoan:** Execute a flash loan on Morpho Midnight.
Long-lived authorizations (granting Midnight or `IntentSettler` access to other contracts) are handled by a separate adapter. See [Authorization Adapter](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#authorization-adapter)
below.
Taking offers
Taking offers is done through the adapter's `execute` function (see [Batch Take Execution](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#batch-take-execution)
below). The adapter does not expose a standalone `midnightTake`.
Ratifier Configuration[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#ratifier-configuration "Direct link to Ratifier Configuration")
-------------------------------------------------------------------------------------------------------------------------------------------------------------
The adapter exposes passthrough functions for managing renewal parameters on the [Migration Intent Ratifier](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
.
* **renewalSetParams:** Sets renewal parameters on behalf of the initiator.
* **renewalResetParams:** Clears renewal parameters.
These passthroughs ensure the adapter is `msg.sender` to the ratifier, so the adapter is the contract the initiator authorizes.
Batch Take Execution[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#batch-take-execution "Direct link to Batch Take Execution")
-------------------------------------------------------------------------------------------------------------------------------------------------------
The adapter's `execute` function batches multiple take actions in a single transaction. See [Tenor Router](https://docs.tenor.finance/technical-docs/contracts/take-router)
for the full call signature, fill semantics, and per-batch invariants. The initiator (the original EOA) is resolved as the taker for `MIDNIGHT_TAKE` actions and as the `onBehalf` passed into `IntentSettler.take` for `TAKE_ON_BEHALF` actions.
Callback Handling[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#callback-handling "Direct link to Callback Handling")
----------------------------------------------------------------------------------------------------------------------------------------------
The adapter implements callback interfaces that let it reenter `Bundler3` from inside a Morpho Midnight operation. This is what enables batch operations (`Bundler3.multicall`) _within_ a callback (e.g. supplying collateral, taking another offer, or repaying source debt as part of the same take).
* **onBuy.** Triggered when a buy (lend) offer is taken.
* **onSell.** Triggered when a sell (borrow) offer is taken.
* **onRepay.** Triggered during repay operations.
* **onLiquidate.** Triggered during liquidations.
* **onFlashLoan.** Triggered during flash loans.
Callback authentication
All callbacks validate that `msg.sender == MORPHO_MIDNIGHT`, so only Morpho Midnight itself can invoke them. This prevents third parties from spoofing callbacks to manipulate the adapter's state.
Authorization Adapter[](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#authorization-adapter "Direct link to Authorization Adapter")
----------------------------------------------------------------------------------------------------------------------------------------------------------
`AuthorizationAdapter` is a separate standalone Bundler3 adapter for granting or revoking long-lived authorizations from inside a bundle. Use it as part of a one-shot setup bundle so users don't need separate out-of-bundle approval transactions.
It exposes four passthroughs, all called on the initiator's behalf:
* **midnightSetIsAuthorized:** Authorize another contract on Morpho Midnight (e.g. a callback or the Tenor Adapter).
* **renewalSetIsAuthorized:** Authorize a ratifier on `IntentSettler` (e.g. the Migration Intent Ratifier).
* **setterRatifierSetIsRootRatified:** Ratify (or un-ratify) an offer-tree root on a Midnight setter ratifier.
* **ecrecoverRatifierCancelRoot:** Cancel a previously-signed offer-tree root on a Midnight ecrecover ratifier.
Authorizations grant broad rights
A single Midnight `setIsAuthorized` grant gives the target contract the ability to act on every Midnight market the user has positions in, **and** to overwrite the user's renewal params on the ratifier (since the ratifier reuses Midnight's auth mapping). Only authorize contracts whose code you have audited and whose ABI you understand.
* [Bundle Composition](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#bundle-composition)
* [Benefits of the Tenor Adapter](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#benefits-of-the-tenor-adapter)
* [Morpho Midnight Operations](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#morpho-midnight-operations)
* [Ratifier Configuration](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#ratifier-configuration)
* [Batch Take Execution](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#batch-take-execution)
* [Callback Handling](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#callback-handling)
* [Authorization Adapter](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter/#authorization-adapter)
---
# Vault V2 Allowlist Gate | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#__docusaurus_skipToContent_fallback)
On this page
The Vault V2 Allowlist Gate gives vault operators full control over who can deposit, withdraw, and transfer shares on a Vault V2, enabling permissioned vaults for institutional or compliance use cases.
For example, an institutional vault curator can deploy an allowlist gate with a list of approved wallets. Only those wallets can deposit into the vault or receive shares, and the vault's fee recipients are allowed automatically.
Setup[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#setup "Direct link to Setup")
---------------------------------------------------------------------------------------------------------------------
Vault V2 Allowlist Gates are deployed via the `VaultV2AllowlistGateFactory` using CREATE2 for deterministic addresses. After deployment, the gate blocks all transfers by default because every permission flag defaults to `false`, with the sole exception of the vault's own fee recipients (described below).
Setup takes two steps:
1. Deploy the gate via the factory, passing the owner address.
2. The owner calls `setAllowlist(roles[])` to configure permissions for participating addresses.
The owner can later call `renounceOwnership()` to make the allowlist permanently immutable.
Gate Logic[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#gate-logic "Direct link to Gate Logic")
------------------------------------------------------------------------------------------------------------------------------------
### Role-Based Permissions[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#role-based-permissions "Direct link to Role-Based Permissions")
Each allowlisted address has four independent permission flags.
* **canReceiveShares:** Whether the address can receive vault shares (for example, via deposit or transfer).
* **canSendShares:** Whether the address can send vault shares (for example, via withdraw or transfer).
* **canReceiveAssets:** Whether the address can receive underlying assets from the vault.
* **canSendAssets:** Whether the address can send underlying assets to the vault.
The gate checks these flags before executing share and asset transfers.
### Ownership[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#ownership "Direct link to Ownership")
The gate uses OpenZeppelin's `Ownable2Step` for safe two-step ownership transfers.
* The owner is the only address that can call `setAllowlist` to update permissions for one or more addresses in a single call.
* The owner can call `renounceOwnership()` to permanently lock the allowlist, making it immutable.
### Gate Interface[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#gate-interface "Direct link to Gate Interface")
The gate implements four view functions that a V2 vault queries:
* `canReceiveShares(address) → bool`
* `canSendShares(address) → bool`
* `canReceiveAssets(address) → bool`
* `canSendAssets(address) → bool`
Fee recipients are auto-allowed
For `canReceiveShares`, `canSendShares`, and `canReceiveAssets`, the gate falls back to the calling vault's `managementFeeRecipient()` and `performanceFeeRecipient()` when an account is not on the allowlist, so both fee recipients are allowed by default for these three flags. This keeps fee accrual working, since a V2 vault skips fee minting when the recipient's gate check returns `false`. `canSendAssets` has **no** such exemption.
Use Cases[](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#use-cases "Direct link to Use Cases")
---------------------------------------------------------------------------------------------------------------------------------
* Permissioned vaults for institutional users.
* Compliance-controlled vaults that restrict participation to KYC-verified addresses.
* Vaults with restricted transfer capabilities, such as non-transferable shares.
* [Setup](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#setup)
* [Gate Logic](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#gate-logic)
* [Role-Based Permissions](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#role-based-permissions)
* [Ownership](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#ownership)
* [Gate Interface](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#gate-interface)
* [Use Cases](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist/#use-cases)
---
# Tenor Router | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/take-router/#__docusaurus_skipToContent_fallback)
On this page
The Tenor Router fills a user's position across multiple offers in a single transaction. Instead of taking offers one at a time, you define a fill target and a price band, and the router iterates through an array of actions, accumulating fills until the target is met or no more offers can be matched.
For example, a borrower initiating a $1M USDC position can submit a batch of offers at increasing rates, a `maxFill` of 1,000,000 USDC, and a price band corresponding to ≤ 6% APR. The router fills from the best rate down until the full amount is matched or the price band trips.
Batch Execution[](https://docs.tenor.finance/technical-docs/contracts/take-router/#batch-execution "Direct link to Batch Execution")
--------------------------------------------------------------------------------------------------------------------------------------
The `execute` function takes two arguments: an `ExecuteParams` struct defining the batch, and an ordered array of `Action` structs defining each take. It returns `(buyerAssetsTotal, sellerAssetsTotal, unitsTotal)` and emits `BatchExecuted` on success.
### ExecuteParams[](https://docs.tenor.finance/technical-docs/contracts/take-router/#executeparams "Direct link to ExecuteParams")
| Field | Description |
| --- | --- |
| `deadline` | Maximum `block.timestamp` for execution. `0` disables the check. Reverts `DeadlineExpired` otherwise. |
| `fillAxis` | `FillAxis.ASSETS` or `FillAxis.UNITS`. Picks which axis accumulates toward `maxFill` / `minFill`. For `ASSETS`, the axis auto-resolves to buyer-assets or seller-assets depending on the batch's side. |
| `maxFill` | Cap on the chosen axis; the loop stops once it is reached. Reverts `FillOvershoot` if a single action's post-fee fill pushes past it. Accepts `type(uint256).max` as the renewal/close-out sentinel (see below). |
| `minFill` | Minimum acceptable total fill on the chosen axis. Reverts `InsufficientFill` if not met after all actions. Accepts the same sentinel as `maxFill`. |
| `minPrice` / `maxPrice` | Post-batch inclusive price band in `D18 {taker-side asset / unit}`. The taker-side asset is auto-anchored (buyer assets on the seller side, seller assets on the buyer side). Use `0` to disable the floor and `type(uint256).max` to disable the ceiling. Reverts `PriceSlippageExceeded` otherwise; a degenerate `units == 0` with `assets > 0` also reverts unless the ceiling is disabled. |
Renewal/close-out sentinel
`maxFill` / `minFill` accept `type(uint256).max` as a sentinel resolved by the adapter against onchain state (e.g. current debt for a borrower close-out, current balance for a vault deposit). If resolution yields zero (no prior position, empty adapter balance), execution reverts `SentinelResolvedToZero`. Opening flows must pass concrete bounds: the sentinel does not mean "fill everything available", and `SentinelNotSupported` reverts if used on the seller-assets axis.
### Action[](https://docs.tenor.finance/technical-docs/contracts/take-router/#action "Direct link to Action")
Each `Action` is one individual take operation:
| Field | Description |
| --- | --- |
| `actionType` | `TAKE_ON_BEHALF` (routes through `IntentSettler` → ratifier → Midnight) or `MIDNIGHT_TAKE` (calls Midnight directly, no ratifier). |
| `data` | ABI-encoded payload for the action. `TakeOnBehalfData` for `TAKE_ON_BEHALF`; `MidnightTakeData` for `MIDNIGHT_TAKE`. |
| `allowRevert` | If `true`, a failed action is skipped and an `ActionReverted` event is emitted instead of reverting the whole batch. If `false`, the first failed action reverts the batch with `ActionFailed`. |
| `offer` | The `Offer` struct used for dispatch and passed to the clamp / fee adjuster. |
| `clamp` | Optional `ITakeClamp` contract that further caps `takeUnits` based on onchain state (balances, allowances, health, callback-internal budget). See [Clamping](https://docs.tenor.finance/technical-docs/contracts/take-router/#clamping)
. |
| `clampData` | Arbitrary data passed to the clamp. |
| `feeAdjuster` | Optional `ICallbackFeeAdjuster` contract used when the action's callback charges a fee. See [Fee Adjustment](https://docs.tenor.finance/technical-docs/contracts/take-router/#fee-adjustment)
. |
| `feeAdjusterData` | Arbitrary data passed to the fee adjuster. |
#### TAKE\_ON\_BEHALF payload[](https://docs.tenor.finance/technical-docs/contracts/take-router/#take_on_behalf-payload "Direct link to TAKE_ON_BEHALF payload")
`TakeOnBehalfData` carries the `IntentSettler` arguments: `takeUnits`, `takerCallback`, `takerCallbackData`, `receiver`, `offerRatifierData`, the `Intent` (`{user, ratifier, data}`), and the inner ratifier data. The taker recorded on Midnight is `intent.user` (independent of the batch initiator), so a keeper can fill renewals on behalf of many distinct users in the same batch.
#### MIDNIGHT\_TAKE payload[](https://docs.tenor.finance/technical-docs/contracts/take-router/#midnight_take-payload "Direct link to MIDNIGHT_TAKE payload")
`MidnightTakeData` carries `takeUnits`, `takerCallback`, `takerCallbackData`, `receiverIfTakerIsSeller`, and the offer's ratifier data. The taker is the batch's initiator. There is no ratifier involvement.
Reentrant takes
If `takerCallback` reenters `Bundler3` (e.g. the Tenor Adapter), at most one such reentrant `TAKE` action may execute per top-level `Bundler3.multicall` call entry. When `allowRevert = true`, follow-up reentrant actions in the same batch silently no-op with `IncorrectReenterHash` instead of failing the call.
Execution Modes[](https://docs.tenor.finance/technical-docs/contracts/take-router/#execution-modes "Direct link to Execution Modes")
--------------------------------------------------------------------------------------------------------------------------------------
The router enforces that every action in a batch shares the same _execution mode_. Mixing reverts `MixedExecutionMode`.
* **First-party.** The initiator is a direct party to the trade. Covers `MIDNIGHT_TAKE` (initiator is the taker), and `TAKE_ON_BEHALF` where `offer.maker == initiator` (initiator is the maker being filled against).
* **Third-party.** The initiator is a keeper relaying for someone else. Only `TAKE_ON_BEHALF` with `offer.maker != initiator`. The actual taker is `intent.user`, gated by the ratifier.
This separation lets keepers batch many distinct users' renewals together (third-party) without those ever mixing into a user-initiated fill (first-party); two parties' flows would otherwise be credited to one identity under the same slippage denominator.
Per-Batch Invariants[](https://docs.tenor.finance/technical-docs/contracts/take-router/#per-batch-invariants "Direct link to Per-Batch Invariants")
-----------------------------------------------------------------------------------------------------------------------------------------------------
Beyond execution mode, every action in a batch must share:
* **Same market.** `action.offer.market` is compared against the first action; mismatches revert `InconsistentMarket(i)`.
* **Same side.** Whether the batch is on the buyer side or the seller side is locked by the first action; mismatches revert `InconsistentSide(i, batchIsBuyerSide)`. Side is determined as follows:
* `MIDNIGHT_TAKE` → initiator is taker → side is `!offer.buy`.
* `TAKE_ON_BEHALF` with `offer.maker == initiator` → initiator is maker → side is `offer.buy`.
* `TAKE_ON_BEHALF` with `offer.maker != initiator` → keeper-orchestrated; side is `!offer.buy` (the taker is `intent.user`).
Clamping[](https://docs.tenor.finance/technical-docs/contracts/take-router/#clamping "Direct link to Clamping")
-----------------------------------------------------------------------------------------------------------------
A clamp is a view-only `ITakeClamp` contract that returns a maximum `takeUnits` value for an action given the current onchain state. The router takes the minimum of the clamp's return and its own running cap, so a clamp can only _reduce_ a take, never grow it.
For each action, the router applies three caps in order:
1. **Remaining batch budget**, converted to units (or `feeAdjuster.beforeDispatch` if a fee adjuster is set).
2. **Structural offer capacity** via `ClampLib.getOfferRemaining` (raw `consumed` vs offer cap). Enforced unconditionally; clamps must _not_ check offer consumption themselves.
3. **The configured `clamp` contract**, if any.
The smallest of the three wins.
Clamps exist to encode the onchain truths the router cannot generically infer: wallet balances, allowances, position health, callback-internal budget math. Different operations get different clamps (renewals, vault rollovers, cross-protocol migrations, etc.), each implementing the constraints specific to its flow.
Clamps return a best-effort cap, not an exact ceiling. They rely on simplifying assumptions about the action's flow and skip checks that would be too expensive to perform onchain (e.g. they do not re-simulate the full post-action position health). A take that passes the clamp can still revert at dispatch if reality diverges from those assumptions, and conversely the clamp may be conservative enough to leave headroom the action could in principle have used.
Fee Adjustment[](https://docs.tenor.finance/technical-docs/contracts/take-router/#fee-adjustment "Direct link to Fee Adjustment")
-----------------------------------------------------------------------------------------------------------------------------------
A `feeAdjuster` is an `ICallbackFeeAdjuster` contract used to correctly size `takeUnits` when the action's callback charges a fee that consumes part of the user's budget. Without an adjuster, the router's default budget→units conversion (`RouterLib.budgetToUnits`) over-sizes the take and busts the budget; for fee-less callbacks, no adjuster is needed.
When set, the adjuster is called twice:
* **`beforeDispatch`** replaces the default budget conversion. It returns the largest `takeUnits` whose effective fill (after fee) does not exceed the remaining batch budget.
* **`afterDispatch`** is called once the take settles. It reports the realized fee back to the router, which shifts the action's recorded fill in the taker-worsening direction so the post-batch price band reflects what the user actually paid.
Clamps and fee adjusters are typically paired for callback-fee operations (e.g. a Midnight renewal where the renewal callback charges a fee).
Events[](https://docs.tenor.finance/technical-docs/contracts/take-router/#events "Direct link to Events")
-----------------------------------------------------------------------------------------------------------
* **`BatchExecuted(initiator, msgSender, params, actionsCount, buyerAssets, sellerAssets, units)`** emitted once per successful `execute`.
* **`ActionReverted(index, reason)`** emitted per action skipped because of `allowRevert = true`.
Custom Errors[](https://docs.tenor.finance/technical-docs/contracts/take-router/#custom-errors "Direct link to Custom Errors")
--------------------------------------------------------------------------------------------------------------------------------
| Error | Trigger |
| --- | --- |
| `DeadlineExpired(deadline, timestamp)` | `block.timestamp > deadline` and `deadline != 0`. |
| `ActionFailed(index, reason)` | An action with `allowRevert = false` reverted. `reason` is the inner revert data. |
| `InsufficientFill(filled, minFill)` | Total fill on the chosen axis stayed below `minFill`. |
| `FillOvershoot(filled, maxFill)` | A single action's post-fee fill pushed past `maxFill`. |
| `PriceSlippageExceeded(price, min, max)` | Realized price escaped `[minPrice, maxPrice]`. Also triggered if `units == 0` while `assets > 0` and `maxPrice != type(uint256).max`. |
| `InconsistentMarket(index)` | Action's `offer.market` differs from the first action's. |
| `InconsistentSide(index, batchIsBuyerSide)` | Action's resolved side differs from the first action's. |
| `MixedExecutionMode(index, expectedFirstParty)` | Action's execution mode (first/third-party) differs from the first action's. |
| `SentinelResolvedToZero(fillIndex)` | `type(uint256).max` sentinel resolved to zero on `maxFill` / `minFill`. |
| `SentinelNotSupported(fillIndex)` | Sentinel used on the seller-assets axis (unsupported). |
| `EmptyActions()` | Adapter was invoked with an empty `actions` array. |
Bundler3 Integration[](https://docs.tenor.finance/technical-docs/contracts/take-router/#bundler3-integration "Direct link to Bundler3 Integration")
-----------------------------------------------------------------------------------------------------------------------------------------------------
The router is exposed through the [Tenor Adapter](https://docs.tenor.finance/technical-docs/contracts/tenor-adapter)
, which is the entry point in production. The initiator (the original EOA) is resolved as the taker for `MIDNIGHT_TAKE` actions and as the `onBehalf` passed into `IntentSettler.take` for `TAKE_ON_BEHALF` actions.
* [Batch Execution](https://docs.tenor.finance/technical-docs/contracts/take-router/#batch-execution)
* [ExecuteParams](https://docs.tenor.finance/technical-docs/contracts/take-router/#executeparams)
* [Action](https://docs.tenor.finance/technical-docs/contracts/take-router/#action)
* [Execution Modes](https://docs.tenor.finance/technical-docs/contracts/take-router/#execution-modes)
* [Per-Batch Invariants](https://docs.tenor.finance/technical-docs/contracts/take-router/#per-batch-invariants)
* [Clamping](https://docs.tenor.finance/technical-docs/contracts/take-router/#clamping)
* [Fee Adjustment](https://docs.tenor.finance/technical-docs/contracts/take-router/#fee-adjustment)
* [Events](https://docs.tenor.finance/technical-docs/contracts/take-router/#events)
* [Custom Errors](https://docs.tenor.finance/technical-docs/contracts/take-router/#custom-errors)
* [Bundler3 Integration](https://docs.tenor.finance/technical-docs/contracts/take-router/#bundler3-integration)
---
# Technical Documentation | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/overview/#__docusaurus_skipToContent_fallback)

Technical documentation for Tenor smart contracts, API, and SDK. Tenor extends the Morpho protocol (Morpho Blue, Morpho Midnight, and Morpho Vaults V2) to enable additional functionality.
### Morpho Midnight
Overview of Morpho Midnight's markets, offers, and callback model, plus how Tenor extends it with custom callbacks and gates.
[More →](https://docs.tenor.finance/technical-docs/architecture)
### Callbacks
Stateless contracts invoked during a Morpho Midnight take. Callbacks enable migrations between vaults, Morpho Blue, and Midnight.
[More →](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview)
### Ratifiers
Ratifiers to pre-approve strategies on Morpho Midnight such as auto-renewal and passive market making.
[More →](https://docs.tenor.finance/technical-docs/contracts/ratifiers/migration-intent)
### Oracles
New Morpho-compatible oracle types built by Tenor to improve the robustness of oracles used in Morpho Blue and Midnight markets.
[More →](https://docs.tenor.finance/technical-docs/oracle-with-validation)
### Vault V2 Gates
Gates that allowlist which addresses can deposit into a vault and receive shares, enabling private vaults for institutional actors.
[More →](https://docs.tenor.finance/technical-docs/contracts/vault-v2-gates/allowlist)
### Midnight Gates
Gates that allowlist specific lenders or borrowers in a Midnight market and customize liquidation behavior with mechanisms such as grace periods.
[More →](https://docs.tenor.finance/technical-docs/contracts/midnight-gates/delayed-liquidation)
### API
GraphQL API for querying markets, positions, offers, and portfolio analytics, plus available offers for routing optimization.
[More →](https://docs.tenor.finance/technical-docs/api)
### SDK
TypeScript library for building and executing transactions. React hooks, approval management, simulation, and Safe wallet support.
[More →](https://docs.tenor.finance/technical-docs/sdk)
### Addresses
Deployed contract addresses for Tenor and Morpho.
[More →](https://docs.tenor.finance/technical-docs/addresses)
---
# SDK | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/sdk/#__docusaurus_skipToContent_fallback)
On this page
The Tenor SDK is a TypeScript library for building and executing transactions on the Tenor protocol. It handles transaction bundling, token approvals, signature management, and wallet compatibility.
How It Works[](https://docs.tenor.finance/technical-docs/sdk/#how-it-works "Direct link to How It Works")
-----------------------------------------------------------------------------------------------------------
All Tenor operations go through Morpho's [Bundler3](https://docs.morpho.org/get-started/resources/contracts/bundlers/)
multicall system. The SDK abstracts this into a simple flow.
You never call Morpho or adapter contracts directly. The SDK assembles the correct multicall bundle for you.
Three Ways to Use It[](https://docs.tenor.finance/technical-docs/sdk/#three-ways-to-use-it "Direct link to Three Ways to Use It")
-----------------------------------------------------------------------------------------------------------------------------------
| Your situation | Use this |
| --- | --- |
| React app, standard operations | **React hooks** (`useBorrow`, `useLend`, `useRepay`, and more) |
| Non-React or custom flows | **ActionsBuilder** (high-level API) |
| Custom multicall bundles | **TenorBundler** (low-level builder) |
React hooks are the recommended approach for most integrations. Every hook follows the same build-simulate-approve-sign-execute pattern and returns consistent status, data, and error fields.
ActionsBuilder provides the same operations as the hooks but for use outside React, in backend services, scripts, or custom integrations.
TenorBundler gives you direct control over the multicall bundle for advanced use cases where the higher-level APIs do not fit.
Supported Operations[](https://docs.tenor.finance/technical-docs/sdk/#supported-operations "Direct link to Supported Operations")
-----------------------------------------------------------------------------------------------------------------------------------
The SDK covers all core Tenor operations.
* **Borrow and lend:** Take offers at the market rate or post offers at a chosen rate.
* **Repay and withdraw:** Close positions and reclaim funds.
* **Collateral management:** Add or remove collateral from existing positions.
* **Auto-renewal:** Enable or disable fixed-to-fixed and fixed-to-variable rolling.
Wallet Support[](https://docs.tenor.finance/technical-docs/sdk/#wallet-support "Direct link to Wallet Support")
-----------------------------------------------------------------------------------------------------------------
The SDK automatically detects whether you are using an EOA (MetaMask and similar) or a smart contract account (Safe, Argent, and similar), and adjusts its behavior.
* **EOA wallets:** Offchain Permit2 signatures, single bundled transaction.
* **SCA wallets:** Onchain approvals, batch proposed for multi-sig approval.
You do not need to handle this distinction yourself. The hooks and ActionsBuilder manage it automatically.
Installation[](https://docs.tenor.finance/technical-docs/sdk/#installation "Direct link to Installation")
-----------------------------------------------------------------------------------------------------------
The SDK is part of the `tenor-app` package. Import from `@/lib/tenor-sdk`.
import { useBorrow, getContracts, ActionsBuilder } from "@/lib/tenor-sdk";
**Dependencies:** [viem](https://viem.sh/)
, [wagmi](https://wagmi.sh/)
(for React hooks).
* [How It Works](https://docs.tenor.finance/technical-docs/sdk/#how-it-works)
* [Three Ways to Use It](https://docs.tenor.finance/technical-docs/sdk/#three-ways-to-use-it)
* [Supported Operations](https://docs.tenor.finance/technical-docs/sdk/#supported-operations)
* [Wallet Support](https://docs.tenor.finance/technical-docs/sdk/#wallet-support)
* [Installation](https://docs.tenor.finance/technical-docs/sdk/#installation)
---
# Oracle with Validation | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/oracle-with-validation/#__docusaurus_skipToContent_fallback)
On this page
Oracle with Validation is a Morpho-compliant oracle type that improves the robustness of the price oracle by mitigating oracle provider failures such as compromise or manipulation. The Oracle with Validation cross-validates a **primary oracle price** against a **validation oracle price** before allowing any take to proceed, always returning the primary oracle's price but reverting if the two feeds diverge beyond a configured deviation threshold.
For example, a cbBTC collateral market with USDC loan token can use Chainlink BTC/USD as the primary oracle price and Redstone BTC/USD as the validation oracle price, with a 10% deviation threshold. As long as the two feeds stay within 10% of each other, the market behaves normally using the Chainlink oracle price. If one feed is manipulated or suffers a compromise, the deviation check trips and all borrow, withdraw collateral, and liquidation actions are effectively paused until the discrepancy resolves.
This design is intended for liquid markets where multiple oracle providers or onchain sources track prices with high fidelity.
Trade-off
Price calls revert whenever the two feeds are outside the deviation limit, which also prevents liquidations from being executed during that time. A threshold that is too narrow can be dangerous: it trades direct manipulation risk for liquidation-availability risk. See [Sizing MAX\_ORACLE\_DEVIATION](https://docs.tenor.finance/technical-docs/oracle-with-validation/#sizing-max_oracle_deviation)
for the bounds to respect.
Oracle Architecture[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#oracle-architecture "Direct link to Oracle Architecture")
---------------------------------------------------------------------------------------------------------------------------------------------------
The contract uses two independent oracle sources.
**Primary oracle:**
* The main price source, such as Chainlink.
* Its price is always returned to callers unless the deviation check against the validation oracle triggers.
* Must return a non-zero price.
**Validation oracle:**
* Secondary price source used purely for validation, such as a TWAP, Chronicle, or Redstone feed.
* Used to detect price manipulation or primary oracle failures.
* Deviation is checked against the primary oracle.
* Its price is never returned to callers and is never read by the Morpho market. It only gates whether the primary price is allowed through.
The absolute deviation is computed as ∣pprimary−pvalidation∣|p\_{\\text{primary}} - p\_{\\text{validation}}|∣pprimary−pvalidation∣. The maximum allowed deviation is
Δmax\=pprimary⋅MAX\_ORACLE\_DEVIATION1018\\Delta\_{\\max} = \\frac{p\_{\\text{primary}} \\cdot \\text{MAX\\\_ORACLE\\\_DEVIATION}}{10^{18}}Δmax\=1018pprimary⋅MAX\_ORACLE\_DEVIATION
If ∣pprimary−pvalidation∣\>Δmax|p\_{\\text{primary}} - p\_{\\text{validation}}| > \\Delta\_{\\max}∣pprimary−pvalidation∣\>Δmax, the price call reverts and the primary value is withheld.
Price Validation[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#price-validation "Direct link to Price Validation")
------------------------------------------------------------------------------------------------------------------------------------------
When `price()` is called, the contract runs these checks in order:
1. Fetches the price from the primary oracle.
2. Reverts with `ZeroPrimaryPrice` if the primary price is zero.
3. If `validationCheckPaused` is `true`, returns the primary price immediately.
4. Otherwise, fetches the price from the validation oracle and computes the absolute deviation. Reverts with `ExcessiveOracleDeviation` if it exceeds Δmax\\Delta\_{\\max}Δmax.
5. Returns the primary price.
Sizing MAX\_ORACLE\_DEVIATION[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#sizing-max_oracle_deviation "Direct link to Sizing MAX_ORACLE_DEVIATION")
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
`MAX_ORACLE_DEVIATION` is expressed in 18 decimals, for example, `5e16` for 5%. It is immutable after deployment.
For Morpho markets, the deployer will typically want the deviation threshold to stay below (1−LLTV)(1 - \\text{LLTV})(1−LLTV) for the check to be useful: any larger overestimate by the primary oracle would let a borrower open a position whose true LTV exceeds 100%, making bad debt unavoidable. Because deviation is measured as a fraction of `primaryPrice` (not `validationPrice`), the effective overshoot when primary is the high side is d1−d\\frac{d}{1 - d}1−dd, so for tight-LLTV markets prefer
MAX\_ORACLE\_DEVIATION≤1−LLTV2−LLTV\\text{MAX\\\_ORACLE\\\_DEVIATION} \\leq \\frac{1 - \\text{LLTV}}{2 - \\text{LLTV}}MAX\_ORACLE\_DEVIATION≤2−LLTV1−LLTV
to keep the effective overshoot within (1−LLTV)(1 - \\text{LLTV})(1−LLTV).
The threshold also cannot be too tight: the validation oracle is not ground truth and can drift independently from the primary due to feed update cadence, decimal rounding, sequencer lag, TWAP smoothing, and so on. A threshold that triggers on honest deviation halts `price()` and blocks liquidations of unhealthy positions while interest accrues. A reasonable starting point is
MAX\_ORACLE\_DEVIATION≤(1−LLTV)−expected\_normal\_deviation\\text{MAX\\\_ORACLE\\\_DEVIATION} \\leq (1 - \\text{LLTV}) - \\text{expected\\\_normal\\\_deviation}MAX\_ORACLE\_DEVIATION≤(1−LLTV)−expected\_normal\_deviation
calibrated against the historical spread between the two feeds.
Validation Oracle Failure Behavior[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#validation-oracle-failure-behavior "Direct link to Validation Oracle Failure Behavior")
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The validation oracle call uses `try/catch`. At deployment, the deployer configures the immutable `REVERT_ON_VALIDATION_ORACLE_FAILURE` flag, which controls what happens when the validation oracle reverts:
* **`true`:** The entire `price()` call reverts with `ValidationOraclePriceFailure`. Use this when you want a malfunctioning validation oracle to halt all operations that depend on the price.
* **`false`:** The primary price is returned without validation. Use this when availability is more important than validation, and the owner can pause/unpause validation manually if needed.
Typically for Tenor markets, a revert of the validation oracle is ignored (`REVERT_ON_VALIDATION_ORACLE_FAILURE = false`) so that the primary oracle price is returned. This is to avoid relying on the validation oracle for liveness.
Owner Controls[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#owner-controls "Direct link to Owner Controls")
------------------------------------------------------------------------------------------------------------------------------------
The contract owner can pause and unpause the validation check. When paused, only the primary oracle is used and no deviation check is performed, which is useful if the validation oracle becomes unreliable.
* `pauseValidationCheck()` disables the deviation check. Reverts if already paused.
* `unpauseValidationCheck()` re-enables it. Reverts if already unpaused.
The contract uses OpenZeppelin's `Ownable2Step` for safe ownership transfers. Calling `renounceOwnership()` makes the oracle immutable: pause/unpause and ownership transfer are permanently disabled. The constructor does not accept `address(0)` as the initial owner, so to deploy an immutable oracle, deploy with a temporary owner, verify that the validation check is unpaused and `price()` returns successfully, then renounce. Renouncing before verifying is risky: if the temporary owner has paused validation first, the oracle is permanently primary-only with no way to re-enable validation.
Key Parameters[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#key-parameters "Direct link to Key Parameters")
------------------------------------------------------------------------------------------------------------------------------------
* **PRIMARY\_ORACLE:** Address of the primary oracle (immutable).
* **VALIDATION\_ORACLE:** Address of the validation oracle (immutable).
* **MAX\_ORACLE\_DEVIATION:** Maximum allowed deviation between oracles in 18 decimals (immutable, must be less than 100%).
* **REVERT\_ON\_VALIDATION\_ORACLE\_FAILURE:** Whether the oracle reverts when the validation oracle's `price()` call reverts (immutable). If `false`, a failing validation oracle is treated as a pass-through and the primary price is still returned.
Factory Deployment[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#factory-deployment "Direct link to Factory Deployment")
------------------------------------------------------------------------------------------------------------------------------------------------
Oracle with Validation instances are deployed via the `OracleWithValidationFactory` using CREATE2 for deterministic addresses.
Factory constraints:
* Neither `primaryOracle` nor `validationOracle` can be the zero address.
* Primary and validation oracle addresses must be different.
* `MAX_ORACLE_DEVIATION` must be strictly less than 100% (`1e18`).
* The factory tracks all deployed oracles via the `isDeployedOracle` mapping.
Security Considerations[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#security-considerations "Direct link to Security Considerations")
---------------------------------------------------------------------------------------------------------------------------------------------------------------
* **Both oracles must be trusted:** A compromised validation oracle can be used to block legitimate primary prices and halt liquidations, even though it cannot directly fabricate the price returned to callers.
* **No staleness check:** This contract does not enforce a maximum age on either feed; staleness handling is delegated to the underlying oracle implementations.
* **Zero primary price always reverts:** `ZeroPrimaryPrice` fires regardless of pause state. Pausing the validation check does not bypass this guard.
Composing Morpho-compatible oracles[](https://docs.tenor.finance/technical-docs/oracle-with-validation/#composing-morpho-compatible-oracles "Direct link to Composing Morpho-compatible oracles")
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The Oracle with Validation type can be paired with other Morpho-compliant oracles as its primary or validation source. For example, one could use the Steakhouse metadeviation oracle, which itself wraps two underlying feeds and uses a timelock-gated challenge mechanism to switch between them: while both feeds agree it reports its primary, and once a deviation between them is challenged and the challenge timelock expires, anyone can call `acceptChallenge` to switch its reported price to the backup feed (with a symmetric `heal` path to switch back).
* [Oracle Architecture](https://docs.tenor.finance/technical-docs/oracle-with-validation/#oracle-architecture)
* [Price Validation](https://docs.tenor.finance/technical-docs/oracle-with-validation/#price-validation)
* [Sizing MAX\_ORACLE\_DEVIATION](https://docs.tenor.finance/technical-docs/oracle-with-validation/#sizing-max_oracle_deviation)
* [Validation Oracle Failure Behavior](https://docs.tenor.finance/technical-docs/oracle-with-validation/#validation-oracle-failure-behavior)
* [Owner Controls](https://docs.tenor.finance/technical-docs/oracle-with-validation/#owner-controls)
* [Key Parameters](https://docs.tenor.finance/technical-docs/oracle-with-validation/#key-parameters)
* [Factory Deployment](https://docs.tenor.finance/technical-docs/oracle-with-validation/#factory-deployment)
* [Security Considerations](https://docs.tenor.finance/technical-docs/oracle-with-validation/#security-considerations)
* [Composing Morpho-compatible oracles](https://docs.tenor.finance/technical-docs/oracle-with-validation/#composing-morpho-compatible-oracles)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/ratifiers/migration-intent#__docusaurus_skipToContent_fallback)
Copy page

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/blue-to-midnight/supply-collateral#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses/contracts/tenor-adapter#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses/contracts/static-rate-policy#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/withdraw-vault-shares#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture/contracts/callbacks/supply-vault-shares#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture/contracts/ratifiers/migration-intent#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses/contracts/callbacks/withdraw-vault-shares#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses/contracts/take-router#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture/contracts/tenor-adapter#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/addresses/contracts/gates/midnight-allowlist#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/architecture/contracts/gates/midnight-allowlist#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Welcome to Tenor docs | Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/lend-midnight-renewal#__docusaurus_skipToContent_fallback)
Copy page

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---
# Tenor
[Skip to main content](https://docs.tenor.finance/technical-docs/contracts/callbacks/overview/borrow-midnight-renewal#__docusaurus_skipToContent_fallback)

Tenor is a non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain, built on the Morpho protocol. Explore key concepts and features of the Tenor platform.
### What is Tenor
A non-custodial platform for fixed-rate, fixed-term borrowing and lending onchain. Built on the Morpho protocol. Tenor extends Morpho with advanced features.
[More →](https://docs.tenor.finance/get-started/what-is-tenor)
### Lend
Fixed-rate, fixed-term lending matched peer-to-peer. Covers how a position locks in its rate and term, the available offer types, and exiting early by reselling a position before maturity.
[More →](https://docs.tenor.finance/get-started/lend/fixed-rate)
### Borrow
Fixed-rate, fixed-term borrowing against collateral. Covers maturity date, repayment, offer types, and exiting early.
[More →](https://docs.tenor.finance/get-started/borrow/fixed-rate)
### Auto-renewal
Rolls a borrow position into a new fixed-rate or variable-rate term at maturity, without action from the borrower. Prevents liquidation triggered by reaching maturity unpaid.
[More →](https://docs.tenor.finance/get-started/borrow/auto-renewal)
### Liquidations
How a borrow position is liquidated when its LTV crosses the LLTV or debt goes unpaid at maturity, plus overcollateralization and the price oracles that drive it.
[More →](https://docs.tenor.finance/get-started/borrow/liquidations)
### OTC
Bespoke agreements outside standard markets, with custom collateral, rates, and terms. Covers creating offers, requesting quotes, and gated markets.
[More →](https://docs.tenor.finance/get-started/otc/create-otc-offers)
### Tenor Prime
Covers organization accounts with role-based access across wallets, multisigs, and vaults, along with OTC offer requests, gated vaults, and SDK and API access.
[More →](https://docs.tenor.finance/get-started/tenor-prime)
### Rewards
Rewards on lending and borrowing positions, distributed weekly through Merkl and claimable from the Tenor interface or Merkl.
[More →](https://docs.tenor.finance/get-started/rewards)
### Fees
Tenor charges no fees for initiating or renewing positions or using the interface.
[More →](https://docs.tenor.finance/get-started/fees)
---