@exponent-labs/market-three-math 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,8 @@
2
2
  * CLMM Add Liquidity simulation
3
3
  * Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
4
4
  */
5
+ import { Ticks } from "@exponent-labs/exponent-fetcher"
6
+
5
7
  import { simulateSwap, simulateSwapExactOut } from "./swapV2"
6
8
  import {
7
9
  AddLiquidityArgs,
@@ -12,15 +14,22 @@ import {
12
14
  MarketThreeState,
13
15
  SwapDirection,
14
16
  } from "./types"
15
- import { EffSnap, normalizedTimeRemaining, TicksWrapper } from "./utilsV2"
16
17
  import { findTickByKey, getSuccessorTickKey } from "./utils"
17
- import { Ticks } from "@exponent-labs/exponent-fetcher"
18
+ import { EffSnap, TicksWrapper, normalizedTimeRemaining } from "./utilsV2"
18
19
 
19
20
  const TICK_KEY_BASE_POINTS = 1_000_000
20
21
  // Keep this in sync with exponent_clmm's add_liquidity helper so off-chain
21
22
  // quotes match the on-chain classic deposit path as closely as possible.
22
- const GAP_TOKEN_NEEDS = 5
23
- const SWAP_EXACT_OUT_SY_HEADROOM = 10
23
+ const GAP_TOKEN_NEEDS = 20
24
+ const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100
25
+ const SWAP_EXACT_OUT_SY_HEADROOM = 100
26
+ const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000
27
+ const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000
28
+ const FLASH_SY_POSITION_HEADROOM = 1_000
29
+ const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000
30
+ const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0
31
+ const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000
32
+ const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0
24
33
 
25
34
  const EMPTY_CROSSING_TICK_STATE: CrossingTickState = {
26
35
  principalPt: 0,
@@ -416,12 +425,12 @@ function simulateAccruePrincipalForDeposit(params: {
416
425
  return predecessor ?? candidateKey
417
426
  }
418
427
 
419
- const visitIntervals = (startKey: number, priceStart: number, priceEnd: number, visitor: (params: {
420
- leftKey: number
421
- rightKey: number
422
- leftPrice: number
423
- rightPrice: number
424
- }) => void) => {
428
+ const visitIntervals = (
429
+ startKey: number,
430
+ priceStart: number,
431
+ priceEnd: number,
432
+ visitor: (params: { leftKey: number; rightKey: number; leftPrice: number; rightPrice: number }) => void,
433
+ ) => {
425
434
  if (!(priceStart < priceEnd)) {
426
435
  return
427
436
  }
@@ -953,8 +962,7 @@ export function simulateWrapperProvideLiquidity(
953
962
  const priceEffUpper = snap.getEffectivePrice(upperPrice)
954
963
 
955
964
  // Step 3: Get crossing tick state (matches wrapper_provide_liquidity.rs)
956
- const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } =
957
- getCrossingTickStateFromTicks(ticks)
965
+ const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks)
958
966
 
959
967
  // Step 4: Calculate mock token needs using compute_token_needs_with_crossing
960
968
  // max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
@@ -997,6 +1005,10 @@ export function simulateWrapperProvideLiquidity(
997
1005
  syExchangeRate,
998
1006
  })
999
1007
 
1008
+ if (!hasLpSupplyCapacity(marketState, depositResult.deltaL)) {
1009
+ return null
1010
+ }
1011
+
1000
1012
  return {
1001
1013
  lpOut: depositResult.deltaL,
1002
1014
  ytOut: Math.floor(ytOut),
@@ -1047,9 +1059,9 @@ export function simulateSwapAndSupply(
1047
1059
  // Wrapper provide-liquidity-base debits base as:
1048
1060
  // base_needed = ceil(total_sy_spent * sy_exchange_rate)
1049
1061
  // So the strict SY budget for a user-provided base input is floor(base / rate).
1050
- const syBudget = convertBaseToSyBudget(amountBase)
1062
+ const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate)
1051
1063
 
1052
- if (syBudget <= 0) {
1064
+ if (syBudget <= 0 || syBudget < MIN_SWAP_AND_SUPPLY_SY_BUDGET) {
1053
1065
  return {
1054
1066
  lpOut: 0,
1055
1067
  ptToBuy: 0,
@@ -1062,6 +1074,11 @@ export function simulateSwapAndSupply(
1062
1074
  }
1063
1075
  }
1064
1076
 
1077
+ const flashSyAvailable = Math.max(0, Number(marketState.financials.syBalance) - FLASH_SY_POSITION_HEADROOM)
1078
+ if (syBudget > flashSyAvailable) {
1079
+ return null
1080
+ }
1081
+
1065
1082
  const MOCK_AMOUNT = 1e9
1066
1083
  const mockNeeds = simulateAddLiquidity(marketState, {
1067
1084
  lowerTick,
@@ -1070,8 +1087,6 @@ export function simulateSwapAndSupply(
1070
1087
  maxPt: MOCK_AMOUNT,
1071
1088
  syExchangeRate,
1072
1089
  })
1073
- const searchBudget = Math.max(0, syBudget - SWAP_EXACT_OUT_SY_HEADROOM)
1074
-
1075
1090
  // Wrapper flow still executes a flash swap even when external_pt_to_buy is 0
1076
1091
  // (effective exact-out target becomes 2 due +2 safety margin).
1077
1092
  let externalPtToBuy = 0
@@ -1103,6 +1118,15 @@ export function simulateSwapAndSupply(
1103
1118
  externalPtToBuy = Math.max(0, Math.floor(guessSwap.amountOut))
1104
1119
  }
1105
1120
  }
1121
+ const baseSwapExactOutSyHeadroom =
1122
+ externalPtToBuy >= SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD
1123
+ ? SWAP_EXACT_OUT_SY_HEADROOM
1124
+ : MIN_SWAP_EXACT_OUT_SY_HEADROOM
1125
+ const swapExactOutSyHeadroom = syBudget >= LARGE_SWAP_AND_SUPPLY_SY_BUDGET ? 0 : baseSwapExactOutSyHeadroom
1126
+ const simulatedOverspendTolerance =
1127
+ syBudget >= LARGE_SWAP_AND_SUPPLY_SY_BUDGET ? MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS : 0
1128
+ const searchBudget = Math.max(0, syBudget - swapExactOutSyHeadroom + simulatedOverspendTolerance)
1129
+ const exactOutEstimateSlack = mockNeeds.sySpent === 0 ? PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK : 0
1106
1130
 
1107
1131
  const candidateCache = new Map<number, SwapAndSupplyCandidate | null>()
1108
1132
  const evaluateCandidate = (ptConstraint: number) => {
@@ -1118,13 +1142,35 @@ export function simulateSwapAndSupply(
1118
1142
  syBudget,
1119
1143
  key,
1120
1144
  syExchangeRate,
1145
+ exactOutEstimateSlack,
1121
1146
  )
1122
1147
  candidateCache.set(key, candidate)
1123
1148
  return candidate
1124
1149
  }
1150
+
1151
+ if (mockNeeds.ptSpent <= 0) {
1152
+ const selected = evaluateCandidate(0)
1153
+ if (!selected || !hasLpSupplyCapacity(marketState, selected.lpOut)) {
1154
+ return null
1155
+ }
1156
+ if (!isSyDebitWithinTolerance(syBudget, selected.totalSySpent, simulatedOverspendTolerance)) {
1157
+ return null
1158
+ }
1159
+
1160
+ return {
1161
+ lpOut: selected.lpOut,
1162
+ ptToBuy: 0,
1163
+ syConstraint: syBudget,
1164
+ syForSwap: selected.tradeSySpent,
1165
+ syRemainder: selected.syRemainderAfterSwap,
1166
+ ptFromSwap: selected.tradePtOut,
1167
+ syDeposited: selected.depositSySpent,
1168
+ ptDeposited: selected.depositPtSpent,
1169
+ }
1170
+ }
1171
+
1125
1172
  const ptCap = 1_000_000_000
1126
1173
  let bestUnderBudget: SwapAndSupplyCandidate | null = null
1127
- let leastOverspend: SwapAndSupplyCandidate | null = null
1128
1174
  const considerCandidate = (candidate: SwapAndSupplyCandidate | null) => {
1129
1175
  if (!candidate) {
1130
1176
  return
@@ -1139,14 +1185,6 @@ export function simulateSwapAndSupply(
1139
1185
  }
1140
1186
  return
1141
1187
  }
1142
-
1143
- if (
1144
- !leastOverspend ||
1145
- candidate.totalSySpent < leastOverspend.totalSySpent ||
1146
- (candidate.totalSySpent === leastOverspend.totalSySpent && candidate.lpOut > leastOverspend.lpOut)
1147
- ) {
1148
- leastOverspend = candidate
1149
- }
1150
1188
  }
1151
1189
 
1152
1190
  // Always evaluate from zero to establish a valid lower bound.
@@ -1200,14 +1238,21 @@ export function simulateSwapAndSupply(
1200
1238
  considerCandidate(evaluateCandidate(pt))
1201
1239
  }
1202
1240
 
1203
- const selected = bestUnderBudget ?? leastOverspend
1241
+ const selected = bestUnderBudget
1204
1242
  if (!selected) {
1205
- throw new Error("Unable to simulate swap & supply for initial PT constraint")
1243
+ throw new Error("Unable to simulate swap & supply within SY budget")
1244
+ }
1245
+ if (!hasLpSupplyCapacity(marketState, selected.lpOut)) {
1246
+ return null
1247
+ }
1248
+ if (!isSyDebitWithinTolerance(syBudget, selected.totalSySpent, simulatedOverspendTolerance)) {
1249
+ return null
1206
1250
  }
1207
1251
  if (process.env.DEBUG_SWAP_SUPPLY === "1") {
1208
1252
  console.log("[simulateSwapAndSupply]", {
1209
1253
  syBudget,
1210
1254
  searchBudget,
1255
+ swapExactOutSyHeadroom,
1211
1256
  mockNeeds,
1212
1257
  externalPtToBuy,
1213
1258
  selected,
@@ -1248,6 +1293,7 @@ function simulateSwapAndSupplyForPtConstraint(
1248
1293
  syBudget: number,
1249
1294
  ptConstraint: number,
1250
1295
  syExchangeRate: number,
1296
+ exactOutEstimateSlack: number,
1251
1297
  ): SwapAndSupplyCandidate | null {
1252
1298
  let swapResult: {
1253
1299
  sySpent: number
@@ -1261,6 +1307,7 @@ function simulateSwapAndSupplyForPtConstraint(
1261
1307
  syBudget,
1262
1308
  ptConstraint,
1263
1309
  syExchangeRate,
1310
+ exactOutEstimateSlack,
1264
1311
  )
1265
1312
  } catch {
1266
1313
  // Budget insufficient for this PT constraint
@@ -1268,7 +1315,6 @@ function simulateSwapAndSupplyForPtConstraint(
1268
1315
  }
1269
1316
 
1270
1317
  const depositState = swapResult.postMarketState ?? marketState
1271
- const syAvailableForDeposit = Math.max(0, syBudget - swapResult.sySpent)
1272
1318
  const depositResult = simulateAddLiquidity(depositState, {
1273
1319
  lowerTick,
1274
1320
  upperTick,
@@ -1302,9 +1348,39 @@ function simulateSwapAndSupplyForPtConstraint(
1302
1348
  }
1303
1349
  }
1304
1350
 
1305
- function convertBaseToSyBudget(amountBase: number): number {
1351
+ function convertBaseToSyBudget(amountBase: number, syExchangeRate: number): number {
1306
1352
  const baseAmount = Math.max(0, Math.floor(amountBase))
1307
- return baseAmount
1353
+ if (syExchangeRate <= 0) {
1354
+ return 0
1355
+ }
1356
+ return Math.floor(baseAmount / syExchangeRate)
1357
+ }
1358
+
1359
+ function hasLpSupplyCapacity(marketState: MarketThreeState, deltaL: number): boolean {
1360
+ const deltaLiquidity = Math.ceil(deltaL)
1361
+ if (!Number.isFinite(deltaLiquidity) || deltaLiquidity < 0 || !Number.isSafeInteger(deltaLiquidity)) {
1362
+ return false
1363
+ }
1364
+
1365
+ const liquidityBalance = BigInt(marketState.financials.liquidityBalance.toString())
1366
+ const maxLpSupply = BigInt(marketState.configurationOptions.maxLpSupply.toString())
1367
+
1368
+ return liquidityBalance + BigInt(deltaLiquidity) <= maxLpSupply
1369
+ }
1370
+
1371
+ function isSyDebitWithinTolerance(
1372
+ syBudget: number,
1373
+ totalSySpent: number,
1374
+ simulatedOverspendTolerance: number,
1375
+ ): boolean {
1376
+ const targetSyDebit = Math.max(0, Math.floor(syBudget))
1377
+ const estimatedSyDebit = Math.max(0, Math.floor(totalSySpent))
1378
+ const underSpendLamports = targetSyDebit - estimatedSyDebit
1379
+ if (underSpendLamports < 0) {
1380
+ return Math.abs(underSpendLamports) <= simulatedOverspendTolerance
1381
+ }
1382
+
1383
+ return underSpendLamports <= MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS
1308
1384
  }
1309
1385
 
1310
1386
  function simulateBuyPtExactOutWrapper(
@@ -1312,12 +1388,14 @@ function simulateBuyPtExactOutWrapper(
1312
1388
  syBudget: number,
1313
1389
  ptConstraint: number,
1314
1390
  syExchangeRate: number,
1391
+ exactOutEstimateSlack: number,
1315
1392
  ): {
1316
1393
  sySpent: number
1317
1394
  ptOut: number
1318
1395
  postMarketState?: MarketThreeState
1319
1396
  } {
1320
1397
  const maxSyBudget = Math.max(0, Math.floor(syBudget))
1398
+ const exactOutSyConstraint = maxSyBudget + Math.max(0, Math.floor(exactOutEstimateSlack))
1321
1399
  const effectivePtOutTarget = Math.max(0, Math.floor(ptConstraint)) + 2
1322
1400
 
1323
1401
  if (maxSyBudget === 0) {
@@ -1334,7 +1412,7 @@ function simulateBuyPtExactOutWrapper(
1334
1412
  amountOut: targetPtOut,
1335
1413
  syExchangeRate,
1336
1414
  isCurrentFlashSwap: true,
1337
- amountInConstraint: maxSyBudget,
1415
+ amountInConstraint: exactOutSyConstraint,
1338
1416
  })
1339
1417
  }
1340
1418
 
package/src/swapV2.ts CHANGED
@@ -3,23 +3,19 @@
3
3
  * Closely mirrors the Rust on-chain implementation from swap.rs
4
4
  * Uses TicksWrapper to simulate RB-tree behavior
5
5
  */
6
- import {
7
- MarketThreeState,
8
- SwapArgs,
9
- SwapDirection,
10
- SwapExactOutArgs,
11
- SwapOutcomeV2,
12
- } from "./types"
13
- import {
14
- EffSnap,
15
- TicksWrapper,
16
- calculateFeeRate,
17
- getFeeFromAmount,
18
- normalizedTimeRemaining,
19
- } from "./utilsV2"
6
+ import { MarketThreeState, SwapArgs, SwapDirection, SwapExactOutArgs, SwapOutcomeV2 } from "./types"
7
+ import { EffSnap, TicksWrapper, calculateFeeRate, getFeeFromAmount, normalizedTimeRemaining } from "./utilsV2"
20
8
 
21
9
  const BASE_POINTS = 10000
22
10
 
11
+ function computeFlashFeeOut(ytValue: number, grossOut: number, lpFeeRate: number): number {
12
+ if (grossOut === 0) {
13
+ return 0
14
+ }
15
+
16
+ return Math.min(grossOut, Math.max(1, getFeeFromAmount(ytValue, lpFeeRate)))
17
+ }
18
+
23
19
  /**
24
20
  * Simulate a swap on the CLMM market (V2 - mirrors Rust closely)
25
21
  * This is a pure function that does not mutate the market state
@@ -364,7 +360,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
364
360
  if (args.isCurrentFlashSwap) {
365
361
  const syInBase = syInSegmentU64 * args.syExchangeRate
366
362
  const ytValue = ptOutGrossU64 - syInBase
367
- totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
363
+ totalFeeOut = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossU64, lpFeeRate)
368
364
  } else {
369
365
  totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
370
366
  }
@@ -605,6 +601,8 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
605
601
 
606
602
  const totalFeeOutBoundary = getFeeFromAmount(ptOutGrossBoundaryU64, lpFeeRate)
607
603
  const ptOutNetBoundary = Math.max(0, ptOutGrossBoundaryU64 - totalFeeOutBoundary)
604
+ let boundarySegmentSeed: ReturnType<typeof calculateSwapSegment> | null = null
605
+ let ptOutNetBoundaryEffectiveSeed: number | null = null
608
606
 
609
607
  if (ptOutNetBoundary === 0) {
610
608
  const boundarySegment = calculateSwapSegment(
@@ -619,87 +617,115 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
619
617
  args.isCurrentFlashSwap,
620
618
  )
621
619
 
622
- let syInBoundary = boundarySegment.syInSegmentU64
623
- if (ptOutGrossBoundaryU64 > 0 && syInBoundary === 0) {
624
- syInBoundary = 1
625
- }
620
+ const ptOutNetBoundaryActual = Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
621
+ if (ptOutNetBoundaryActual > 0) {
622
+ boundarySegmentSeed = boundarySegment
623
+ ptOutNetBoundaryEffectiveSeed = ptOutNetBoundaryActual
624
+ } else {
625
+ let syInBoundary = boundarySegment.syInSegmentU64
626
+ if (ptOutGrossBoundaryU64 > 0 && syInBoundary === 0) {
627
+ syInBoundary = 1
628
+ }
626
629
 
627
- if (
628
- amountInConstraint !== undefined &&
629
- amountInConsumed + syInBoundary > amountInConstraint
630
- ) {
631
- throw new Error("Insufficient SY budget for exact-out trade")
632
- }
630
+ if (amountInConstraint !== undefined && amountInConsumed + syInBoundary > amountInConstraint) {
631
+ throw new Error("Insufficient SY budget for exact-out trade")
632
+ }
633
633
 
634
- amountInConsumed += syInBoundary
634
+ amountInConsumed += syInBoundary
635
635
 
636
- if (boundarySegment.totalFeeOut > 0) {
637
- const protocolFeeOut = Math.floor((boundarySegment.totalFeeOut * protocolFeeBps) / BASE_POINTS)
638
- const lpFeeOut = boundarySegment.totalFeeOut - protocolFeeOut
639
- feeLpOutU64 += lpFeeOut
640
- feeProtocolOutU64 += protocolFeeOut
641
- }
636
+ if (boundarySegment.totalFeeOut > 0) {
637
+ const protocolFeeOut = Math.floor((boundarySegment.totalFeeOut * protocolFeeBps) / BASE_POINTS)
638
+ const lpFeeOut = boundarySegment.totalFeeOut - protocolFeeOut
639
+ feeLpOutU64 += lpFeeOut
640
+ feeProtocolOutU64 += protocolFeeOut
641
+ }
642
642
 
643
- if (syInBoundary > 0 || boundarySegment.ptOutGrossActual > 0) {
644
- ticksWrapper.setPrincipals(
643
+ if (syInBoundary > 0 || boundarySegment.ptOutGrossActual > 0) {
644
+ ticksWrapper.setPrincipals(
645
+ currentLeftBoundaryKey,
646
+ principalPt - BigInt(boundarySegment.ptOutGrossActual),
647
+ principalSy + BigInt(syInBoundary),
648
+ )
649
+ }
650
+
651
+ currentPriceSpot = boundarySegment.spotPriceNew
652
+
653
+ const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
645
654
  currentLeftBoundaryKey,
646
- principalPt - BigInt(boundarySegment.ptOutGrossActual),
647
- principalSy + BigInt(syInBoundary),
648
- )
655
+ currentPriceSpot,
656
+ activeLiquidityU64,
657
+ activeLiquidityF64,
658
+ })
659
+ if (!crossed) break
660
+ currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
661
+ currentPriceSpot = crossed.currentPriceSpot
662
+ activeLiquidityU64 = crossed.activeLiquidityU64
663
+ activeLiquidityF64 = crossed.activeLiquidityF64
664
+ continue
649
665
  }
666
+ }
650
667
 
651
- currentPriceSpot = boundarySegment.spotPriceNew
652
-
653
- const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
654
- currentLeftBoundaryKey,
655
- currentPriceSpot,
656
- activeLiquidityU64,
657
- activeLiquidityF64,
658
- })
659
- if (!crossed) break
660
- currentLeftBoundaryKey = crossed.currentLeftBoundaryKey
661
- currentPriceSpot = crossed.currentPriceSpot
662
- activeLiquidityU64 = crossed.activeLiquidityU64
663
- activeLiquidityF64 = crossed.activeLiquidityF64
664
- continue
668
+ let ptOutNetBoundaryEffective = ptOutNetBoundaryEffectiveSeed ?? ptOutNetBoundary
669
+ let precomputedBoundarySegment: ReturnType<typeof calculateSwapSegment> | null = null
670
+ if (amountOutLeft >= ptOutNetBoundary) {
671
+ const boundarySegment =
672
+ boundarySegmentSeed ??
673
+ calculateSwapSegment(
674
+ ptOutGrossBoundaryU64,
675
+ lTradeF64,
676
+ currentPriceSpot,
677
+ duToLeft,
678
+ anchorULeft,
679
+ snapshot,
680
+ cEffOld,
681
+ lpFeeRate,
682
+ args.isCurrentFlashSwap,
683
+ )
684
+ const ptOutNetBoundaryActual =
685
+ ptOutNetBoundaryEffectiveSeed ?? Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
686
+ ptOutNetBoundaryEffective = ptOutNetBoundaryActual
687
+ if (ptOutNetBoundaryActual > 0 && amountOutLeft >= ptOutNetBoundaryActual) {
688
+ precomputedBoundarySegment = boundarySegment
689
+ }
665
690
  }
666
691
 
667
- const targetNet = Math.min(amountOutLeft, ptOutNetBoundary)
668
- const grossCandidate = findGrossForTargetNet(targetNet, 0, ptOutGrossBoundaryU64, (gross) =>
669
- calculateNetPtOut(
670
- gross,
692
+ const targetNet = Math.min(amountOutLeft, ptOutNetBoundaryEffective)
693
+ const grossCandidate = precomputedBoundarySegment
694
+ ? ptOutGrossBoundaryU64
695
+ : findGrossForTargetNet(targetNet, Math.min(targetNet, ptOutGrossBoundaryU64), ptOutGrossBoundaryU64, (gross) =>
696
+ calculateNetPtOut(
697
+ gross,
698
+ lTradeF64,
699
+ currentPriceSpot,
700
+ duToLeft,
701
+ anchorULeft,
702
+ lpFeeRate,
703
+ args.isCurrentFlashSwap,
704
+ snapshot,
705
+ cEffOld,
706
+ ),
707
+ )
708
+
709
+ const segment =
710
+ precomputedBoundarySegment ??
711
+ calculateSwapSegment(
712
+ grossCandidate,
671
713
  lTradeF64,
672
714
  currentPriceSpot,
673
715
  duToLeft,
674
716
  anchorULeft,
675
- lpFeeRate,
676
- args.isCurrentFlashSwap,
677
717
  snapshot,
678
718
  cEffOld,
679
- ),
680
- )
681
-
682
- const segment = calculateSwapSegment(
683
- grossCandidate,
684
- lTradeF64,
685
- currentPriceSpot,
686
- duToLeft,
687
- anchorULeft,
688
- snapshot,
689
- cEffOld,
690
- lpFeeRate,
691
- args.isCurrentFlashSwap,
692
- )
719
+ lpFeeRate,
720
+ args.isCurrentFlashSwap,
721
+ )
693
722
 
694
723
  let syInSegmentU64 = segment.syInSegmentU64
695
724
  if (grossCandidate > 0 && syInSegmentU64 === 0) {
696
725
  syInSegmentU64 = 1
697
726
  }
698
727
 
699
- if (
700
- amountInConstraint !== undefined &&
701
- amountInConsumed + syInSegmentU64 > amountInConstraint
702
- ) {
728
+ if (amountInConstraint !== undefined && amountInConsumed + syInSegmentU64 > amountInConstraint) {
703
729
  throw new Error("Insufficient SY budget for exact-out trade")
704
730
  }
705
731
 
@@ -781,27 +807,11 @@ function computeKappaAndLTrade(
781
807
  let kappaPt: number
782
808
 
783
809
  if (direction === SwapDirection.SyToPt) {
784
- kappaPt = isSyOnlyRange
785
- ? 0
786
- : ptMaxToLeftF > 0
787
- ? principalPtInInterval / ptMaxToLeftF
788
- : Infinity
789
- kappaSy = isPtOnlyRange
790
- ? 1
791
- : yMaxToBoundaryF > 0
792
- ? principalSyInInterval / yMaxToBoundaryF
793
- : Infinity
810
+ kappaPt = isSyOnlyRange ? 0 : ptMaxToLeftF > 0 ? principalPtInInterval / ptMaxToLeftF : Infinity
811
+ kappaSy = isPtOnlyRange ? 1 : yMaxToBoundaryF > 0 ? principalSyInInterval / yMaxToBoundaryF : Infinity
794
812
  } else {
795
- kappaSy = isPtOnlyRange
796
- ? 0
797
- : yMaxToBoundaryF > 0
798
- ? principalSyInInterval / yMaxToBoundaryF
799
- : Infinity
800
- kappaPt = isSyOnlyRange
801
- ? 1
802
- : ptMaxToLeftF > 0
803
- ? principalPtInInterval / ptMaxToLeftF
804
- : Infinity
813
+ kappaSy = isPtOnlyRange ? 0 : yMaxToBoundaryF > 0 ? principalSyInInterval / yMaxToBoundaryF : Infinity
814
+ kappaPt = isSyOnlyRange ? 1 : ptMaxToLeftF > 0 ? principalPtInInterval / ptMaxToLeftF : Infinity
805
815
  }
806
816
 
807
817
  const kappa = Math.min(kappaPt, kappaSy, 1)
@@ -823,16 +833,94 @@ function findGrossForTargetNet(
823
833
  maxGross: number,
824
834
  calculateNet: (gross: number) => number,
825
835
  ): number {
836
+ const MAX_INTERPOLATION_STEPS = 6
837
+ const MAX_LOCAL_ADJUST_STEPS = 24
838
+ const MAX_BINARY_FALLBACK_STEPS = 12
839
+
826
840
  if (minGross >= maxGross) {
827
841
  return minGross
828
842
  }
829
843
 
830
844
  let left = minGross
845
+ let leftNet = calculateNet(left)
846
+ if (leftNet >= targetNet) {
847
+ return left
848
+ }
849
+
831
850
  let right = maxGross
832
- let bestGross = minGross
851
+ let rightNet = calculateNet(right)
852
+ if (rightNet <= targetNet) {
853
+ return right
854
+ }
855
+
856
+ for (let i = 0; i < MAX_INTERPOLATION_STEPS; i++) {
857
+ if (right <= left + 1) {
858
+ return left
859
+ }
860
+ if (rightNet <= leftNet) {
861
+ break
862
+ }
863
+
864
+ const span = right - left
865
+ const targetDelta = Math.max(0, targetNet - leftNet)
866
+ const denominator = rightNet - leftNet
867
+ if (denominator === 0) {
868
+ break
869
+ }
870
+
871
+ const estimatedOffset = Number((BigInt(targetDelta) * BigInt(span)) / BigInt(denominator))
872
+ let estimate = left + estimatedOffset
873
+ if (estimate <= left) {
874
+ estimate = left + 1
875
+ }
876
+ if (estimate >= right) {
877
+ estimate = right - 1
878
+ }
879
+
880
+ const estimateNet = calculateNet(estimate)
881
+ if (estimateNet === targetNet) {
882
+ return estimate
883
+ }
884
+ if (estimateNet < targetNet) {
885
+ left = estimate
886
+ leftNet = estimateNet
887
+ } else {
888
+ right = estimate
889
+ rightNet = estimateNet
890
+ }
891
+ }
892
+
893
+ let best = left
894
+ for (let i = 0; i < MAX_LOCAL_ADJUST_STEPS; i++) {
895
+ if (best >= right) {
896
+ break
897
+ }
898
+ const next = best + 1
899
+ if (next > right) {
900
+ break
901
+ }
902
+ const nextNet = calculateNet(next)
903
+ if (nextNet > targetNet) {
904
+ break
905
+ }
906
+ best = next
907
+ if (nextNet === targetNet) {
908
+ return best
909
+ }
910
+ }
911
+
912
+ if (right <= best + 1) {
913
+ return best
914
+ }
915
+
916
+ let low = best
917
+ let high = right
918
+ let bestGross = best
919
+ let steps = 0
833
920
 
834
- while (left <= right) {
835
- const mid = left + Math.floor((right - left) / 2)
921
+ while (low <= high && steps < MAX_BINARY_FALLBACK_STEPS) {
922
+ steps += 1
923
+ const mid = low + Math.floor((high - low) / 2)
836
924
  const net = calculateNet(mid)
837
925
 
838
926
  if (net === targetNet) {
@@ -841,12 +929,12 @@ function findGrossForTargetNet(
841
929
 
842
930
  if (net < targetNet) {
843
931
  bestGross = mid
844
- left = mid + 1
932
+ low = mid + 1
845
933
  } else {
846
934
  if (mid === 0) {
847
935
  break
848
936
  }
849
- right = mid - 1
937
+ high = mid - 1
850
938
  }
851
939
  }
852
940
 
@@ -871,10 +959,7 @@ function calculateNetPtOut(
871
959
  spotPriceNew = anchorULeft
872
960
  }
873
961
 
874
- const ptOutGrossActual = Math.max(
875
- 0,
876
- Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
877
- )
962
+ const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
878
963
 
879
964
  const totalFeeOut = isCurrentFlashSwap
880
965
  ? (() => {
@@ -882,7 +967,7 @@ function calculateNetPtOut(
882
967
  const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
883
968
  const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
884
969
  const ytValue = ptOutGrossActual - syInBase
885
- return getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
970
+ return computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
886
971
  })()
887
972
  : getFeeFromAmount(ptOutGrossActual, lpFeeRate)
888
973
 
@@ -915,15 +1000,12 @@ function calculateSwapSegment(
915
1000
  const cEffNew = snapshot.getEffectivePrice(spotPriceNew)
916
1001
  const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
917
1002
  const syInSegmentU64 = Math.max(0, Math.ceil(syInSegmentF))
918
- const ptOutGrossActual = Math.max(
919
- 0,
920
- Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)),
921
- )
1003
+ const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
922
1004
 
923
1005
  const normalFee = getFeeFromAmount(ptOutGrossActual, lpFeeRate)
924
1006
  const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
925
1007
  const ytValue = ptOutGrossActual - syInBase
926
- const flashFee = getFeeFromAmount(Math.max(0, Math.floor(ytValue)), lpFeeRate)
1008
+ const flashFee = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
927
1009
  const totalFeeOut = isCurrentFlashSwap ? flashFee : normalFee
928
1010
 
929
1011
  return {