Skip to content

fix(api): value offramp discount input in USD before applying inverted oracle rate#1275

Merged
ebma merged 2 commits into
stagingfrom
fix/offramp-discount-usd-denomination
Jul 17, 2026
Merged

fix(api): value offramp discount input in USD before applying inverted oracle rate#1275
ebma merged 2 commits into
stagingfrom
fix/offramp-discount-usd-denomination

Conversation

@ebma

@ebma ebma commented Jul 17, 2026

Copy link
Copy Markdown
Member

Problem

The offramp discount engine computes expectedOutput = inputAmount × (1 / oraclePrice), which assumes request.inputAmount is USD-denominated. That held for the legacy USDC offramps, but the EVM offramp routes accept BRLA and EURC as input:

  • BRLA → PIX: a 1000 BRLA offramp was treated as 1000 USD, inflating expectedOutput to ~5127 BRL instead of ~999 BRL. Since the subsidy cap is maxSubsidy × expectedOutput, the cap was inflated ~5× too — the observed quote paid a 23.07 BRL subsidy where ~4.5 BRL was intended. This is a real over-subsidy leak on every BRLA→PIX offramp.
  • EURC → SEPA: the mirror-image bug — input undervalued ~×0.87, so those quotes effectively never received a subsidy.

Onramps are unaffected (their input is fiat, matching the non-inverted rate).

Fix

New getUsdDenominatedInputAmount helper values the request input in USD before the inverted-rate conversion:

  • USD-like stables (USD, USDC, USDT, USDC.e, axlUSDC) pass through unchanged — existing USDC/AssetHub flows are byte-identical.
  • Fiat-pegged stables are valued at their peg's FIAT-USD oracle rate (BRLA → BRL, EURC → EUR). For BRLA→BRL this is the same feed as oraclePrice, so the conversion cancels exactly and expected output ≈ input × (1 + discount).
  • Any other token falls back to the bridged USDC amount (evmToEvm.outputAmountDecimal) when available.

Wired into both OffRampDiscountEngine and OffRampAlfredpayDiscountEngine (which had the identical assumption and accepts the same input tokens). Both emit a quote note whenever the USD valuation differs from the raw input amount, so quote metadata makes the denomination visible.

Tests

  • Regression tests in helpers.test.ts, including one reproducing the observed quote's numbers (1000 BRLA at rate 0.194757 must target ~1000 BRL, not ~5134).
  • Full quote-service suite passes (40 tests), typecheck and Biome clean.

Security spec

docs/security-spec/03-ramp-engine/discount-mechanism.md updated: expected-output formula description now documents the USD-valuation step, and a new invariant #13 covers it.

Verification after deploy

Request a BRLA→PIX quote on staging: the subsidy block should show expectedOutputAmountDecimal ≈ input amount (not ~5×), and the notes should include the new valued input … USD line.

…d oracle rate

The offramp expected-output math multiplies the request input amount by the
inverted FIAT-USD oracle rate, assuming the input is USD-denominated. On the
EVM offramp routes the input can be BRLA or EURC: a 1000 BRLA -> PIX offramp
was treated as 1000 USD, inflating expectedOutput ~5x (and with it the
maxSubsidy cap), over-paying the subsidy on every such quote. EURC -> SEPA
offramps were symmetrically under-subsidized to zero.

Value the input in USD first: USD-like stables pass through, fiat-pegged
stables (BRLA, EURC) are valued at their peg's oracle rate, and other tokens
fall back to the bridged USDC amount. Applied to both OffRampDiscountEngine
and OffRampAlfredpayDiscountEngine, with a quote note whenever the valuation
differs from the raw input amount.
@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for vrtx-dashboard canceled.

Name Link
🔨 Latest commit 5fece49
🔍 Latest deploy log https://app.netlify.com/projects/vrtx-dashboard/deploys/6a5a7407ef77a700082be93c

@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 5fece49
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6a5a74074f5fde0007254fc4
😎 Deploy Preview https://deploy-preview-1275--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 5fece49
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6a5a74070dda25000807a8a3
😎 Deploy Preview https://deploy-preview-1275--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI 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.

Pull request overview

This PR fixes a denomination bug in the API quote discount engines for offramps where request.inputAmount was implicitly treated as USD, causing over-subsidy on BRLA→PIX and under-subsidy on EURC→SEPA. It introduces a USD-valuation step for offramp inputs before applying the inverted FIAT-USD oracle rate, and updates the security spec accordingly.

Changes:

  • Add getUsdDenominatedInputAmount to value offramp input amounts in USD (USD-like passthrough, fiat-pegged stable valuation via FIAT-USD rate, fallback to bridged USDC amount when present).
  • Wire the USD-valuation into both OffRampDiscountEngine and OffRampAlfredpayDiscountEngine, including quote notes when valuation differs from raw input.
  • Add regression tests for the helper and update the discount mechanism security spec + invariants.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/security-spec/03-ramp-engine/discount-mechanism.md Documents the USD-valuation step for offramp expectedOutput and adds an invariant to prevent misdenomination.
apps/api/src/api/services/quote/engines/discount/offramp.ts Values offramp input in USD before computing expectedOutput with an inverted oracle rate; emits an explanatory note.
apps/api/src/api/services/quote/engines/discount/offramp-alfredpay.ts Applies the same USD-valuation fix to the Alfredpay offramp discount engine; emits an explanatory note.
apps/api/src/api/services/quote/engines/discount/helpers.ts Adds getUsdDenominatedInputAmount and stablecoin/peg mappings used by offramp discount engines.
apps/api/src/api/services/quote/engines/discount/helpers.test.ts Adds tests and a regression case covering the BRLA misdenomination behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/api/src/api/services/quote/engines/discount/helpers.ts
Comment thread apps/api/src/api/services/quote/engines/discount/helpers.test.ts
…allback

Address CI failure and PR review comments:

- Update the SELL 100 BRLA → BRL pricing golden from 500.00 to 100.00 BRL.
  The old value encoded the pre-fix bug (100 BRLA valued as 100 USD → 500 BRL);
  100 BRLA is worth ~100 BRL at the 1:1 peg. Only outputAmount changed.
- getUsdDenominatedInputAmount no longer throws on a FIAT-USD rate-feed failure
  for a pegged stable; it falls back to the bridged USDC amount (or raw input),
  so a transient price-feed outage cannot fail the quote from discount math.
- Add EURC valuation + under-subsidy regression tests and rate-failure fallback
  tests to the helper suite.
- Reflect the graceful fallback in the discount-mechanism security spec.
@ebma

ebma commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Pushed 5fece49 addressing the CI failure and both review comments.

CI (golden test). The failing SELL 100 BRLA on Base → BRL golden encoded the pre-fix bug: it expected outputAmount: 500.00 (100 BRLA valued as 100 USD → 500 BRL at the 5 BRL/USD fixture). With the fix, 100 BRLA is correctly valued at ~20 USD and the offramp yields ~100 BRL (the 1:1 BRLA↔BRL peg). Updated the golden to 100.00only outputAmount changed; every fee field is unchanged. This is the intentional-golden-update path the test file calls for, not a fix-to-green.

Comment 1 (rate-fetch can fail the quote). Good catch — getUsdDenominatedInputAmount now wraps the FIAT-USD lookup in a try/catch and falls back to the bridged USDC amount (evmToEvm.outputAmountDecimal), or the raw input as a last resort, logging a warning instead of throwing. A transient price-feed outage can no longer fail a quote from discount math. Covered by two new fallback tests; the security spec notes the behavior.

Comment 2 (no EURC coverage). Added an explicit EURC valuation test plus an EURC→SEPA under-subsidy regression test (mirror of the BRLA case: raw 1000 EURC would undershoot to ~862 EUR, silently zeroing the subsidy).

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

apps/api/src/api/services/quote/engines/discount/offramp.ts:64

  • OffRampDiscountEngine is used for both PIX and SEPA-EVM routes (see offramp-to-sepa-evm.strategy.ts), but the anchor-fee variable name and log/quote-note strings still hard-code “BRL”. This makes quote notes and logs misleading for non-BRL offramps (e.g. EURC→SEPA) even though the math is currency-agnostic.
    // Account for the anchor fee deducted in the Finalize stage, which reduces the user's received amount.
    // We need to add it back to the expected output to calculate the subsidy correctly.
    const anchorFeeInBrl = ctx.fees?.displayFiat?.anchor ? new Big(ctx.fees.displayFiat.anchor) : new Big(0);

@ebma
ebma merged commit 45aa108 into staging Jul 17, 2026
9 checks passed
@ebma
ebma deleted the fix/offramp-discount-usd-denomination branch July 17, 2026 18:48
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