fix(sdk): Ambient route encodes slippage 10x looser than selected (bps treated as per-mille) - #1144
Merged
Merged
Conversation
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 detectedLatest commit: c690e2f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
✅ Deploy Preview for sovryn-dapp ready!
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
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%).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The smart-router passes
options.slippagein basis points (10_000 = 100% — documented ininternal/utils.tsgetMinReturnandconstants.ts; the frontend sendstolerance% × 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:
slippage = 1.0→ min-out of 0Present 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?? 50no-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.)ambient-route.test.ts(4 tests, no network): mocks the sdexCrocEnvboundary and captures what the route actually hands over —getMinReturnbps semantics for 10/50/100/1000 bpsOrderDirectiveencoder (spied),calcImpactmocked to a unit sqrt price → each pool's encodedlimitPriceis1 ± 0.005, buy and sell legs/1000divisor@sovryn/sdkpatch.Verification
tsc --noEmitclean apart from a pre-existing_tests/_fixtures/chain.tserror already ondevelopFollow-ups (not in this PR)
BasisPointstypeSWAP_ROUTES