@exponent-labs/market-three-math 0.9.15 → 0.9.17
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/build/addLiquidity.d.ts +11 -41
- package/build/addLiquidity.js +169 -201
- package/build/addLiquidity.js.map +1 -1
- package/build/bisect.js +1 -2
- package/build/bisect.js.map +1 -1
- package/build/existingPositionEqualization.d.ts +3 -3
- package/build/existingPositionEqualization.js +45 -66
- package/build/existingPositionEqualization.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +1 -3
- package/build/index.js.map +1 -1
- package/build/liquidityHistogram.js +2 -3
- package/build/liquidityHistogram.js.map +1 -1
- package/build/quote.js +2 -2
- package/build/quote.js.map +1 -1
- package/build/swap.js +2 -2
- package/build/swap.js.map +1 -1
- package/build/swapV2.js +56 -20
- package/build/swapV2.js.map +1 -1
- package/build/types.d.ts +4 -21
- package/build/utils.js +17 -17
- package/build/utils.js.map +1 -1
- package/build/utilsV2.js +10 -7
- package/build/utilsV2.js.map +1 -1
- package/build/withdrawLiquidity.d.ts +1 -1
- package/build/withdrawLiquidity.js +120 -72
- package/build/withdrawLiquidity.js.map +1 -1
- package/build/ytTrades.js +3 -4
- package/build/ytTrades.js.map +1 -1
- package/build/ytTradesLegacy.js +3 -4
- package/build/ytTradesLegacy.js.map +1 -1
- package/package.json +2 -2
- package/src/addLiquidity.ts +203 -246
- package/src/existingPositionEqualization.test.ts +33 -0
- package/src/existingPositionEqualization.ts +52 -83
- package/src/index.ts +0 -4
- package/src/swap.ts +1 -0
- package/src/swapV2.ts +96 -18
- package/src/types.ts +4 -23
- package/src/utilsV2.ts +9 -4
- package/src/withdrawLiquidity.test.ts +189 -0
- package/src/withdrawLiquidity.ts +148 -89
package/src/addLiquidity.ts
CHANGED
|
@@ -2,19 +2,17 @@
|
|
|
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"
|
|
5
|
+
import type { LpPositionCLMM, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
6
6
|
|
|
7
|
+
import { computeExistingPositionBudgetEffect } from "./existingPositionEqualization"
|
|
7
8
|
import { simulateSwap, simulateSwapExactOut } from "./swapV2"
|
|
8
9
|
import {
|
|
9
10
|
AddLiquidityArgs,
|
|
10
11
|
AddLiquidityOutcome,
|
|
11
|
-
CrossingScaleParams,
|
|
12
|
-
CrossingTickState,
|
|
13
12
|
LiquidityNeeds,
|
|
14
13
|
MarketThreeState,
|
|
15
14
|
SwapDirection,
|
|
16
15
|
} from "./types"
|
|
17
|
-
import { findTickByKey, getSuccessorTickKey } from "./utils"
|
|
18
16
|
import { EffSnap, TicksWrapper, normalizedTimeRemaining } from "./utilsV2"
|
|
19
17
|
|
|
20
18
|
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
@@ -24,49 +22,16 @@ const GAP_TOKEN_NEEDS = 20
|
|
|
24
22
|
const MIN_SWAP_EXACT_OUT_SY_HEADROOM = 100
|
|
25
23
|
const SWAP_EXACT_OUT_SY_HEADROOM = 100
|
|
26
24
|
const SWAP_EXACT_OUT_HEADROOM_PT_THRESHOLD = 10_000_000
|
|
27
|
-
const MIN_SWAP_AND_SUPPLY_SY_BUDGET = 1_000_000
|
|
28
25
|
const FLASH_SY_POSITION_HEADROOM = 1_000
|
|
29
26
|
const MAX_SWAP_AND_SUPPLY_UNDERSPEND_LAMPORTS = 1_000
|
|
30
27
|
const MAX_SWAP_AND_SUPPLY_SIMULATED_OVERSPEND_LAMPORTS = 0
|
|
31
28
|
const LARGE_SWAP_AND_SUPPLY_SY_BUDGET = 50_000_000_000
|
|
32
29
|
const PT_ONLY_SWAP_EXACT_OUT_ESTIMATE_SLACK = 0
|
|
30
|
+
const ZERO_BIGINT = 0n
|
|
31
|
+
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER)
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
principalSy: 0,
|
|
37
|
-
principalShareSupply: 0,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get crossing tick state and price boundaries from Ticks.
|
|
42
|
-
* Matches wrapper_provide_liquidity.rs: current node's spot_price, principal_pt/sy/supply, successor's spot_price.
|
|
43
|
-
*/
|
|
44
|
-
export function getCrossingTickStateFromTicks(ticks: Ticks): {
|
|
45
|
-
crossingTickState: CrossingTickState
|
|
46
|
-
crossingTickPriceLeft: number
|
|
47
|
-
crossingTickPriceRight: number
|
|
48
|
-
} {
|
|
49
|
-
const currentTickNode = ticks.ticksTree[ticks.currentTick - 1]
|
|
50
|
-
if (!currentTickNode || ticks.currentTick <= 0) {
|
|
51
|
-
return {
|
|
52
|
-
crossingTickState: EMPTY_CROSSING_TICK_STATE,
|
|
53
|
-
crossingTickPriceLeft: 0,
|
|
54
|
-
crossingTickPriceRight: 0,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const crossingTickPriceLeft = currentTickNode.impliedRate
|
|
58
|
-
const successorKey = getSuccessorTickKey(ticks, currentTickNode.apyBasePoints)
|
|
59
|
-
const successorTick = successorKey != null ? findTickByKey(ticks, successorKey)?.tick : null
|
|
60
|
-
const crossingTickPriceRight = successorTick?.impliedRate ?? Number.POSITIVE_INFINITY
|
|
61
|
-
return {
|
|
62
|
-
crossingTickState: {
|
|
63
|
-
principalPt: Number(currentTickNode.principalPt),
|
|
64
|
-
principalSy: Number(currentTickNode.principalSy),
|
|
65
|
-
principalShareSupply: Number(currentTickNode.principalShareSupply),
|
|
66
|
-
},
|
|
67
|
-
crossingTickPriceLeft,
|
|
68
|
-
crossingTickPriceRight,
|
|
69
|
-
}
|
|
33
|
+
type AddLiquiditySimulationOptions = {
|
|
34
|
+
existingPosition?: LpPositionCLMM
|
|
70
35
|
}
|
|
71
36
|
|
|
72
37
|
/**
|
|
@@ -103,11 +68,6 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
103
68
|
ptNeeded: 0,
|
|
104
69
|
priceSplitForNeed: lowerPrice,
|
|
105
70
|
priceSplitTickIdx: lowerTickIdx,
|
|
106
|
-
// No crossing possible when below range
|
|
107
|
-
originalMaxSy: maxSy,
|
|
108
|
-
originalMaxPt: maxPt,
|
|
109
|
-
duLeftTotal: 0,
|
|
110
|
-
deltaCRightTotal: deltaCTotal,
|
|
111
71
|
}
|
|
112
72
|
} else if (spotPriceCurrent >= upperPrice) {
|
|
113
73
|
// Above range: PT only (NOT SY!)
|
|
@@ -122,11 +82,6 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
122
82
|
ptNeeded: Math.floor(ptNeed),
|
|
123
83
|
priceSplitForNeed: upperPrice,
|
|
124
84
|
priceSplitTickIdx: upperTickIdx,
|
|
125
|
-
// No crossing possible when above range
|
|
126
|
-
originalMaxSy: maxSy,
|
|
127
|
-
originalMaxPt: maxPt,
|
|
128
|
-
duLeftTotal: duTotal,
|
|
129
|
-
deltaCRightTotal: 0,
|
|
130
85
|
}
|
|
131
86
|
} else {
|
|
132
87
|
// Inside range: both sides - crossing tick possible
|
|
@@ -147,95 +102,39 @@ export function computeLiquidityTargetAndTokenNeeds(
|
|
|
147
102
|
ptNeeded: Math.floor(ptNeed),
|
|
148
103
|
priceSplitForNeed: spotPriceCurrent,
|
|
149
104
|
priceSplitTickIdx: currentIndex,
|
|
150
|
-
// Store for crossing tick scaling
|
|
151
|
-
originalMaxSy: maxSy,
|
|
152
|
-
originalMaxPt: maxPt,
|
|
153
|
-
duLeftTotal: duLeft,
|
|
154
|
-
deltaCRightTotal: deltaCRight,
|
|
155
105
|
}
|
|
156
106
|
}
|
|
157
107
|
}
|
|
158
108
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
* When adding liquidity that spans the current price (crossing tick), tokens get trimmed twice:
|
|
163
|
-
* 1. First by CLMM formula in computeLiquidityTargetAndTokenNeeds
|
|
164
|
-
* 2. Then by AMM formula in simulateAddLiquidityProportional
|
|
165
|
-
*
|
|
166
|
-
* This function calculates the segment's proportion of the total distribution and scales
|
|
167
|
-
* the original max values accordingly, ensuring maximum token utilization.
|
|
168
|
-
*
|
|
169
|
-
* @param scaleParams - Original max values and CLMM distribution extents
|
|
170
|
-
* @param duPtPart - PT segment length (spot price delta for this segment)
|
|
171
|
-
* @param syPerL1 - SY per unit liquidity for this segment
|
|
172
|
-
* @param clmmPtDelta - PT amount calculated from CLMM formula
|
|
173
|
-
* @param clmmSyDelta - SY amount calculated from CLMM formula
|
|
174
|
-
* @returns Tuple of [scaledPtIn, scaledSyIn] - maximum of CLMM-calculated and scaled original values
|
|
175
|
-
*/
|
|
176
|
-
export function scaleCrossingTickInputs(
|
|
177
|
-
scaleParams: CrossingScaleParams,
|
|
178
|
-
duPtPart: number,
|
|
179
|
-
syPerL1: number,
|
|
180
|
-
clmmPtDelta: number,
|
|
181
|
-
clmmSyDelta: number,
|
|
182
|
-
): [number, number] {
|
|
183
|
-
const { duLeftTotal, deltaCRightTotal, originalMaxPt, originalMaxSy } = scaleParams
|
|
184
|
-
|
|
185
|
-
if (duLeftTotal > 0 && deltaCRightTotal > 0) {
|
|
186
|
-
// Calculate segment's proportion of total distribution
|
|
187
|
-
const ptSegmentRatio = duPtPart / duLeftTotal
|
|
188
|
-
const sySegmentRatio = syPerL1 / deltaCRightTotal
|
|
189
|
-
|
|
190
|
-
// Scale original max values by segment proportion
|
|
191
|
-
const scaledPt = Math.ceil(originalMaxPt * ptSegmentRatio)
|
|
192
|
-
const scaledSy = Math.ceil(originalMaxSy * sySegmentRatio)
|
|
109
|
+
function toU64BigInt(value: number): bigint {
|
|
110
|
+
return BigInt(Math.max(0, Math.floor(value)))
|
|
111
|
+
}
|
|
193
112
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
113
|
+
function lamportsNumberToBigInt(value: number, label: string): bigint {
|
|
114
|
+
const lamports = Math.max(0, Math.floor(value))
|
|
115
|
+
if (!Number.isSafeInteger(lamports)) {
|
|
116
|
+
throw new Error(`${label} exceeds JS safe integer range: ${value}`)
|
|
197
117
|
}
|
|
198
|
-
|
|
199
|
-
// Fallback to original logic if no crossing scale params
|
|
200
|
-
return [clmmPtDelta, clmmSyDelta]
|
|
118
|
+
return BigInt(lamports)
|
|
201
119
|
}
|
|
202
120
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
*/
|
|
207
|
-
function simulateAddLiquidityProportional(
|
|
208
|
-
intentPt: number,
|
|
209
|
-
intentSy: number,
|
|
210
|
-
marketTotalPt: number,
|
|
211
|
-
marketTotalSy: number,
|
|
212
|
-
marketTotalLp: number,
|
|
213
|
-
): [number, number] {
|
|
214
|
-
if (marketTotalLp === 0 || marketTotalPt === 0 || marketTotalSy === 0) {
|
|
215
|
-
// Empty tick - use all intended amounts
|
|
216
|
-
return [intentPt, intentSy]
|
|
121
|
+
function safeLamportsBigIntToNumber(value: bigint, label: string): number {
|
|
122
|
+
if (value < ZERO_BIGINT || value > MAX_SAFE_INTEGER_BIGINT) {
|
|
123
|
+
throw new Error(`${label} exceeds JS safe integer range: ${value.toString()}`)
|
|
217
124
|
}
|
|
218
125
|
|
|
219
|
-
|
|
220
|
-
const lpFromSy = (marketTotalLp * intentSy) / marketTotalSy
|
|
221
|
-
|
|
222
|
-
if (lpFromPt < lpFromSy) {
|
|
223
|
-
// PT is the limiting factor
|
|
224
|
-
const lpTokensOut = lpFromPt
|
|
225
|
-
const usedPt = intentPt
|
|
226
|
-
const usedSy = Math.ceil((marketTotalSy * lpTokensOut) / marketTotalLp)
|
|
227
|
-
return [usedPt, usedSy]
|
|
228
|
-
} else {
|
|
229
|
-
// SY is the limiting factor
|
|
230
|
-
const lpTokensOut = lpFromSy
|
|
231
|
-
const usedSy = intentSy
|
|
232
|
-
const usedPt = Math.ceil((marketTotalPt * lpTokensOut) / marketTotalLp)
|
|
233
|
-
return [usedPt, usedSy]
|
|
234
|
-
}
|
|
126
|
+
return Number(value)
|
|
235
127
|
}
|
|
236
128
|
|
|
237
|
-
function
|
|
238
|
-
|
|
129
|
+
function finalizeEqualizedSpend(params: {
|
|
130
|
+
simulatedSpend: number
|
|
131
|
+
fixedSpent: bigint
|
|
132
|
+
fixedReleased: bigint
|
|
133
|
+
label: string
|
|
134
|
+
}): number {
|
|
135
|
+
const grossSpend = lamportsNumberToBigInt(params.simulatedSpend, params.label) + params.fixedSpent
|
|
136
|
+
const finalSpend = grossSpend > params.fixedReleased ? grossSpend - params.fixedReleased : ZERO_BIGINT
|
|
137
|
+
return safeLamportsBigIntToNumber(finalSpend, params.label)
|
|
239
138
|
}
|
|
240
139
|
|
|
241
140
|
function ceilDivBigInt(numerator: bigint, denominator: bigint): bigint {
|
|
@@ -285,6 +184,104 @@ function getVirtualTickState(
|
|
|
285
184
|
return state
|
|
286
185
|
}
|
|
287
186
|
|
|
187
|
+
function ceilNormalizedPrincipal(value: number, cap: bigint): bigint {
|
|
188
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
189
|
+
throw new Error("Invalid active tick normalization value")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const capNumber = Number(cap)
|
|
193
|
+
if (value >= capNumber) {
|
|
194
|
+
return cap
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return BigInt(Math.min(Math.ceil(value), capNumber))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function computeNormalizedActiveTickPrincipals(params: {
|
|
201
|
+
principalPt: bigint
|
|
202
|
+
principalSy: bigint
|
|
203
|
+
ptPerL: number
|
|
204
|
+
syPerL: number
|
|
205
|
+
}): { normalizedPt: bigint; normalizedSy: bigint } {
|
|
206
|
+
const { principalPt, principalSy, ptPerL, syPerL } = params
|
|
207
|
+
if (!Number.isFinite(ptPerL) || !Number.isFinite(syPerL) || ptPerL < 0 || syPerL < 0) {
|
|
208
|
+
throw new Error("Invalid active tick normalization ratio")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (ptPerL === 0 && syPerL === 0) {
|
|
212
|
+
return { normalizedPt: 0n, normalizedSy: 0n }
|
|
213
|
+
}
|
|
214
|
+
if (ptPerL === 0) {
|
|
215
|
+
return { normalizedPt: 0n, normalizedSy: principalSy }
|
|
216
|
+
}
|
|
217
|
+
if (syPerL === 0) {
|
|
218
|
+
return { normalizedPt: principalPt, normalizedSy: 0n }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const lFromPt = Number(principalPt) / ptPerL
|
|
222
|
+
const lFromSy = Number(principalSy) / syPerL
|
|
223
|
+
if (!Number.isFinite(lFromPt) || !Number.isFinite(lFromSy)) {
|
|
224
|
+
throw new Error("Invalid active tick normalization liquidity")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (lFromPt <= lFromSy) {
|
|
228
|
+
return {
|
|
229
|
+
normalizedPt: principalPt,
|
|
230
|
+
normalizedSy: ceilNormalizedPrincipal(lFromPt * syPerL, principalSy),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
normalizedPt: ceilNormalizedPrincipal(lFromSy * ptPerL, principalPt),
|
|
236
|
+
normalizedSy: principalSy,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeActiveTickPrincipalsBeforeDeposit(
|
|
241
|
+
ticksWrapper: TicksWrapper,
|
|
242
|
+
snap: EffSnap,
|
|
243
|
+
lowerTickKey: number,
|
|
244
|
+
upperTickKey: number,
|
|
245
|
+
): void {
|
|
246
|
+
if (ticksWrapper.currentPrefixSum === 0n) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const currentTickKey = ticksWrapper.currentTickKey
|
|
251
|
+
if (lowerTickKey > currentTickKey || currentTickKey >= upperTickKey) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const rightTickKey = ticksWrapper.successorKey(currentTickKey)
|
|
256
|
+
if (rightTickKey == null) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const spotPrice = ticksWrapper.currentSpotPrice
|
|
261
|
+
const leftPrice = ticksWrapper.getSpotPrice(currentTickKey)
|
|
262
|
+
const rightPrice = ticksWrapper.getSpotPrice(rightTickKey)
|
|
263
|
+
if (spotPrice < leftPrice || spotPrice > rightPrice) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentTickKey)
|
|
268
|
+
if (principalPt === 0n && principalSy === 0n) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const ptPerL = Math.max(spotPrice - leftPrice, 0)
|
|
273
|
+
const syPerL =
|
|
274
|
+
spotPrice >= rightPrice ? 0 : snap.getEffectivePrice(spotPrice) - snap.getEffectivePrice(rightPrice)
|
|
275
|
+
const { normalizedPt, normalizedSy } = computeNormalizedActiveTickPrincipals({
|
|
276
|
+
principalPt,
|
|
277
|
+
principalSy,
|
|
278
|
+
ptPerL,
|
|
279
|
+
syPerL,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
ticksWrapper.setPrincipals(currentTickKey, normalizedPt, normalizedSy)
|
|
283
|
+
}
|
|
284
|
+
|
|
288
285
|
function simulateMintSharesForTickPrestateCalcUsed(params: {
|
|
289
286
|
key: number
|
|
290
287
|
dptIn: bigint
|
|
@@ -390,8 +387,6 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
390
387
|
lowerTickKey: number
|
|
391
388
|
upperTickKey: number
|
|
392
389
|
liquidityTarget: number
|
|
393
|
-
originalMaxSy: number
|
|
394
|
-
originalMaxPt: number
|
|
395
390
|
}): { sySpent: number; ptSpent: number } {
|
|
396
391
|
const {
|
|
397
392
|
ticks,
|
|
@@ -403,19 +398,18 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
403
398
|
lowerTickKey,
|
|
404
399
|
upperTickKey,
|
|
405
400
|
liquidityTarget,
|
|
406
|
-
originalMaxSy,
|
|
407
|
-
originalMaxPt,
|
|
408
401
|
} = params
|
|
409
402
|
|
|
410
403
|
const ticksWrapper = new TicksWrapper(ticks)
|
|
411
404
|
ticksWrapper.upsertBoundaryTick(lowerTickKey, snap)
|
|
412
405
|
ticksWrapper.upsertBoundaryTick(upperTickKey, snap)
|
|
406
|
+
normalizeActiveTickPrincipalsBeforeDeposit(ticksWrapper, snap, lowerTickKey, upperTickKey)
|
|
413
407
|
const virtualStates = new Map<number, VirtualTickState>()
|
|
414
408
|
|
|
415
409
|
let totalSySpend = 0n
|
|
416
410
|
let totalPtSpend = 0n
|
|
417
|
-
let pendingCrossPt: { leftKey: number;
|
|
418
|
-
let pendingCrossSy: {
|
|
411
|
+
let pendingCrossPt: { leftKey: number; ptDelta: bigint } | null = null
|
|
412
|
+
let pendingCrossSy: { syDelta: bigint } | null = null
|
|
419
413
|
|
|
420
414
|
const resolveTraversalStartKey = (candidateKey: number): number => {
|
|
421
415
|
if (ticksWrapper.getTickByKey(candidateKey)) {
|
|
@@ -473,9 +467,7 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
473
467
|
if (isCrossing) {
|
|
474
468
|
pendingCrossPt = {
|
|
475
469
|
leftKey,
|
|
476
|
-
rightKey,
|
|
477
470
|
ptDelta: principalPtDelta,
|
|
478
|
-
duPart: segmentLength,
|
|
479
471
|
}
|
|
480
472
|
return
|
|
481
473
|
}
|
|
@@ -516,10 +508,7 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
516
508
|
const isCrossing = leftPrice < priceSplitForNeed && rightPrice > priceSplitForNeed
|
|
517
509
|
if (isCrossing) {
|
|
518
510
|
pendingCrossSy = {
|
|
519
|
-
leftKey,
|
|
520
|
-
rightKey,
|
|
521
511
|
syDelta: principalSyDelta,
|
|
522
|
-
syPerL,
|
|
523
512
|
}
|
|
524
513
|
return
|
|
525
514
|
}
|
|
@@ -540,15 +529,10 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
540
529
|
})
|
|
541
530
|
|
|
542
531
|
if (pendingCrossPt && pendingCrossSy) {
|
|
543
|
-
const ptLeft = BigInt(Math.max(0, Math.floor(originalMaxPt) - Number(totalPtSpend) - GAP_TOKEN_NEEDS))
|
|
544
|
-
const syLeft = BigInt(Math.max(0, Math.floor(originalMaxSy) - Number(totalSySpend) - GAP_TOKEN_NEEDS))
|
|
545
|
-
const scaledPtIn = pendingCrossPt.ptDelta > ptLeft ? pendingCrossPt.ptDelta : ptLeft
|
|
546
|
-
const scaledSyIn = pendingCrossSy.syDelta > syLeft ? pendingCrossSy.syDelta : syLeft
|
|
547
|
-
|
|
548
532
|
const minted = simulateMintSharesForTickPrestateCalcUsed({
|
|
549
533
|
key: pendingCrossPt.leftKey,
|
|
550
|
-
dptIn:
|
|
551
|
-
dsyIn:
|
|
534
|
+
dptIn: pendingCrossPt.ptDelta,
|
|
535
|
+
dsyIn: pendingCrossSy.syDelta,
|
|
552
536
|
ticksWrapper,
|
|
553
537
|
virtualStates,
|
|
554
538
|
})
|
|
@@ -569,10 +553,7 @@ function simulateAccruePrincipalForDeposit(params: {
|
|
|
569
553
|
return { sySpent, ptSpent }
|
|
570
554
|
}
|
|
571
555
|
|
|
572
|
-
/**
|
|
573
|
-
* Compute token needs with crossing tick adjustment
|
|
574
|
-
* This matches the Rust compute_token_needs_with_crossing function
|
|
575
|
-
*/
|
|
556
|
+
/** Compute curve-level token needs without raw crossing-tick cap scaling. */
|
|
576
557
|
export function computeTokenNeedsWithCrossing(
|
|
577
558
|
snap: EffSnap,
|
|
578
559
|
spotPriceCurrent: number,
|
|
@@ -583,9 +564,6 @@ export function computeTokenNeedsWithCrossing(
|
|
|
583
564
|
maxSy: number,
|
|
584
565
|
maxPt: number,
|
|
585
566
|
epsilonClamp: number,
|
|
586
|
-
crossingTickState: CrossingTickState,
|
|
587
|
-
crossingTickPriceLeft: number,
|
|
588
|
-
crossingTickPriceRight: number,
|
|
589
567
|
): [number, number] {
|
|
590
568
|
// Below range: SY only
|
|
591
569
|
if (spotPriceCurrent <= lowerPrice) {
|
|
@@ -612,62 +590,8 @@ export function computeTokenNeedsWithCrossing(
|
|
|
612
590
|
const liquidityFromSy = maxSy / deltaCRight
|
|
613
591
|
|
|
614
592
|
const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// Apply crossing tick adjustment if the tick has existing liquidity
|
|
619
|
-
// Match Rust: !crossing_tick_state.is_empty() where is_empty = (principal_share_supply == 0)
|
|
620
|
-
const isCrossingTickActive =
|
|
621
|
-
crossingTickState.principalShareSupply > 0 &&
|
|
622
|
-
crossingTickPriceLeft < spotPriceCurrent &&
|
|
623
|
-
crossingTickPriceRight > spotPriceCurrent
|
|
624
|
-
|
|
625
|
-
if (isCrossingTickActive) {
|
|
626
|
-
// Calculate PT and SY portions that would go into the crossing tick
|
|
627
|
-
// PT portion: from crossing_tick_price_left to spot_price_current (match Rust)
|
|
628
|
-
const crossingPtSegment = spotPriceCurrent - Math.max(crossingTickPriceLeft, lowerPrice)
|
|
629
|
-
const crossingPtIntended = Math.ceil(liquidityTarget * crossingPtSegment)
|
|
630
|
-
|
|
631
|
-
// SY portion: from spotPriceCurrent to crossingTickPriceRight
|
|
632
|
-
const crossingSySegmentStart = Math.max(spotPriceCurrent, crossingTickPriceLeft)
|
|
633
|
-
const crossingSySegmentEnd = Math.min(crossingTickPriceRight, upperPrice)
|
|
634
|
-
const syPerL = snap.getEffectivePrice(crossingSySegmentStart) - snap.getEffectivePrice(crossingSySegmentEnd)
|
|
635
|
-
const crossingSyIntended = Math.ceil(liquidityTarget * Math.max(syPerL, 0))
|
|
636
|
-
|
|
637
|
-
if (crossingPtIntended > 0 && crossingSyIntended > 0) {
|
|
638
|
-
// Tokens already allocated to non-crossing segments before crossing processing.
|
|
639
|
-
const ptOutsideCrossing = Math.max(ptNeed - crossingPtIntended, 0)
|
|
640
|
-
const syOutsideCrossing = Math.max(syNeed - crossingSyIntended, 0)
|
|
641
|
-
|
|
642
|
-
// Apply scaling from original max values to prevent double trimming
|
|
643
|
-
const [scaledPtIn, scaledSyIn] =
|
|
644
|
-
duLeft > 0 && deltaCRight > 0
|
|
645
|
-
? (() => {
|
|
646
|
-
const totalPtSpend = Math.ceil(ptOutsideCrossing)
|
|
647
|
-
const totalSySpend = Math.ceil(syOutsideCrossing)
|
|
648
|
-
const ptLeft = Math.max(maxPt - totalPtSpend - GAP_TOKEN_NEEDS, 0)
|
|
649
|
-
const syLeft = Math.max(maxSy - totalSySpend - GAP_TOKEN_NEEDS, 0)
|
|
650
|
-
|
|
651
|
-
// Use max of CLMM-calculated value and scaled original value
|
|
652
|
-
return [Math.max(crossingPtIntended, ptLeft), Math.max(crossingSyIntended, syLeft)]
|
|
653
|
-
})()
|
|
654
|
-
: [crossingPtIntended, crossingSyIntended]
|
|
655
|
-
|
|
656
|
-
// Simulate add_liquidity proportional logic with scaled inputs
|
|
657
|
-
const [usedPt, usedSy] = simulateAddLiquidityProportional(
|
|
658
|
-
scaledPtIn,
|
|
659
|
-
scaledSyIn,
|
|
660
|
-
crossingTickState.principalPt,
|
|
661
|
-
crossingTickState.principalSy,
|
|
662
|
-
crossingTickState.principalShareSupply,
|
|
663
|
-
)
|
|
664
|
-
|
|
665
|
-
// Adjust needs based on what would actually be used
|
|
666
|
-
ptNeed = ptOutsideCrossing + usedPt
|
|
667
|
-
|
|
668
|
-
syNeed = syOutsideCrossing + usedSy
|
|
669
|
-
}
|
|
670
|
-
}
|
|
593
|
+
const ptNeed = liquidityTarget * duLeft
|
|
594
|
+
const syNeed = liquidityTarget * deltaCRight
|
|
671
595
|
|
|
672
596
|
return [Math.ceil(syNeed), Math.ceil(ptNeed)]
|
|
673
597
|
}
|
|
@@ -728,8 +652,6 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
|
|
|
728
652
|
lowerTickKey: args.lowerTick,
|
|
729
653
|
upperTickKey: args.upperTick,
|
|
730
654
|
liquidityTarget: liquidityNeeds.liquidityTarget,
|
|
731
|
-
originalMaxSy: liquidityNeeds.originalMaxSy,
|
|
732
|
-
originalMaxPt: liquidityNeeds.originalMaxPt,
|
|
733
655
|
})
|
|
734
656
|
|
|
735
657
|
// Enforce budgets
|
|
@@ -750,6 +672,45 @@ export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiq
|
|
|
750
672
|
}
|
|
751
673
|
}
|
|
752
674
|
|
|
675
|
+
function simulateAddLiquidityForPosition(
|
|
676
|
+
marketState: MarketThreeState,
|
|
677
|
+
args: AddLiquidityArgs,
|
|
678
|
+
existingPosition?: LpPositionCLMM,
|
|
679
|
+
): AddLiquidityOutcome {
|
|
680
|
+
if (!existingPosition) {
|
|
681
|
+
return simulateAddLiquidity(marketState, args)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const budgetEffect = computeExistingPositionBudgetEffect({
|
|
685
|
+
ticks: marketState.ticks,
|
|
686
|
+
position: existingPosition,
|
|
687
|
+
userMaxSy: lamportsNumberToBigInt(args.maxSy, "user max SY"),
|
|
688
|
+
userMaxPt: lamportsNumberToBigInt(args.maxPt, "user max PT"),
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
const addLiquidityResult = simulateAddLiquidity(marketState, {
|
|
692
|
+
...args,
|
|
693
|
+
maxSy: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxSy, "effective max SY"),
|
|
694
|
+
maxPt: safeLamportsBigIntToNumber(budgetEffect.effectiveMaxPt, "effective max PT"),
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
deltaL: addLiquidityResult.deltaL,
|
|
699
|
+
sySpent: finalizeEqualizedSpend({
|
|
700
|
+
simulatedSpend: addLiquidityResult.sySpent,
|
|
701
|
+
fixedSpent: budgetEffect.fixedSySpent,
|
|
702
|
+
fixedReleased: budgetEffect.fixedSyReleased,
|
|
703
|
+
label: "equalized SY spend",
|
|
704
|
+
}),
|
|
705
|
+
ptSpent: finalizeEqualizedSpend({
|
|
706
|
+
simulatedSpend: addLiquidityResult.ptSpent,
|
|
707
|
+
fixedSpent: budgetEffect.fixedPtSpent,
|
|
708
|
+
fixedReleased: budgetEffect.fixedPtReleased,
|
|
709
|
+
label: "equalized PT spend",
|
|
710
|
+
}),
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
753
714
|
/**
|
|
754
715
|
* Calculate the LP tokens that will be received for a given deposit
|
|
755
716
|
* This is useful for UI display and slippage calculations
|
|
@@ -813,12 +774,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
813
774
|
expirationTs: number
|
|
814
775
|
currentSpotPrice: number
|
|
815
776
|
syExchangeRate: number
|
|
816
|
-
/** Optional crossing tick state for accurate ratio prediction.
|
|
817
|
-
* When provided, the prediction accounts for the existing PT/SY proportions
|
|
818
|
-
* in the active tick, matching the on-chain behaviour more closely. */
|
|
819
|
-
crossingTickState?: CrossingTickState
|
|
820
|
-
crossingTickPriceLeft?: number
|
|
821
|
-
crossingTickPriceRight?: number
|
|
822
777
|
epsilonClamp?: number
|
|
823
778
|
}) {
|
|
824
779
|
const {
|
|
@@ -828,9 +783,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
828
783
|
lowerPrice,
|
|
829
784
|
upperPrice,
|
|
830
785
|
baseTokenAmount,
|
|
831
|
-
crossingTickState = EMPTY_CROSSING_TICK_STATE,
|
|
832
|
-
crossingTickPriceLeft = 0,
|
|
833
|
-
crossingTickPriceRight = 0,
|
|
834
786
|
epsilonClamp = 1e-18,
|
|
835
787
|
} = params
|
|
836
788
|
|
|
@@ -848,10 +800,9 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
848
800
|
const priceEffUpper = effSnap.getEffectivePrice(upperPrice)
|
|
849
801
|
|
|
850
802
|
// We mirror the on-chain logic in `wrapper_provide_liquidity`:
|
|
851
|
-
// 1. Use
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
// PT/SY proportions in the active tick, significantly improving accuracy.
|
|
803
|
+
// 1. Use curve-level token needs to infer the SY/PT ratio for this range.
|
|
804
|
+
// Raw active-tick proportions are ignored because the deposit CPI
|
|
805
|
+
// normalizes the active tick before minting new shares.
|
|
855
806
|
// 2. Use that ratio plus the current SY exchange rate to decide how much of the
|
|
856
807
|
// minted SY should be stripped into PT (calc_strip_amount).
|
|
857
808
|
const [syMock, ptMock] = computeTokenNeedsWithCrossing(
|
|
@@ -864,9 +815,6 @@ export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
|
864
815
|
1e9,
|
|
865
816
|
1e9,
|
|
866
817
|
epsilonClamp,
|
|
867
|
-
crossingTickState,
|
|
868
|
-
crossingTickPriceLeft,
|
|
869
|
-
crossingTickPriceRight,
|
|
870
818
|
)
|
|
871
819
|
|
|
872
820
|
// Total SY the user would get by minting SY from base off-chain
|
|
@@ -922,6 +870,7 @@ function calcStripAmount(totalAmountSy: number, curSyRate: number, marketPtLiq:
|
|
|
922
870
|
* @param lowerTick - Lower tick (APY in basis points)
|
|
923
871
|
* @param upperTick - Upper tick (APY in basis points)
|
|
924
872
|
* @param syExchangeRate - SY exchange rate
|
|
873
|
+
* @param options - Optional existing LP position to include fixed crossing-split equalization spend
|
|
925
874
|
* @returns Simulation result with LP out, YT out, amounts spent, etc.
|
|
926
875
|
*/
|
|
927
876
|
export function simulateWrapperProvideLiquidity(
|
|
@@ -930,6 +879,7 @@ export function simulateWrapperProvideLiquidity(
|
|
|
930
879
|
lowerTick: number,
|
|
931
880
|
upperTick: number,
|
|
932
881
|
syExchangeRate: number,
|
|
882
|
+
options: AddLiquiditySimulationOptions = {},
|
|
933
883
|
): {
|
|
934
884
|
lpOut: number
|
|
935
885
|
ytOut: number
|
|
@@ -961,10 +911,7 @@ export function simulateWrapperProvideLiquidity(
|
|
|
961
911
|
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
962
912
|
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
963
913
|
|
|
964
|
-
// Step 3:
|
|
965
|
-
const { crossingTickState, crossingTickPriceLeft, crossingTickPriceRight } = getCrossingTickStateFromTicks(ticks)
|
|
966
|
-
|
|
967
|
-
// Step 4: Calculate mock token needs using compute_token_needs_with_crossing
|
|
914
|
+
// Step 3: Calculate curve-level token needs.
|
|
968
915
|
// max_sy = syAmount, max_pt = syAmount * syExchangeRate (as on-chain)
|
|
969
916
|
const maxPt = syAmount * syExchangeRate
|
|
970
917
|
const [syMock, ptMock] = computeTokenNeedsWithCrossing(
|
|
@@ -977,26 +924,23 @@ export function simulateWrapperProvideLiquidity(
|
|
|
977
924
|
syAmount,
|
|
978
925
|
maxPt,
|
|
979
926
|
configurationOptions.epsilonClamp,
|
|
980
|
-
crossingTickState,
|
|
981
|
-
crossingTickPriceLeft,
|
|
982
|
-
crossingTickPriceRight,
|
|
983
927
|
)
|
|
984
928
|
|
|
985
|
-
// Step
|
|
929
|
+
// Step 4: Calculate how much SY to strip (calc_strip_amount on-chain)
|
|
986
930
|
// Cap syToStrip to syAmount to prevent negative remainder from ceil rounding
|
|
987
931
|
const syToStripRaw = calcStripAmount(syAmount, syExchangeRate, ptMock, syMock)
|
|
988
932
|
const syToStrip = Math.min(syToStripRaw, syAmount)
|
|
989
933
|
|
|
990
|
-
// Step
|
|
934
|
+
// Step 5: Calculate PT and YT from stripping
|
|
991
935
|
// When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
|
|
992
936
|
const ptFromStrip = syToStrip * syExchangeRate
|
|
993
937
|
const ytOut = ptFromStrip // YT amount equals PT amount from strip
|
|
994
938
|
|
|
995
|
-
// Step
|
|
939
|
+
// Step 6: Calculate remaining SY after strip
|
|
996
940
|
// Use Math.max to ensure non-negative (safety net for floating point edge cases)
|
|
997
941
|
const syRemainder = Math.max(0, syAmount - syToStrip)
|
|
998
942
|
|
|
999
|
-
// Step
|
|
943
|
+
// Step 7: Simulate deposit liquidity with remaining SY and PT
|
|
1000
944
|
const depositResult = simulateAddLiquidity(marketState, {
|
|
1001
945
|
lowerTick,
|
|
1002
946
|
upperTick,
|
|
@@ -1037,6 +981,7 @@ export function simulateWrapperProvideLiquidity(
|
|
|
1037
981
|
* @param lowerTick - Lower tick key (APY in basis points)
|
|
1038
982
|
* @param upperTick - Upper tick key (APY in basis points)
|
|
1039
983
|
* @param syExchangeRate - SY exchange rate
|
|
984
|
+
* @param options - Optional existing LP position to include fixed crossing-split equalization spend
|
|
1040
985
|
* @returns Simulation result with LP out, PT to buy, SY constraint, etc.
|
|
1041
986
|
*/
|
|
1042
987
|
export function simulateSwapAndSupply(
|
|
@@ -1045,6 +990,7 @@ export function simulateSwapAndSupply(
|
|
|
1045
990
|
lowerTick: number,
|
|
1046
991
|
upperTick: number,
|
|
1047
992
|
syExchangeRate: number,
|
|
993
|
+
options: AddLiquiditySimulationOptions = {},
|
|
1048
994
|
): {
|
|
1049
995
|
lpOut: number
|
|
1050
996
|
ptToBuy: number
|
|
@@ -1061,7 +1007,7 @@ export function simulateSwapAndSupply(
|
|
|
1061
1007
|
// So the strict SY budget for a user-provided base input is floor(base / rate).
|
|
1062
1008
|
const syBudget = convertBaseToSyBudget(amountBase, syExchangeRate)
|
|
1063
1009
|
|
|
1064
|
-
if (syBudget <= 0
|
|
1010
|
+
if (syBudget <= 0) {
|
|
1065
1011
|
return {
|
|
1066
1012
|
lpOut: 0,
|
|
1067
1013
|
ptToBuy: 0,
|
|
@@ -1143,6 +1089,7 @@ export function simulateSwapAndSupply(
|
|
|
1143
1089
|
key,
|
|
1144
1090
|
syExchangeRate,
|
|
1145
1091
|
exactOutEstimateSlack,
|
|
1092
|
+
options.existingPosition,
|
|
1146
1093
|
)
|
|
1147
1094
|
candidateCache.set(key, candidate)
|
|
1148
1095
|
return candidate
|
|
@@ -1294,6 +1241,7 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1294
1241
|
ptConstraint: number,
|
|
1295
1242
|
syExchangeRate: number,
|
|
1296
1243
|
exactOutEstimateSlack: number,
|
|
1244
|
+
existingPosition?: LpPositionCLMM,
|
|
1297
1245
|
): SwapAndSupplyCandidate | null {
|
|
1298
1246
|
let swapResult: {
|
|
1299
1247
|
sySpent: number
|
|
@@ -1315,13 +1263,22 @@ function simulateSwapAndSupplyForPtConstraint(
|
|
|
1315
1263
|
}
|
|
1316
1264
|
|
|
1317
1265
|
const depositState = swapResult.postMarketState ?? marketState
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1266
|
+
let depositResult: AddLiquidityOutcome
|
|
1267
|
+
try {
|
|
1268
|
+
depositResult = simulateAddLiquidityForPosition(
|
|
1269
|
+
depositState,
|
|
1270
|
+
{
|
|
1271
|
+
lowerTick,
|
|
1272
|
+
upperTick,
|
|
1273
|
+
maxSy: syBudget,
|
|
1274
|
+
maxPt: swapResult.ptOut,
|
|
1275
|
+
syExchangeRate,
|
|
1276
|
+
},
|
|
1277
|
+
existingPosition,
|
|
1278
|
+
)
|
|
1279
|
+
} catch {
|
|
1280
|
+
return null
|
|
1281
|
+
}
|
|
1325
1282
|
|
|
1326
1283
|
// Swap & supply must actually add liquidity.
|
|
1327
1284
|
// Discard candidates that end up as swap-only (deltaL == 0).
|