@exponent-labs/market-three-math 0.9.17 → 0.9.19
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/CHANGELOG.md +4 -0
- package/build/addLiquidity.js +5 -5
- package/build/addLiquidity.js.map +1 -1
- package/build/existingPositionEqualization.js +3 -15
- package/build/existingPositionEqualization.js.map +1 -1
- package/build/swapV2.js +115 -132
- package/build/swapV2.js.map +1 -1
- package/build/tickTree.d.ts +4 -0
- package/build/tickTree.js +95 -0
- package/build/tickTree.js.map +1 -0
- package/build/utils.js +5 -22
- package/build/utils.js.map +1 -1
- package/build/utilsV2.js +3 -2
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.js +3 -15
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.js +3 -3
- package/build/ytTrades.js.map +1 -1
- package/package.json +2 -2
- package/src/addLiquidity.ts +5 -6
- package/src/existingPositionEqualization.test.ts +12 -0
- package/src/existingPositionEqualization.ts +4 -17
- package/src/swapV2.ts +187 -155
- package/src/tickTree.ts +119 -0
- package/src/utils.ts +6 -28
- package/src/utilsV2.ts +4 -2
- package/src/withdrawLiquidity.test.ts +19 -0
- package/src/withdrawLiquidity.ts +4 -17
- package/src/ytTrades.ts +3 -3
package/src/swapV2.ts
CHANGED
|
@@ -16,6 +16,112 @@ function computeFlashFeeOut(ytValue: number, grossOut: number, lpFeeRate: number
|
|
|
16
16
|
return Math.min(grossOut, Math.max(1, getFeeFromAmount(ytValue, lpFeeRate)))
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function ceilNonNegative(value: number): number {
|
|
20
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Math.ceil(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ytSyPriceFromSpot(currentPriceSpot: number, snapshot: EffSnap): number {
|
|
28
|
+
if (currentPriceSpot <= 0 || snapshot.syExchangeRate <= 0) {
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ptAssetPrice = Math.pow(currentPriceSpot, -snapshot.timeFactor)
|
|
33
|
+
if (!Number.isFinite(ptAssetPrice)) {
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Math.max(0, 1 - ptAssetPrice) / snapshot.syExchangeRate
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function flashFeeBasisSy(ytAmount: number, ytSyPrice: number): number {
|
|
41
|
+
return ceilNonNegative(ytAmount * ytSyPrice)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function flashFeeBasisPt(ptOutGross: number, ytSyPrice: number, syExchangeRate: number): number {
|
|
45
|
+
return ceilNonNegative(ptOutGross * ytSyPrice * syExchangeRate)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function estimateExactOutGross(
|
|
49
|
+
targetNet: number,
|
|
50
|
+
maxGross: number,
|
|
51
|
+
lpFeeRate: number,
|
|
52
|
+
isFlashSwap: boolean,
|
|
53
|
+
ytSyPrice: number,
|
|
54
|
+
syExchangeRate: number,
|
|
55
|
+
): number {
|
|
56
|
+
if (targetNet === 0 || maxGross === 0) {
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const feeDelta = Math.max(0, lpFeeRate - 1)
|
|
61
|
+
const feeBasisPerGross = isFlashSwap ? Math.max(0, ytSyPrice * syExchangeRate) : 1
|
|
62
|
+
const netPerGross = 1 - feeDelta * feeBasisPerGross
|
|
63
|
+
if (!Number.isFinite(netPerGross) || netPerGross <= 0) {
|
|
64
|
+
return maxGross
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const targetWithFeeFloor = isFlashSwap || feeDelta > 0 ? targetNet + 1 : targetNet
|
|
68
|
+
return Math.min(maxGross, ceilNonNegative(targetWithFeeFloor / netPerGross))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function solveExactOutGrossDirect(
|
|
72
|
+
targetNet: number,
|
|
73
|
+
minGross: number,
|
|
74
|
+
maxGross: number,
|
|
75
|
+
grossEstimate: number,
|
|
76
|
+
calculateNet: (gross: number) => number,
|
|
77
|
+
): number {
|
|
78
|
+
if (targetNet === 0 || maxGross === 0) {
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const correctionSteps = 64
|
|
83
|
+
let gross = Math.min(maxGross, Math.max(minGross, grossEstimate))
|
|
84
|
+
let net = calculateNet(gross)
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < correctionSteps; i++) {
|
|
87
|
+
if (net >= targetNet || gross >= maxGross) {
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
gross += 1
|
|
91
|
+
net = calculateNet(gross)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (net < targetNet) {
|
|
95
|
+
let lo = gross
|
|
96
|
+
let hi = maxGross
|
|
97
|
+
if (calculateNet(hi) < targetNet) {
|
|
98
|
+
return maxGross
|
|
99
|
+
}
|
|
100
|
+
while (lo < hi) {
|
|
101
|
+
const mid = lo + Math.floor((hi - lo) / 2)
|
|
102
|
+
if (calculateNet(mid) >= targetNet) {
|
|
103
|
+
hi = mid
|
|
104
|
+
} else {
|
|
105
|
+
lo = mid + 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
gross = lo
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < correctionSteps; i++) {
|
|
112
|
+
if (gross === 0) {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
const prevNet = calculateNet(gross - 1)
|
|
116
|
+
if (prevNet < targetNet) {
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
gross -= 1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return gross
|
|
123
|
+
}
|
|
124
|
+
|
|
19
125
|
function boundaryPrincipalRemainder(
|
|
20
126
|
computedGrossOut: number,
|
|
21
127
|
fullOutgoingPrincipal: number,
|
|
@@ -214,6 +320,8 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
214
320
|
continue
|
|
215
321
|
}
|
|
216
322
|
|
|
323
|
+
const ytSyPrice = ytSyPriceFromSpot(currentPriceSpot, snapshot)
|
|
324
|
+
|
|
217
325
|
if (args.direction === SwapDirection.PtToSy) {
|
|
218
326
|
// PT -> SY swap
|
|
219
327
|
|
|
@@ -265,14 +373,10 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
265
373
|
}
|
|
266
374
|
|
|
267
375
|
// Fees in token_out (SY)
|
|
268
|
-
// For flash swaps, fee is based on YT value = (pt - sy × sy_exchange_rate) / sy_exchange_rate (in SY terms)
|
|
269
376
|
if (syOutGrossU64 > 0 || syRemainderAtBoundary > 0) {
|
|
270
377
|
let totalFeeOut: number
|
|
271
378
|
if (args.isCurrentFlashSwap) {
|
|
272
|
-
|
|
273
|
-
const ytValueBase = ptInSegment - syOutBase
|
|
274
|
-
const ytValueSy = ytValueBase / args.syExchangeRate
|
|
275
|
-
totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValueSy)), lpFeeRate)
|
|
379
|
+
totalFeeOut = computeFlashFeeOut(flashFeeBasisSy(ptInSegment, ytSyPrice), syOutGrossU64, lpFeeRate)
|
|
276
380
|
} else {
|
|
277
381
|
totalFeeOut = getFeeFromAmount(syOutGrossU64, lpFeeRate)
|
|
278
382
|
}
|
|
@@ -393,12 +497,13 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
393
497
|
|
|
394
498
|
if (ptOutGrossU64 > 0 || ptRemainderAtBoundary > 0) {
|
|
395
499
|
// Fees in token_out (PT)
|
|
396
|
-
// For flash swaps, fee is based on YT value = pt - sy × sy_exchange_rate
|
|
397
500
|
let totalFeeOut: number
|
|
398
501
|
if (args.isCurrentFlashSwap) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
502
|
+
totalFeeOut = computeFlashFeeOut(
|
|
503
|
+
flashFeeBasisPt(ptOutGrossU64, ytSyPrice, args.syExchangeRate),
|
|
504
|
+
ptOutGrossU64,
|
|
505
|
+
lpFeeRate,
|
|
506
|
+
)
|
|
402
507
|
} else {
|
|
403
508
|
totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
|
|
404
509
|
}
|
|
@@ -481,6 +586,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
481
586
|
let currentLeftBoundaryKey = ticksWrapper.currentTickKey
|
|
482
587
|
let activeLiquidityU64 = ticksWrapper.currentPrefixSum
|
|
483
588
|
let activeLiquidityF64 = Number(activeLiquidityU64)
|
|
589
|
+
const ytSyPrice = ytSyPriceFromSpot(currentPriceSpot, snapshot)
|
|
484
590
|
|
|
485
591
|
const lpFeeRate = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
|
|
486
592
|
const protocolFeeBps = configurationOptions.treasuryFeeBps
|
|
@@ -517,7 +623,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
517
623
|
let iterations = 0
|
|
518
624
|
const MAX_ITERATIONS = 1000
|
|
519
625
|
|
|
520
|
-
while (amountOutLeft >
|
|
626
|
+
while (amountOutLeft > 0 && iterations < MAX_ITERATIONS) {
|
|
521
627
|
iterations++
|
|
522
628
|
|
|
523
629
|
const rightBoundaryKeyOpt = ticksWrapper.successorKey(currentLeftBoundaryKey)
|
|
@@ -671,6 +777,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
671
777
|
cEffOld,
|
|
672
778
|
lpFeeRate,
|
|
673
779
|
args.isCurrentFlashSwap,
|
|
780
|
+
ytSyPrice,
|
|
674
781
|
)
|
|
675
782
|
|
|
676
783
|
const ptOutNetBoundaryActual = Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
|
|
@@ -745,6 +852,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
745
852
|
cEffOld,
|
|
746
853
|
lpFeeRate,
|
|
747
854
|
args.isCurrentFlashSwap,
|
|
855
|
+
ytSyPrice,
|
|
748
856
|
)
|
|
749
857
|
const ptOutNetBoundaryActual =
|
|
750
858
|
ptOutNetBoundaryEffectiveSeed ?? Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
|
|
@@ -755,20 +863,35 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
755
863
|
}
|
|
756
864
|
|
|
757
865
|
const targetNet = Math.min(amountOutLeft, ptOutNetBoundaryEffective)
|
|
866
|
+
const maxGross = ptOutGrossBoundaryU64
|
|
867
|
+
const minGross = Math.min(targetNet, maxGross)
|
|
868
|
+
const usedPrecomputedSegment = precomputedBoundarySegment !== null
|
|
758
869
|
const grossCandidate = precomputedBoundarySegment
|
|
759
|
-
?
|
|
760
|
-
:
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
870
|
+
? maxGross
|
|
871
|
+
: solveExactOutGrossDirect(
|
|
872
|
+
targetNet,
|
|
873
|
+
minGross,
|
|
874
|
+
maxGross,
|
|
875
|
+
estimateExactOutGross(
|
|
876
|
+
targetNet,
|
|
877
|
+
maxGross,
|
|
767
878
|
lpFeeRate,
|
|
768
879
|
args.isCurrentFlashSwap,
|
|
769
|
-
|
|
770
|
-
|
|
880
|
+
ytSyPrice,
|
|
881
|
+
snapshot.syExchangeRate,
|
|
771
882
|
),
|
|
883
|
+
(gross) =>
|
|
884
|
+
calculateNetPtOut(
|
|
885
|
+
gross,
|
|
886
|
+
lTradeF64,
|
|
887
|
+
currentPriceSpot,
|
|
888
|
+
duToLeft,
|
|
889
|
+
anchorULeft,
|
|
890
|
+
lpFeeRate,
|
|
891
|
+
args.isCurrentFlashSwap,
|
|
892
|
+
snapshot,
|
|
893
|
+
ytSyPrice,
|
|
894
|
+
),
|
|
772
895
|
)
|
|
773
896
|
|
|
774
897
|
const segment =
|
|
@@ -783,6 +906,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
783
906
|
cEffOld,
|
|
784
907
|
lpFeeRate,
|
|
785
908
|
args.isCurrentFlashSwap,
|
|
909
|
+
ytSyPrice,
|
|
786
910
|
)
|
|
787
911
|
|
|
788
912
|
let syInSegmentU64 = segment.syInSegmentU64
|
|
@@ -794,20 +918,41 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
794
918
|
throw new Error("Insufficient SY budget for exact-out trade")
|
|
795
919
|
}
|
|
796
920
|
|
|
797
|
-
const
|
|
798
|
-
|
|
921
|
+
const reachesLeftBoundary =
|
|
922
|
+
Math.abs(snapshot.getEffectivePrice(segment.spotPriceNew) - cEffLeft) <= eps ||
|
|
923
|
+
segment.ptOutGrossActual === ptOutGrossBoundaryU64
|
|
924
|
+
const rawNetActual = segment.ptOutGrossActual - segment.totalFeeOut
|
|
925
|
+
if (rawNetActual < 0) {
|
|
799
926
|
throw new Error("Exact-out net underflow")
|
|
800
927
|
}
|
|
801
|
-
if (
|
|
802
|
-
throw new Error("Exact-out
|
|
928
|
+
if (rawNetActual === 0 && !reachesLeftBoundary) {
|
|
929
|
+
throw new Error("Exact-out segment produced no PT output")
|
|
930
|
+
}
|
|
931
|
+
if (rawNetActual < targetNet && !reachesLeftBoundary) {
|
|
932
|
+
throw new Error("Exact-out segment under-delivered target")
|
|
933
|
+
}
|
|
934
|
+
const netActual = Math.min(rawNetActual, targetNet)
|
|
935
|
+
const exactOutRoundingSurplus = rawNetActual - netActual
|
|
936
|
+
if (exactOutRoundingSurplus > 0 && !usedPrecomputedSegment && grossCandidate > 0) {
|
|
937
|
+
const prevNetActual = calculateNetPtOut(
|
|
938
|
+
grossCandidate - 1,
|
|
939
|
+
lTradeF64,
|
|
940
|
+
currentPriceSpot,
|
|
941
|
+
duToLeft,
|
|
942
|
+
anchorULeft,
|
|
943
|
+
lpFeeRate,
|
|
944
|
+
args.isCurrentFlashSwap,
|
|
945
|
+
snapshot,
|
|
946
|
+
ytSyPrice,
|
|
947
|
+
)
|
|
948
|
+
if (prevNetActual >= targetNet) {
|
|
949
|
+
throw new Error("Exact-out gross candidate is not minimal")
|
|
950
|
+
}
|
|
803
951
|
}
|
|
804
952
|
amountOutLeft -= netActual
|
|
805
953
|
amountOutAccum += netActual
|
|
806
954
|
amountInConsumed += syInSegmentU64
|
|
807
955
|
|
|
808
|
-
const reachesLeftBoundary =
|
|
809
|
-
Math.abs(snapshot.getEffectivePrice(segment.spotPriceNew) - cEffLeft) <= eps ||
|
|
810
|
-
segment.ptOutGrossActual === ptOutGrossBoundaryU64
|
|
811
956
|
const ptRemainderAtBoundary = boundaryPrincipalRemainder(
|
|
812
957
|
segment.ptOutGrossActual,
|
|
813
958
|
Number(principalPt),
|
|
@@ -818,7 +963,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
818
963
|
ptRemainderAtBoundary,
|
|
819
964
|
protocolFeeBps,
|
|
820
965
|
)
|
|
821
|
-
feeLpOutU64 += lpFeeOut
|
|
966
|
+
feeLpOutU64 += lpFeeOut + exactOutRoundingSurplus
|
|
822
967
|
feeProtocolOutU64 += protocolFeeOut
|
|
823
968
|
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
824
969
|
|
|
@@ -832,7 +977,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
832
977
|
|
|
833
978
|
const cEffNew = snapshot.getEffectivePrice(currentPriceSpot)
|
|
834
979
|
if (
|
|
835
|
-
amountOutLeft >
|
|
980
|
+
amountOutLeft > 0 &&
|
|
836
981
|
(Math.abs(cEffNew - cEffLeft) <= eps || segment.ptOutGrossActual === ptOutGrossBoundaryU64)
|
|
837
982
|
) {
|
|
838
983
|
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
@@ -849,7 +994,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
849
994
|
}
|
|
850
995
|
}
|
|
851
996
|
|
|
852
|
-
if (amountOutLeft
|
|
997
|
+
if (amountOutLeft !== 0) {
|
|
853
998
|
throw new Error("Insufficient PT output for exact-out trade")
|
|
854
999
|
}
|
|
855
1000
|
|
|
@@ -905,120 +1050,6 @@ function computeKappaAndLTrade(
|
|
|
905
1050
|
return lTradeF64
|
|
906
1051
|
}
|
|
907
1052
|
|
|
908
|
-
function findGrossForTargetNet(
|
|
909
|
-
targetNet: number,
|
|
910
|
-
minGross: number,
|
|
911
|
-
maxGross: number,
|
|
912
|
-
calculateNet: (gross: number) => number,
|
|
913
|
-
): number {
|
|
914
|
-
const MAX_INTERPOLATION_STEPS = 6
|
|
915
|
-
const MAX_LOCAL_ADJUST_STEPS = 24
|
|
916
|
-
const MAX_BINARY_FALLBACK_STEPS = 12
|
|
917
|
-
|
|
918
|
-
if (minGross >= maxGross) {
|
|
919
|
-
return minGross
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
let left = minGross
|
|
923
|
-
let leftNet = calculateNet(left)
|
|
924
|
-
if (leftNet >= targetNet) {
|
|
925
|
-
return left
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
let right = maxGross
|
|
929
|
-
let rightNet = calculateNet(right)
|
|
930
|
-
if (rightNet <= targetNet) {
|
|
931
|
-
return right
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
for (let i = 0; i < MAX_INTERPOLATION_STEPS; i++) {
|
|
935
|
-
if (right <= left + 1) {
|
|
936
|
-
return left
|
|
937
|
-
}
|
|
938
|
-
if (rightNet <= leftNet) {
|
|
939
|
-
break
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const span = right - left
|
|
943
|
-
const targetDelta = Math.max(0, targetNet - leftNet)
|
|
944
|
-
const denominator = rightNet - leftNet
|
|
945
|
-
if (denominator === 0) {
|
|
946
|
-
break
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const estimatedOffset = Number((BigInt(targetDelta) * BigInt(span)) / BigInt(denominator))
|
|
950
|
-
let estimate = left + estimatedOffset
|
|
951
|
-
if (estimate <= left) {
|
|
952
|
-
estimate = left + 1
|
|
953
|
-
}
|
|
954
|
-
if (estimate >= right) {
|
|
955
|
-
estimate = right - 1
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const estimateNet = calculateNet(estimate)
|
|
959
|
-
if (estimateNet === targetNet) {
|
|
960
|
-
return estimate
|
|
961
|
-
}
|
|
962
|
-
if (estimateNet < targetNet) {
|
|
963
|
-
left = estimate
|
|
964
|
-
leftNet = estimateNet
|
|
965
|
-
} else {
|
|
966
|
-
right = estimate
|
|
967
|
-
rightNet = estimateNet
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
let best = left
|
|
972
|
-
for (let i = 0; i < MAX_LOCAL_ADJUST_STEPS; i++) {
|
|
973
|
-
if (best >= right) {
|
|
974
|
-
break
|
|
975
|
-
}
|
|
976
|
-
const next = best + 1
|
|
977
|
-
if (next > right) {
|
|
978
|
-
break
|
|
979
|
-
}
|
|
980
|
-
const nextNet = calculateNet(next)
|
|
981
|
-
if (nextNet > targetNet) {
|
|
982
|
-
break
|
|
983
|
-
}
|
|
984
|
-
best = next
|
|
985
|
-
if (nextNet === targetNet) {
|
|
986
|
-
return best
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (right <= best + 1) {
|
|
991
|
-
return best
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
let low = best
|
|
995
|
-
let high = right
|
|
996
|
-
let bestGross = best
|
|
997
|
-
let steps = 0
|
|
998
|
-
|
|
999
|
-
while (low <= high && steps < MAX_BINARY_FALLBACK_STEPS) {
|
|
1000
|
-
steps += 1
|
|
1001
|
-
const mid = low + Math.floor((high - low) / 2)
|
|
1002
|
-
const net = calculateNet(mid)
|
|
1003
|
-
|
|
1004
|
-
if (net === targetNet) {
|
|
1005
|
-
return mid
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (net < targetNet) {
|
|
1009
|
-
bestGross = mid
|
|
1010
|
-
low = mid + 1
|
|
1011
|
-
} else {
|
|
1012
|
-
if (mid === 0) {
|
|
1013
|
-
break
|
|
1014
|
-
}
|
|
1015
|
-
high = mid - 1
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
return bestGross
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1053
|
function calculateNetPtOut(
|
|
1023
1054
|
grossCandidate: number,
|
|
1024
1055
|
lTradeF64: number,
|
|
@@ -1028,7 +1059,7 @@ function calculateNetPtOut(
|
|
|
1028
1059
|
lpFeeRate: number,
|
|
1029
1060
|
isCurrentFlashSwap: boolean,
|
|
1030
1061
|
snapshot: EffSnap,
|
|
1031
|
-
|
|
1062
|
+
ytSyPrice: number,
|
|
1032
1063
|
): number {
|
|
1033
1064
|
const duCandidate = grossCandidate / lTradeF64
|
|
1034
1065
|
const duClamped = Math.min(duCandidate, duToLeft)
|
|
@@ -1040,13 +1071,11 @@ function calculateNetPtOut(
|
|
|
1040
1071
|
const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
|
|
1041
1072
|
|
|
1042
1073
|
const totalFeeOut = isCurrentFlashSwap
|
|
1043
|
-
? (
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
|
|
1049
|
-
})()
|
|
1074
|
+
? computeFlashFeeOut(
|
|
1075
|
+
flashFeeBasisPt(ptOutGrossActual, ytSyPrice, snapshot.syExchangeRate),
|
|
1076
|
+
ptOutGrossActual,
|
|
1077
|
+
lpFeeRate,
|
|
1078
|
+
)
|
|
1050
1079
|
: getFeeFromAmount(ptOutGrossActual, lpFeeRate)
|
|
1051
1080
|
|
|
1052
1081
|
return Math.max(0, ptOutGrossActual - totalFeeOut)
|
|
@@ -1062,6 +1091,7 @@ function calculateSwapSegment(
|
|
|
1062
1091
|
cEffOld: number,
|
|
1063
1092
|
lpFeeRate: number,
|
|
1064
1093
|
isCurrentFlashSwap: boolean,
|
|
1094
|
+
ytSyPrice: number,
|
|
1065
1095
|
): {
|
|
1066
1096
|
spotPriceNew: number
|
|
1067
1097
|
syInSegmentU64: number
|
|
@@ -1081,9 +1111,11 @@ function calculateSwapSegment(
|
|
|
1081
1111
|
const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
|
|
1082
1112
|
|
|
1083
1113
|
const normalFee = getFeeFromAmount(ptOutGrossActual, lpFeeRate)
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1114
|
+
const flashFee = computeFlashFeeOut(
|
|
1115
|
+
flashFeeBasisPt(ptOutGrossActual, ytSyPrice, snapshot.syExchangeRate),
|
|
1116
|
+
ptOutGrossActual,
|
|
1117
|
+
lpFeeRate,
|
|
1118
|
+
)
|
|
1087
1119
|
const totalFeeOut = isCurrentFlashSwap ? flashFee : normalFee
|
|
1088
1120
|
|
|
1089
1121
|
return {
|
package/src/tickTree.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
2
|
+
|
|
3
|
+
const SENTINEL_TICK_INDEX = 0
|
|
4
|
+
|
|
5
|
+
type TickTreeNode = {
|
|
6
|
+
left: number
|
|
7
|
+
right: number
|
|
8
|
+
parent: number
|
|
9
|
+
colorOrFree: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TreeTick = Tick & {
|
|
13
|
+
treeNode?: TickTreeNode
|
|
14
|
+
isInitialized?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const asTreeTick = (tick: Tick | null | undefined): TreeTick | null | undefined => tick as TreeTick | null | undefined
|
|
18
|
+
|
|
19
|
+
export const isLiveTick = (tick: Tick | null | undefined): tick is Tick => {
|
|
20
|
+
const treeTick = asTreeTick(tick)
|
|
21
|
+
if (!treeTick) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
if (treeTick.isInitialized != null) {
|
|
25
|
+
return treeTick.isInitialized
|
|
26
|
+
}
|
|
27
|
+
if (treeTick.apyBasePoints > 0) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
treeTick.impliedRate > 0 ||
|
|
33
|
+
treeTick.liquidityGross !== 0n ||
|
|
34
|
+
treeTick.principalShareSupply !== 0n ||
|
|
35
|
+
treeTick.principalPt !== 0n ||
|
|
36
|
+
treeTick.principalSy !== 0n ||
|
|
37
|
+
treeTick.frozenLiquidity !== 0n
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const successorTickIdx = (ticks: Ticks, tickIdx: number): number | null => {
|
|
42
|
+
const tick = asTreeTick(ticks.ticksTree[tickIdx - 1])
|
|
43
|
+
if (!isLiveTick(tick)) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const node = tick.treeNode
|
|
48
|
+
if (node) {
|
|
49
|
+
let right = node.right
|
|
50
|
+
if (right !== SENTINEL_TICK_INDEX) {
|
|
51
|
+
while (asTreeTick(ticks.ticksTree[right - 1])?.treeNode?.left) {
|
|
52
|
+
right = asTreeTick(ticks.ticksTree[right - 1])!.treeNode!.left
|
|
53
|
+
}
|
|
54
|
+
return isLiveTick(ticks.ticksTree[right - 1]) ? right : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let cursor = tickIdx
|
|
58
|
+
let parent = node.parent
|
|
59
|
+
while (parent !== SENTINEL_TICK_INDEX && cursor === asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.right) {
|
|
60
|
+
cursor = parent
|
|
61
|
+
parent = asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.parent ?? SENTINEL_TICK_INDEX
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return parent !== SENTINEL_TICK_INDEX && isLiveTick(ticks.ticksTree[parent - 1]) ? parent : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let best: { tickIdx: number; key: number } | null = null
|
|
68
|
+
for (let index = 0; index < ticks.ticksTree.length; index++) {
|
|
69
|
+
const candidate = ticks.ticksTree[index]
|
|
70
|
+
if (!isLiveTick(candidate) || candidate.apyBasePoints <= tick.apyBasePoints) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (!best || candidate.apyBasePoints < best.key) {
|
|
74
|
+
best = { tickIdx: index + 1, key: candidate.apyBasePoints }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return best?.tickIdx ?? null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const predecessorTickIdx = (ticks: Ticks, tickIdx: number): number | null => {
|
|
82
|
+
const tick = asTreeTick(ticks.ticksTree[tickIdx - 1])
|
|
83
|
+
if (!isLiveTick(tick)) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const node = tick.treeNode
|
|
88
|
+
if (node) {
|
|
89
|
+
let left = node.left
|
|
90
|
+
if (left !== SENTINEL_TICK_INDEX) {
|
|
91
|
+
while (asTreeTick(ticks.ticksTree[left - 1])?.treeNode?.right) {
|
|
92
|
+
left = asTreeTick(ticks.ticksTree[left - 1])!.treeNode!.right
|
|
93
|
+
}
|
|
94
|
+
return isLiveTick(ticks.ticksTree[left - 1]) ? left : null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let cursor = tickIdx
|
|
98
|
+
let parent = node.parent
|
|
99
|
+
while (parent !== SENTINEL_TICK_INDEX && cursor === asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.left) {
|
|
100
|
+
cursor = parent
|
|
101
|
+
parent = asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.parent ?? SENTINEL_TICK_INDEX
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parent !== SENTINEL_TICK_INDEX && isLiveTick(ticks.ticksTree[parent - 1]) ? parent : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let best: { tickIdx: number; key: number } | null = null
|
|
108
|
+
for (let index = 0; index < ticks.ticksTree.length; index++) {
|
|
109
|
+
const candidate = ticks.ticksTree[index]
|
|
110
|
+
if (!isLiveTick(candidate) || candidate.apyBasePoints >= tick.apyBasePoints) {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (!best || candidate.apyBasePoints > best.key) {
|
|
114
|
+
best = { tickIdx: index + 1, key: candidate.apyBasePoints }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return best?.tickIdx ?? null
|
|
119
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
6
6
|
|
|
7
|
+
import { isLiveTick, predecessorTickIdx, successorTickIdx } from "./tickTree"
|
|
8
|
+
|
|
7
9
|
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
|
|
8
10
|
// const BASE_POINTS = 10000
|
|
9
11
|
|
|
@@ -117,7 +119,7 @@ export function getSuccessorTickKey(ticks: Ticks, currentTickKey: number): numbe
|
|
|
117
119
|
let minKey: number | null = null
|
|
118
120
|
|
|
119
121
|
for (const tick of ticks.ticksTree) {
|
|
120
|
-
if (tick.apyBasePoints > currentTickKey) {
|
|
122
|
+
if (isLiveTick(tick) && tick.apyBasePoints > currentTickKey) {
|
|
121
123
|
if (minKey === null || tick.apyBasePoints < minKey) {
|
|
122
124
|
minKey = tick.apyBasePoints
|
|
123
125
|
}
|
|
@@ -132,19 +134,7 @@ export function getSuccessorTickKey(ticks: Ticks, currentTickKey: number): numbe
|
|
|
132
134
|
* This is almost equivalent to the Rust successor_idx method
|
|
133
135
|
*/
|
|
134
136
|
export function getSuccessorTickIdxByIdx(ticks: Ticks, tickIdx: number): number | null {
|
|
135
|
-
|
|
136
|
-
const tick = ticks.ticksTree.at(tickIdx - 1) ?? null
|
|
137
|
-
|
|
138
|
-
if (!tick) return null
|
|
139
|
-
|
|
140
|
-
// Find ticks with apyBasePoints greater than tickIdx
|
|
141
|
-
const successorTicks = ticks.ticksTree
|
|
142
|
-
.filter((t) => t.apyBasePoints > tick.apyBasePoints)
|
|
143
|
-
.sort((a, b) => a.apyBasePoints - b.apyBasePoints)
|
|
144
|
-
|
|
145
|
-
const successorTick = successorTicks.at(0) ?? null
|
|
146
|
-
|
|
147
|
-
return !!successorTick ? ticks.ticksTree.indexOf(successorTick) + 1 : null
|
|
137
|
+
return successorTickIdx(ticks, tickIdx)
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
/**
|
|
@@ -152,19 +142,7 @@ export function getSuccessorTickIdxByIdx(ticks: Ticks, tickIdx: number): number
|
|
|
152
142
|
* This is the inverse of getSuccessorTickIdxByIdx - finds the tick with the largest apyBasePoints that is still less than the current tick
|
|
153
143
|
*/
|
|
154
144
|
export function getPredecessorTickIdxByIdx(ticks: Ticks, tickIdx: number): number | null {
|
|
155
|
-
|
|
156
|
-
const tick = ticks.ticksTree.at(tickIdx - 1) ?? null
|
|
157
|
-
|
|
158
|
-
if (!tick) return null
|
|
159
|
-
|
|
160
|
-
// Find ticks with apyBasePoints less than current tick
|
|
161
|
-
const predecessorTicks = ticks.ticksTree
|
|
162
|
-
.filter((t) => t.apyBasePoints < tick.apyBasePoints)
|
|
163
|
-
.sort((a, b) => b.apyBasePoints - a.apyBasePoints) // Sort descending to get the largest that's still less
|
|
164
|
-
|
|
165
|
-
const predecessorTick = predecessorTicks.at(0) ?? null
|
|
166
|
-
|
|
167
|
-
return !!predecessorTick ? ticks.ticksTree.indexOf(predecessorTick) + 1 : null
|
|
145
|
+
return predecessorTickIdx(ticks, tickIdx)
|
|
168
146
|
}
|
|
169
147
|
|
|
170
148
|
/**
|
|
@@ -178,7 +156,7 @@ export function getPredecessorTickKey(ticks: Ticks, currentTickKey: number): num
|
|
|
178
156
|
let maxKey: number | null = null
|
|
179
157
|
|
|
180
158
|
for (const tick of ticks.ticksTree) {
|
|
181
|
-
if (tick.apyBasePoints < currentTickKey) {
|
|
159
|
+
if (isLiveTick(tick) && tick.apyBasePoints < currentTickKey) {
|
|
182
160
|
if (maxKey === null || tick.apyBasePoints > maxKey) {
|
|
183
161
|
maxKey = tick.apyBasePoints
|
|
184
162
|
}
|
package/src/utilsV2.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
7
7
|
|
|
8
|
+
import { isLiveTick } from "./tickTree"
|
|
9
|
+
|
|
8
10
|
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
|
|
9
11
|
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
10
12
|
const PRECISE_NUMBER_DENOM = 1_000_000_000_000_000_000n
|
|
@@ -74,7 +76,7 @@ export class TicksWrapper {
|
|
|
74
76
|
|
|
75
77
|
// Build map from apyBasePoints -> Tick
|
|
76
78
|
for (const tick of this.baseTicksTree) {
|
|
77
|
-
if (tick
|
|
79
|
+
if (isLiveTick(tick)) {
|
|
78
80
|
this.ticksByKey.set(tick.apyBasePoints, tick)
|
|
79
81
|
}
|
|
80
82
|
}
|
|
@@ -91,7 +93,7 @@ export class TicksWrapper {
|
|
|
91
93
|
// In Rust, currentTick is stored as 1-based index into the ticks tree
|
|
92
94
|
// We need to find the corresponding key (apyBasePoints)
|
|
93
95
|
const currentTickFromArray = this.baseTicksTree[ticks.currentTick - 1]
|
|
94
|
-
if (currentTickFromArray
|
|
96
|
+
if (isLiveTick(currentTickFromArray)) {
|
|
95
97
|
this.currentTickKey = currentTickFromArray.apyBasePoints
|
|
96
98
|
} else {
|
|
97
99
|
// Fallback: find tick with spot price closest to currentSpotPrice
|