# @1delta/margin-fetcher

Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound
V2/V3, Euler, Init, Lista DAO, and Morpho Midnight. Provides public market data
(rates, TVL, configs) and per-user position data (balances, shares, collateral)
in a unified format.

## How Morpho Blue Works

### Protocol overview

Morpho Blue is an **isolated-market** lending protocol. Unlike pooled protocols
(Aave, Compound) where all assets share a single pool, each Morpho market is a
**standalone pair** defined by five parameters:

| Parameter         | Description                                      |
| ----------------- | ------------------------------------------------ |
| `loanToken`       | The asset that can be supplied and borrowed      |
| `collateralToken` | The asset deposited as collateral                |
| `oracle`          | Price oracle for the pair                        |
| `irm`             | Interest Rate Model contract                     |
| `lltv`            | Liquidation Loan-To-Value ratio (18-decimal WAD) |

These five parameters are hashed into a `uniqueKey` (bytes32) that identifies
the market on-chain.

### Interest rate model

Morpho uses an **adaptive curve IRM** that adjusts rates based on utilization
relative to a 90% target:

```
utilization = totalBorrowAssets / totalSupplyAssets

If utilization > 90%: rateAtTarget increases over time (via exponential adjustment)
If utilization < 90%: rateAtTarget decreases over time

borrowRate = curve(rateAtTarget, utilization)
supplyRate = borrowRate * utilization * (1 - fee)
```

The curve uses a steepness factor of 4x, meaning at full utilization the rate is
4x the rate-at-target. Rates are stored as per-second WAD values and compounded
continuously.

Key constants:

- **Target utilization**: 90%
- **Curve steepness**: 4x
- **Min rate at target**: ~0.1% APY
- **Max rate at target**: ~200% APY
- **Adjustment speed**: 50x/year (exponential)

### Share-based accounting

Positions are tracked via **shares** rather than raw assets. This allows
interest to accrue without storage updates per-user:

```
assets = shares * (totalAssets + 1) / (totalShares + VIRTUAL_SHARES)
```

Where `VIRTUAL_SHARES = 1e6` prevents share inflation attacks.

### Market data representation

Each Morpho market produces **two entries** in the normalized output:

1. **Loan entry** — the borrowable asset
   - Has `borrowingEnabled: true`, `collateralActive: false`
   - Contains deposit/borrow APRs, total supply/debt, liquidity
   - Rewards (MORPHO token incentives) attached here

2. **Collateral entry** — the collateral asset
   - Has `borrowingEnabled: false`, `collateralActive: true`
   - LTV factors derived from the market's `lltv`
   - No interest rates (collateral doesn't earn yield in Morpho)

### Whitelisted vs unlisted markets

Morpho markets can be **whitelisted** (curated, visible in the Morpho UI) or
**unlisted** (removed from curation). The `includeUnlistedMorphoMarkets` flag
controls whether unlisted markets are fetched from the API. Unlisted markets are
tagged with `isListed: false` on the `MorphoMarket` params.

## Architecture

### Data fetching strategy

The fetcher uses a **hybrid approach** that routes each lender to either an API
or on-chain path:

```
getLenderPublicDataAll(chainId, lenders, ...)
├── lenderCanUseApi(lender, chainId)?
│   ├── YES → getLenderPublicDataViaApi()  →  Morpho GraphQL API
│   └── NO  → getLenderPublicData()        →  On-chain multicall via Morpho Lens
└── Promise.all([onChain, api]) → merged result
```

**Morpho Blue uses the API** on most chains. On-chain fallback is used for
chains without API support (OP Mainnet, Soneium, Hemi, Berachain, Sei).

### API path (GraphQL)

Fetches from `https://blue-api.morpho.org/graphql`:

```
fetchMorphoMarkets(chainId, includeUnlisted)
  → GraphQL query (paginated: 200/page, 2 pages for Ethereum mainnet)
  → GetMarketsResponse
  → convertMarketsToMorphoResponse()
  → { [marketId]: MorphoGeneralPublicResponse }
```

The API provides pre-computed APYs, USD values, and reward data. APY→APR
conversion is applied during normalization.

### On-chain path (Morpho Lens)

Uses a custom lens contract that returns compact binary-encoded market data:

```
buildMorphoCall(chainId)
  → multicall to MORPHO_LENS.getMarketDataCompact(morpho, marketHashes[])
  → chunks of 50 markets per call (calldata size limit)
  → raw bytes response

getMorphoMarketDataConverter()
  → decodeMarkets(bytes)           // 256-byte records → Market[]
  → MathLib.getBorrowApy(...)      // compute rates from rateAtTarget + utilization
  → MathLib.getSupplyApy(...)
  → { [marketId]: MorphoGeneralPublicResponse }
```

#### Binary encoding (256 bytes per market)

```
Offset  Size   Field
0       20     loanToken (address)
20      20     collateralToken (address)
40      20     oracle (address)
60      20     irm (address)
80      16     lltv (uint128)
96      32     price (uint256) — collateral/loan exchange rate in WAD
128     32     rateAtTarget (uint256) — per-second interest rate
160     16     totalSupplyAssets (uint128)
176     16     totalSupplyShares (uint128)
192     16     totalBorrowAssets (uint128)
208     16     totalBorrowShares (uint128)
224     16     lastUpdate (uint128) — timestamp
240     16     fee (uint128)
```

### User data path

```
buildMorphoUserCallWithLens(chainId, account, lender, marketIds)
  → MORPHO_LENS.getUserDataCompact(marketHashes[], account, morpho)
  → chunks of 100 markets per call

decodePackedMorphoUserDataset(hex)
  → [uint16 count] + [130-byte records × count]
  → BalanceInfo[] { index, supplyShares, borrowShares, supplyAssets, borrowAssets, collateral }

getMorphoUserDataConverterWithLens()
  → map balances to market IDs via index
  → separate loan positions (supply/borrow) from collateral positions
  → format to UserData with USD values
```

## File structure

```
src/lending/
├── public-data/
│   ├── fetchLenderAll.ts          # Hybrid router (API vs on-chain)
│   ├── fetchLender.ts             # On-chain multicall dispatcher
│   ├── fetchLenderExt.ts          # API dispatcher
│   └── morpho/
│       ├── fetchPublic.ts         # GraphQL query + fetch logic
│       ├── convertPublic.ts       # API response → normalized format
│       ├── publicCallBuild.ts     # Morpho Lens call builder
│       ├── getMarketsFromChain.ts # On-chain response → normalized format
│       └── utils/
│           ├── evmParser.ts       # Binary decoder (256-byte records)
│           ├── mathLib.ts         # WAD math, rate calculations
│           └── parsers.ts         # LTV parsing, number formatting
├── user-data/
│   └── morpho/
│       ├── userCallBuild.ts       # User position call builder
│       ├── decoder.ts             # User data binary decoder
│       ├── userCallParse.ts       # Balance → UserData conversion
│       ├── morphoLib.ts           # Share ↔ asset conversion
│       └── types.ts               # Parameter position enums
└── ...

src/types/lender/
└── morpho-types.ts                # GetMarketsResponse, MorphoMarket, MorphoGeneralPublicResponse
```

## Lista DAO extension

Lista is a Morpho Blue fork with additional per-market fields. Uses a 357-byte
binary record instead of 256:

| Extra field          | Type    | Description                           |
| -------------------- | ------- | ------------------------------------- |
| `minLoan`            | uint128 | Minimum loan amount                   |
| `hasWhitelist`       | bool    | Whether the market has access control |
| `loanProvider`       | address | Yield source for loan token           |
| `collateralProvider` | address | Yield source for collateral           |
| `broker`             | address | Authorized broker contract            |

User data also includes per-market whitelist flags prepended before the balance
records.

### Fixed-term broker (`LendingBroker`)

Some Lista markets register a **`LendingBroker`** — the mandatory debt-side
gateway of a Moolah (Morpho-fork) market. Collateral supply/withdraw still go
through the normal Moolah path, but **borrow and repay run through the broker**,
which overlays a richer debt model on top of the raw Moolah position. The
on-chain broker is an ERC1967 proxy (e.g. BNB `0x1Fa2…8b54` → impl
`0xb680…58bb`), verified source `LendingBroker.sol` / `BrokerMath.sol`. Fetcher
support lives in
[`lending/public-data/lista/listaBroker.ts`](src/lending/public-data/lista/listaBroker.ts);
the composer encoders live in `contracts-delegation` (`ListaBrokerLending.sol`,
`CalldataLib.encodeListaBroker*`).

**Two debt buckets per user.** A brokered market has **no flexible/variable
borrowing** at the Moolah layer — instead each user holds:

- **N fixed-term positions** — each a `{posId, principal, apr, start, end, …}`
  tranche with a _locked_ APR and a maturity. Identified by `posId`.
- **One dynamic position** — a variable-rate position, the catch-all bucket.
  Targeted on repay by the sentinel `posId == type(uint128).max`
  (`LISTA_BROKER_DYNAMIC_POS`).

`broker.getUserTotalDebt(user)` is the authoritative total (fixed + dynamic +
all interest).

#### Borrow

The composer calls `borrow(amount, termId, user, receiver)`. `user` (the
position owner) is **always the authenticated caller** and must have authorized
the composer in Moolah (`setAuthorization`) — debt can never be opened against
someone else. The broker borrows from Moolah and forwards the loan token to
`receiver`.

> **Native:** the broker _can_ unwrap WBNB→BNB, but only on its EOA-direct
> `borrow(amount)` / `borrow(amount, termId)` overloads that pay `msg.sender`.
> The `receiver`-variant the composer must use (Moolah requires
> `receiver == broker` when a broker is registered) **always sends ERC20 WBNB,
> never native**. To deliver native BNB, chain an unwrap step after the borrow.

#### Repay

`encodeListaBrokerRepay(loanToken, assets, native, broker, posId, onBehalf)` →
`repay(amount, posId, onBehalf)` (fixed) or `repay(amount, onBehalf)` (dynamic,
sentinel `posId`).

- **Interest-first, then principal.**
- **`onBehalf`** is taken straight from calldata (exactly like Morpho's repay
  `onBehalfOf`) — repaying only ever pays debt down and refunds excess to the
  composer, so **repay-on-behalf is permissionless**. Must be non-zero (broker
  reverts `ZeroAddress`); there is no caller fallback.
- **`assets == 0`** repays the composer's full balance (`balanceOf` /
  `selfbalance`); the broker refunds any excess. The clean "close it out"
  pattern: over-fund slightly, repay with `assets == 0`, take the dust.
- **Native:** both repay overloads are `payable` and wrap `msg.value` internally
  (gated on `LOAN_TOKEN == WBNB`, else `NativeNotSupported`). The composer's
  native path forwards the value and skips the ERC20 approve. Excess refunds as
  native BNB to the composer (un-swept on explicit amounts).

#### Early-repayment penalty

Repaying a fixed position's **principal before `end`** incurs a penalty
(`BrokerMath.getPenaltyForFixedPosition`):

```
penalty = ceil( ceil(repayPrincipal × aprPerSecond / RATE_SCALE) × timeLeft / 2 )
```

≈ **half the interest that principal would still accrue over the remaining
term**. It's skimmed off the principal payment and supplied to the Moolah vault
as revenue, so it is **additive** — on top of `outstanding + accruedInterest`.
Penalty is **0 once matured** (`block.timestamp > end`). The fetcher surfaces it
per loan as `earlyRepayPenalty` (computed for the _full outstanding principal_;
scale linearly for partials). Use the broker's
`previewRepayFixedLoanPosition(user, amount, posId)` for the exact
`(interest, penalty, principal)` split.

#### Maturity & refinancing

A fixed position's **interest freezes at `end`**
(`getAccruedInterestForFixedPosition` caps at `position.end`); after maturity it
accrues no further fixed interest and carries no penalty.

`refinanceMaturedFixedPositions(user, posIds)` is a **bot-only** function
(`onlyRole(BOT)`) — users and the composer **cannot** call it, so it is **not**
exposed as an SDK/composer op. It migrates matured fixed positions into the
dynamic bucket: net principal + frozen interest are normalized at the current
dynamic rate and added to the dynamic position, and the fixed position is
**deleted**. The window between maturity and the bot's call is interest-free
(the protocol absorbs it).

#### Repaying late

| When                               | Target               | Cost                                                                         |
| ---------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
| Before `end`                       | fixed `posId`        | principal + accrued interest + **early penalty**                             |
| After `end`, before bot refinances | fixed `posId`        | principal + **frozen** interest, **no penalty** — cheapest                   |
| After bot refinances               | **dynamic** sentinel | principal + interest captured at refinance + **dynamic-rate interest since** |

> **Refinance is a race.** A repay targeting a matured `posId` **reverts**
> (`position not found`) if the bot refinances first; fall back to the dynamic
> repay. The matured-but-not-yet-refinanced window (`isMatured: true`,
> `earlyRepayPenalty: '0'`) is the cheapest time to repay.

#### Data shape

Brokered markets emit **no variable debt** — the position's `debt` is `0`, the
authoritative total sits in `debtStable`, and each fixed loan is itemized in a
`terms[]` array (`ListaTermLoan`):

| Field                   | Description                                                         |
| ----------------------- | ------------------------------------------------------------------- |
| `loanId`                | `posId` — the repay target for the `LISTA_BROKER_REPAY` composer op |
| `debt`                  | outstanding (principal + accrued interest), loan-token units        |
| `aprFraction`           | locked fixed APR as a fraction                                      |
| `maturity` / `termDays` | unix maturity timestamp / term length                               |
| `accruedInterest`       | outstanding accrued interest                                        |
| `earlyRepayPenalty`     | penalty to close the loan now; `0` once matured                     |
| `isMatured`             | `now >= end`                                                        |

> **Note:** `terms[]` currently itemizes only fixed loans. The dynamic position
> (post-refinance) is included in the `debtStable` total but not yet surfaced as
> its own repayable term.

## Morpho Midnight extension

Morpho Midnight is a Base-only, **fixed-rate / fixed-maturity** lending protocol
built on an **order book of off-chain signed offers** (zero-coupon credit/debt
units) — it is **NOT a Morpho Blue fork**, so it is modeled as its own
`'midnight'` provider (`Lender.MORPHO_MIDNIGHT`, `isMidnight`), not
`isMorphoType`. There is no pool and no utilization curve: rates are derived
from each offer's `tick` → price and its time-to-maturity, knowable only via the
API. Each maturity is its own isolated market, keyed
`MORPHO_MIDNIGHT_<marketId>`.

### API-first fetching

Unlike Morpho Blue's GraphQL-then-on-chain hybrid, Midnight public data is
**API-only** — there is no on-chain public fallback in `fetchLenderAll`. The
fetcher lives in
[`lending/public-data/midnight/`](src/lending/public-data/midnight/):

- **`apiClient` / `fetchPublic`** — an `ApiBookSource` reads the order book per
  market (`GET /books/{id}`) from the Midnight API. The base URL resolves
  override → config → hosted default (`https://api.morpho.org/v0/midnight`); the
  worker can point it at a dev API via `MIDNIGHT_API_BASE` (see `data-sdk`
  `setMidnightApiBase`).
- **`math.ts`** — ported `TickLib` (`tickToApr` etc.), bit-verified against
  `@morpho-org/midnight-sdk`.
- **`convertPublic`** — normalizes the book into a `MorphoGeneralPublicResponse`
  keyed by `MORPHO_MIDNIGHT_<id>`, emitting the fixed borrow/supply APR and a
  single-term `params.market.terms[]`
  (`{termId, durationSecs, durationDays, apr}`) carrying the maturity — so the
  maturity flows through the same generic `terms[]` path as Lista's fixed loans.
  Units are in the **loan token's decimals** (not 18):
  `assets = units × price / WAD`, so `units ≈ assets`.

User positions (`{credit, debt, collateral[128], collateralBitmap}`) are read
via multicall in
[`lending/user-data/midnight/`](src/lending/user-data/midnight/), one `UserData`
per market.

> **Scope:** the fetcher and direct spot actions (supply collateral /
> borrow-via-`take` / repay / withdraw) are wired. Composer-routed leverage and
> native ETH are deferred — see
> [`calldata-sdk/src/generic/midnight/COMPOSER.md`](../calldata-sdk/src/generic/midnight/COMPOSER.md).
