# Contract ground truth

Where the truth about the protocol lives. Consult this (and the sources it
points to) before touching indexer handlers, the decoder, or anything that
interprets on-chain data. Protocol *semantics* live in `architecture.md`; the
mempool wire format lives in `decoder.md`; this file is the map of sources,
addresses, and events.

## Source repositories (the ground truth)

When decoding or event-semantics questions arise, validate against the
contract sources — not memory, not docs:

- **Midnight protocol**: https://github.com/morpho-org/midnight/blob/main/src
  - `Midnight.sol` — the singleton: markets, offers, ticks, collateral,
    settlement, fees.
  - `libraries/TickLib.sol` (tick→price math, vendored into
    `packages/shared/src/ticks-table.ts`), `libraries/IdLib.sol` (market ids —
    market params stored as contract runtime bytecode, SSTORE2-style; mirrored
    by `computeMarketId` in shared).
  - `Midnight.isHealthy` + `ConstantsLib.ORACLE_PRICE_SCALE` (1e36) — mirrored
    by `packages/shared/src/health.ts` (`collateralValue` / `maxDebt` /
    `isHealthy`), which `/risk` grades positions with. TWO `mulDivDown`s per
    collateral, in that order: folding them into one expression rounds
    differently and disagrees with the chain at the boundary. Liquidation has
    two paths — unhealthy before maturity, and UNCONDITIONAL after it (every
    position still holding debt, however healthy, with the incentive ramping
    over the following hour).
  - The collateral oracle interface — `price()` alone, in
    `packages/shared/src/abis/oracle.ts`. Quoted in loan token per collateral
    token with BOTH tokens' decimals already folded in, which is why health
    math needs no decimals and why collateral never needs a USD price of its
    own.
  - `ratifiers/EcrecoverRatifier.sol`, `ratifiers/SetterRatifier.sol` —
    Merkle-tree offer ratification (leaf-wise EIP-712; the ratifier hash is
    NOT our `offerHash`).
  - `periphery/log/Log.sol` — the Mempool broadcast contract (fallback always
    emits `Data(msg.data)`).
  - `periphery/blue-buy-callback/` — `BlueBuyCallback` parks idle maker funds
    on Morpho Blue; `callbackData` = ABI-encoded Blue `MarketParams`.
  - `interfaces/ICallbacks.sol` — offers carry `callback` + `callbackData`;
    liquidity is sourced at settlement through these.
- **Morpho bundles**: https://github.com/morpho-org/bundles/tree/main/src
  - `midnight/MidnightBundlesV1.sol` — Morpho's user-facing Midnight router
    (fixed buy/sell/repay+collateral entry points, called directly by users;
    Midnight event `caller` = MidnightBundlesV1 for Morpho-app flows).
- **Morpho bots**: https://github.com/morpho-org/morpho-bots
  - `packages/contracts/solidity/CrossedBooksResolver.sol` — permissionless
    crossed-book taker: `resolve(sellOffers, buyOffers, minimumProfit)` takes a
    crossed pair of offers on Midnight and pays the spread to `msg.sender`
    (the bot lives in `bots/midnight-crossed-books`). Morpho's own bot drives
    it on Base; Tenor did once, through Multicall3 — those transactions are
    attributed to Tenor by hash, see Router attribution below.
- **Tenor contracts**: https://github.com/tenor-labs/tenor-contracts/tree/main/src
  - `router/TenorRouter.sol` + `clamps/` — batch execution over offers.
  - `periphery/MidnightTakeAndRepay.sol` — stateless, permissionless
    take-and-repay entry point: `takeAndRepay(offer, ratifierData, units)`
    takes a BUY offer at par as the borrower and repays the debt inside its
    `onSell` callback, one call. It is the taker, so Midnight records it as
    the `caller` of BOTH the Take and the Repay — see Router attribution.
    (As of 2026-09-25 the file is in the local checkout only, not yet on the
    repo's `main`; the deployments below were matched to the compiled artifact
    by runtime bytecode, differing only in the Midnight immutable.)
  - `bundler/` — Bundler3 adapters (TenorAdapter, AuthorizationAdapter).
  - `callbacks/` — renewal (`*MidnightRenewalCallback`) and migration
    (Blue/Vault ↔ Midnight) callbacks; these are the addresses offers name in
    their `callback` field.
  - `ratifiers/MigrationRatifier.sol` + `policies/` (StaticRatePolicy,
    MarketMakingPolicy, `FourWeekCadence.sol` — term boundaries every 28 days
    from the Unix epoch).
  - `gates/` (allowlist + delayed-liquidation gates), `oracles/OracleWithValidation.sol`.
  - `libraries/TenorMarketIdLib.sol` — renewal-stable market identity over
    `(chainId, midnight, loanToken, collateralParams, rcfThreshold, enterGate,
    liquidatorGate)`; reproduced by `computeTenorMarketId` in shared — the
    home "Largest markets" section and /markets group Midnight markets by it.

## Deployed addresses

**Rule: deployed addresses live in exactly three files** —
`packages/shared/src/chains.ts` (the contracts the indexer watches: midnight,
mempool, migrationRatifier, blueBuyCallbackFactory — each of the last three
carries its own deploy block because each was deployed after Midnight core),
`packages/shared/src/known-contracts.ts` (the label
registry for callbacks/ratifiers/gates/policies, seeded from the tables
below), and `packages/shared/src/vaults.ts` (the Tenor vaults the indexer
snapshots — address, role and deploy block only; every other field is read
on-chain). Never hardcode an address anywhere else; when the tables in this
file change, update those registries with them.

Official registries (check on every chain addition or protocol upgrade):

- Morpho (incl. Midnight core): https://docs.morpho.org/developers/contracts/addresses/
- Tenor: https://www.docs.tenor.finance/technical-docs/addresses

### Base (chain id 8453)

Verified against both registries on **2026-08-02**:

| Contract | Address |
| --- | --- |
| Midnight | `0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A` |
| Mempool (Log) | `0xdD6DCE32e21f7b020898a8258dA37355b4017993` |
| SetterRatifier | `0x800B5F12A61B8198a5a6EfD794Cac6699B294d63` |
| EcrecoverRatifier | `0xd6e70365C8E8DDa9a4ca662C07bbE663b017755E` |
| RateRatifierV1 | `0x3B2FbA901C88f026BF50A12c8B28484Cd81F9526` (added 2026-10-06, see below) |
| PriceRatifierV1 | `0x08a0e69772315652165921D722B3caAea4C326e6` (added 2026-10-06) |
| BlueFallbackRolling | `0x7bfA8207B818cc0e23a7aBdCBe56572fAbCa831F` (added 2026-10-06; indexed since 2026-10-21 — see BlueFallbackRolling below) |
| WhitelistEnterGateFactory | `0x0198bab349bf9B1cB84E3f47e07e44759F586BC2` (added 2026-10-06) |
| EcrecoverAuthorizer | `0x292bEa9f1443d54E0E509120c919106765c6a493` |
| Morpho Blue | `0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb` |
| Bundler3 | `0x6BFd8137e702540E7A42B74178A4a49Ba43920C4` |
| MidnightBundlesV1 | `0x091183d729BE9f808c212b475E387A12E67850A7` |
| MidnightBundlesV2 | `0x0bF30B530f816a619C3F28a3b9f44C441122555D` (added 2026-10-13, user direction: Morpho's successor bundler, Base only so far. Block 52251677, bracketed with `eth_getCode` on two independent nodes; `MIDNIGHT()` returns Base's Midnight; 20,185 bytes of runtime code against V1's 16,513. A `known-contracts.ts` router row under `morpho`, like V1) |
| CrossedBooksResolver | `0x576558a6c6740C6C95567b63000fd7a989410c3f` (deliberately NOT a `known-contracts.ts` row — see Router attribution; in neither registry and unverified on Blockscout — identified 2026-08-26 by its `Resolve(address,bytes32,uint256,uint256,uint256,uint256)` logs and `MIDNIGHT()` returning Midnight) |

Tenor infrastructure (same date, Base):

| Contract | Address |
| --- | --- |
| MigrationRatifier | `0xf96EC15aC7d0F8fa3560B6DdF9d1F9D54F31DAed` (deployed at block 48332040 — indexed, see the event catalog) |
| FourWeekCadence | `0xb73C9075643e32466c70FCa96F2f23a8a9bb803C` |
| LastFridayOfMonthCadence | `0xc934aB6F04F5059A6d308f79295bC08F50fB2f09` |
| MarketMakingPolicy | `0x8bB1Fe0c780115b6CC5D96BfD3b7bBd751252CA8` |
| PausableMarketMakingPolicy | `0xABE1ccB0C5Bc0B30976d9435084Af9a7d8Fe5107` |
| PausableStaticRatePolicy | `0xe6e49216D44464fB348EDB9163c5929325A4229b`, `0xB8A084E509ee04d94C7aE10f934b31F5b1811a5F` |
| TenorAdapter | `0x5675892d48828872fbFD2720489f98b42f48138F`, `0x3f60566413487406F80572A4Eb91163aC097E415` (earlier deploy, still used by Tenor staging) |
| MidnightTakeAndRepay | `0x51F3660e608EFb7d6f65beE3a33607CdA7fa4E0a` (block 50480132, by `0x5F2F4562…F1ba` — the EOA behind its takes; registered 2026-09-25 with six transactions already indexed through it, blocks 51687829–51757942) |
| TenorRouter (unverified) | `0xf304Fd30EF75b065bC629FA5DA76B8a403A12515` (block 53212827, by Tenor's deployer `0x8f4452bB…41d3`; exposes `MORPHO_MIDNIGHT` / `onBuy` / `isRatified` but matches no tenor-contracts artifact, so the registry gives it a generic name; registered 2026-10-28 for its withdraws, see Router attribution) |
| AuthorizationAdapter | `0xDbb74BF80bD05a1959C8f21e2b821dcFEd87104C` |
| MidnightVaultExecutor | `0xb03015B3aA6f5Eda3AD03c8B3e5553b2665174a2` |
| BorrowMidnightRenewalCallback | `0x1524CB7499735379b34ba3b7C6F150808094d2d2` |
| LendMidnightRenewalCallback | `0x93cfC1178C3cc6c315F6505C3A927B7974425188` |
| BorrowBlueToMidnightCallback | `0x7d4A92F88c22c6dD31F0fB67F27cAFC8e90f5F18` |
| LendVaultToMidnightCallback | `0xc0b438Bbf5a8cC4e57C0D81e1818C069e413DdDf` |
| BorrowMidnightToBlueCallback | `0x6Bb533CF102d164106A5F83739fd1C0078F3E2DA` |
| LendMidnightToVaultCallback | `0x1A4eD5BC9adC11a7Aa7fCB8f045A7B51604bBC50` |
| MidnightSupplyCollateralCallback | `0x191c821B2681B098E05dd84B1e5433d194c94383` |
| MidnightSupplyVaultSharesCallback | `0xAbae3A858f53C7010D07ee930b3a0621adcFBBA8` |
| MidnightWithdrawVaultSharesCallback | `0x519d415bf44C623B8E1226E9959E4548cb289F42` |
| VaultV2AllowlistGate | `0x3d7F6B28F46E44E3C40391f2b5f9f27ba067b8B7` |
| DelayedLiquidationGateFactory | `0xbB91265Fd479094d7f1B1B20AF79A07442df9850` |
| MidnightAllowlistGateFactory | `0x508BC8B7AbB0EE4c885acf7aC3D58941baF76F7c` |
| VaultV2AllowlistGateFactory | `0x73ef8184cEb6C1D668BcB04C41a264c9468ac670` |
| OracleWithValidationFactory | `0xEe80d70B5671E17e88e2654F613A288af5689412` |

Known-contract labels used by the web app (offer callback/ratifier tagging)
are seeded from these tables in `packages/shared/src/known-contracts.ts` —
update both together — and per chain: see the address-collision warning under
Ethereum below before reusing anything from this table.

**Factory-deployed contracts are NOT listed here** — the indexer discovers them
from each factory's deployment event (Ponder `factory()`), so a new gate or
oracle needs no edit to this file or to `known-contracts.ts`. As of 2026-08-10
the four factories had deployed exactly ONE contract between them: the oracle
`0xac2d52439742a902efb0c6f542c5d26b574bc3ed`.

**Except the ones deployed without their factory.** Two live VaultV2 gates —
`0x1c7556434e8977d5a88c2a739d03525aa1cced87` (block 48331675) and
`0x3d7f6b28f46e44e3c40391f2b5f9f27ba067b8b7` (block 48505340) — answer
`isDeployedGate() == false`, so no deployment event exists to discover them
from. They are named explicitly in `chains.ts` under `tenorDirectGates`. The
second is actively managing an allowlist (5 `VaultV2AllowlistUpdated` events),
so omitting them made /tools/vault-gates read "none deployed yet" while a real
gate was in use. **If Tenor deploys another gate outside a factory, add it
there** — nothing else will find it.

Seen on chain but NOT in the official registry (so deliberately unlabelled —
it renders as a plain address until Tenor publishes it): a second cadence at
`0xfc6722b202eb47576da35692eb2dd487102de7e1`, named by live ratifier params.
Its runtime bytecode is byte-identical to the registered FourWeekCadence
except for the period constant — `0x093a80` (7 days) against `0x24ea00`
(28 days) — i.e. the same source deployed on a weekly cadence.

### Ethereum (chain id 1)

Morpho core added **2026-08-24**; Tenor's own deployment added **2026-09-01**,
from the deployment record of **2026-08-31**.

> **Never copy an address from the Base tables above.** Tenor's deployer
> (`0xFF706EaeEf1b13D7b5231c236C25E4F815522bDe`) walked the same nonce sequence
> on both chains but deployed the contracts in a DIFFERENT ORDER, so a dozen
> addresses appear on both chains holding **entirely different contracts**:
>
> | Address | On Base | On Ethereum |
> | --- | --- | --- |
> | `0xf96EC15a…1DAed` | MigrationRatifier | LendVaultToMidnightCallback |
> | `0x7d4A92F8…F5F18` | BorrowBlueToMidnightCallback | **BorrowMidnightToBlueCallback** |
> | `0xbB91265F…f9850` | DelayedLiquidationGateFactory | BorrowBlueToMidnightCallback |
> | `0x73ef8184…ac670` | VaultV2AllowlistGateFactory | CallbackFeeAdjuster |
> | `0x508BC8B7…F76F7C` | MidnightAllowlistGateFactory | LastFridayOfMonthCadence |
> | `0xEe80d70B…689412` | OracleWithValidationFactory | VaultSetupBatcher |
> | `0xb03015B3…174a2` | MidnightVaultExecutor | BorrowBlueToMidnightClamp |
> | `0xFc6722B2…De7e1` | unregistered weekly cadence | **TenorAdapter** |
> | `0x8bB1Fe0c…52CA8` | MarketMakingPolicy | PausableRatePolicy |
> | `0xABE1ccB0…Fe5107` | PausableMarketMakingPolicy | PausableRatePolicy |
> | `0x93cfC117…425188` | LendMidnightRenewalCallback | MidnightSupplyCollateralCallback |
> | `0xAbae3A85…cFBBA8` | MidnightSupplyVaultSharesCallback | BorrowMidnightRenewalCallback |
> | `0x519d415b…289F42` | MidnightWithdrawVaultSharesCallback | MidnightSupplyVaultSharesCallback |
> | `0x6Bb533CF…F3E2DA` | BorrowMidnightToBlueCallback | LendMidnightRenewalCallback |
> | `0x1A4eD5BC…4bBC50` | LendMidnightToVaultCallback | MidnightWithdrawVaultSharesCallback |
> | `0xc0b438Bb…13DdDf` | LendVaultToMidnightCallback | LendMidnightToVaultCallback |
>
> Confirmed on chain, not inferred: `0xf96EC15a…` carries 3,137 bytes of code on
> Ethereum against the real MigrationRatifier's 12,891, and `0xEe80d70B…` is
> 3,757 bytes on Base against 10,413 on Ethereum. The row-2 swap is the
> dangerous one — `BORROW_MIGRATION_KINDS` in `known-contracts.ts` maps
> `BorrowMidnightRenewalCallback → fixed` and `BorrowMidnightToBlueCallback →
> variable`, so a copied table would invert every borrower's maturity policy.
> Both registries are chain-keyed, which is what makes this safe: take each row
> from the deployment record for the chain in hand.

| Contract | Address | Deploy block |
| --- | --- | --- |
| Midnight | `0x471686c42792F93528B000beF54bC10E3aa2045f` | 25798183 |
| Mempool (Log) | `0xde2d62449301a09A51EbF9326EA60d2e8BF4A8F7` | 25798183 |
| SetterRatifier | `0xb72c416382c8A6399D0765CebfB032F040B00B3c` | 25798183 |
| EcrecoverRatifier | `0xAC439c81CAA6ef4C7B7E8F0110F8CE63A4b6D43e` | 25798183 |
| RateRatifierV1 | `0x14A4cd3D7a28d90D37fd0917EDeaa496E5931095` | (label only, added 2026-10-06) |
| PriceRatifierV1 | `0x5A7CbC4458344fd11E1ED81D7834082a1138f975` | (label only, added 2026-10-06) |
| BlueFallbackRolling | `0x6910E21c20a09BB3a2026a24585aF1E63d769fe6` | 25897717 (indexed since 2026-10-21 — see BlueFallbackRolling below) |
| WhitelistEnterGateFactory | `0x76b676Fa04dEE92a00EFCbD9d351f75B21ab12f5` | (label only, added 2026-10-06) |
| EcrecoverAuthorizer | `0xfC3303119E46AF831CacdBDB6e1A04C9C369ffF7` | 25798183 |
| MidnightBundlesV1 | `0x7c00dBB2b6b6b9B28745332e550dC8782Fcf77EC` | 25798264 |
| BlueBuyCallback factory | `0x172d1FdC5f79bFe1ED46448f18541E591E5c93a7` | 25798272 |
| Morpho Blue | `0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb` | 18883124 |

All five Midnight-suite contracts landed in ONE block (25798183). Deploy blocks
were established by binary-searching `eth_getCode` across four independent
archive nodes; the same search reproduced Morpho Blue's and Multicall3's known
deploy blocks, which is what validates the method.

**Midnight's and the Mempool's runtime bytecode is byte-for-byte identical to
Base's** (sha256 over `eth_getCode` matches), so the ABIs in
`packages/shared/src/abis/` and every event handler apply unchanged.

Tenor infrastructure, deployed 2026-08-31 in blocks 25878175–25878285. Deploy
blocks bracketed with `eth_getCode`; every address confirmed to carry code:

| Contract | Address | Deploy block |
| --- | --- | --- |
| MigrationRatifier | `0x4Bf0Ef9B8FF6535b371c2BAD12Da67dC59d62A5C` | 25878265 (indexed) |
| VaultV2AllowlistGateFactory | `0xf6732Aa6eAaF3754ed0290Beb5dcE17D77aBD802` | 25878175 (indexed) |
| DelayedLiquidationGateFactory | `0xd554bB25Ee3314a0012A844dDBBa53A62477E045` | 25878262 (indexed) |
| MidnightAllowlistGateFactory | `0x782b3770E209fbD1f14a233256eE01Ff04d4db3a` | 25878281 (indexed) |
| TenorAdapter | `0xFc6722B202Eb47576dA35692EB2dd487102De7e1` | 25878270 |
| MidnightTakeAndRepay | `0x9F993355DF8Eeb02f3a15b281580A4F74d40C922` (by `0x8f4452bB…41d3`; the SAME address as Arc's, see below) | 26027976 |
| AuthorizationAdapter | `0xa55f66f0e5bAF1C9b36BBe49cb7Fb645FA5089c6` | |
| MidnightVaultExecutor | `0x5eF72B12fF9df27a68F0b736B3617F907D74036D` | |
| CallbackFeeAdjuster | `0x73ef8184cEb6C1D668BcB04C41a264c9468ac670` | |
| VaultSetupBatcher | `0xEe80d70B5671E17e88e2654F613A288af5689412` | |
| LastFridayOfMonthCadence | `0x508BC8B7AbB0EE4c885acf7aC3D58941baF76F7c` | |
| MarketMakingPolicy | `0xbb5eE0bfa44a4b4F80467cC96734fdac1f1bc043` | |
| PausableMarketMakingPolicy | `0x504b1a3C053Df3cc932B63D12173190c2ecA228F` | |
| PausableRatePolicy | `0x8bB1Fe0c780115b6CC5D96BfD3b7bBd751252CA8`, `0xABE1ccB0C5Bc0B30976d9435084Af9a7d8Fe5107` | |
| BorrowBlueToMidnightClamp | `0xb03015B3aA6f5Eda3AD03c8B3e5553b2665174a2` | |
| BorrowMidnightRenewalCallback | `0xAbae3A858f53C7010D07ee930b3a0621adcFBBA8` | |
| LendMidnightRenewalCallback | `0x6Bb533CF102d164106A5F83739fd1C0078F3E2DA` | |
| BorrowBlueToMidnightCallback | `0xbB91265Fd479094d7f1B1B20AF79A07442df9850` | |
| LendVaultToMidnightCallback | `0xf96EC15aC7d0F8fa3560B6DdF9d1F9D54F31DAed` | |
| BorrowMidnightToBlueCallback | `0x7d4A92F88c22c6dD31F0fB67F27cAFC8e90f5F18` | |
| LendMidnightToVaultCallback | `0xc0b438Bbf5a8cC4e57C0D81e1818C069e413DdDf` | |
| MidnightSupplyCollateralCallback | `0x93cfC1178C3cc6c315F6505C3A927B7974425188` | |
| MidnightSupplyVaultSharesCallback | `0x519d415bf44C623B8E1226E9959E4548cb289F42` | |
| MidnightWithdrawVaultSharesCallback | `0x1A4eD5BC9adC11a7Aa7fCB8f045A7B51604bBC50` | |
| Bundler3 (Morpho, `routesFor: tenor`) | `0x6566194141eefa99Af43Bb5Aa71460Ca2Dc90245` | |

Two differences from Base, both load-bearing:

- **No OracleWithValidationFactory.** The deployment record's
  `validationCheckFactory` is null and it built no oracles from one, so
  `tenorFactories.oracleWithValidation` is optional in `ChainInfo` and
  `OracleWithValidation` keeps a `base` entry alone in `ponder.config.ts`.
- **No `tenorDirectGates`.** The one live gate here,
  `0x9185EFeB2BE57aa2914C34f91Ce92E589E42a0D4` (block 25878181), came from its
  factory — `VaultV2AllowlistGateDeployed(gate, owner)` is on chain — so Ponder
  discovers it. It already carries five allowlist entries (Midnight, the
  MidnightVaultExecutor and three callbacks).

**MidnightBundlesV1 is deployed here too**, at `0x7c00dBB2…` (Etherscan tag
"Morpho: Midnight Bundles V1"; user direction, 2026-09-10). Block 25798264 —
sixteen minutes after Midnight, in the same Morpho deployment window —
bracketed with `eth_getCode` on two independent nodes. Its runtime bytecode is
Base's MidnightBundlesV1 byte for byte except for the `MIDNIGHT` immutable
(every occurrence reads Ethereum's Midnight, and `MIDNIGHT()` returns
`0x471686c4…`), so it is a `known-contracts.ts` router row under `morpho`
exactly like Base's — at ITS OWN address, never Base's `0x091183…`. Until it
was registered, every Morpho-app take on mainnet read "direct".

As of 2026-09-01 the chain was still in configuration (15 Midnight config
logs, three Mempool `Data`, a MigrationRatifier ownership transfer) with **no
markets and no takes**. Trading started on 2026-09-03 (first take in block
25899690); by 2026-09-10 production had indexed 61 markets and 58 takes across
55 transactions. Take callers so far: Tenor's `TenorAdapter` (34), a Gnosis
Safe proxy (10 — direct), `MidnightBundlesV1` (7) and an EIP-7702-delegated
EOA (7 — direct), so both registered routers are already live here.

### Arc (chain id 5042)

Added **2026-09-16**, the day Arc mainnet opened, from Tenor's deployment record
of the same date (`arc-production`, `stage3_core` + `stage4_policies`
completed; oracles, markets and vaults still pending). Circle's L1: gas is paid
in USDC, which carries 18 decimals at the native layer and 6 through its ERC-20
face at `0x3600…0000`. Blocks run ~0.51s (831,287 blocks from Midnight's
deployment to Tenor's).

Every deploy block below was bracketed with `eth_getCode` on
`rpc.mainnet.arc.io` and re-checked on a second node; Midnight's, the Mempool's
and the BlueBuyCallback factory's also match the blocks morpho-org/sdks
registers (sdks#1092). Midnight and Mempool runtime bytecode is byte-for-byte
Base's, and the two allowlist gate factories match Base's and Ethereum's.

The never-copy rule above applies here too. **Morpho Blue is not `0xBBBB…`** on
Arc, and nothing below may be read off another chain's table.

| Contract | Address | Deploy block |
| --- | --- | --- |
| Midnight | `0x208786922BE56fDE2D1Fa60e6b9eC5D723e8d7b0` | 20320779 |
| Mempool (Log) | `0x26bded5Fb01373CE875dEa14E52799D04C839C1A` | 20320779 |
| Morpho Blue | `0x34CD04070dD72b14E241112F6d83812Df5Af7fCD` | 1208685 |
| MigrationRatifier | `0x6487784572adeE9e8c3d9f592acdA1cbe6333F48` | 21152066 |
| DelayedLiquidationGateFactory | `0x5140fEf83821a2cb7cfBE1FF8CFB29A11F6aD2C0` | 21152044 |
| VaultV2AllowlistGateFactory | `0xEB23d45ac20F3EaA20dB8C6Af5F7EF33EBE1E9e9` | 21152194 |
| MidnightAllowlistGateFactory | `0xFaBA21ddc6C79699499635a32F673C2cfBFDc5c5` | 21152213 |
| Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | genesis predeploy |

No OracleWithValidation factory (`validationCheckFactory: null`, as on
Ethereum) and no directly-deployed gates. Two open-term Tenor vaults landed
three hours after the core contracts (see [Tenor vaults](#tenor-vaults-erc-4626)),
so Arc has a `VaultSnapshot` filter from their first aligned tick.

Labels only (`known-contracts.ts`, not indexed), each confirmed to carry code:

| Contract | Address |
| --- | --- |
| SetterRatifier | `0x3915156EBFC246Ee9aC3236af561546B7D9D924c` |
| EcrecoverRatifier | `0xA3B53aDe6668b6ceC03a9E56a993B47034F43715` |
| RateRatifierV1 | `0x9b11e691E79d6978c78BF8de8B898CDeb89bEADf` (added 2026-10-06) |
| PriceRatifierV1 | `0x55163660Ff35955712339d465dc6D9D583dA8ac2` (added 2026-10-06) |
| BlueFallbackRolling | `0x405711971ef6Be74114778CEfba9a2F2e4648f3d` (added 2026-10-06; indexed since 2026-10-21 — see BlueFallbackRolling below) |
| WhitelistEnterGateFactory | `0x52Ae81CEeb7fdC2A142F896923FF2d94502a10f3` (added 2026-10-06) |
| EcrecoverAuthorizer | `0xe1dccAdc10c35AE9e2207059B233fE5c634CF20f` |
| Bundler3 | `0x855dbe13C409Df75CAF6a985Cf6993a4D0319Feb` |
| MidnightBundlesV1 (morpho-org/sdks) | `0x3609525024c88f794CBE09e4832810E2bd737beC` |
| TenorAdapter | `0xC223069271970009CaEc5c757223959d85F65062` |
| MidnightTakeAndRepay | `0x9F993355DF8Eeb02f3a15b281580A4F74d40C922` (block 22056889, 2026-09-21, by `0x8f4452bB…41d3` — the same deployer at the same nonce as on Ethereum, so the address is legitimately shared: bracketed with `eth_getCode` and verified to be this contract on both chains, with Arc's Midnight as its immutable; it holds no code on Base, whose deploy is `0x51F3660e…`) |
| AuthorizationAdapter | `0x0AC96E91F9183633c2eE225bb3398D845846DbC5` |
| MidnightVaultExecutor | `0x961d26E63128E493C8ed78a91cc65df5d391966b` |
| CallbackFeeAdjuster | `0xD7018a8D23c3d8B992b2083673E79c077d8dD46A` |
| LastFridayOfMonthCadence | `0x22432535057e1E241cc18D79265D4E9b3F85641B` |
| MarketMakingPolicy | `0xF176f1a9F20AB3337B9179fDB938032632a9ACeE` |
| PausableMarketMakingPolicy | `0xC8833ea7555F4090310f0e2131f3fb71023F5905` |
| PausableRatePolicy | `0x1B5c3204c709e9b989C08De5Cd1e09A113f194D0`, `0xCF5d8930465184b8047c5993Df342dc27c5677b4` |
| BorrowBlueToMidnightClamp | `0x185c3cd6810e19936D5e11441108a718839F9f87` |
| BorrowMidnightRenewalCallback | `0x59B8ad50AA5A9E54e5e68873AAcfd8274b8DE342` |
| LendMidnightRenewalCallback | `0x498Ee05ee229f3FAcc1777Ad10992Ee065301f9f` |
| BorrowMidnightToBlueCallback | `0x403b98EA9F0A0E8A989c9336f4579DEF80C91C0f` |
| BorrowBlueToMidnightCallback | `0xF22B41a36D0F125B0fA01CDa6d2295266CD1BdA1` |
| LendMidnightToVaultCallback | `0xDfdD0c73Fb9127C906E1861AaD03D972399A7e74` |
| LendVaultToMidnightCallback | `0x09A22Fa7dcc288241B223A29f3d826DF2eFd63B3` |
| MidnightSupplyCollateralCallback | `0x330E5DF9116BA914852D6592EE8CD77465986C4F` |
| MidnightSupplyVaultSharesCallback | `0x357690B2DDE260c0c2F5d86826d1BF08D1525abB` |
| MidnightWithdrawVaultSharesCallback | `0x009452c50E9F6f6e9B8B3aBD32e04c03E0359288` |

Arc opened with **no markets and no takes** (the 2026-09-16 deployment record's
`markets` is empty). By 2026-09-24 it carried two whitelisted Tenor markets,
cirBTC/USDC and WETH/USDC (five Midnight maturities between them, each also
accepting USDC itself as collateral at 98% LLTV), and 16 takes — every one
through TenorAdapter, so its routing volume attributes as Tenor's.

### Tempo (chain id 4217)

Added **2026-10-05**, from Morpho's registry of the same date. Stripe and
Paradigm's stablecoin-payments L1 (mainnet 2026-03-18), ~0.56s blocks
(measured: 100,000 blocks over 55,695s). **No gas token**: fees are quoted in
attodollars (1e-18 USD) per gas and paid in a 6-decimal TIP-20 stablecoin, so a
receipt's `gasUsed × effectiveGasPrice` is an 18-decimal USD figure
(`nativeSymbol: "USD"`), and `eth_getBalance` answers a constant placeholder.

**Morpho only.** Tenor has not deployed here: its deployer
(`0xFF706EaeEf1b13D7b5231c236C25E4F815522bDe`) has nonce 0 on this chain and
the Tenor API's `chains` lists base, ethereum and arc alone. So no
MigrationRatifier, no factories, no gates, no vaults — `CHAINS.tempo` carries
none of the `tenor*` fields and the chain appears in none of the Tenor entries
of `ponder.config.ts`. Morpho's own suite is smaller here than elsewhere too:
no SetterRatifier and no EcrecoverRatifier.

Deploy blocks bracketed with `eth_getCode` on `rpc.tempo.xyz` (serves archive
state; caps `eth_getLogs` at 100k blocks) and each matching the block of the
contract's first log — Midnight's `Constructor` event is in 42413205. Midnight
and Mempool runtime bytecode is byte-for-byte Base's (sha256 over
`eth_getCode`); the BlueBuyCallback factory differs only in its Midnight
immutable. Multicall3 is at the canonical address with the canonical bytecode.

| Contract | Address | Deploy block |
| --- | --- | --- |
| Midnight | `0x31Ff800b0FA4AA42071d4ae9D0392e84f256D83D` | 42413205 (2026-10-03 12:33 UTC) |
| Mempool (Log) | `0x947F74a8809a45edc03B6101bD40ACdFBE97553a` | 42413247 |
| BlueBuyCallback factory | `0x0b92694fA046303dB9371FD0A5722e0f3dC2C323` | 42413255 |
| Morpho Blue | `0x10EE9AAC980A180dd4DcFc96C746d60B0EA88f97` | (not indexed; answers `DOMAIN_SEPARATOR()` and `owner()`) |

Labels only (`known-contracts.ts`), each confirmed to carry code:

| Contract | Address |
| --- | --- |
| RateRatifierV1 | `0x1eAC83708c7413a697F686bcAF7B5aa2D13eA61f` |
| PriceRatifierV1 | `0xBea71461665fe20f74d1265f3B82707407dEb6A8` |
| EcrecoverAuthorizer | `0xD05f2BdEd597053bdAfe14f75365eCA4bb2f9f3D` |
| BlueFallbackRolling | `0x9b28416022e883BCaf8DFb967e0DE345F444658c` (indexed since 2026-10-21 — see BlueFallbackRolling below) |
| WhitelistEnterGateFactory | `0xc957b9E2703cE58A1622eb6f69c56eF89c47568f` |
| Bundler3 | `0x5A3B829dcDBE1979cdD9F648Afb6D39dCF805E33` (no `routesFor` — no Tenor here, see Router attribution) |
| MidnightBundlesV1 | `0x74B67918BE193f2FB06e6F8C6519F4274F83e9af` (`MIDNIGHT()` returns this chain's Midnight; 16,513 bytes like Base's) |
| AdaptiveCurveIrm | `0x112fd4042E442C3C12C67AD23587b0afe36eB74E` (Blue's IRM; not a registry row) |

As of 2026-10-05 the chain carried Midnight's deployment config (nine
`EnableLltv`, one each of `Constructor`, `EnableLiquidationCursor`,
`SetConfigurator`) and nothing else: **no markets, no takes, no broadcasts**.
DeFiLlama prices the chain under `tempo:` (checked on pathUSD,
`0x20C0000000000000000000000000000000000000`); Morpho's app slug is `tempo`.

### Robinhood Chain (chain id 4663)

Added **2026-10-05**, from Morpho's registry of the same date. Robinhood's
Arbitrum Orbit L2 settling to Ethereum (public mainnet 2026-07-01), ~0.1s
blocks (measured: 100,000 blocks over 10,162s — the nominal 100ms). Gas in
ETH. The explorer is the Blockscout the chain's own docs name
(`robinhoodchain.blockscout.com`); Morpho's registry links the same addresses
on `robin.etherscan.io`.

**Morpho only**, as on Tempo — Tenor's deployer has nonce 0 here and the Tenor
API knows no such chain, so no `tenor*` fields and no vaults. Morpho ships its
full suite (both Merkle ratifiers included).

Deploy blocks bracketed with `eth_getCode` on dRPC's archive endpoint
(`robinhood.drpc.org` — the chain's own public RPC keeps only ~1k blocks of
state, though it takes `eth_getLogs` ranges of 10M blocks) and each matching
the block of the contract's first log — Midnight's `Constructor` event is in
65366296. Midnight and Mempool runtime bytecode is byte-for-byte Base's; the
factory differs only in its immutable. Multicall3 is canonical.

| Contract | Address | Deploy block |
| --- | --- | --- |
| Midnight | `0x6120765Ba5336150BbdDdD0Cd9108B5bFD369632` | 65366296 (2026-09-17 12:27 UTC) |
| Mempool (Log) | `0xcEF685D4796FA80F71a97e803D2c0b6719F1b4E2` | 65366296 (same block) |
| BlueBuyCallback factory | `0x53cbCd884CABA07762c72F43283D3fa72de42D4f` | 65391242 |
| Morpho Blue | `0x9D53d5E3bd5E8d4Cbfa6DB1ca238AEA02E651010` | 286 (not indexed; answers `DOMAIN_SEPARATOR()` and `owner()`) |

Labels only (`known-contracts.ts`), each confirmed to carry code:

| Contract | Address |
| --- | --- |
| SetterRatifier | `0x708d6Bf6F847202a0755bb5636bE663B174242ea` |
| EcrecoverRatifier | `0x90B800999e4ACd1bD20283BD450bBd2e06D91F7C` |
| RateRatifierV1 | `0xDd0588d7D3Aaa76a33A7d054B0b107eB3bCD3F11` |
| PriceRatifierV1 | `0xF65E0be166AAF2F8993e617189beFB452bf38DB1` |
| EcrecoverAuthorizer | `0x75FCdD113fe33a8bEd3CD3C35955DE094Bd2bdf8` |
| BlueFallbackRolling | `0x1D0A555d4fd62BbBEeA9A441aa3D8FD7D4B42878` (indexed since 2026-10-21 — see BlueFallbackRolling below) |
| WhitelistEnterGateFactory | `0x8EB889FFF78C9b7125bb58582E701774c6cD77C5` |
| Bundler3 | `0x6478e9393d4C5bB4d53ee881d1DE78786A0344a6` (no `routesFor`, as on Tempo) |
| MidnightBundlesV1 | `0x71aa985ff80AbcE3b8b443845633674Ca9f7575C` (`MIDNIGHT()` returns this chain's Midnight) |
| AdaptiveCurveIrm | `0x2BD3d5965B26B51814AC95127B2b80dD6CcC0fa1` (Blue's IRM; not a registry row) |

**The indexer's RPC for this chain must serve archive state.** The handlers
read token metadata and market params at the EVENT's block (`ensureToken`,
`ensureMarket`), and the public endpoint keeps ~1k blocks of state: in a
local backfill against it, WETH came back as `???` / 18 decimals while the
reads that happened to land on an archive node resolved — the Arc placeholder
bug all over again, from the endpoint rather than the code. Ponder also does
NOT fail over to another listed URL on a `-32000 "historical state … not
available"` answer (only on HTTP-level errors), so a mixed list does not help;
the secret must point at an archive endpoint. Tempo's public RPC serves
archive state, so the same caveat does not apply there.

**It trades.** By 2026-10-05: 32 markets (first created 2026-09-24 11:08 UTC),
all with USDG (`0x5fc5360D…`, 6 decimals) as loan token and USDe, WETH, the
tokenized stocks SPY, NVDA, TSLA and SGOV, spUSDG and USDG itself as
collateral; 9 takes (first 2026-09-24 21:31 UTC, block 71693705), 4
liquidations, 2 repays and 44 Mempool broadcasts. Five of the nine takes name
MidnightBundlesV1 as `caller`, the other four three different addresses
(EOAs or unregistered contracts — "direct"). DeFiLlama prices every one of
those tokens under `robinhood:`; Morpho's app slug is `robinhood-chain`.

The newer Morpho periphery labeled on these two chains — RateRatifierV1,
PriceRatifierV1, BlueFallbackRolling, WhitelistEnterGateFactory — is also
deployed on Base, Ethereum and Arc. Those rows were added to their tables
above on **2026-10-06**, each chain's own addresses from docs.morpho.org
(matching tenor-sdk's `contracts.ts`), every one confirmed to carry code with
the same runtime size as Robinhood's: PriceRatifierV1 5,234 bytes,
RateRatifierV1 5,819, BlueFallbackRolling 5,232, WhitelistEnterGateFactory
3,562. As of that date no broadcast on Base (the last ~300k blocks) or
Robinhood Chain (all history) named either V1 ratifier yet.

### BlueBuyCallback factory (Morpho periphery, every chain)

| Chain | Address | Deploy block |
| --- | --- | --- |
| Base | `0x7337f119Eca028bD39E0e543cEf71631D2333425` | 49544552 |
| Ethereum | `0x172d1FdC5f79bFe1ED46448f18541E591E5c93a7` | 25798272 |
| Arc | `0x9bD11e1EC7bf3520896F8e3e63d4B70f8d6d177E` | 20322111 |
| Tempo | `0x0b92694fA046303dB9371FD0A5722e0f3dC2C323` | 42413255 |
| Robinhood Chain | `0x53cbCd884CABA07762c72F43283D3fa72de42D4f` | 65391242 |

Source: `src/periphery/blue-buy-callback` in morpho-org/midnight. Unlike Tenor's
factories this is Morpho periphery, so it ships with Midnight and exists on
every chain Midnight does. The callbacks it deploys are recorded from its
`CreateBlueBuyCallback` event into `blueBuyCallback`, so they are NOT listed here.
On Tempo and Robinhood Chain the factory deployed exactly one callback in its
own deployment block (one `CreateBlueBuyCallback` log each, 2026-10-05) —
part of Morpho's setup there, indexed like any other.

Two shape facts that differ from Tenor's factories, both load-bearing:

- **The deployed child is NOT an indexed parameter.** The event is
  `CreateBlueBuyCallback(address indexed caller, address indexed owner, bytes32
  salt, address callback)` — the child is the second non-indexed word of `data`.
  Ponder's `factory({ parameter: "callback" })` still resolves it (to
  `offset32`), but the "child is topic1" reasoning in `tenor-factories.ts` does
  NOT carry over here.
- **One instance is per `(owner, salt)` — per USER, not per market.** A callback
  is market-agnostic: the Blue market it draws from is named per OFFER in that
  offer's `callbackData`, so one instance can serve several Blue markets at
  once and there is no `marketId` getter to ask.

**Factory-deployed ONLY — a deliberate scope choice, not a gap.** At least three
Base addresses answer `OWNER()`/`MIDNIGHT()`/`BLUE()` correctly and are
byte-identical to a factory child except at the `OWNER` immutable sites, yet the
factory reports `isBlueBuyCallback == false` for them
(`0x39f948ee7185bab91961fbe203ac3c2eff2abf19`,
`0x846a52f3a802210d9e891857b6b15e9b3ac7357c`,
`0x823063297d54eb73ea5d34cc07263a597e92fd03`). A fourth
(`0x65d6fc29a9129812c7752dfd1ecd84e74fd3481b`) answers the same getters with a
much smaller code size and is plausibly an earlier revision with a different ABI.

**These are intentionally NOT indexed** (decision, 2026-08-24): a row in
`blueBuyCallback` means "the factory deployed this", and that is exactly what
makes decoding an offer's `callbackData` as Blue `MarketParams` sound. Widening
membership to a bytecode or getter probe would trade a provable test for a
heuristic — and the fourth address above, same getters but a different ABI, is
precisely what such a heuristic would get wrong.

The cost is bounded and one-directional: an offer naming one of these renders
with a raw payload instead of a decoded Blue market and a bare hex instead of
the "◆ BlueBuyCallback" label (`useKnownContracts` in the web app resolves the
label from this same table), and its balance is absent from
`/tools/blue-buy-callbacks`. **Under-reporting, never mis-reporting.**
Deliberately UNLIKE the VaultV2 gates in `tenorDirectGates`, which are named by
hand because omitting them made a tool claim "none deployed yet" while a live
gate was in use — a false statement. Here the equivalent omission simply shows
less, and nothing on the page becomes untrue.

**Do NOT classify a callback by payload length.** `callbackData` is 160 bytes
for a BlueBuyCallback, but the highest-volume callback on Base carries 160 bytes
too and is an entirely different contract (it reverts on `OWNER()`). Membership
in `blueBuyCallback` is the only sound test.

### BlueFallbackRolling (Morpho periphery, every chain)

| Chain | Address | Deploy block |
| --- | --- | --- |
| Base | `0x7bfA8207B818cc0e23a7aBdCBe56572fAbCa831F` | 50830304 |
| Ethereum | `0x6910E21c20a09BB3a2026a24585aF1E63d769fe6` | 25897717 |
| Arc | `0x405711971ef6Be74114778CEfba9a2F2e4648f3d` | 20322491 |
| Tempo | `0x9b28416022e883BCaf8DFb967e0DE345F444658c` | 42413263 |
| Robinhood Chain | `0x1D0A555d4fd62BbBEeA9A441aa3D8FD7D4B42878` | 65397441 |

Source: `src/periphery/blue-fallback-rolling` in morpho-org/midnight. A
borrower opts in with `setConfig(user, midnightId, blueId, start, end,
incentiveAtStart, incentiveAtEnd, minRollableAssets, enabled)` (the user or a
Midnight-authorized delegate); while `start <= now <= end`, ANYONE may `roll`
that user's Midnight debt into the Blue market — it borrows on Blue, repays
Midnight, moves the (single activated) collateral, and pays the roller a WAD
share of the debt rolled that ramps linearly from `incentiveAtStart` to
`incentiveAtEnd` across the window. Every block bracketed 2026-10-21 with
`eth_getCode` (codeless one block before, 5,232 bytes at it) and `MIDNIGHT()`
answering that chain's own core; Robinhood's on dRPC's archive (whose
non-archive nodes answer with an error, never a false `0`).

Activity as of 2026-10-21: **Base only** — 580 `SetConfig` + 224 `Roll` logs
(first block 51659513, 2026-09-22) across five Midnight markets, all from
Coinbase Smart Wallets through EntryPoint v0.6 (the rollers too); Ethereum,
Tempo and Robinhood Chain had none (Arc's RPC refuses wide `eth_getLogs`, so it
was not counted). Two config shapes appear on chain: `start` = the moment of
writing, `end` = the market's maturity, no incentive, `minRollableAssets` =
uint256 max (whole position only) — rolled by a keeper minutes later (median 5.6 min on the busiest market);
and the six hours before maturity with the same terms — a true at-maturity
fallback. A roll never disables its config, so an open config makes the user's
NEXT borrow in that market rollable at once.

### Tenor vaults (ERC-4626)

Two vaults flank each Midnight market pair: the **collateral** vault, whose
shares borrowers post (each address below is an enabled collateral token in
Midnight markets), and the **open term** vault, holding the variable-rate
lending leg. Registered in `packages/shared/src/vaults.ts` and snapshotted
hourly by the indexer (`apps/indexer/src/vaults.ts`).

Cross-checked on **2026-08-03** against the Tenor API
(`tenorMarkets { metadata { vault openTermVault } }`) and the deployed
contracts' own `asset()` / `name()` / `symbol()` (Base, chain id 8453):

| Pair | Role | Address | Asset | Deploy block |
| --- | --- | --- | --- | --- |
| cbBTC/USDC | collateral | `0xf6a70085b7f79FA76B04EbF7A2D7D87C3c5c04BC` | USDC | 48505353 |
| WETH/USDC | collateral | `0xe690a58EF52854513462745237F6A213a0d54dF1` | USDC | 48505357 |
| cbETH/WETH | collateral | `0xFa750DD0099eAdB72d401244De73ce7B89edf90F` | WETH | 48505360 |
| cbBTC/USDC | open term | `0x3aE6e4D46A49e493Bf7a2CeC0313334791A23B1c` | USDC | 48505395 |
| WETH/USDC | open term | `0xA02565dEF276e195644044Bf01faE02769Cac3ed` | USDC | 48505399 |
| cbETH/WETH | open term | `0x29f5cD67E543db9aC652eF9327009Db8Df1FD943` | WETH | 48505403 |
| cbXRP/USDC | collateral | `0x46B5E25E46416cf728Fdf9dc400D3ce628F7FB05` | USDC | 51708071 |
| cbXRP/USDC | open term | `0xbA05969999a555631A989CF8420Ef61dd786342d` | USDC | 51714102 |
| savUSD/USDC | collateral | `0x5cC424CCd104d81190006D52e1c2EA4235863240` | USDC | 52043443 |
| savUSD/USDC | open term | `0xcEc2F717ed411E0d982697F87D76D9C60B29A5F5` | USDC | 52043485 |

The cbXRP/USDC pair (last two rows) landed on 2026-09-23 and 2026-09-24 and
was verified the same way on 2026-09-28; its collateral token cbXRP
(`0xcb585250f852C6c6bf90434AB21A00f02833a4af`, 6 decimals) is the market's
own `collateralTokens` row, read back by `symbol()` + `decimals()`.

The savUSD/USDC pair (last two rows) landed on 2026-10-01, 84 seconds apart,
and was verified on 2026-10-02. **The Tenor API names only its lending vault**
(`metadata.openTermVault`); its collateral `vault` is null — and has since
become null for cbXRP/USDC too, whose collateral vault is live and registered
above — so the API's null is no longer evidence that a pair is lend-only. The
collateral vault came from the deployer's (`0xff706eae…522bde`) preceding
`setupVaults` transaction through Tenor's vault setup batcher
(`0xb1426505…9Eb09d`, Blockscout), and both were then read back from the
contracts. The pair's collateral token savUSD
(`0xa6EF20FE18CD529cA15A8a53A29D1832B1F848ff`, "Staked avUSD", 18 decimals)
is the market's own `collateralTokens` row, read back by `symbol()` +
`decimals()`; it is a different contract from Ethereum's savUSD.

The same transaction listing shows one more Base pair the registry does NOT
carry: reUSD/USDC, both roles, deployed 2026-09-03 (collateral vault
`0x46Be3439252498146A5Dc619c62d08529152c97b` at 50835837, lending vault
`0xb6Ac654874217572d482b1A55D616b1899096f56` at 50835855, each read back as
"Tenor reUSD/USDC … Vault" with USDC as `asset()`). No Base market in the
Tenor API or in the index names either share, so they stay unregistered until
one does.

Ethereum (chain id 1), deployed 2026-08-31 and verified the same way on
2026-09-01 — `asset()` / `name()` / `symbol()` on every vault, deploy blocks
bracketed with `eth_getCode`:

| Pair | Role | Address | Asset | Deploy block |
| --- | --- | --- | --- | --- |
| WETH/USDC | collateral | `0x7579a75658A7A0b7d277296dFaEecF4018746091` | USDC | 25878186 |
| USD3/USDC | collateral | `0x26C46ACb48B46cC99ccB3cf6C365BdaFa2556E80` | USDC | 25878187 |
| wsrUSD/USDC | collateral | `0x09409fa71bCfd3f433571dbeec0C0A9C19B0d30E` | USDC | 25878189 |
| reUSD/USDC | collateral | `0xe3891d8cA7E00Bc42157D9bb5797C88D9159AAC6` | USDC | 25878190 |
| strUSD/USDC | collateral | `0xef6955d886fce26D87753dA45192eA50F59dc91c` | USDC | 25878192 |
| siUSD/USDC | collateral | `0x226ecbf4755a5F81ed721E18c3fDA11f004Ea9e0` | USDC | 25878194 |
| wstETH/WETH | collateral | `0xe1Bdb88eE5DBaCE653ecD2123E8396BB5BA8adc5` | WETH | 25878195 |
| cbBTC/USDC | open term | `0x7BF5F45aE10a3782078c7A3e4B3D29f7C41098c5` | USDC | 25878203 |
| WBTC/USDC | open term | `0x1ccc6CbfECFABF4360352c03E4A16b9445B99681` | USDC | 25878204 |
| WETH/USDC | open term | `0x5ae4b79fb6364Ffc941b72B22e0D02a1a8A3fe21` | USDC | 25878205 |
| USD3/USDC | open term | `0x7cd63e9159D2F6cB7be2FE2A67b67812eb23C7Ae` | USDC | 25878207 |
| wsrUSD/USDC | open term | `0x5C6E4268319a1d3790322F71C217762533811b9A` | USDC | 25878208 |
| reUSD/USDC | open term | `0xb03348Ff405F244C80648D36A12Cd5D3C5ED2FCe` | USDC | 25878209 |
| strUSD/USDC | open term | `0x12a3b3fa107C4847BF214C29e6573Cb8d2E7A879` | USDC | 25878210 |
| siUSD/USDC | open term | `0x58E7224b989b59Ee3d0Fcd8066c93b427234E6e7` | USDC | 25878211 |
| wstETH/WETH | open term | `0xc397f7837B9726777F011255b46929988A2183CD` | WETH | 25878213 |
| savUSD/USDC | collateral | `0x10FFd9ca0CBF71fFf388bc9d6e032BA0D4fCdE8d` | USDC | 26055952 |
| savETH/WETH | collateral | `0x4F65De1753F4A6e2942A5e3fb0fF295b4cC94F91` | WETH | 26055953 |
| savUSD/USDC | open term | `0xF305C7799a8474800a4A74f598CEBc0dECfF169E` | USDC | 26055958 |
| savETH/WETH | open term | `0x598Ef2FfF6137b076103Fe535927B41049746D34` | WETH | 26055959 |
| wemUSD/USDC | collateral | `0x8E44B1cbfAD6b63F27A074Ff50dF30eDbAa8B934` | USDC | 26193061 |
| wemUSD/USDC | open term | `0x784650Ba36Fb0Fca780B2beba81245E7357A17F1` | USDC | 26193113 |

cbBTC/USDC and WBTC/USDC are **lend-only** here — no collateral vault was
deployed for either, which the registry allows.

The savUSD/USDC and savETH/WETH pairs (rows 17–20) were deployed together
on 2026-09-25 and verified the same way on 2026-09-28. Their collaterals,
savUSD (`0xb8D89678E75a973E74698c976716308abB8a46A4`) and savETH
(`0xDA06eE2dACF9245Aa80072a4407deBDea0D7e341`), both 18 decimals, are each
market's own `collateralTokens` row, read back by `symbol()` + `decimals()`.

The wemUSD/USDC pair (last two rows) was deployed on 2026-10-14, ten minutes
apart, by Tenor's deployer (`0xff706eae…522bde`) through its setup batcher,
and verified on 2026-10-15. The lending vault came from the user; the
collateral vault is the same deployer's vault-creation log (topic0
`0x612d665a…`) 52 blocks earlier. Both read back as "Tenor wemUSD/USDC …
Vault" with USDC as `asset()`, deploy blocks bracketed with `eth_getCode` on
two providers. The collateral, wemUSD
(`0xefE3c772C7020F14d5f6b9d7436f1b8Dfa8fB5AF`, 18 decimals), is the pair's
one indexed market's (created 2026-10-12) own `collateralConfig` row, read
back by `symbol()` + `decimals()`; that market takes wemUSD directly, so no
market names the collateral vault's shares yet.

Arc (chain id 5042), deployed 2026-09-16 three hours after Tenor's core
contracts there, verified on 2026-09-24: addresses from the Tenor API's
`tenorMarkets { metadata { openTermVault } }` for Arc's two whitelisted
markets, then `asset()` / `name()` / `symbol()` read back from the contracts
and each deploy block bracketed with `eth_getCode`:

| Pair | Role | Address | Asset | Deploy block |
| --- | --- | --- | --- | --- |
| cirBTC/USDC | open term | `0x6788802A7437A45792384f21c52D75a34da04FB8` | USDC | 21174522 |
| WETH/USDC | open term | `0x26C0a3012bF8256aE73A4EEd68cE2D202886b4eE` | USDC | 21174541 |
| USDC/MXNB | collateral | `0xd68c6B17FEE2D1442d1063613845cFD52af3E1CA` | MXNB | 25506891 |
| USDC/MXNB | open term | `0xa1f2AE057dD498c13559d4D85a910069432020f6` | MXNB | 25506985 |
| USDC/EURC | collateral | `0x13cb74900467dFcDC71d4b380f34E7561fC41032` | EURC | 26095418 |
| USDC/EURC | open term | `0x7d5CD97E8C3f7Ed0b9e878C142Ed74B90B7639DB` | EURC | 26095496 |

The first two pairs are **lend-only** (the API's collateral `vault` is null for each):
Arc's markets take cirBTC and WETH as collateral directly. The pair's
collateral addresses in `vaults.ts` are those markets' own `collateralConfig`
rows, checked against the deployed ERC-20 by `symbol()` + `decimals()`. USDC
here is the chain's gas token behind its ERC-20 face at `0x3600…0000`.

The FX pairs (last four rows, user direction, 2026-10-15) flip that: USDC is
the COLLATERAL, lent against EURC (`0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1`)
and MXNB (`0xF197FFC28c23E0309B5559e7a166f2c6164C80aA`), both 6 decimals and
each pair's `asset()`. USDC/MXNB was deployed 2026-10-12, USDC/EURC on
2026-10-15, each by Tenor's deployer (`0xff706eae…522bde`) through its setup
batcher, collateral vault first. The lending vaults came from the user; the
collateral vaults are the same deployer's vault-creation logs (topic0
`0x612d665a…`) a few blocks earlier. All four read back as "Tenor USDC/… …
Vault" with the right `asset()`, deploy blocks bracketed with `eth_getCode`
on two providers on 2026-10-15. No Arc market names either pair yet, so the
collateral side is the vault's own name over Arc's one USDC. USDC/EURC is the
pair's **second** deployment: the first, on 2026-10-12
(`0xfccaCD30AAB8104bd2667EaC5b41589fa33b150a` collateral at 25506871,
`0x01Fc69C6497D84c0eBE7A1b22dabC1E4601ce868` lending at 25506964), was
superseded before any market used it and is not registered.

**The pair's collateral token cannot be read from the vault.** A collateral
vault's `asset()` is the LOAN token, and none of these has been allocated into
a Morpho market yet (their oracles price at par with zero feeds), so nothing on
chain links a vault to its collateral. The twelve Ethereum token addresses in
`vaults.ts` come from Tenor's own token registry and were then verified against
the deployed ERC-20 by `symbol()` + `decimals()`. Two needed care: `reUSD` is
**Re Protocol's** deposit token `0x5086bf35…` and not Resupply's same-symbol
token, pinned by Tenor's `reUSD/USD exchange rate` feed row; `strUSD`
(`0x280839980a7eD0D7717F64125fE241012E5F5815`, "Staked trUSD") is in neither
registry and came from the protocol team.

Because a pair and role no longer identify a vault across chains (WETH/USDC and
wstETH/WETH exist on both), `vaultLabel` in the web app is **chain-qualified** —
"WETH/USDC collateral · Base". `seriesKey` was already chain-keyed.

A new market pair means a new registry row (with its deploy block) — and, like
any schema/handler edit, a full re-index on the next deploy.

### Router attribution (activity `via`)

Every `activity` row stores `via`: the event's `caller` arg (the msg.sender
into Midnight) when the event has one, else the transaction's `to`. The web
tags rows Morpho / Tenor / Coinbase / direct via `routerProtocolFor()` in
`known-contracts.ts` — the transaction's by-hash attribution first, then the
acting account's tag (`accountTag`, below), then the address through
`routerProtocol()`:

- **Morpho** — `MidnightBundlesV1`, users call it directly — a different
  address on each chain (`0x091183…` on Base, `0x7c00dbb2…` on Ethereum).
  Also its successor `MidnightBundlesV2` (`0x0bf30b53…`, Base only so far;
  registered 2026-10-13 with three takes already indexed through it, every
  one "direct" until then). A Coinbase wallet reaching either through the
  EntryPoint is still tagged Coinbase, since the tag reads the transaction
  target, not the router.
- **Tenor** — `TenorAdapter` (both deploys) and the other Tenor infra; Tenor
  flows enter via `Bundler3.multicall`, so the Midnight caller is the adapter.
  Also `MidnightTakeAndRepay` (registered 2026-09-25, user direction): it is
  the taker of its own `take`, so Midnight records the contract as `caller` of
  both the Take and the Repay it emits — the stored `via` IS its address, and
  a registry row was all it took (six Base and five Ethereum transactions,
  every one "direct" until then, flipped without a re-index). It is
  permissionless like the resolver below, but Tenor-deployed and Tenor's own
  flow, so it gets an address row rather than a hash list.
  Likewise `0xf304Fd30…2515` on Base (registered 2026-10-28, user direction):
  it withdraws its own Midnight credit, driven through Bundler3, so Midnight
  records it as the Withdraw's `caller` — one row moves those withdraws from
  "direct" to Tenor (one in production when registered, block 53213393).
  Bundler3 itself carries `routesFor: "tenor"`: it is Morpho-deployed shared
  infra, but every Midnight/Mempool call entering through it is a Tenor-app
  multicall (e.g. offer broadcasts ride a Bundler3 multicall next to
  AuthorizationAdapter calls; Morpho's MidnightBundlesV1 has no broadcast
  entry point). On Base, Ethereum and Arc, that is — on Tempo and Robinhood
  Chain there is no Tenor, so their Bundler3 rows carry no `routesFor` and
  fall through to `protocol: "morpho"`: the only app that can multicall
  through them is Morpho's.
- **By transaction hash** — `ROUTED_TRANSACTIONS` in `known-contracts.ts`,
  consulted before the address (`routerProtocolFor` for a row,
  `routedTransactions` for the SQL side of the /activity filter and
  /tools/routers): Tenor's one-off uses of Morpho's permissionless
  `CrossedBooksResolver` — four transactions from `0x5d183c7e…1c5b`
  (`resolve` plus a profit sweep): three Multicall3 `aggregate3` calls
  (blocks 50579573–50667081; user direction, 2026-08-31) and a fourth through
  executor `0x6bfd8137…20c4` (block 50786842, sender verified on-chain; user
  direction, 2026-09-02). Midnight records the resolver as `caller`,
  Morpho's own bot drives the same contract directly, and a take row keeps no
  transaction sender — so the resolver is deliberately NOT a registry row (an
  address would hand Morpho's resolves to Tenor) and the transactions are
  named individually. A one-off: recurring flow gets a router address.
- **Coinbase** — by ACCOUNT, not by contract: `accountTag` on every Midnight
  event row (`coinbase`), written by the indexer. Ladder
  (`routerProtocolFor`, the web's `routeSql`/`routerCondition`): hash, then a
  Tenor router, then the account tag, then Morpho's router — a tagged wallet
  using the Tenor app is Tenor flow there (user direction, 2026-10-12).
  Coinbase's integration puts each borrower behind an ERC-4337 smart wallet:
  the transaction goes bundler → EntryPoint v0.6 (`0x5FF137D4…2789`, same
  address on every chain) → wallet → `MidnightBundlesV1` → Midnight, so the
  log's `caller` is Morpho's bundler and reads exactly like a Morpho-app take
  (production, 2026-10-09: all ~2,000 EntryPoint-targeted Base transactions
  with a take went through MidnightBundlesV1; 533 taker wallets). The
  transaction TARGET is the tell, so the indexer decides at index time from
  `event.transaction.to` (`resolveAccountTag` in `src/lib/helpers.ts`, rule in
  `packages/shared/src/account-tags.ts`): only the TAKER of a take in a
  transaction sent to the EntryPoint is tagged, in `account_tag` (one row per
  chain + account: entry, first block/tx, take count), and from then on EVERY
  action of that account carries the tag — a Coinbase borrower's repay through
  the bundler is Coinbase flow (user direction, 2026-10-09). Nothing else
  earns it (user direction, 2026-10-12): inside an EntryPoint transaction a
  collateral supply can be the MAKER's, run by the maker's own offer callback
  while the wallet takes its offer — that is how a Tenor maker was once tagged
  Coinbase. The wallet's own collateral supply, which precedes its first take
  in the same transaction, is back-filled when the take tags it (raw SQL
  update scoped to that transaction and account). Any other 4337 wallet taking
  through the v0.6 EntryPoint earns the same tag: on Midnight that flow IS the
  integration. A second EntryPoint is a row in `ENTRY_POINTS` — a handler
  change, so a full re-index.
- **direct** — everything else: the Midnight/Mempool contracts themselves,
  EOAs, Safes, custom bot contracts.

Because the raw address is stored, a new router only needs a
`known-contracts.ts` entry — no re-index. The account tag is the exception:
it is a fact about the transaction, which no event row kept, so it had to be
resolved at index time (schema change, 2026-10-09).

## Event catalog (event → handler → tables)

All 25 Midnight events + the Mempool event are handled
(`apps/indexer/src/midnight.ts`, `apps/indexer/src/mempool.ts`). Every event
also writes a generic `activity` row; config-only events write `configEvent`.

Morpho's BlueBuyCallback factory is handled in
`apps/indexer/src/blue-buy-callback.ts` and writes `blueBuyCallback`. The
callbacks it DEPLOYS are not watched: they emit two events (`Skim`,
`SetAuthorizationWithSig`) and neither fires on a fill, so a per-child filter
would buy nothing. A callback's SIZE is a Morpho Blue supply position, READ
into `blueBuyCallbackPosition` (one row per callback × Blue market an offer
names): when `Mempool:Data` first names a fillable pair, when a `Take`'s
`payer` is a BlueBuyCallback (Midnight pays a fill from the buyer's callback),
and on the hourly `BlueBuyCallbackSnapshot` block tick. Funding is a
permissionless Blue supply on the callback's behalf and accrual moves the
value; neither emits anything watched here, and Ponder cannot filter Blue's own
`Supply`/`Withdraw` logs to factory children (arg filters are static).

Morpho's BlueFallbackRolling is handled in `apps/indexer/src/blue-rolling.ts`
and writes ONLY `blueRollingAction` (one row per `SetConfig` or `Roll`, a
`kind` discriminator). A roll's Midnight side needs nothing extra — its
`Repay` and `WithdrawCollateral` are Midnight's own events, `caller` = this
contract. The web replays a market's (or a user's) `setConfig` rows into each
position's live configs (`apps/web/src/lib/blue-rolling.ts`, the latest write
per `(user, configId)`, enabled with an unclosed window) and folds them into
the Renewal column as a second road to Blue (`withBlueFallback`,
`apps/web/src/lib/renewal.ts`), so no "current config" table is indexed.

The Tenor MigrationRatifier's 5 events are handled separately
(`apps/indexer/src/ratifier.ts`) and write ONLY `ratifierAction` — they are
deliberately NOT in `activity`, which is the Midnight/Mempool feed that
`/activity` and `/tx` read. The web merges a user's own `setParams` /
`clearParams` rows into their `/address` Recent activity client-side
(`apps/web/src/lib/renewal.ts`, with `eventLabel` entries in
`apps/web/src/lib/activity.ts`) and replays the same rows into each position's
CURRENT renewal policy — the latest write per `(user, callback, source,
target)` — so no "current params" table is indexed.

| Event | Typed tables written | State updated |
| --- | --- | --- |
| `MarketCreated` | `market`, `collateralConfig` | — |
| `Take` | `take` (incl. per-side splits), `offerFill`, `groupConsumption` | positions, market totals, `tokenStat`, `blueBuyCallbackPosition` (when `payer` is a BlueBuyCallback) |
| `UpdatePosition` | — | position (lazy lossFactor application) |
| `Repay` | `repay` | position debt, market, `tokenStat` |
| `Withdraw` | `withdraw` | position credit, market withdrawable, `tokenStat` |
| `SupplyCollateral` / `WithdrawCollateral` | `collateralEvent` (`kind` discriminator) | `positionCollateral`, `tokenStat` |
| `Liquidate` | `liquidation` | position, market (lossFactor on bad debt), collateral, `tokenStat` |
| `SetConsumed` | — | `groupConsumption` (absolute, monotone) |
| `FlashLoan`, `ClaimContinuousFee`, fee/tick-spacing setters | — (`configEvent` where config) | market / `feeDefault` |
| 9 config-only events (auth, LLTV enable, …) | `configEvent` | — |
| `Mempool:Data` | `mempoolMessage`, `mempoolOffer` (incl. `callback`/`callbackData`), `lendOfferGroup` (each consumption group's possibly-live lend offers), `announcedMarket` (first sight of each market named), `blueBuyCallbackPosition` (first sight of each fillable callback × Blue market pair) | — |
| `MigrationRatifier:ParamsSet` / `ParamsCleared` | `ratifierAction` (`kind` discriminator; the params columns are the set payload) | — |
| `MigrationRatifier:FeeConfigSet` | `ratifierAction` (`feeRate`/`feeRecipient`; `feeTenorMarketId` = bytes32(0) is the action-level default) | — |
| `MigrationRatifier:OwnershipTransferStarted` / `OwnershipTransferred` | `ratifierAction` (`previousOwner`/`newOwner`) | — |
| The 4 factories' `*Deployed` events | `tenorDeployment` (the contract + its configuration) AND `tenorContractAction` (`kind: "deployed"`) | — |
| `BlueBuyCallbackFactory:CreateBlueBuyCallback` | `blueBuyCallback` (address, owner, salt) | `blueBuyCallbackPosition` (pairs from offers broadcast before the deployment) |
| `BlueFallbackRolling:SetConfig` | `blueRollingAction` (`kind: "setConfig"`; window, incentives, `minRollableAssets`, `enabled`; `configId` re-hashed from the parameters) | — |
| `BlueFallbackRolling:Roll` | `blueRollingAction` (`kind: "roll"`; `configId`, `debtAssets`, `collateralAssets`, `incentiveAssets`) | — |
| `BlueBuyCallbackSnapshot:block` (hourly) | — | `blueBuyCallbackPosition` (every pair re-read from Blue) |
| `OracleWithValidation:ValidationCheckPaused` / `Unpaused` | `tenorContractAction` | — |
| `MidnightAllowlistGate:MidnightAllowlistUpdated`, `VaultV2AllowlistGate:VaultV2AllowlistUpdated` | `tenorContractAction` (`user` + `permissions` json) | — |
| `DelayedLiquidationGate:GracePeriodStarted` | `tenorContractAction` (`user` = borrower, `marketId`) | — |
| `OwnershipTransferStarted` / `OwnershipTransferred` on the three ownable children | `tenorContractAction` | — |

## Semantics quick-facts (details in `architecture.md`)

- 1 unit = 1 loan-token smallest-unit at settlement; units↔assets 1:1
  everywhere except `take` (tick price = the discount = the interest).
- Buyer = lender, seller = borrower.
- `lossFactor` scale is `uint128.max`, NOT WAD.
- MigrationRatifier params: `limitRatePerSecond` is WAD **per second**, the
  same scale as a market's `continuousFee` (`formatContinuousFeeApr` reads
  both); the three durations are plain seconds. `setParams`/`clearParams` take
  an `onBehalf`, so the event's `user` is whose policy moved and the tx sender
  may be a delegate they authorized on Midnight. That sender column is called
  `sender`, NOT `caller`: everywhere else `caller` is an event's own arg — the
  msg.sender into Midnight, i.e. the router — and the ratifier's events have no
  such arg, so a bundler or keeper CONTRACT in the middle is the party the
  contract actually authorized and no log can reveal it. Its two market ids are TENOR
  market ids (maturity excluded), so one row governs every maturity — and
  source == target is a renewal in place.
- BlueFallbackRolling configs have no slot of their own: `configId =
  keccak256(abi.encode(midnightId, blueId, start, end, incentiveAtStart,
  incentiveAtEnd, minRollableAssets))` and `isConfig[user][configId]` is the
  only state. `SetConfig` carries the parameters but not the id, `Roll` the id
  but not the parameters — the indexer re-hashes the former
  (`blueRollingConfigId`, test-pinned to a real Base pair) so the two join.
  Neither the user nor `enabled` is in the preimage. The window columns are
  `windowStart`/`windowEnd` (`end` is an SQL keyword).
- `consumed` is keyed `(maker, group)` GLOBALLY — one group can span markets.
- `offerHash = keccak256(abi.encode(offer))` — NOT the EIP-712 hash ratifiers
  sign. `marketId` = CREATE2/SSTORE2 derivation (`computeMarketId`).
- A RateRatifierV1 maker signs a per-second rate, not the tick: the posted
  tick is filler the TAKER replaces at take time. Since the tick is part of
  `offerHash`, a rate offer's takes carry a hash its broadcast never had, so
  `offerFill` never lines up with the broadcast offer (consumption, keyed
  `(maker, group)`, still does). The mempool handler stores such an offer's
  `priceWad` as null and its `signedRate` / `allowedTaker` from
  `ratifierData` (`offer-decoder/ratifier-data.ts`).
- The Offer struct includes `callback` + `callbackData` (settlement liquidity
  sourcing) and `receiverIfMakerIsSeller` — see
  `packages/shared/src/offer-decoder/offer-abi.ts`, which MUST stay
  byte-identical to `Midnight.take`'s offer input (test-enforced).

## Updating ABIs

ABIs live in `packages/shared/src/abis/` (generated; 25 events for Midnight).
On a protocol upgrade: refresh from the verified deployment (via the source
repos above), re-run the offer-abi byte-identity test, and follow
`docs/decoder.md` + the `update-decoder` skill for wire-format changes.
Tooling note: if the GitHub MCP server reports bad credentials, use the `gh`
CLI instead.
