Skip to content

fix(swap-vm): single-sided fromPriceBounds throws at max bound#78

Merged
krboktv merged 2 commits into
masterfrom
cursor/single-sided-frompricebounds-failing-test-f9be
Jul 12, 2026
Merged

fix(swap-vm): single-sided fromPriceBounds throws at max bound#78
krboktv merged 2 commits into
masterfrom
cursor/single-sided-frompricebounds-failing-test-f9be

Conversation

@krboktv

@krboktv krboktv commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the asymmetric edge case in PriceRange.fromPriceBounds reported in 1inch/dapp-ui#3938 (found while fixing DAPP-4462, 1inch/dapp-ui#3954), together with regression tests that reproduce it.

The bug

For a valid single-sided composition at the max bound, fromPriceBounds threw maxPrice should be >= spotPrice, while the mirror case at the min bound round-tripped fine.

Repro (WBTC 8dp / USDT 6dp, range 58,946 → 63,752 USDT per WBTC):

  • reserveLt = 0.442142 WBTC, reserveGt = 0 → implied spot 58,946.000012 ✅ (min side OK, but off by rounding dust)
  • reserveLt = 0, reserveGt = 28,207 USDT → throws ❌

In exact arithmetic the implied spot equals the max bound (amount0 = 0), but integer truncation in computeLiquidityAndPrice (virtualLt = L / sqrtPmax rounds down) lands the derived sqrtSpot ~3.4e-10 (relative) above sqrtPmax, and the PriceRange constructor assert rejects the legitimate composition.

The fix

For single-sided compositions the spot is a bound by construction (bLt = L·(√Pmax − √Pspot)/… = 0 ⟺ spot = max; bGt = L·(√Pspot − √Pmin) = 0 ⟺ spot = min), so fromPriceBounds now short-circuits:

  • reserve0 == 0spotPrice = maxPrice
  • reserve1 == 0spotPrice = minPrice
  • both zero → explicit at least one reserve must be positive assert (previously a cryptic mulDiv: division by zero from deep inside the math)

No math is run for these cases, so no truncation dust — the spot is exactly the bound on both sides now (the min side previously came out at 58,946.000012 instead of 58,946). The constructor assert stays untouched. The both-reserves-positive path is unchanged.

Tests

Added under fromPriceBounds > single-sided compositions:

  • token0-only reserves → spot exactly at min bound (58946)
  • token1-only reserves → spot exactly at max bound (63752) — this was the throwing case
  • both reserves zero → throws at least one reserve must be positive
Test Files  44 passed (44)
     Tests  595 passed (595)

Lint and type-check pass.

Open in Web Open in Cursor 

cursoragent and others added 2 commits July 12, 2026 13:24
…max bound

PriceRange.fromPriceBounds throws 'maxPrice should be >= spotPrice' for a
valid token1-only composition (WBTC/USDT, range 58946-63752): integer
truncation in computeLiquidityAndPrice lands the derived sqrtSpot slightly
above sqrtPmax. The mirror token0-only case at the min bound passes.

Co-authored-by: Kirill <krboktv@gmail.com>
… bound

For single-sided compositions the spot price is a range bound by
construction (reserve0 depletes at max, reserve1 at min), so derive it
directly instead of running computeLiquidityAndPrice, whose integer
truncation could land the implied sqrt spot just outside the bounds and
trip the PriceRange constructor assert. Also reject the degenerate
both-reserves-zero input with a clear error.

Co-authored-by: Kirill <krboktv@gmail.com>
@cursor cursor Bot changed the title test(swap-vm): failing repro — single-sided fromPriceBounds throws at max bound fix(swap-vm): single-sided fromPriceBounds throws at max bound Jul 12, 2026
@sonarqubecloud

Copy link
Copy Markdown

@krboktv krboktv marked this pull request as ready for review July 12, 2026 13:41
@krboktv krboktv merged commit dabecd4 into master Jul 12, 2026
2 checks passed
@krboktv krboktv deleted the cursor/single-sided-frompricebounds-failing-test-f9be branch July 12, 2026 13:41
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