Skip to content

fix(sdk): Ambient route encodes slippage 10x looser than selected (bps treated as per-mille) - #1144

Merged
tjcloa merged 3 commits into
developfrom
fix/ambient-slippage-bps
Aug 6, 2026
Merged

fix(sdk): Ambient route encodes slippage 10x looser than selected (bps treated as per-mille)#1144
tjcloa merged 3 commits into
developfrom
fix/ambient-slippage-bps

Conversation

@tjcloa

@tjcloa tjcloa commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

The smart-router passes options.slippage in basis points (10_000 = 100% — documented in internal/utils.ts getMinReturn and constants.ts; the frontend sends tolerance% × 100). The Ambient route divided that value by 1000 instead of 10000 before handing it to @sovryn/sdex, which consumes a plain fraction (0.01 = 1%).

Result: the slippage bound actually encoded into BOB swap calldata was 10× looser than the tolerance the user selected and the UI displayed as Minimum received:

  • default 0.5% tolerance → enforced as 5% (single-pool min-out)
  • multi-hop: the fraction bounds a Q64.64 sqrt price, so ≈ +10.25% / −9.75% by direction
  • a user entering 10 (%) → slippage = 1.0 → min-out of 0

Present since the BOB launch (0cfa22b0, May 2024) and in released @sovryn/sdk ≤ 2.0.10. Reported via advisory GHSA-jx33-xg6c-px39 (filed under Sovryn-smart-contracts, but the defect lives here; no contracts affected — they faithfully enforce whatever the calldata says).

Fix

  • ambient.ts: divide by 10000. The ?? 50 no-option fallback is kept and now means what the author intended: 50 bps = 0.5%, matching the UI's preset tolerance on both Sovryn AMM and Sovryn DEX on BOB. (The UI always passes an explicit value, so the fallback only affects direct SDK consumers.)
  • New regression suite ambient-route.test.ts (4 tests, no network): mocks the sdex CrocEnv boundary and captures what the route actually hands over —
    • 50 bps → 0.005 on the single-pool path
    • no-option fallback → 0.005 (0.5%)
    • cross-check against getMinReturn bps semantics for 10/50/100/1000 bps
    • multi-hop: real OrderDirective encoder (spied), calcImpact mocked to a unit sqrt price → each pool's encoded limitPrice is 1 ± 0.005, buy and sell legs
    • each test verified to fail against the old /1000 divisor
  • Changeset: @sovryn/sdk patch.

Verification

  • sdk jest suite 77/77 green; eslint clean; tsc --noEmit clean apart from a pre-existing _tests/_fixtures/chain.ts error already on develop
  • ambient.ts line coverage 21% → 54%; every remaining uncovered line is outside the slippage flow (quote/pairs/approve, error paths)

Follow-ups (not in this PR)

  • Centralize bps→fraction conversion in the smart-router layer so routes can't each pick a divisor; consider a branded BasisPoints type
  • Cross-route invariant test spanning all SWAP_ROUTES
  • Clamp the Convert-page tolerance input (currently accepts up to 100%)

tjcloa added 2 commits August 6, 2026 17:45
The smart-router passes slippage in basis points (10_000 = 100%,
documented in internal/utils.getMinReturn and constants.ts), and sdex's
CrocSwapPlan consumes a plain fraction (0.01 = 1%). The Ambient route
divided the bps value by 1000, so the slippage bound encoded into BOB
swap calldata was 10x looser than the tolerance the user selected and
the UI displayed as 'Minimum received' (default 0.5% enforced as 5%;
~10.25% on multi-hop routes because the bound is applied to a Q64.64
sqrt price). Reported in advisory GHSA-jx33-xg6c-px39.

Divide by 10000 and use the shared DEFAULT_SWAP_SLIPPAGE constant as
the fallback (1%, consistent with getMinReturn's default) instead of a
bare literal. Add route-level regression tests that capture the
fraction handed to sdex and assert it matches getMinReturn's
basis-point semantics across tolerance values.
Extend the slippage-encoding suite to the long-form multi-hop path:
drive an A->B->C swap through the real OrderDirective encoder (spied),
with calcImpact mocked to a unit Q64.64 sqrt price, and assert each
pool's encoded limitPrice is finalPrice * (1 +/- 0.005) for 50 bps —
buy and sell legs. Verified to fail (1 +/- 0.05) when the old /1000
divisor is reintroduced. ambient.ts coverage 21% -> 54%; all remaining
uncovered lines are outside the slippage flow.
@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c690e2f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sovryn/sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for sovryn-dapp ready!

Name Link
🔨 Latest commit c690e2f
🔍 Latest deploy log https://app.netlify.com/projects/sovryn-dapp/deploys/6a74a65e962967000866340b
😎 Deploy Preview https://deploy-preview-1144.preview.sovryns.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

Decision (Tyrone): the UI preset tolerance is 0.5% on both Sovryn AMM
and Sovryn DEX on BOB, so the route's no-option fallback keeps the
original author's intended 50 bps = 0.5% instead of the SDK-wide
DEFAULT_SWAP_SLIPPAGE (1%) introduced in the previous commit. The UI
always passes an explicit value; this only affects direct SDK
consumers.
@tjcloa
tjcloa merged commit 6917f90 into develop Aug 6, 2026
7 checks passed
@tjcloa
tjcloa deleted the fix/ambient-slippage-bps branch August 6, 2026 23:29
tjcloa added a commit that referenced this pull request Aug 6, 2026
…9) (#1148)

options.slippage crosses every route.swap() in basis points (10_000 =
100%). The Ambient route once divided by 1_000, encoding a 10x looser
min-out bound (GHSA-jx33-xg6c-px39, fixed in #1144). This suite pins,
fully offline (fake providers / mocked protocol boundaries):

- AMM and Ambient — the only routes consuming options.slippage — derive
  the same effective min-out fraction (1 - bps/10_000) for a fixed
  tolerance, checked across 10..1000 bps.
- MyntBasset, MyntFixedRate and MocIntegration produce bit-identical
  calldata regardless of the slippage option, proving no bps-derived
  quantity exists in them to mis-convert.
- ZeroRedemption passes no bps-derived tolerance across the lib-ethers
  boundary (maxRedemptionRate stays undefined); its swap() needs live
  Liquity chain state, so that boundary is mocked and documented.
- A completeness guard fails when a route is added to smartRoutes or
  DEFAULT_SWAP_ROUTES without extending this suite.

Mutation-verified: reintroducing the /1000 bug fails exactly the two
cross-route fraction tests.

Branch note: stacked on fix/ambient-slippage-bps (PR #1144) so the
Ambient assertions run against the fixed conversion. Committed with
--no-verify: the husky test:staged hook mis-detects changed files under
linked git worktrees and runs unrelated full suites against the main
checkout (duplicate-React crashes); the sdk suite (86 tests), sdk
eslint, and lint-staged were all run manually and pass.
tjcloa added a commit that referenced this pull request Aug 6, 2026
… at 49% (#1146)

* feat(convert): warn above 5% slippage tolerance, hard-cap input at 49%

Post GHSA-jx33-xg6c-px39 (#1144) the Ambient route encodes slippage
correctly, but a 100% tolerance still encodes a zero minimum output.
Close the top-of-range footgun in the Convert page UI:

- warning badge (frontrun risk) when entered tolerance exceeds 5%
- >49% is invalid: input flagged, error badge shown, confirm disabled;
  enforced in logic because the HTML max attribute alone does not
  block typed values (max lowered from 100 to 49 as well)
- boundary tests: 5 / 5.01 / 6 / 49 / 49.01 / 50 / 100

DEFAULT_SLIPPAGE_TOLERANCE (0.5) and the unrelated price-impact gate
MAXIMUM_ALLOWED_SLIPPAGE (1.5) are unchanged. UI-only; SDK untouched.

* feat(convert): make the 5% slippage warning inclusive (>=5, not >5)

Tyrone's call: warn at exactly 5% too, to catch 5 typed instead of 0.5.
Boundary tests updated: 4.99 no warning, 5 warns; visually verified on
the dev server (badge shows at exactly 5%).
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.

1 participant