Skip to content

built-with-deviations Built, with deviations. This is a decision record, not documentation; the body is annotated where the build diverged.

What is still current: Multi-strategy holdings state Balance in the fund's declared payout token; Yield earned stays in the fund's ETH or USD book like every other holdings band, a deviation from contract points 2 and 3 taken because a yield-bearing payout token made the earned figure read as performance above holding that token. Portfolio totals and return accounting remain in their established ETH or USD book.

Landed: PR #885

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

Portfolio multi-strategy fund balances and earnings in redemption units ​

Request and delivery ​

Fred requests every multi-strategy fund's portfolio Balance and Yield earned to use the asset the holding redeems into, rather than a fund-share count. Implement on top of PR #884, then open a PR targeting staging, obtain independent review, resolve findings, run final browser QA and CI, and report when ready for Fred to merge. Do not merge or deploy. The implementer must be GPT-5.6 Sol, with a separate fresh-context reviewer.

Starting point: PR #884, fix/shared-multi-strategy-identity, commit 2b383b16bf75c1a2fd7ed6d6f1f7598a1c87775c. Isolated worktree: /Users/friedrichcoen/claude/oc-fund-redeemable; branch fix/portfolio-fund-redeemable-units. Recheck the parent PR before publishing and before the final gate; integrate relevant updates without modifying its branch. Clearly state the dependency in the new PR. If #884 is not yet merged, the PR may target staging but must state that #884 merges first. Do not claim it independently mergeable while that dependency is unresolved.

Verified starting behavior ​

  • src/data/fund-registry.ts declares ten multi-strategy funds. Six use the ERC-4626 portfolio reader, which already produces deposit-asset token quantities. Four use wallet holdings: Lido Earn ETH, Lido Earn USD, ether.fi Liquid ETH and Treehouse ETH. Their current quantity is a share-token count.
  • src/lib/portfolio/ledger-v2-api.ts builds served positions using tokenAmountForRow; that conversion handles rebasing wallet tokens, not managed-fund redemption display. Existing quantity has a documented accounting-token contract. Do not silently change that contract across all consumers.
  • src/components/portfolio/PortfolioDashboard.tsx sums quantity in heldBalance and prints it in BalanceCell without an explicit unit. Standalone Yield earned uses the existing book-denominated accrual. PR #884 contributes a shared fund identity across both reader paths.
  • assetAddress/assetSymbol and the rate getter's quote asset in the fund registry serve valuation and deposit accounting. They are NOT authoritative declarations of the withdrawal asset: earnETH is valued in ETH and Treehouse composes wstETH into ETH today.
  • Lido's current primary documentation, https://docs.lido.fi/earn/ (read 2026-09-16), states earnETH withdrawals pay wstETH and earnUSD withdrawals pay USDC. Fred's example said stETH. An optional clarification was sent; absent another instruction, implement the general requested rule using the actual withdrawal asset, thus wstETH for earnETH. If Fred instead chooses stETH equivalents, label that honestly and apply the conversion at the same observed block. Do not assert direct stETH withdrawal without evidence.

Product contract ​

  1. A multi-strategy row states its balance as the currently attributable redemption amount and explicitly names the payout token. Fund name, mark and deep link from #884 remain fund identity; the payout symbol is a separate fact.
  2. Its Yield earned cell uses that same token as display denomination. Preserve the existing definition of accrual: exclude secondary-market price gains and deposits/withdrawals; maintain certification and missing-data withholding. Do not turn this display request into a change to the portfolio's ETH/USD return calculation or APY.
  3. A yield-bearing payout token creates a distinction: existing total ETH accrual expressed in wstETH is not the fund's excess growth over holding wstETH. For this request, display the existing accrual converted into the payout token at the row's observed redemption rate. Document and briefly explain it as an equivalent amount of that token, not a historical count of tokens paid out, a new performance benchmark, or withdrawable rewards separately credited. Financial review must explicitly verify this distinction. Never use the share price as the payout-token conversion price.
  4. Portfolio summary, charts, category totals, Value column, and sort/filter thresholds retain their established ETH/USD denominations. Never sum USDC and WETH as though they share a unit. The per-fund display has its own explicit unit.
  5. Balance and yield use the position observation's own rate/block, including historical/as-of views. Switching market/redemption valuation cannot change the redeemable balance or earned-yield display. Never use today's rate to relabel an older observation.
  6. Missing, zero/negative, non-finite, suspicious, or unavailable required rates produce an unavailable amount with the known unit, not zero, guessed parity, a stale unrelated rate, or a share count wearing the asset label. Known zero and negative earned yield remain valid values. Partial/mixed-unit multi-wallet data must not produce a misleading complete sum.
  7. This is a redemption-value statement, not an executable instant withdrawal quote. Do not imply immediate liquidity or include invented slippage/fees. Preserve existing queue/availability semantics.

Implementation work ​

A. Verify and declare the entire fund coverage ​

Create a compact evidence matrix for all ten funds, with share address, reader path, actual withdrawal asset/address/decimals, valuation quote asset, conversion source and primary-source evidence. Verify from existing checked contract calls and official fund docs/contracts; do not infer from ticker or broad ETH/USD denomination.

Coverage checklist: Fluid Lite ETH; Lido Earn ETH; YO ETH; ether.fi Liquid ETH; Treehouse ETH; IPOR Liquity Carry; Fluid Lite USD; YO USD; Yearn V3 USD; Lido Earn USD. Pay particular attention to Treehouse's wstETH conversion, multiple exit assets at ether.fi, Lido's inverted oracle, IPOR's 20-decimal shares, 6-decimal USDC and share/asset decimal mismatches.

Prefer one authoritative client-safe declaration associated with the shared fund registry/identity. Separate withdrawal-display metadata from the valuation registry's underlying fields. State a canonical standard exit for funds supporting multiple routes and document why. Require every ever-listed multi-strategy fund to have a valid declaration and supported conversion; keep delisted holdings covered. Extend the existing exhaustive shared-identity coverage validation so future fund additions cannot silently omit denomination support. Reject unknown declarations safely at runtime as well as in tests.

B. Introduce an explicit server-served display contract ​

Trace both wallet and ERC-4626 positions through the observed, historical, and multi-wallet served paths. Add an explicit typed object/fields for fund payout identity and nullable balance/earned-yield amount, or an equally clear contract. Preserve raw quantity, accounting identity, ledger math, original book-valued performance and non-fund behavior. Wire at one shared seam rather than individual fund JSX branches.

Use already anchored redemption values when mathematically sufficient. For payout A, with R(A,t) = book units per human payout token at the position's observation t:

balance_A(t) = position_redemption_value_in_book(t) / R(A,t)

yield_A_equivalent(t) = certified_existing_accrual_in_book(t) / R(A,t)

An already correctly token-denominated ERC-4626 quantity may be reused if the accounting asset exactly matches the payout asset. Never multiply it by the fund rate twice. For wallet fund shares, prove the conversion through fund NAV and then the payout rate, with both scales and directions checked. Where an oracle supplies the true payout quote, prefer the coherent anchored quote rather than inferring a potentially different route from an unrelated valuation unit; reconcile it against existing book values and document any difference.

This is an observed requirement for earnETH, not a hypothetical: pinned calls in portfolio-fund-redemption-evidence.md show a supported direct wstETH oracle quote and a small but nonzero difference from ETH NAV divided by the current wrapper exchange rate. Its balance should use that direct valid payout quote times shares, with its own failure withholding. Earned-yield equivalents can still express existing book accrual using the observation's payout-token book rate. Include a regression where a lagged fund ETH report and the wrapper's newer accrual would otherwise disagree with the direct payout quote.

Inspect rate-getters.ts, unit-prices.ts, valuation-sources.ts, the loaded histories/rate context, and the existing registry generation/cache rules before adding I/O. Reuse batching/memoization for equal asset/block requests. Keep reads pinned, null-propagating and bounded; no per-cell RPC and no unbounded archive request for each history point. If persistence or snapshot extension is necessary, use backward-compatible migration/reader fallback; never run production jobs. Document local/staging rollout requirements, but aim for read-time display support without rewriting accounting history.

C. Render and merge consistently ​

Update the multi-strategy Balance and Yield earned cells with explicit matching token symbols and appropriate precision. Use established typography/layout and genuine token icons if needed. Preserve names and deep links from #884. Thread display data through signed-in-model.ts, all-wallet merges and sorting as applicable. Validate same asset identity before adding quantities; one unavailable leg cannot become an apparently complete total. Explain any yield equivalent succinctly in product language, using existing tooltip patterns. Both ETH/USD books and All view must agree on each fund's payout display.

Update relevant documentation, principally docs/portfolio.md, and add/update metrics/database documentation only if their contracts change. Explain how payout-denominated earned yield relates to the unchanged book-denominated portfolio totals.

D. Tests that catch the actual regression ​

  • Exhaustive ten-fund coverage, dynamic/unknown additions, delisted holdings and canonical identity preserved.
  • Wallet-read and ERC-4626-read funds, materially non-1 share rates, correct same-block conversion and inversion, USDC/share decimals and IPOR's 20 decimals.
  • earnETH balance/yield in the verified chosen unit, and a fixture where ETH, wstETH and share counts are deliberately different. A relabel-only fix MUST fail.
  • Treehouse and Fluid Lite ETH to catch wrapper conversion and double-conversion; at least one USD fund to catch USDC vs USD/share assumptions.
  • Growing payout-token exchange rate: existing accrued return expressed in payout units must not silently become excess return or include market dislocation.
  • Historical observations use historical rates; market/redemption toggle leaves these two cells stable; All/ETH/USD and multi-wallet merge agreement.
  • Missing/suspicious/non-finite rate, null accrual under incomplete history, genuine zero/negative yield, partial multi-wallet inputs, mixed-unit merge protection.
  • Existing non-fund quantity/performance unchanged, stETH rebasing behavior preserved, portfolio totals/APY unchanged.
  • Browser regression in tests/e2e/portfolio.spec.ts (or matching existing spec) exercises the signed-in full page with deterministic fixtures and asserts numeric relationships plus unit labels, not just presence of text. Verify the regression fails when display conversion is reverted.

Required delivery sequence ​

Read worktree AGENTS.md and .claude/skills/qa/SKILL.md. Use Node 20 (/Users/friedrichcoen/.nvm/versions/node/v20.20.1/bin). Clone dependencies with APFS cp -cR from an existing checkout when useful; do not symlink node_modules (Turbopack restriction). Use only isolated local fixture DB/app ports; never disturb other worktrees' running QA.

  1. Implement and run targeted meaningful tests, TypeScript checks, required full suite and docs build. Commit only explicit changed paths.
  2. Push feature branch and open PR into staging, accurately explaining #884 dependency, concrete old/new behavior, denomination/yield semantics, validation, docs impact and any operational requirements.
  3. Obtain fresh-context independent code review by a separate GPT-5.6 Sol agent. Review all first/second/third-order effects, unit math, withdrawal metadata evidence, compatibility, timing and financial soundness. Post findings as a PR comment. Fix/review again until no blockers remain; no self-review substitution.
  4. Run final full-page browser QA after review converges, using project QA skill, screenshots and real hydrated interactions. Locally cover desktop and laptop (units affect widths); CI covers narrow/reduced-motion/zoom. If CI skips those jobs, cover them locally per project instructions. View screenshots. Browser regressions and any fixes must be on the final reviewed commit; fixes re-enter review before final QA.
  5. Verify CI green on that same final commit, PR mergeability and parent PR state. Do not merge. Give the coordinator PR URL, final SHA, parent dependency status, evidence matrix, test results, review comment URL(s), screenshots/QA results, unresolved limitations (if any). Never call it ready before these gates pass.

Fred-facing progress and final messages must stay concise and in plain product language. Technical details belong in this plan, code, docs and the PR.

Private documentation. creddit.xyz