@exponent-labs/market-three-math 0.9.11 → 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.
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 {