Skip to content

Feat/ projected APY estimates in form summaries#372

Draft
Seranged wants to merge 9 commits into
developmentfrom
feature/eul4-189-improve-form-roenet-apy-simulation-utilization-aware-rates
Draft

Feat/ projected APY estimates in form summaries#372
Seranged wants to merge 9 commits into
developmentfrom
feature/eul4-189-improve-form-roenet-apy-simulation-utilization-aware-rates

Conversation

@Seranged

@Seranged Seranged commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make form summary APY and ROE estimates reflect post-operation vault utilization instead of reusing current rates.
  • Keep ROE and Net APY composition consistent across borrow, multiply, supply, withdraw, repay, and swap flows.
  • Account for positions backed by multiple collateral vaults by weighting collateral APYs by USD value.
  • Fix follow-up math review issues around reward composition and partial swap portfolio totals.

Changes

  • Add a batched projected-rate helper over the existing EVC lens batching path while preserving the single-vault API.
  • Add a reusable collateral APY snapshot composable that loads enabled collateral balances, projects changed collateral rates, and returns USD-weighted supply APY.
  • Add getNetAPYFromWeightedSupplySnapshot so reward-inclusive weighted collateral APYs cannot double-count supply rewards when passed into Net APY calculations.
  • Route position and form ROE calculations through shared rewards-aware helpers and remove duplicate repay-only ROE math.
  • Update affected summary flows to use projected borrow or supply rates for their after values, including collateral swap, debt swap, repay, and multiply paths.
  • Fix collateral swap ROE-after to use the full projected collateral portfolio: remaining source collateral plus target collateral, with source withdrawal and target deposit rate deltas.
  • Fix debt swap ROE-after to include remaining old debt plus new debt, projecting both old-vault repay and new-vault borrow rates and weighting borrow APY by USD debt value.
  • Guard async swap ROE estimates so stale in-flight snapshot or quote results cannot repopulate after fast source, target, or quote changes.

Test plan

  • npm run typecheck
  • npm run lint
  • npm run test:run -- tests/entities/vault/apy.test.ts tests/utils/repay-utils.test.ts
  • npm run test:run -- tests/entities/vault/apy.test.ts tests/utils/repay-utils.test.ts tests/composables/useSavingsRepay.test.ts
  • npm run test:run -- tests/entities/vault/apy.test.ts tests/utils/repay-utils.test.ts tests/composables/useSavingsRepay.test.ts tests/composables/repay-review-amount.test.ts
  • npm run build
  • Local built route smoke for /, /borrow?network=1, /position/0/repay?network=1, /position/0/multiply?network=1, /position/0/supply?network=1, /position/0/withdraw?network=1, /position/0/borrow?network=1, and /position/0/collateral/swap?network=1.
  • Browser smoke on PR preview for spy account 0x68e7E72938db36a5CBbCa7b52c71DBBaaDfB8264, position 1: overview, borrow, multiply, repay, borrow swap, and collateral swap routes loaded without PR-specific crashes.
  • Manual multiply quote smoke on PR preview: route rendered, quotes fetched, selected route updated Routed via, projected ROE/health/LTV/liquidation fields updated, and the previous TDZ crash did not recur.
  • External route matrix on local built PR server, PR preview, and development comparator: 13/13 routes returned HTTP 200 with no material route errors for each target.

External Review Notes

  • Initial smoke on head a968fd6f passed typecheck, lint, targeted Vitest, build, Railway preview deploy, and 13-route local/preview/comparator matrix.
  • Initial math review found three real composition issues:
    • supply rewards could be double-counted when a reward-inclusive weighted collateral snapshot was passed into getNetAPY with a separate supply reward APY;
    • collateral swap ROE-after considered only the target collateral leg for partial swaps;
    • debt swap ROE-after considered only the new debt leg for partial swaps.
  • Follow-up fix on head e092c5db was reviewed and verified:
    • supply reward double-count fixed;
    • collateral swap ROE-after uses full projected collateral snapshot with source withdrawal and target deposit deltas;
    • debt swap ROE-after includes remaining old debt plus new debt and weights projected borrow APY by USD value;
    • getRoe / getNetAPY argument order looked correct.
  • Additional diff-only review on head 2b6bc3bf verified the stale async guards:
    • collateral swap invalidates in-flight ROE snapshots before early-return resets;
    • borrow swap invalidates stale quote/projection batches before writing ROE-after state.
  • Latest helper refactor on head 73b4a6ae adds focused APY tests covering reward-inclusive weighted snapshots and fallback reward behavior.

Smoke Fixture

  • Spy account: 0x68e7E72938db36a5CBbCa7b52c71DBBaaDfB8264
  • Network: 1
  • Positions used for route matrix: 1, 2
  • PR preview: https://production-master-branch-euler-lite-pr-372.up.railway.app
  • Development comparator: https://euler-development-production.up.railway.app

Smoke Route Matrix

Routes checked across local built PR server, PR preview, and development comparator:

/position/1
/position/1/borrow
/position/1/multiply
/position/1/supply
/position/1/withdraw
/position/1/repay
/position/1/borrow/swap
/position/1/collateral/swap
/position/2
/position/2/multiply
/position/2/repay
/position/2/borrow/swap
/position/2/collateral/swap

Result:

  • Local built PR: 13/13 HTTP 200, no material route errors.
  • PR preview: 13/13 HTTP 200, no material route errors.
  • Development comparator: 13/13 HTTP 200, no material route errors.

Summary by CodeRabbit

  • New Features

    • Collateral APY snapshot hook for weighted-supply snapshots used across position flows.
  • Improvements

    • Batched vault rate projections for faster, consistent APY estimates.
    • Net APY and ROE use weighted-supply snapshots and projected rates for more accurate forward-looking metrics.
    • ROE computations now handle nullable/edge cases and projected-rate inputs.
  • Tests

    • Updated tests to cover snapshot-based APY helper and revised ROE logic.

- batch vault projected-rate reads for borrow and multiply summaries
- weight position collateral APYs across all enabled collaterals
- route repay and swap ROE through the shared rewards-aware helper
@railway-app

railway-app Bot commented May 5, 2026

Copy link
Copy Markdown

🚅 Deployed to the euler-lite-pr-372 environment in euler-lite

Service Status Web Updated (UTC)
dev-build ✅ Success (View Logs) Web May 20, 2026 at 2:27 am

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6e073589-03e1-4ee5-8fa0-302c5ff9e3de

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds batched vault rate projections, a collateral-weighted APY snapshot hook, nullable-safe getRoe/getNetAPYFromWeightedSupplySnapshot helpers, and updates composables, pages, and tests to use batch projections and snapshot-driven APY/ROE flows.

Changes

Rate Projection and APY Calculation Refactor

Layer / File(s) Summary
Data Shape & Interfaces
entities/vault/apy.ts
New interfaces ProjectedRates, ProjectedRatesRequest, and internal types supporting batched projections and weighted-supply snapshots.
Core Projection APIs
entities/vault/apy.ts
getProjectedRatesBatch added to fetch multiple vault projections; getProjectedRates delegates to the batch API; getNetAPYFromWeightedSupplySnapshot and nullable-safe getRoe overloads added.
Collateral APY Composition
composables/usePositionCollateralApy.ts
New hook usePositionCollateralApy() exposing getCollateralApySnapshot(position, liabilityVault, options?) returning { supplyUsd, weightedSupplyApy }.
Borrow & Multiply Composables
composables/borrow/useBorrowForm.ts, composables/borrow/useMultiplyForm.ts
Replaced per-vault getProjectedRates calls with getProjectedRatesBatch; switched ROE callsites to getRoe; added projected-rate state and race-guards.
Repay & Health Composables
composables/repay/*, composables/position/useCollateralForm.ts
Integrated collateral snapshots via usePositionCollateralApy; compute net APY via getNetAPYFromWeightedSupplySnapshot; use effectiveCollateralSupplyApy fallback; removed some direct USD-price derivations.
Pages - Position Views
pages/position/[number]/*
Pages now obtain collateral snapshots and call getNetAPYFromWeightedSupplySnapshot; ROE previews use getRoe with snapshot-weighted APYs and race-guards; added next-state preview state (nextBorrowApy, nextSupplyValueUsd, etc.).
ROE Calculation Migration
utils/repayUtils.ts, entities/vault/apy.ts
Removed calculateRoe from utils; getRoe provided by vault module with reordered args and null-safety. Note: utils/repayUtils.ts contains stray numeric tokens introduced in this change.
Tests & Validation
tests/entities/vault/apy.test.ts, tests/utils/repay-utils.test.ts, tests/composables/useSavingsRepay.test.ts
Tests updated for getNetAPYFromWeightedSupplySnapshot; ROE tests migrated to getRoe; added global stub for usePositionCollateralApy in savings repay tests.

Sequence Diagram

sequenceDiagram
    actor Comp as Composable/Page
    participant Vault as Vault Module
    participant Snapshot as usePositionCollateralApy
    participant RPC as RPC/Lens

    rect rgba(100, 150, 200, 0.5)
    note over Comp,RPC: Old Flow (getNetAPY)
    Comp->>Comp: Fetch collateralUsd, borrowUsd
    Comp->>Vault: getNetAPY(collateralUsd, borrowUsd, ...)
    Vault-->>Comp: netAPY
    end

    rect rgba(200, 150, 100, 0.5)
    note over Comp,RPC: New Flow (getNetAPYFromWeightedSupplySnapshot)
    Comp->>Snapshot: getCollateralApySnapshot(position, vault)
    Snapshot->>RPC: Fetch collateral assets & rates
    Snapshot->>Vault: getProjectedRatesBatch(requests)
    Vault->>RPC: Multicall projections
    RPC-->>Vault: projected rates array
    Vault-->>Snapshot: ProjectedRates[]
    Snapshot->>Snapshot: Aggregate weightedSupplyApy & supplyUsd
    Snapshot-->>Comp: CollateralApySnapshot { supplyUsd, weightedSupplyApy }
    Comp->>Vault: getNetAPYFromWeightedSupplySnapshot(snapshot, ...)
    Vault-->>Comp: netAPY
    end
Loading
sequenceDiagram
    actor Page as Page Component
    participant Vault as Vault Module
    participant Comp as Composables

    rect rgba(150, 200, 100, 0.5)
    note over Page,Comp: Old ROE Calculation (calculateRoe)
    Comp->>Comp: calculateRoe(supplyUsd, borrowUsd, supplyApy, borrowApy)
    Comp-->>Page: roe
    end

    rect rgba(200, 100, 150, 0.5)
    note over Page,Comp: New ROE Calculation (getRoe + null-safety)
    Page->>Vault: getRoe(supplyUsd, supplyApy, borrowUsd, borrowApy)
    Vault->>Vault: Handle null inputs, validate finite values
    Vault-->>Page: roe | null
    Page->>Page: Render with null coalescing (roe ?? 0)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • euler-xyz/euler-lite#126: Modifies projected-rate based estimation flows across composables/pages; this PR builds batched projections and snapshot-based APY handling on that foundation.
  • euler-xyz/euler-lite#114: Related — similar swap page ROE refactors and removal of calculateRoe; touches same UI paths.
  • euler-xyz/euler-lite#91: Related — changes to vault APY/getRoe logic; overlaps with core entities/vault/apy edits.

Suggested reviewers

  • kasperpawlowski
  • VSBDev

Poem

🐰 Batching hops across the vaults so keen,
With snapshots gathered in between!
Old rates gave way to weighted grace,
ROE now safe in its new place. ✨
From null to zero, projections gleam— a rabbit's refactoring dream!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: improving projected APY estimates in form summaries, which is the core objective of this PR across multiple composables and pages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/eul4-189-improve-form-roenet-apy-simulation-utilization-aware-rates

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 5, 2026 22:54 Destroyed
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 6, 2026 13:28 Destroyed
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 6, 2026 13:51 Destroyed
- Avoid double-counting supply rewards in weighted collateral Net APY
- Weight collateral swap ROE across remaining and target collateral
- Weight debt swap ROE across remaining and target liabilities
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 6, 2026 16:21 Destroyed
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 6, 2026 16:48 Destroyed
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 6, 2026 17:01 Destroyed
@Seranged Seranged marked this pull request as ready for review May 6, 2026 17:14
@Seranged Seranged requested a review from kasperpawlowski May 6, 2026 17:55
@Seranged

Seranged commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
pages/position/[number]/index.vue (1)

394-408: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't coerce an unavailable ROE into a positive 0.

When getRoe() is unavailable, the card now renders -, but these paths still treat it as 0: the text color stays in the non-negative state and the info modal receives 0. That makes missing data look valid once the user interacts with it. Keep the null state through styling/modal props, or disable the ROE info action until the value is finite.

Also applies to: 772-782

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/position/`[number]/index.vue around lines 394 - 408, The onRoeInfoClick
handler (and the analogous handler around lines 772-782) is coercing an
unavailable roe into 0; instead, preserve the absent/null state and avoid
treating it as non-negative: stop using "roe.value ?? 0" and pass the raw roe
value (or null) into modal.open so the modal receives a null/undefined when
getRoe() is unavailable, and update any UI color/enable logic to check
Number.isFinite(roe) (or Number.isFinite(roe.value)) before rendering positive
styling or enabling the info action—if roe is not finite, disable the ROE info
action (or short-circuit opening the modal) and keep the display as "-" so
missing data isn’t shown as valid.
composables/repay/useWalletRepay.ts (1)

274-299: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t keep rendering the previous net APY after a snapshot/projection failure.

This new Promise.all(...) adds another fallible request, but the failure path still leaves _estimateNetAPY untouched while hasEstimate stays true. If any await rejects, the “after” APY for the current repay amount can silently reuse the previous input’s value. Reset the async APY state on failure so the UI falls back to a safe value instead of stale data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composables/repay/useWalletRepay.ts` around lines 274 - 299, The Promise.all
call combining getProjectedRates, getCollateralApySnapshot and
getAssetUsdValueOrZero can reject and currently leaves _estimateNetAPY and
hasEstimate stale; wrap that await in a try/catch around the block that computes
projectedBorrowApy and _estimateNetAPY (references: getProjectedRates,
getCollateralApySnapshot, getAssetUsdValueOrZero,
asyncEstimatesGuard.isStale(gen), _estimateNetAPY, hasEstimate) and on any
exception set/clear the async APY state (e.g. reset _estimateNetAPY to a safe
default/null and set hasEstimate to false) before returning so the UI won’t
display previous stale APY.
composables/repay/useWalletSwapRepay.ts (1)

461-485: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset the async APY fallback when the new snapshot request fails.

updateSyncEstimates() has already set hasEstimate to true, so if getCollateralApySnapshot(...) or one of the other awaited calls rejects here, _estimateNetAPY keeps the previous successful value and the summary shows the wrong “after” APY for the current quote. Clear the async APY state in the failure path before dropping the loading flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composables/repay/useWalletSwapRepay.ts` around lines 461 - 485, The awaited
Promise.all call that computes projected, collateralSnapshot and borrowUsd can
throw and leave the previous _estimateNetAPY.value in place; wrap the
Promise.all (and subsequent projectedBorrowApy/_estimateNetAPY calculation) in a
try/catch, and in the catch reset the async APY fallback state (clear
_estimateNetAPY.value to null or an explicit “no async estimate” value set by
updateSyncEstimates()) before returning/propagating so the UI doesn't show the
stale “after” APY; reference getProjectedRates, getCollateralApySnapshot,
getAssetUsdValueOrZero, estimatesGuard.isStale(gen), and _estimateNetAPY in your
change.
composables/position/useCollateralForm.ts (1)

193-212: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing race guard in async watchEffect for netAPY.

The watchEffect at line 193 performs async operations but lacks a race guard, unlike updateAsyncEstimates which uses asyncEstimatesGuard. If position, borrowVault, or collateralVault changes rapidly, stale promises could overwrite netAPY.value with outdated results.

Consider adding a race guard similar to the pattern used in updateAsyncEstimates:

🛡️ Suggested fix
+const netAPYGuard = createRaceGuard()
+
 watchEffect(async () => {
   if (!position.value || !borrowVault.value || !collateralVault.value) {
     netAPY.value = 0
     return
   }
+  const gen = netAPYGuard.next()

   const [collateralSnapshot, borrowedUsd] = await Promise.all([
     getCollateralApySnapshot(position.value, borrowVault.value),
     getAssetUsdValueOrZero(position.value.borrowed ?? 0n, borrowVault.value, 'off-chain'),
   ])

+  if (netAPYGuard.isStale(gen)) return
+
   netAPY.value = getNetAPYFromWeightedSupplySnapshot(
     collateralSnapshot,
     collateralSupplyApy.value,
     borrowedUsd,
     borrowApy.value,
     collateralSupplyRewardApy.value || null,
     borrowRewardApy.value || null,
   )
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composables/position/useCollateralForm.ts` around lines 193 - 212, The async
watchEffect that computes netAPY (watchEffect, netAPY.value) can suffer from
races and stale overwrites; wrap the async work with the same race-guard pattern
used in updateAsyncEstimates (the asyncEstimatesGuard token/counter) so only the
latest invocation writes netAPY.value: capture a local guard token before
awaiting getCollateralApySnapshot and getAssetUsdValueOrZero, then after the
awaits verify the token is still current via asyncEstimatesGuard (or
increment/compare the guard) and only then call
getNetAPYFromWeightedSupplySnapshot and assign netAPY.value; ensure you
reference the existing asyncEstimatesGuard mechanism rather than introducing a
new ad-hoc flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@composables/repay/useCollateralSwapRepay.ts`:
- Around line 190-205: The code only preserves currentSnapshot.weightedSupplyApy
in weightedCollateralSupplyApy so both before/after ROE calculations reuse the
old APY; capture and store nextSnapshot.weightedSupplyApy too and make
useRepayHealthMetrics (roeAfter) use the post-repay value. Concretely: keep both
current and next APY values (e.g., set weightedCollateralSupplyApyBefore =
currentSnapshot.weightedSupplyApy and weightedCollateralSupplyApyAfter =
nextSnapshot.weightedSupplyApy or store nextSnapshot into a dedicated ref), then
ensure effectiveCollateralSupplyApy/roeAfter resolution uses the “after” ref for
the post-repay calculation (also apply same change for the 208–218 block where
snapshots are computed).

In `@composables/repay/useRepayHealthMetrics.ts`:
- Around line 73-101: The async watchEffect updating projectedBorrowApy can be
overwritten by stale async completions; add a generation/race guard and failure
fallback: inside the watchEffect (the block using watchEffect, borrowVault,
position, debtRepaid, getProjectedRates and updating projectedBorrowApy) create
a local incremental token/generation (or use an AbortController-like guard used
elsewhere in the PR), capture the current token before awaiting
getProjectedRates, and after the await verify the token still matches before
assigning projectedBorrowApy; also wrap the getProjectedRates call in try/catch
and set projectedBorrowApy.value = null on any failure or if the token mismatch
indicates a stale response.

In `@composables/usePositionCollateralApy.ts`:
- Around line 84-169: getCollateralApySnapshot currently lets any async
rejection (config load, getOrFetch, getProjectedRatesBatch, price reads, etc.)
bubble up to callers; wrap the entire body of getCollateralApySnapshot in a
try/catch that catches all errors and returns the safe fallback { supplyUsd: 0,
weightedSupplyApy: null } on any failure, optionally logging the error; keep
existing early-return behavior when position/liabilityVault are missing and
ensure awaits like loadEulerConfig(), until(isVaultsReady).toBe(true),
getOrFetch(), getProjectedRatesBatch(), and getCollateralUsdValueOrZero() remain
inside the try so their failures are caught.

In `@pages/position/`[number]/borrow/swap.vue:
- Around line 281-340: The early return when quote.value is missing skips the
same-asset refinance path; change the condition to allow processing when
isSameAsset is true. Specifically, replace the "if (!quote.value ||
!toVault.value || !fromVault.value) { ... return }" guard to only return when
neither quote.value nor isSameAsset is present (and still require
toVault/fromVault). When isSameAsset is true derive newBorrowAmount from the
moved debt (use valueToNano(fromAmount.value, fromVault.value.decimals) or the
appropriate moved-amount variable) and then run the same Promise.all logic using
fromVault/toVault, getAssetUsdValue, and getProjectedRates so nextBorrowValueUsd
and nextBorrowApy are computed for same-asset refinances; keep the stale-gen
check (nextBorrowGuard.isStale(gen)) and all subsequent calculations unchanged.

In `@pages/position/`[number]/collateral/swap.vue:
- Around line 364-399: The current watchEffect uses roeSnapshotGuard.next() and
awaits getCollateralApySnapshot(...) but if that Promise rejects it leaves
supplyValueUsd, weightedSupplyApy, nextSupplyValueUsd and nextWeightedSupplyApy
with stale values; wrap the Promise.all/await in a try/catch (around the call
that assigns currentSnapshot/nextSnapshot) and in the catch check
roeSnapshotGuard.isStale(gen) before returning, otherwise set
supplyValueUsd.value = null, weightedSupplyApy.value = null,
nextSupplyValueUsd.value = null and nextWeightedSupplyApy.value = null and
return so failed snapshot requests clear the refs; keep using the existing
symbols getCollateralApySnapshot, roeSnapshotGuard.next()/isStale, and the four
ref names to locate changes.

In `@pages/position/`[number]/multiply.vue:
- Around line 295-310: The async watchEffect that calls getCollateralApySnapshot
can write stale results when inputs change rapidly; add a race guard using
onInvalidate (or a local cancellation token) inside the watchEffect: create a
local token (or boolean cancelled) at the start, register onInvalidate(() =>
mark token cancelled), capture the snapshot into a local variable, then before
assigning snapshot.supplyUsd to nextSupplyValueUsd.value and
snapshot.weightedSupplyApy to multiplyWeightedSupplyApy.value, check the token
and skip the assignment if invalidated; reference the existing watchEffect,
getCollateralApySnapshot, and the target variables
nextSupplyValueUsd/multiplyWeightedSupplyApy and inputs position,
multiplyShortVault, multiplyLongVault, multiplySwapAmountOut when implementing
the guard.
- Around line 275-284: The async watchEffect using getCollateralApySnapshot can
have race conditions where slower snapshot promises overwrite newer values;
modify the watchEffect to use Vue's onInvalidate (or a local cancel token)
inside the async callback (e.g., let cancelled = false; onInvalidate(()=>
cancelled = true)) and after awaiting getCollateralApySnapshot check the cancel
flag (or compare a local requestId) before assigning currentSupplyValueUsd.value
and currentWeightedSupplyApy.value, and ensure the early-return branch also sets
the cancel token so stale async results never mutate the refs (references:
watchEffect, getCollateralApySnapshot, currentSupplyValueUsd,
currentWeightedSupplyApy, position, multiplyShortVault).

---

Outside diff comments:
In `@composables/position/useCollateralForm.ts`:
- Around line 193-212: The async watchEffect that computes netAPY (watchEffect,
netAPY.value) can suffer from races and stale overwrites; wrap the async work
with the same race-guard pattern used in updateAsyncEstimates (the
asyncEstimatesGuard token/counter) so only the latest invocation writes
netAPY.value: capture a local guard token before awaiting
getCollateralApySnapshot and getAssetUsdValueOrZero, then after the awaits
verify the token is still current via asyncEstimatesGuard (or increment/compare
the guard) and only then call getNetAPYFromWeightedSupplySnapshot and assign
netAPY.value; ensure you reference the existing asyncEstimatesGuard mechanism
rather than introducing a new ad-hoc flag.

In `@composables/repay/useWalletRepay.ts`:
- Around line 274-299: The Promise.all call combining getProjectedRates,
getCollateralApySnapshot and getAssetUsdValueOrZero can reject and currently
leaves _estimateNetAPY and hasEstimate stale; wrap that await in a try/catch
around the block that computes projectedBorrowApy and _estimateNetAPY
(references: getProjectedRates, getCollateralApySnapshot,
getAssetUsdValueOrZero, asyncEstimatesGuard.isStale(gen), _estimateNetAPY,
hasEstimate) and on any exception set/clear the async APY state (e.g. reset
_estimateNetAPY to a safe default/null and set hasEstimate to false) before
returning so the UI won’t display previous stale APY.

In `@composables/repay/useWalletSwapRepay.ts`:
- Around line 461-485: The awaited Promise.all call that computes projected,
collateralSnapshot and borrowUsd can throw and leave the previous
_estimateNetAPY.value in place; wrap the Promise.all (and subsequent
projectedBorrowApy/_estimateNetAPY calculation) in a try/catch, and in the catch
reset the async APY fallback state (clear _estimateNetAPY.value to null or an
explicit “no async estimate” value set by updateSyncEstimates()) before
returning/propagating so the UI doesn't show the stale “after” APY; reference
getProjectedRates, getCollateralApySnapshot, getAssetUsdValueOrZero,
estimatesGuard.isStale(gen), and _estimateNetAPY in your change.

In `@pages/position/`[number]/index.vue:
- Around line 394-408: The onRoeInfoClick handler (and the analogous handler
around lines 772-782) is coercing an unavailable roe into 0; instead, preserve
the absent/null state and avoid treating it as non-negative: stop using
"roe.value ?? 0" and pass the raw roe value (or null) into modal.open so the
modal receives a null/undefined when getRoe() is unavailable, and update any UI
color/enable logic to check Number.isFinite(roe) (or Number.isFinite(roe.value))
before rendering positive styling or enabling the info action—if roe is not
finite, disable the ROE info action (or short-circuit opening the modal) and
keep the display as "-" so missing data isn’t shown as valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9129e768-b439-4d87-86fd-faa5056fba3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca0a3a and 73b4a6a.

📒 Files selected for processing (21)
  • composables/borrow/useBorrowForm.ts
  • composables/borrow/useMultiplyForm.ts
  • composables/position/useCollateralForm.ts
  • composables/repay/useCollateralSwapRepay.ts
  • composables/repay/useRepayHealthMetrics.ts
  • composables/repay/useSavingsRepay.ts
  • composables/repay/useWalletRepay.ts
  • composables/repay/useWalletSwapRepay.ts
  • composables/usePositionCollateralApy.ts
  • entities/vault/apy.ts
  • entities/vault/index.ts
  • pages/position/[number]/borrow/index.vue
  • pages/position/[number]/borrow/swap.vue
  • pages/position/[number]/collateral/swap.vue
  • pages/position/[number]/index.vue
  • pages/position/[number]/multiply.vue
  • pages/position/[number]/repay.vue
  • tests/composables/useSavingsRepay.test.ts
  • tests/entities/vault/apy.test.ts
  • tests/utils/repay-utils.test.ts
  • utils/repayUtils.ts
💤 Files with no reviewable changes (1)
  • utils/repayUtils.ts

Comment thread composables/repay/useCollateralSwapRepay.ts
Comment thread composables/repay/useRepayHealthMetrics.ts
Comment thread composables/usePositionCollateralApy.ts
Comment thread pages/position/[number]/borrow/swap.vue Outdated
Comment thread pages/position/[number]/collateral/swap.vue
Comment thread pages/position/[number]/multiply.vue
Comment thread pages/position/[number]/multiply.vue
@Seranged Seranged marked this pull request as draft May 7, 2026 18:33
@Seranged Seranged removed the request for review from kasperpawlowski May 7, 2026 18:33
@Seranged Seranged changed the title Fix projected APY estimates in form summaries Feat/ projected APY estimates in form summaries May 7, 2026
@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 8, 2026 14:27 Destroyed
LeonardEulerXYZ

This comment was marked as outdated.

@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-372 May 8, 2026 14:45 Destroyed
LeonardEulerXYZ

This comment was marked as outdated.

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep agent-assisted re-review

Reviewed current head 504c9e7 with four independent passes: APY/ROE math, async/reactive behavior, transaction/same-asset trust boundaries, and tests/coverage. I then re-anchored the material findings against the current code before posting.

Validation run from /home/leonard/euler-lite-worktrees/pr-372:

  • npm run typecheck -- --noEmit
  • npm run test:run -- tests/entities/vault/apy.test.ts tests/utils/repay-utils.test.ts tests/composables/useSavingsRepay.test.ts ✅ (60 tests)
  • npm run test:run ✅ (623 passed, 1 skipped)
  • npm run build

CodeRabbit critique status

I re-checked the 7 CodeRabbit critiques on the current head:

  1. useCollateralSwapRepay post-repay weighted collateral APY for roeAfter — resolved; current and next weighted APYs are now separated.
  2. useRepayHealthMetrics stale projected borrow APY updates — resolved; the projected borrow watcher uses a race guard and active-failure clearing.
  3. usePositionCollateralApy safe fallback — resolved; getCollateralApySnapshot() now catches and returns { supplyUsd: 0, weightedSupplyApy: null }.
  4. borrow refinance same-asset projected path — same-asset path itself is resolved; the watcher no longer requires quote.value for same-asset and uses currentDebt.value there.
  5. collateral swap ROE snapshot failure clearing — no longer actionable as originally stated because the shared snapshot helper now catches and returns the safe snapshot.
  6. multiply current snapshot race guard — resolved.
  7. multiply next snapshot race guard — resolved.

Blocking finding

One blocker remains from the deeper transaction-path pass:

  • Cross-asset debt refinance still derives the quote amount and projected old-debt reduction from the truncated display string fromAmount, while the actual transaction plan/verifier is full-refinance (targetDebt: 0n, currentDebt: currentDebt.value). The same-asset path was fixed to use exact currentDebt.value, but the cross-asset path still has a quote/summary-vs-plan mismatch. For example, 1234567890123456789 at 18 decimals is displayed as 1.23456, which parses back to 1234560000000000000, short by 7890123456789 wei. Dust-sized debts can be much worse proportionally.

Expected invariant: for a “refinance full current debt” flow, the quote request and projected debt summary should use the same exact debt amount as the transaction/verifier path, not the shortened UI display string.

Non-blocking hardening

  • The new collateral portfolio watchers in useSavingsRepay / useCollateralSwapRepay clear refs on invalid dependencies before advancing their race guard, so an older in-flight snapshot can theoretically repopulate stale derived ROE/APY values after invalidation. This is display/derived-metric stale-state hardening, not a direct transaction construction issue, so I’m not treating it as the blocker here.

Test coverage note

The green suite is useful compatibility evidence, but there is still no focused regression coverage for the cross-asset debt-refinance exact-amount invariant. A good test would mount/harness pages/position/[number]/borrow/swap.vue with a high-precision currentDebt, assert the displayed max may be shortened, but verify the quote request/projected summary/full-refinance plan all use exact currentDebt.value for the debt being refinanced.

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