Direct-hybrid swap shows raw indexer output/slippage with no wallet reconcile, unlike the multi-hop path #471

Closed
opened 2026-07-01 12:57:09 +00:00 by Brouie · 5 comments
Brouie commented 2026-07-01 12:57:09 +00:00 (Migrated from gitlab.com)

Came out of the frontend leg of the security sweep (the #381 hardening umbrella, same indexer-trust family as #449/#450). The direct-hybrid swap path trusts the indexer's "you receive" and slippage numbers straight through to the pre-sign summary, while the multi-hop path in the same file recomputes them against the wallet. So on a direct CW20↔CW20 hybrid trade a tampered/buggy indexer can display an output the chain won't actually hand you.

Where

frontend-dapp/src/utils/directHybridQuote.ts — the indexer branch of quoteDirectHybridSwap:

  • directHybridQuote.ts:66 return_amount: idx.estimated_amount_out
  • directHybridQuote.ts:72 routeSlippagePercent: idx.slippage_percent

Both are lifted verbatim from postRouteSolve (line 60) with no wallet cross-check. The only thing this branch runs against the wallet is preflightSwapRouteSpread (line 64), which checks per-hop spread — it does not recompute the return amount.

Those values flow into the summary in SwapPage.tsx:

  • SwapPage.tsx:851 const outputAmount = simData?.return_amount ?? ''
  • SwapPage.tsx:860-862 expectedSlippagePct from simData.routeSlippagePercent

Contrast the multi-hop indexer branch, which DOES reconcile — SwapPage.tsx:544 runs simulateMultiHopSwap on the wallet and returns result.amount as return_amount (line 561), and derives slippage from that wallet amount via resolveRouteSlippagePercent(result.amount, ...) (lines 564-568). The direct path skips that entirely.

The disclosure copy makes it worse: directHybridQuote.ts:26 (and :28) tells the user the estimate is "checked against your wallet before submit." In this branch nothing shown is checked against the wallet. The only wallet-derived guard is the submit floor, computeDirectHybridMinReturn at SwapPage.tsx:797-807 — and that value is never surfaced in the UI, it's passed straight into swap(...) as minReturn (line 810).

How to hit it

  1. Direct CW20↔CW20 pair, hybrid book leg enabled (willSubmitHybrid true) so the quote goes through quoteDirectHybridSwap → indexer POST /route/solve at directHybridQuote.ts:60, not the LCD fallback at line 81.
  2. Indexer returns an inflated estimated_amount_out (and/or a low slippage_percent).
  3. Summary shows that inflated number as "you receive" with no visible mismatch.
  4. On chain the trade delivers the real pool+book output, bounded below by the wallet-derived min_return from computeDirectHybridMinReturn — which can be well under the displayed figure.

Impact

UX / display-integrity, not a direct drain — funds are still floored by min_return on submit, so a manipulated indexer can't make you accept an arbitrarily bad fill silently; worst case the tx reverts or fills at the wallet floor. But the user signs against a number that isn't real, and the "checked against your wallet before submit" copy actively tells them it is. That's a trust gap, and it's exactly the direct-path blind spot #450 closed on multi-hop.

Fix direction

Two clean options:

  • Mirror the multi-hop path: in the indexer branch of quoteDirectHybridSwap, run the wallet sim (simulateHybridSwap, which the fallback at line 81 already uses) and reconcile — show the wallet amount as return_amount and derive slippage from it like resolveRouteSlippagePercent does, only using the indexer number as a hint. On a big mismatch, warn instead of silently displaying the indexer figure.
  • Or, cheaper: surface the computeDirectHybridMinReturn floor as a "minimum received" line in the summary so what's signed is visible, and drop/qualify the "checked against your wallet" copy for this branch since it isn't.

First option is the real fix and keeps the two paths consistent.

Came out of the frontend leg of the security sweep (the #381 hardening umbrella, same indexer-trust family as #449/#450). The direct-hybrid swap path trusts the indexer's "you receive" and slippage numbers straight through to the pre-sign summary, while the multi-hop path in the same file recomputes them against the wallet. So on a direct CW20↔CW20 hybrid trade a tampered/buggy indexer can display an output the chain won't actually hand you. ## Where `frontend-dapp/src/utils/directHybridQuote.ts` — the indexer branch of `quoteDirectHybridSwap`: - `directHybridQuote.ts:66` `return_amount: idx.estimated_amount_out` - `directHybridQuote.ts:72` `routeSlippagePercent: idx.slippage_percent` Both are lifted verbatim from `postRouteSolve` (line 60) with no wallet cross-check. The only thing this branch runs against the wallet is `preflightSwapRouteSpread` (line 64), which checks per-hop spread — it does not recompute the return amount. Those values flow into the summary in `SwapPage.tsx`: - `SwapPage.tsx:851` `const outputAmount = simData?.return_amount ?? ''` - `SwapPage.tsx:860-862` `expectedSlippagePct` from `simData.routeSlippagePercent` Contrast the multi-hop indexer branch, which DOES reconcile — `SwapPage.tsx:544` runs `simulateMultiHopSwap` on the wallet and returns `result.amount` as `return_amount` (line 561), and derives slippage from that wallet amount via `resolveRouteSlippagePercent(result.amount, ...)` (lines 564-568). The direct path skips that entirely. The disclosure copy makes it worse: `directHybridQuote.ts:26` (and :28) tells the user the estimate is "checked against your wallet before submit." In this branch nothing shown is checked against the wallet. The only wallet-derived guard is the submit floor, `computeDirectHybridMinReturn` at `SwapPage.tsx:797-807` — and that value is never surfaced in the UI, it's passed straight into `swap(...)` as `minReturn` (line 810). ## How to hit it 1. Direct CW20↔CW20 pair, hybrid book leg enabled (`willSubmitHybrid` true) so the quote goes through `quoteDirectHybridSwap` → indexer `POST /route/solve` at `directHybridQuote.ts:60`, not the LCD fallback at line 81. 2. Indexer returns an inflated `estimated_amount_out` (and/or a low `slippage_percent`). 3. Summary shows that inflated number as "you receive" with no visible mismatch. 4. On chain the trade delivers the real pool+book output, bounded below by the wallet-derived `min_return` from `computeDirectHybridMinReturn` — which can be well under the displayed figure. ## Impact UX / display-integrity, not a direct drain — funds are still floored by `min_return` on submit, so a manipulated indexer can't make you accept an arbitrarily bad fill silently; worst case the tx reverts or fills at the wallet floor. But the user signs against a number that isn't real, and the "checked against your wallet before submit" copy actively tells them it is. That's a trust gap, and it's exactly the direct-path blind spot #450 closed on multi-hop. ## Fix direction Two clean options: - Mirror the multi-hop path: in the indexer branch of `quoteDirectHybridSwap`, run the wallet sim (`simulateHybridSwap`, which the fallback at line 81 already uses) and reconcile — show the wallet amount as `return_amount` and derive slippage from it like `resolveRouteSlippagePercent` does, only using the indexer number as a hint. On a big mismatch, warn instead of silently displaying the indexer figure. - Or, cheaper: surface the `computeDirectHybridMinReturn` floor as a "minimum received" line in the summary so what's signed is visible, and drop/qualify the "checked against your wallet" copy for this branch since it isn't. First option is the real fix and keeps the two paths consistent.
PlasticDigits commented 2026-07-01 14:02:15 +00:00 (Migrated from gitlab.com)

Mirror the multi hop path approvdd

Mirror the multi hop path approvdd
PlasticDigits commented 2026-07-01 14:09:14 +00:00 (Migrated from gitlab.com)

mentioned in commit 8750c78583

mentioned in commit 8750c78583fc3995ab774ea92ed11b2d224c35f1
PlasticDigits commented 2026-07-01 14:09:50 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1005

mentioned in merge request !1005
PlasticDigits commented 2026-07-02 02:00:53 +00:00 (Migrated from gitlab.com)

mentioned in commit a793eb6b84

mentioned in commit a793eb6b84b3200f925e40f7a26b3884285fae39
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-02 02:00:55 +00:00
PlasticDigits commented 2026-08-18 00:44:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #559

mentioned in issue #559
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#471
No description provided.