Skip to content

Plan: Consistent market vs redemption basis for non-numeraire assets and non-traded wrappers

Status: DRAFT (for review). Author handoff for the portfolio mark pipeline. Origin: iETHv2 ("Fluid Lite ETH") shows a ~15 ETH gap between the redemption and market views for wallet 0xaae6ae…d050, and a flat market cumulative-yield curve.


1. The bug (what the user saw)

  • iETHv2 (0xa0d3707c569ff8c87fa923d3823ec5d81c98be78, "Instadapp ETH v2", Fluid Lite ETH), booked in the ETH book, value_market resolves to null at every snapshot, while value_redemption is correct.
  • buildBookCurve.bookValueAt (pnl.ts:540) skips any leg whose mark value is null, so the market book total silently drops iETHv2 (~15 ETH), and legIntervalYield (pnl.ts:229,238) attributes 0 market yield to it → the ETH market cum-yield curve is flat.

Verified root cause (staging DB + on-chain, 2026-07-21)

  • iETHv2 has 0 rows in token_price_bars (no DEX secondary market; it is a lending receipt). WETH has 8,777 over the same year.
  • iETHv2 is in MIRROR_EXTRA with basisClass: "market" (dune.ts:148) → its market mark MUST come from a Dune bar that does not exist.
  • It is NOT in AGGREGATOR_REGISTRY (no Kyber mid) and NOT pinned → nothing rescues the null.
  • asset() = 0xae7ab965…fe84 = stETH; convertToAssets(1e18) = 1.215e18, so 1 iETHv2 = 1.215 stETH. It redeems into stETH, not WETH.
  • stETH itself has 0 mirror bars and is modeled as pure par-to-ETH identity (PAR_ACCOUNTING_ASSETS, valuation-math.ts:118) — so even a direct stETH holding would have a null market mark.

2. Current state — two registries that drifted apart

Redemption rate and market mark are resolved from two different tables:

  • PAR_ACCOUNTING_ASSETS (valuation-math.ts): redemption rate = identity (1 unit of the book numeraire per token). ~30 assets: WETH/ETH, stETH, eETH, WBTC/cbBTC/ LBTC, and ~25 stables.
  • BASIS_TOKENS (basis.ts) ∪ MIRROR_EXTRA (dune.ts): the mirror-tracked set. Each row carries numeraire, redemption (par | share_rate), and basisClass (market | pinned). A market row gets a Dune bar → a real market price.

Key fact: an asset in BOTH lists with redemption: "par" + basisClass: "market" already has a market-vs-redemption basis today — redemption = par (1), market = its Dune bar. This is true for USDC, USDT, DAI, GHO, USDe, USDtb, WBTC, cbBTC and (via share_rate) the LST wrappers wstETH, weETH, rETH, cbETH, osETH, ezETH. Confirmed live: the test wallet's USD book market (833.5741) ≠ redemption (833.5732).

Dune's USD feed is genuinely dollar-denominated (not a stablecoin cross): over the last year USDC floated 0.99893–1.00760, USDT 0.99818–1.01016, DAI 0.99880–1.00811, each on an independent path (σ ≈ 3–8 bps). So the USD-book basis = Dune bar − 1 is real; it is small in calm markets (near the feed-noise floor) and only large in a depeg (SVB: USDC → 0.88).

The gaps (assets with NO honest market mark → excluded from every market total)

  • Gap A — identity-only base assets (not mirror-tracked): stETH, eETH (the "rebasing = pure par" choice, inconsistent with the mirror-tracked wstETH beside them), the long-tail par stables (USDS, USDf, PYUSD, RLUSD, USR, eUSD, AUSD, rUSD, USD1, USD0, GUSD, TUSD, USDD, FDUSD, crvUSD, USDG, FRAX), and LBTC.
  • Gap B — non-traded wrappers (MIRROR_EXTRA market, but Dune has no bars):iETHv2 (→stETH), yoETH (→WETH), yoUSD/fLiteUSD/yvUSD (→USDC). Confirmed 0 mirror bars each; each has a token_yield_apy.share_rate (redemption is fine).

3. Target model (the agreed rule)

Exactly one par (zero-basis) unit per book; everything else carries a basis:

BookPar numeraire (basis ≡ 0)Everything else
ETHETH / WETHstETH, eETH, wstETH, all LSTs → basis vs ETH
USDsynthetic $1 (Dune's USD denomination)every stablecoin → basis vs $1
BTCsynthetic BTC (chain-0 ref)WBTC/cbBTC/LBTC → basis vs BTC
  • Base asset: redemption = par-to-numeraire (1), market = own Dune bar, basis = market − 1. NB "par" describes ONLY the redemption leg; the market leg floats from the mirror bar, which is why USDC/WBTC already carry a (small) basis today. "Give stETH the USDC treatment" = keep redemption par (1 ETH) but ADD a market bar (stETH_usd/weth_usd) so its market leg floats. (Already true for USDC/WBTC/…; extend to Gap A.)
  • Non-traded wrapper W over underlying U: value by composition
    • market(W) = qty × shareRate(W→U) × market_in_book(U)
    • redemption(W) = qty × shareRate(W→U) × redemption_in_book(U)
    • so U's basis flows through, and W contributes no basis of its own.
    • A wrapper that does trade (its own Dune bar exists: tETH, liquidETH) keeps using its own bar — no composition, so basis is never double-counted.

Consistency check: iETHv2 → 1.215 stETH → market = 1.215 × stETH_market_in_ETH, redemption = 1.215 × 1; its basis == stETH's basis == what a direct stETH holding shows. The stETH-par / wstETH-market inconsistency disappears.

4. Work (phased)

Phase 1 — ETH book (this is what fixes iETHv2)

  1. De-identity stETH + eETH: give them the USDC/WBTC treatment — mirror-tracked, numeraire: ETH, redemption: par, basisClass: market. Redemption stays par (1 ETH; Lido/eETH rebase 1:1); market comes from a Dune bar → basis.
    • Backfill their Dune bars. Verify Dune prices stETH first (wstETH is already mirrored, so the LST complex is covered). Fallback if Dune lacks raw stETH: derive stETH_market_in_ETH = wstETH_market_in_ETH ÷ wstETH_share_rate (identical ETH-basis, from data already in the mirror).
  2. Composition layer for non-traded wrappers: resolve asset() (a cached wrapper→underlying registry, seeded from on-chain), and when the wrapper has no own Dune bar, set market_in_book(W) = shareRate × market_in_book(U) and redemption_in_book(W) = shareRate × redemption_in_book(U). Guard: "non-traded" = not present in the mirror-with-bars set; a wrapper with bars uses them.
  3. The composition layer is book-agnostic, and WETH/USDC are ALREADY mirrored, so it fixes ALL five non-traded vaults (Gap B) in this phase:
    • iETHv2 → stETH (real basis, once step 1 gives stETH a bar)
    • yoETH → WETH (numeraire, basis 0 → market ≡ redemption, correct)
    • yoUSD / fLiteUSD / yvUSD → USDC (already mirrored → USDC's small basis flows through). These are VAULTS handled by composition, NOT base stables.
  4. Re-mark ETH-book history: scripts/repair/remark-snapshot-marks.ts + remark-flow-marks.ts so stored snapshots/flows get the new market marks. This is a rewrite-in-place repair (it overwrites stored value_market/basis) — run it on staging, verify, and leave the prod run as a gated manual step for after merge.
  5. Tests: composition math (both marks); stETH basis; iETHv2 end-to-end (market no longer null, curve slopes); mirror-coverage.test.ts (invariant becomes: every market-class base asset is mirror-tracked; non-traded wrappers are exempt because they compose).
  6. Docs (same PR): docs/portfolio.md, the basis/mirror docs, this plan.

Phase 2 — USD book (base-stable cleanup only)

NB the USD VAULTS (yoUSD/fLiteUSD/yvUSD) are already fixed by Phase 1's composition layer (USDC is mirrored). Phase 2 is ONLY the remaining Gap-A base STABLES:

  • Give the long-tail identity-only par stables (USDS, USDf, PYUSD, RLUSD, USR, eUSD, AUSD, rUSD, USD1, USD0, GUSD, TUSD, USDD, FDUSD, crvUSD, USDG, FRAX) the USDC treatment (mirror-tracked, par redemption, market bar). Most already have Dune bars. Only matters when one is held directly.
  • Note explicitly in the UI/docs that USD basis is ~3–8 bps (near the Dune feed noise floor) and is a depeg-capture signal, not a calm-market signal (the token_basis noise-floor caveat: a level-basis is real, but a 6h step at ~3bps is at the feed noise floor).
  • Re-mark USD-book history.

Phase 3 — BTC book

  • Add LBTC to the market-basis treatment (WBTC/cbBTC already there); compose any BTC wrappers (e.g. LBTC-based vaults) that lack their own bar. eBTC stays unrated (no honest redemption rate → skipped, unchanged).
  • Re-mark BTC-book history.

5. Risks / open questions

  • Dune stETH coverage — verify before committing the backfill; wstETH-derivation is the guaranteed fallback.
  • Double-counting basis — the traded-wrapper guard (own-bar wins over composition) must be airtight; add a test that tETH/liquidETH keep their own bar.
  • Soft-dollar redemption — USDe/GHO "par" redemption is already an assumption we carry; de-par'ing does not change it (basis stays on the market side).
  • JIT/"now" path — the aggregator-mid tier already gives LST wrappers a live market mid; ensure the composition and the mid do not both fire for the same leg.
  • USD/BTC numeraire is synthetic — par is Dune's USD denomination / the chain-0 BTC ref, not a token; acceptable and already how the USD/BTC books work.
  • Interim iETHv2 pin? — optional Phase 0 to unblock the market view before Phase 1 lands (pin = basis 0, which is what a direct stETH shows today). Some throwaway. Decision pending.

6. Sequencing rationale

ETH first: highest-signal basis (stETH/ETH is a deep, arbitraged market priced by the same-bar ratio), it is exactly what fixes the reported iETHv2 view, and it repairs the stETH-par / wstETH-market inconsistency. USD/BTC follow; their basis is real but small.

Private documentation. creddit.xyz