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_marketresolves to null at every snapshot, whilevalue_redemptionis correct. buildBookCurve.bookValueAt(pnl.ts:540) skips any leg whose mark value is null, so the market book total silently drops iETHv2 (~15 ETH), andlegIntervalYield(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_EXTRAwithbasisClass: "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 NOTpinned→ 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 carriesnumeraire,redemption(par|share_rate), andbasisClass(market|pinned). Amarketrow 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), andLBTC. - 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 atoken_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:
| Book | Par numeraire (basis ≡ 0) | Everything else |
|---|---|---|
| ETH | ETH / WETH | stETH, eETH, wstETH, all LSTs → basis vs ETH |
| USD | synthetic $1 (Dune's USD denomination) | every stablecoin → basis vs $1 |
| BTC | synthetic 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)
- 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).
- 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
- 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, setmarket_in_book(W) = shareRate × market_in_book(U)andredemption_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. - 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.
- Re-mark ETH-book history:
scripts/repair/remark-snapshot-marks.ts+remark-flow-marks.tsso 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. - 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). - 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.