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

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.
@@ -0,0 +1,367 @@
1
+ import type { LpPositionCLMM, Tick, Ticks } from "@exponent-labs/exponent-fetcher"
2
+
3
+ const SENTINEL_TICK_INDEX = 0xffffffff
4
+ const PRECISE_NUMBER_DENOM = 1_000_000_000_000n
5
+ const U64_MAX = (1n << 64n) - 1n
6
+ const U128_MAX = (1n << 128n) - 1n
7
+
8
+ export type CrossingEqualizationDirection = "add" | "remove"
9
+
10
+ export type CrossingEqualizationPlanStep = {
11
+ shareIndex: number
12
+ tickIdx: number
13
+ direction: CrossingEqualizationDirection
14
+ shareDeltaRaw: bigint
15
+ ptDelta: bigint
16
+ syDelta: bigint
17
+ }
18
+
19
+ export type ExistingPositionEqualization = {
20
+ sySpent: bigint
21
+ ptSpent: bigint
22
+ syReleased: bigint
23
+ ptReleased: bigint
24
+ plan: CrossingEqualizationPlanStep[]
25
+ }
26
+
27
+ export type ExistingPositionBudgetEffect = {
28
+ userMaxSy: bigint
29
+ userMaxPt: bigint
30
+ effectiveMaxSy: bigint
31
+ effectiveMaxPt: bigint
32
+ fixedSySpent: bigint
33
+ fixedPtSpent: bigint
34
+ fixedSyReleased: bigint
35
+ fixedPtReleased: bigint
36
+ }
37
+
38
+ const invariantError = (message: string) => new Error(`CrossingEqualizationInvariantViolated: ${message}`)
39
+
40
+ const assertU64 = (value: bigint, label: string): bigint => {
41
+ if (value < 0n || value > U64_MAX) {
42
+ throw invariantError(`${label} does not fit in u64: ${value.toString()}`)
43
+ }
44
+ return value
45
+ }
46
+
47
+ const assertU128 = (value: bigint, label: string): bigint => {
48
+ if (value < 0n || value > U128_MAX) {
49
+ throw invariantError(`${label} does not fit in u128: ${value.toString()}`)
50
+ }
51
+ return value
52
+ }
53
+
54
+ const checkedAddU64 = (left: bigint, right: bigint, label: string) => assertU64(left + right, label)
55
+
56
+ const checkedSubU64 = (left: bigint, right: bigint, label: string) => {
57
+ if (right > left) {
58
+ throw invariantError(`${label} underflow: ${left.toString()} - ${right.toString()}`)
59
+ }
60
+ return left - right
61
+ }
62
+
63
+ const checkedMulU128 = (left: bigint, right: bigint, label: string) => assertU128(left * right, label)
64
+
65
+ const ceilMulDivU128 = (left: bigint, right: bigint, denominator: bigint, label: string) => {
66
+ if (denominator <= 0n) {
67
+ throw invariantError(`${label} division by zero`)
68
+ }
69
+
70
+ const product = checkedMulU128(left, right, `${label} product`)
71
+ return assertU128(product + denominator - 1n, `${label} ceil numerator`) / denominator
72
+ }
73
+
74
+ const fastFloorU128 = (rawPreciseNumber: bigint) =>
75
+ assertU128(rawPreciseNumber, "precise number raw") / PRECISE_NUMBER_DENOM
76
+
77
+ const fastMulRatioRaw = (rawPreciseNumber: bigint, numerator: bigint, denominator: bigint, label: string) => {
78
+ if (denominator <= 0n) {
79
+ throw invariantError(`${label} division by zero`)
80
+ }
81
+
82
+ const raw = assertU128(rawPreciseNumber, `${label} raw precise number`)
83
+ const product = checkedMulU128(raw, numerator, `${label} product`)
84
+ return product / denominator
85
+ }
86
+
87
+ const getTickByIndex = (ticks: Ticks, tickIdx: number): Tick => {
88
+ const tick = ticks.ticksTree[tickIdx - 1]
89
+ if (!tick || tick.apyBasePoints <= 0) {
90
+ throw invariantError(`missing tick at index ${tickIdx}`)
91
+ }
92
+
93
+ return tick
94
+ }
95
+
96
+ const getTickKey = (ticks: Ticks, tickIdx: number) => getTickByIndex(ticks, tickIdx).apyBasePoints
97
+
98
+ const successorIdx = (ticks: Ticks, tickIdx: number): number | null => {
99
+ const tickKey = getTickKey(ticks, tickIdx)
100
+ let best: { tickIdx: number; key: number } | null = null
101
+
102
+ for (let index = 0; index < ticks.ticksTree.length; index++) {
103
+ const key = ticks.ticksTree[index].apyBasePoints
104
+ if (key <= tickKey) {
105
+ continue
106
+ }
107
+ if (!best || key < best.key) {
108
+ best = { tickIdx: index + 1, key }
109
+ }
110
+ }
111
+
112
+ return best?.tickIdx ?? null
113
+ }
114
+
115
+ const isCrossingSplitActive = (position: LpPositionCLMM) =>
116
+ position.crossingSplit.crossLeftIdx !== SENTINEL_TICK_INDEX &&
117
+ position.crossingSplit.crossRightIdx !== SENTINEL_TICK_INDEX
118
+
119
+ const shareIsWithinCrossingRange = (
120
+ ticks: Ticks,
121
+ position: LpPositionCLMM,
122
+ share: LpPositionCLMM["shareTrackers"][number],
123
+ ): boolean => {
124
+ const shareLeftKey = getTickKey(ticks, share.tickIdx)
125
+ let shareRightIdx: number | null = share.rightTickIdx
126
+ if (share.rightTickIdx === SENTINEL_TICK_INDEX) {
127
+ shareRightIdx = successorIdx(ticks, share.tickIdx)
128
+ }
129
+
130
+ if (shareRightIdx == null) {
131
+ return false
132
+ }
133
+
134
+ const shareRightKey = getTickKey(ticks, shareRightIdx)
135
+ const crossLeftKey = getTickKey(ticks, position.crossingSplit.crossLeftIdx)
136
+ const crossRightKey = getTickKey(ticks, position.crossingSplit.crossRightIdx)
137
+
138
+ return shareLeftKey >= crossLeftKey && shareRightKey <= crossRightKey
139
+ }
140
+
141
+ const computeAddDeltas = (params: {
142
+ ownedPt: bigint
143
+ ownedSy: bigint
144
+ liquidityDelta: bigint
145
+ lActual: bigint
146
+ }): [bigint, bigint] => {
147
+ const ptDelta =
148
+ params.ownedPt > 0n
149
+ ? assertU64(
150
+ ceilMulDivU128(params.ownedPt, params.liquidityDelta, params.lActual, "PT equalization in"),
151
+ "PT equalization in",
152
+ )
153
+ : 0n
154
+ const syDelta =
155
+ params.ownedSy > 0n
156
+ ? assertU64(
157
+ ceilMulDivU128(params.ownedSy, params.liquidityDelta, params.lActual, "SY equalization in"),
158
+ "SY equalization in",
159
+ )
160
+ : 0n
161
+
162
+ return [ptDelta, syDelta]
163
+ }
164
+
165
+ const computeRemoveDeltas = (params: { shareDeltaRaw: bigint; node: Tick; supply: bigint }): [bigint, bigint] => {
166
+ const burnShares = fastFloorU128(params.shareDeltaRaw)
167
+ const ptDelta = assertU64(
168
+ checkedMulU128(burnShares, params.node.principalPt, "PT equalization out product") / params.supply,
169
+ "PT equalization out",
170
+ )
171
+ const syDelta = assertU64(
172
+ checkedMulU128(burnShares, params.node.principalSy, "SY equalization out product") / params.supply,
173
+ "SY equalization out",
174
+ )
175
+
176
+ return [ptDelta, syDelta]
177
+ }
178
+
179
+ const summarizePlan = (plan: CrossingEqualizationPlanStep[]): ExistingPositionEqualization => {
180
+ let sySpent = 0n
181
+ let ptSpent = 0n
182
+ let syReleased = 0n
183
+ let ptReleased = 0n
184
+
185
+ for (const step of plan) {
186
+ if (step.direction === "add") {
187
+ sySpent = checkedAddU64(sySpent, step.syDelta, "equalization sy spent")
188
+ ptSpent = checkedAddU64(ptSpent, step.ptDelta, "equalization pt spent")
189
+ continue
190
+ }
191
+
192
+ syReleased = checkedAddU64(syReleased, step.syDelta, "equalization sy released")
193
+ ptReleased = checkedAddU64(ptReleased, step.ptDelta, "equalization pt released")
194
+ }
195
+
196
+ return {
197
+ sySpent,
198
+ ptSpent,
199
+ syReleased,
200
+ ptReleased,
201
+ plan,
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Mirrors `precalc_existing_position_add_liquidity` from the CLMM program.
207
+ *
208
+ * The returned `ptSpent` and `sySpent` are the exact token budget that must be
209
+ * reserved before adding more liquidity to a position with an active crossing split.
210
+ * All PreciseNumber operations are performed on raw fixed-point `bigint` values so
211
+ * the rounding matches the contract's `fast_floor_u128`, `fast_mul_ratio`, and
212
+ * `muldiv_ceil_u128` paths.
213
+ */
214
+ export function computeExistingPositionEqualization(
215
+ ticks: Ticks,
216
+ position: LpPositionCLMM,
217
+ ): ExistingPositionEqualization {
218
+ if (!isCrossingSplitActive(position)) {
219
+ return summarizePlan([])
220
+ }
221
+
222
+ const lActual = assertU64(position.crossingSplit.lpBalanceCrossing, "lpBalanceCrossing")
223
+ const lpBalance = assertU64(position.lpBalance, "lpBalance")
224
+
225
+ if (lpBalance === lActual) {
226
+ return summarizePlan([])
227
+ }
228
+ if (lActual === 0n) {
229
+ throw invariantError("active crossing split has zero actual liquidity")
230
+ }
231
+
232
+ const direction: CrossingEqualizationDirection = lpBalance > lActual ? "add" : "remove"
233
+ const liquidityDelta = lpBalance > lActual ? lpBalance - lActual : lActual - lpBalance
234
+ const plan: CrossingEqualizationPlanStep[] = []
235
+ let sawCrossingRangeShare = false
236
+ let skippedCrossingShareDueToDust = false
237
+
238
+ for (let shareIndex = 0; shareIndex < position.shareTrackers.length; shareIndex++) {
239
+ const share = position.shareTrackers[shareIndex]
240
+ if (!shareIsWithinCrossingRange(ticks, position, share)) {
241
+ continue
242
+ }
243
+
244
+ sawCrossingRangeShare = true
245
+ const node = getTickByIndex(ticks, share.tickIdx)
246
+ const supply = fastFloorU128(node.principalShareSupply)
247
+ if (supply === 0n) {
248
+ skippedCrossingShareDueToDust = true
249
+ continue
250
+ }
251
+
252
+ const shareFloor = fastFloorU128(share.lpShare)
253
+ if (shareFloor === 0n) {
254
+ skippedCrossingShareDueToDust = true
255
+ continue
256
+ }
257
+
258
+ const ownedPt = assertU64(checkedMulU128(shareFloor, node.principalPt, "owned PT product") / supply, "owned PT")
259
+ const ownedSy = assertU64(checkedMulU128(shareFloor, node.principalSy, "owned SY product") / supply, "owned SY")
260
+ const shareDeltaRaw = fastMulRatioRaw(share.lpShare, liquidityDelta, lActual, "share delta")
261
+
262
+ let deltas: [bigint, bigint]
263
+ if (direction === "add") {
264
+ deltas = computeAddDeltas({
265
+ ownedPt,
266
+ ownedSy,
267
+ liquidityDelta,
268
+ lActual,
269
+ })
270
+ } else {
271
+ deltas = computeRemoveDeltas({
272
+ shareDeltaRaw,
273
+ node,
274
+ supply,
275
+ })
276
+ }
277
+ const [ptDelta, syDelta] = deltas
278
+
279
+ if (shareDeltaRaw === 0n && ptDelta === 0n && syDelta === 0n) {
280
+ skippedCrossingShareDueToDust = true
281
+ continue
282
+ }
283
+
284
+ plan.push({
285
+ shareIndex,
286
+ tickIdx: share.tickIdx,
287
+ direction,
288
+ shareDeltaRaw,
289
+ ptDelta,
290
+ syDelta,
291
+ })
292
+ }
293
+
294
+ if (plan.length === 0 && sawCrossingRangeShare && skippedCrossingShareDueToDust) {
295
+ return summarizePlan([])
296
+ }
297
+ if (plan.length === 0) {
298
+ throw invariantError("active crossing split has no equalizable crossing-range shares")
299
+ }
300
+
301
+ return summarizePlan(plan)
302
+ }
303
+
304
+ /**
305
+ * Mirrors the contract's `compute_existing_position_budget_effect`.
306
+ *
307
+ * It throws the same invariant class when the supplied user budgets cannot cover
308
+ * the fixed equalization spend, for example `userMaxPt=0` while `ptSpent>0`.
309
+ */
310
+ export function computeExistingPositionBudgetEffect(params: {
311
+ ticks: Ticks
312
+ position: LpPositionCLMM
313
+ userMaxSy: bigint
314
+ userMaxPt: bigint
315
+ }): ExistingPositionBudgetEffect {
316
+ const equalization = computeExistingPositionEqualization(params.ticks, params.position)
317
+ const effectiveMaxSy = checkedAddU64(
318
+ checkedSubU64(params.userMaxSy, equalization.sySpent, "effective max SY"),
319
+ equalization.syReleased,
320
+ "effective max SY",
321
+ )
322
+ const effectiveMaxPt = checkedAddU64(
323
+ checkedSubU64(params.userMaxPt, equalization.ptSpent, "effective max PT"),
324
+ equalization.ptReleased,
325
+ "effective max PT",
326
+ )
327
+
328
+ return {
329
+ userMaxSy: params.userMaxSy,
330
+ userMaxPt: params.userMaxPt,
331
+ effectiveMaxSy,
332
+ effectiveMaxPt,
333
+ fixedSySpent: equalization.sySpent,
334
+ fixedPtSpent: equalization.ptSpent,
335
+ fixedSyReleased: equalization.syReleased,
336
+ fixedPtReleased: equalization.ptReleased,
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Mirrors `required_user_max_sy/pt`: expands a desired post-equalization budget
342
+ * into the user-facing max token amounts that should be passed to the instruction.
343
+ */
344
+ export function computeRequiredUserMaxForExistingPositionEqualization(params: {
345
+ ticks: Ticks
346
+ position: LpPositionCLMM
347
+ desiredEffectiveMaxSy: bigint
348
+ desiredEffectiveMaxPt: bigint
349
+ }): { userMaxSy: bigint; userMaxPt: bigint; equalization: ExistingPositionEqualization } {
350
+ const equalization = computeExistingPositionEqualization(params.ticks, params.position)
351
+ const userMaxSy = checkedSubU64(
352
+ checkedAddU64(params.desiredEffectiveMaxSy, equalization.sySpent, "required user max SY"),
353
+ equalization.syReleased,
354
+ "required user max SY",
355
+ )
356
+ const userMaxPt = checkedSubU64(
357
+ checkedAddU64(params.desiredEffectiveMaxPt, equalization.ptSpent, "required user max PT"),
358
+ equalization.ptReleased,
359
+ "required user max PT",
360
+ )
361
+
362
+ return {
363
+ userMaxSy,
364
+ userMaxPt,
365
+ equalization,
366
+ }
367
+ }
package/src/index.ts CHANGED
@@ -56,6 +56,17 @@ export {
56
56
  simulateWrapperProvideLiquidity,
57
57
  simulateSwapAndSupply,
58
58
  } from "./addLiquidity"
59
+ export {
60
+ computeExistingPositionBudgetEffect,
61
+ computeExistingPositionEqualization,
62
+ computeRequiredUserMaxForExistingPositionEqualization,
63
+ } from "./existingPositionEqualization"
64
+ export type {
65
+ CrossingEqualizationDirection,
66
+ CrossingEqualizationPlanStep,
67
+ ExistingPositionBudgetEffect,
68
+ ExistingPositionEqualization,
69
+ } from "./existingPositionEqualization"
59
70
 
60
71
  export { getPtAndSyOnWithdrawLiquidity } from "./withdrawLiquidity"
61
72
 
package/src/utils.ts CHANGED
@@ -219,18 +219,20 @@ export function findTickByIndex(ticks: Ticks, index: number): Tick {
219
219
  return ticks.ticksTree.at(index - 1) ?? null
220
220
  }
221
221
 
222
+ const CLMM_TICK_KEY_SCALE = 10_000
223
+
222
224
  /**
223
- * Convert APY percentage to basis points
225
+ * Convert APY percentage to CLMM tick-key units.
224
226
  */
225
227
  export function convertApyToApyBp(apyPercent: number): number {
226
- return Math.round(apyPercent * 100)
228
+ return Math.round(apyPercent * CLMM_TICK_KEY_SCALE)
227
229
  }
228
230
 
229
231
  /**
230
- * Convert basis points to APY percentage
232
+ * Convert CLMM tick-key units to APY percentage.
231
233
  */
232
234
  export function convertApyBpToApy(apyBp: number): number {
233
- return apyBp / 100
235
+ return apyBp / CLMM_TICK_KEY_SCALE
234
236
  }
235
237
 
236
238
  export function bigIntMax(...args: bigint[]): bigint {