# @1delta/margin-fetcher

Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound V2/V3, Euler, Init, and Lista DAO. 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.
