Skip to content

superseded Superseded. Another plan owns this decision now.

What is still current: The repo-tab integration and the market Type column it introduced are still on the lending page. The hardcoded top-2 selection and the discovery query here are not canonical; the registry owns membership.

Landed: migrations 035 and 041 (v0.1.0 to v0.3.0); membership governance moved to morpho-markets-expansion-plan.md on 2026-07-08

Header updated 2026-09-14. The body below is frozen history. All plans.

Plan: Morpho Blue isolated markets in the Repo markets table ​

SUPERSEDED for membership governance (2026-07-08). Market membership, discovery, and the admission rule are now owned by morpho-markets-expansion-plan.md and documented in processes.md A.7 (morpho_market_registry + scripts/morpho-discovery.ts). This doc remains the record of the original repo-tab integration + the Type column; the hardcoded top-2 selection and the discovery query here are no longer canonical.

Add the top 2 Morpho Blue markets (by USD borrowed) whose loan token is USDC or USDT to the existing Repo markets table, introduce a market Type column (pooled / pooled liquidity, isolated borrowing / isolated), and lay the foundation for live (event-indexed) Morpho state.

Grounded in the repo's existing patterns: the MARKETS matrix in src/lib/data/money-market-rates.ts, the per-protocol *_reserve_apy tables + getTrailingApy (src/lib/data/apy.ts), the {slug, refresh} registry in scripts/refresh-assets.ts, and the existing creddit-indexer (rindexer) repo.


0. RPC-cost reality check (the dev's assumption is correct, one caveat) ​

A single Morpho Blue market emits Supply / Withdraw / Borrow / Repay / SupplyCollateral / WithdrawCollateral / Liquidate / AccrueInterest at tens-to-low-hundreds of events per day even for a $100M book — two to three orders of magnitude below a DEX pool's swap stream. Forward indexing is near-free in Alchemy compute units.

Caveat: Morpho is a singleton (0xBBBB…FFCb); every market's events come from one contract. An unscoped subscription would ingest all several-hundred markets. Every Morpho event carries the market id as indexed topic1, so scope getLogs to the two chosen ids (or index the singleton and partition in SQL — volume is still small). The only real spend is the one-time backfill from market creation (~2024), a bounded getLogs range scan comparable to what scripts/refreshers/lending-positions.ts already does for Aave debt-token logs.

Verdict: proceed; cost is not a concern.


1. Phasing — ship the feature without blocking on the indexer ​

Same end-state data model, two phases that coexist with no rework:

  • Phase 1 (ships the 2 markets): a 6h snapshot refresher exactly like scripts/refreshers/aave-v3.ts / sparklend.ts. ~6 eth_calls per run.
  • Phase 2 (live state): index the events in creddit-indexer, derive live market state, and switch the state reads (borrowed / supplied / liquidity, later per-borrower health) from snapshot to indexed.

Trailing APY stays snapshot-based in both phases (an index-ratio over time needs periodic share-rate observations regardless), so the phases do not conflict.


2. Market selection (one-time, on the server) ​

Discover via the Morpho API (egress is open on the server), then hardcode — the chain stays canonical, the API is discovery-only:

bash
curl -s -X POST https://blue-api.morpho.org/graphql -H 'Content-Type: application/json' \
 --data '{"query":"{markets(first:60,orderBy:BorrowAssetsUsd,orderDirection:Desc,where:{chainId_in:[1],loanAssetAddress_in:[\"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\"0xdac17f958d2ee523a2206206994597c13d831ec7\"]}){items{uniqueKey lltv loanAsset{symbol} collateralAsset{symbol} oracleAddress irmAddress state{borrowAssetsUsd supplyAssetsUsd}}}}"}' | jq

Take the top 2 by borrowAssetsUsd (expect PT-sUSDe/USDC, wstETH/USDC or cbBTC/USDC class). Create a MORPHO_MARKETS registry constant holding each market's full MarketParams {loanToken, collateralToken, oracle, irm, lltv}and its marketId.

Test: assert keccak256(abi.encode(params)) === marketId (viem encodeAbiParameters + keccak256) so a typo'd param can't silently point at the wrong market.


3. Contract surface (Phase-1 reads) ​

  • Singleton Morpho = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; AdaptiveCurve IRM 0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC (verify both against the deployed registry during implementation).
  • Morpho.market(id) -> {totalSupplyAssets, totalSupplyShares, totalBorrowAssets, totalBorrowShares, lastUpdate, fee} (uint128s).
  • Supply share rate = totalSupplyAssets / totalSupplyShares — the Aave liquidityIndex analogue. Trailing APY = annualizeRatio(rate_now/rate_then, elapsed) reuses apy.ts unchanged, so Morpho rows are methodologically identical to every other row (the consistency promise now documented in docs/metrics.md).
  • Spot APY for display: borrowRate = IRM.borrowRateView(params, market) (per-second, 1e18 -> compound to annual); supplyApy = borrowApy x utilization x (1 - fee).
  • availableLiquidity = totalSupplyAssets - totalBorrowAssets; utilization = totalBorrow / totalSupply.
  • The stored totals are stale since lastUpdate. Either apply Morpho's expectedMarketBalances periphery math, or accept <= 6h staleness like the other rows. Accept it for Phase 1; document it.

4. Schema — scripts/sql/035-morpho-market-apy.sql (next free number) ​

Mirror 007-aave-v3-reserve-apy.sql, PK (snapshot_ts, market_id):

market_id text (0x-hex), loan_symbol, collateral_symbol, share_rate numeric (the index), supply_apy, supply_apy_24h, total_supplied, total_borrowed, available_liquidity, utilization, lltv, oracle_address, irm_address, block_number, updated_at.

Additive + idempotent (ADD COLUMN IF NOT EXISTS style) + role GRANTs, per house style. Run before deploy (standard ordering).


5. Phase-1 refresher — scripts/refreshers/morpho.ts ​

Clone aave-v3.ts's shape:

  1. anchor block -> multicall3([market(id), borrowRateView(...)] x 2 markets)
  2. compute share rate, spot APY, and the 24h-trailing APY via the at-or-before-24h anchor pattern from fluid-ll.ts (NOT the exact-match token-yields pattern we fixed — that one blanks on a missed cron)
  3. upsert into morpho_market_apy.

Register {slug: morphoMeta.slug, refresh: refreshMorpho} in scripts/refresh-assets.ts. Failed reads skip the write (previous snapshot stands) — house failure-containment rule. A small backfill script (blockByTimestamp walk, like backfill-aave-v3.ts) can follow if chart history is wanted.

Also write the exposure row: one slice per market into market_collateral_exposure with a new basis 'isolated_market' — collateral symbol, exposure = total borrowed, LLTV, oracle identity. Keep it deliberately thin: one collateral, its LLTV, its oracle is the whole story for an isolated market, which is exactly the "simpler than pooled" expectation. (Posted- collateral total is not on market(); defer the collateral-USD figure to Phase 2 or read it from the collateral token balance at the Morpho address scoped to the market via indexed positions.)


6. Reader — src/lib/data/money-market-rates.ts (+ apy.ts) ​

  • MoneyMarketProtocol += 'Morpho'.
  • Extend the IndexTable union in apy.ts with 'morpho_market_apy' (supply column share_rate, no borrow leg — same shape as token_yield_apy).
  • MARKETS += two entries, e.g. { id: 'morpho-wsteth-usdc', protocol: 'Morpho', asset: 'USDC', table: 'morpho_market_apy', token: <marketId> }. Keying nuance: this table keys by market_id, not loan-token address; thread that through getTrailingApy's $1 param (already an opaque string, so no signature change).
  • New marketType field on MoneyMarketRow: 'pooled' | 'pooled_liquidity_isolated_borrowing' | 'isolated'. Aave/Spark -> pooled; Fluid -> pooled_liquidity_isolated_borrowing; Morpho -> isolated. Derived from protocol in the reader; no schema needed.
  • Morpho row label carries the collateral: "wstETH -> USDC" (for an isolated market the collateral is the market's identity).

7. UI — MoneyMarketTable.tsx + UnderwrittenCapital.tsx ​

Type column (small mono chip): Pooled / Isolated / Pooled liq, isolated borrow. One InfoTooltip on the header:

  • Pooled: one shared book; all collateral cross-margined; a failure in one collateral can socialise losses to every lender.
  • Pooled liquidity, isolated borrowing: lenders share one liquidity layer, but each borrow position lives in an isolated vault with its own collateral, oracle and limits (Fluid). Lender risk spans every vault borrowing the asset; borrower risk is per-vault.
  • Isolated: a standalone market against a single collateral with immutable terms (oracle, max LTV fixed at creation); losses are contained to that market; the lender is the sole underwriter of that one collateral.

Expansion for Morpho rows: a slimmed Underwritten-capital panel — single collateral row (100% of book), LLTV, oracle identity, utilization / available, and "Supply cap: none (market-level)". Morpho Blue markets have no caps (caps live in the vault layer), which plugs straight into the max-potential-exposure column (PR #221) as the honest "uncapped" case. Skip the proximity strip and e-mode share (Phase 2 brings per-borrower data).


8. Phase 2 — indexer (creddit-indexer repo) ​

  • Add a rindexer contract entry for the Morpho singleton with the 8 events above, filtered to the two market ids via topic1 (rindexer supports indexed-topic filters; if the installed version doesn't, index unfiltered and constrain in SQL — volume is still fine).
  • Backfill from each market's CreateMarket block (one bounded historical getLogs scan; reuse the getLogsChunked span-halving pattern if done app-side).
  • Derived state: a positions table (per-borrower supplyShares / borrowShares / collateral from event deltas — Morpho events carry exact share/asset amounts), market totals reconstructable and reconciled against Morpho.market() snapshots each 6h (cheap invariant check: indexed totals vs on-chain totals, alert on drift — same honesty pattern as the Fluid tick reconciliation).
  • Payoff: live borrowed / supplied / utilization between crons, per-borrower distance-to-liquidation for the proximity strip (needs an oracle price read at evaluation time), and liquidation-event history for the panel.

9. Tests, docs, rollout ​

Tests

  • marketId <-> params keccak check.
  • share-rate APY math against a hand-computed fixture.
  • marketType mapping locked per protocol.
  • tooltip copy em-dash lint (house rule).

Docs

  • docs/metrics.md (Type column + isolated expansion; Morpho share-rate = the same index-ratio method). Was docs/money-market-rates.md + docs/methodology.md before the internal-docs rewrite (PR #282).
  • docs/data-pipeline.md (+ refreshers/morpho.ts).
  • AGENTS.md data-layer note.

Rollout order

  1. migration 035
  2. deploy (refresher live on the next 6h tick)
  3. verify two Morpho rows render with the Type column + slimmed expansion
  4. Phase 2 indexer as a separate PR in creddit-indexer, then a follow-up app PR switching state reads from snapshot to indexed.

Out of scope here

  • Curated funds table (separate track per the pooled-vs-fund classification discussion).
  • More than 2 markets (the MORPHO_MARKETS registry constant makes adding more a few-line change).

Open decision for product before the dev starts ​

Confirm the Morpho row id/label carries the collateral (morpho-wsteth-usdc, "wstETH -> USDC"). Recommended yes: for isolated markets the collateral is the market's identity, and it visually reinforces the Type distinction in the table.

Private documentation. creddit.xyz