Skip to content

Fix interest earned calculation to measure realized profit only - #2315

Open
MusabShakeel576 wants to merge 1 commit into
masterfrom
claude/interest-counter-behavior-wvtid0
Open

Fix interest earned calculation to measure realized profit only#2315
MusabShakeel576 wants to merge 1 commit into
masterfrom
claude/interest-counter-behavior-wvtid0

Conversation

@MusabShakeel576

Copy link
Copy Markdown
Contributor

Summary

This PR fixes a critical bug in the "Interest earned" counter that caused it to swing by ~2x when vault APY rates changed or deposit history became temporarily unavailable. The counter now correctly reports realized profit only (total value - total deposited) instead of layering an APY projection on top of it.

Key Changes

  • Redefined interest modes as measurements, not forecasts: CURRENT and INTEREST_ONLY modes now return only realized profit from deposit history, eliminating the double-counting of the holding period that occurred when APY projections were added on top.

  • Introduced INTEREST_UNAVAILABLE sentinel: When deposit history cannot be established (subgraph unavailable, no deposits yet), the interest modes now return -1 instead of guessing with an APY projection. This prevents the ~2x swing that occurred when the fallback calculation was triggered.

  • Separated APY projection to per-second tick: The useSavingsYield hook now applies APY projections only for elapsed time since the last measurement, keeping the counter smooth without retroactively re-pricing the entire holding period when rates change.

  • Fixed APY source for live counters: Changed from using all-time APY (apys.allTime) to the current max APY (maxAPY) for ticking the live interest counter, ensuring the displayed rate matches the headline rate advertised on the card.

  • Improved backend summary usage: The /savings/summary endpoint is now the preferred source for interest earned on all vaults (not just FUSE), as it measures against a high-water-mark exchange rate and prevents the figure from stepping backwards on transient NAV dips.

  • Updated interest conversion logic: Fixed ETH vault interest conversion to properly denominate in USD (was previously left in ETH while being displayed with a $ sign).

  • Added comprehensive regression tests: New test suite validates that interest earned is stable across APY changes, handles missing deposit history gracefully, and correctly reports realized profit.

Notable Implementation Details

  • The fallback APY calculation (when deposit history is unavailable) is still used for projecting modes (TOTAL_USD, TOTAL, etc.), but interest modes now signal INTEREST_UNAVAILABLE instead.
  • Callers of calculateYield in interest modes must check for INTEREST_UNAVAILABLE and preserve the last known value rather than updating to the sentinel.
  • The per-second tick in useSavingsYield now only projects forward from the anchor measurement time, not from the deposit start time, preventing rate changes from affecting historical periods.

https://claude.ai/code/session_011uPvndf13ix7roj8jJcdLX

…rojection

"Interest earned" showed +1.50 at a 14% headline rate; the rate moved to 13.59%
and it dropped to +0.75 — a ~2x fall from a ~3% rate change. The counter was a
hybrid of a measurement and a forecast, so a rate change re-priced the entire
holding period.

- calculateYield's interest modes (CURRENT / INTEREST_ONLY) now return realized
  profit only (balance x rate - deposited). They previously added
  `balance x APY x (now - firstDeposit)` on top, double-counting the period the
  realized figure already covered and making a measurement scale with the APY.
- When deposit history is unavailable, the interest modes return the new
  INTEREST_UNAVAILABLE sentinel instead of a bare APY projection. That silent
  fallback returned roughly half of realized+projection, so the number swung by
  ~2x whenever the subgraph query flickered. Callers hold the last known value.
- The interest modes no longer require a valid APY or start timestamp: a failed
  APY fetch must not blank out interest already sitting on-chain.
- Route interest earned through /savings/summary for every vault, not just
  soFUSE. It measures against a high-water-mark rate, so the figure can't step
  backwards on a transient NAV dip.
- Tick the live counters at the APY the screen displays as "Current Yield"
  (maxAPY) rather than the all-time APY, so the headline rate and the rate the
  numbers move at agree. Per-second smoothing still projects forward only, from
  the instant the measurement was taken.
- Convert soFUSE interest to USD on the redesigned savings card. It was left in
  FUSE units, rendered with a $ sign and subtracted from a USD total.
- Label the month-to-date stat "This month (est.)" — it is an APY projection,
  not a measurement.

Adds unit coverage asserting interest is APY-invariant, survives a failed APY
fetch, clamps underwater positions to zero, and reports unavailability instead
of guessing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uPvndf13ix7roj8jJcdLX
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
solid-app Ignored Ignored Jul 30, 2026 10:45am
solid-app-staging Ignored Ignored Jul 30, 2026 10:45am

Request Review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants