@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
|
@@ -30,7 +30,9 @@ const makeTicks = (): Ticks => ({
|
|
|
30
30
|
farms: [],
|
|
31
31
|
emissions: [],
|
|
32
32
|
lastSplitEpoch: 0n,
|
|
33
|
+
splitParentEpoch: 0n,
|
|
33
34
|
frozenLiquidity: 0n,
|
|
35
|
+
splitParentIdx: SENTINEL_TICK_INDEX,
|
|
34
36
|
},
|
|
35
37
|
{
|
|
36
38
|
apyBasePoints: 65_000,
|
|
@@ -45,7 +47,9 @@ const makeTicks = (): Ticks => ({
|
|
|
45
47
|
farms: [],
|
|
46
48
|
emissions: [],
|
|
47
49
|
lastSplitEpoch: 0n,
|
|
50
|
+
splitParentEpoch: 0n,
|
|
48
51
|
frozenLiquidity: 0n,
|
|
52
|
+
splitParentIdx: SENTINEL_TICK_INDEX,
|
|
49
53
|
},
|
|
50
54
|
],
|
|
51
55
|
market: PUBLIC_KEY,
|
|
@@ -103,6 +107,35 @@ describe("existing position equalization", () => {
|
|
|
103
107
|
])
|
|
104
108
|
})
|
|
105
109
|
|
|
110
|
+
it("rejects overfilled crossing positions instead of producing remove plans", () => {
|
|
111
|
+
const position = makePosition()
|
|
112
|
+
position.lpBalance = 90n
|
|
113
|
+
|
|
114
|
+
expect(() => computeExistingPositionEqualization(makeTicks(), position)).toThrow(
|
|
115
|
+
"CrossingEqualizationInvariantViolated",
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("keeps fractional crossing shares and rounds token deltas up", () => {
|
|
120
|
+
const position = makePosition()
|
|
121
|
+
position.shareTrackers[0].lpShare = PRECISE_NUMBER_DENOM / 2n
|
|
122
|
+
|
|
123
|
+
const equalization = computeExistingPositionEqualization(makeTicks(), position)
|
|
124
|
+
|
|
125
|
+
expect(equalization.ptSpent).toBe(3n)
|
|
126
|
+
expect(equalization.sySpent).toBe(4n)
|
|
127
|
+
expect(equalization.plan).toEqual([
|
|
128
|
+
{
|
|
129
|
+
shareIndex: 0,
|
|
130
|
+
tickIdx: 1,
|
|
131
|
+
direction: "add",
|
|
132
|
+
shareDeltaRaw: PRECISE_NUMBER_DENOM / 2n,
|
|
133
|
+
ptDelta: 3n,
|
|
134
|
+
syDelta: 4n,
|
|
135
|
+
},
|
|
136
|
+
])
|
|
137
|
+
})
|
|
138
|
+
|
|
106
139
|
it("throws when user budgets cannot cover fixed equalization spend", () => {
|
|
107
140
|
expect(() =>
|
|
108
141
|
computeExistingPositionBudgetEffect({
|
|
@@ -5,7 +5,7 @@ const PRECISE_NUMBER_DENOM = 1_000_000_000_000n
|
|
|
5
5
|
const U64_MAX = (1n << 64n) - 1n
|
|
6
6
|
const U128_MAX = (1n << 128n) - 1n
|
|
7
7
|
|
|
8
|
-
export type CrossingEqualizationDirection = "add"
|
|
8
|
+
export type CrossingEqualizationDirection = "add"
|
|
9
9
|
|
|
10
10
|
export type CrossingEqualizationPlanStep = {
|
|
11
11
|
shareIndex: number
|
|
@@ -62,15 +62,6 @@ const checkedSubU64 = (left: bigint, right: bigint, label: string) => {
|
|
|
62
62
|
|
|
63
63
|
const checkedMulU128 = (left: bigint, right: bigint, label: string) => assertU128(left * right, label)
|
|
64
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
65
|
const fastFloorU128 = (rawPreciseNumber: bigint) =>
|
|
75
66
|
assertU128(rawPreciseNumber, "precise number raw") / PRECISE_NUMBER_DENOM
|
|
76
67
|
|
|
@@ -80,8 +71,11 @@ const fastMulRatioRaw = (rawPreciseNumber: bigint, numerator: bigint, denominato
|
|
|
80
71
|
}
|
|
81
72
|
|
|
82
73
|
const raw = assertU128(rawPreciseNumber, `${label} raw precise number`)
|
|
83
|
-
const
|
|
84
|
-
|
|
74
|
+
const q = raw / denominator
|
|
75
|
+
const r = raw % denominator
|
|
76
|
+
const qProduct = checkedMulU128(q, numerator, `${label} quotient product`)
|
|
77
|
+
const rProduct = checkedMulU128(r, numerator, `${label} remainder product`)
|
|
78
|
+
return assertU128(qProduct + rProduct / denominator, `${label} result`)
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
const getTickByIndex = (ticks: Ticks, tickIdx: number): Tick => {
|
|
@@ -138,39 +132,41 @@ const shareIsWithinCrossingRange = (
|
|
|
138
132
|
return shareLeftKey >= crossLeftKey && shareRightKey <= crossRightKey
|
|
139
133
|
}
|
|
140
134
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
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
|
|
135
|
+
const mulNumberRatioCeilU64 = (amount: bigint, valueRaw: bigint, denominator: bigint, label: string): bigint => {
|
|
136
|
+
if (amount === 0n || valueRaw === 0n) {
|
|
137
|
+
return 0n
|
|
138
|
+
}
|
|
139
|
+
if (denominator <= 0n) {
|
|
140
|
+
throw invariantError(`${label} division by zero`)
|
|
141
|
+
}
|
|
161
142
|
|
|
162
|
-
|
|
143
|
+
const raw = assertU128(valueRaw, `${label} raw precise number`)
|
|
144
|
+
const int = raw / PRECISE_NUMBER_DENOM
|
|
145
|
+
const frac = raw % PRECISE_NUMBER_DENOM
|
|
146
|
+
const intNumerator = checkedMulU128(amount, int, `${label} integer numerator`)
|
|
147
|
+
const quotient = intNumerator / denominator
|
|
148
|
+
const remainder = intNumerator % denominator
|
|
149
|
+
const fractionalNumerator = checkedMulU128(amount, frac, `${label} fractional numerator`)
|
|
150
|
+
const denominatorScaled = checkedMulU128(denominator, PRECISE_NUMBER_DENOM, `${label} scaled denominator`)
|
|
151
|
+
const remainderNumerator = checkedMulU128(remainder, PRECISE_NUMBER_DENOM, `${label} remainder numerator`)
|
|
152
|
+
const ceilNumerator = assertU128(remainderNumerator + fractionalNumerator, `${label} ceil numerator`)
|
|
153
|
+
const roundedRemainder = (ceilNumerator + denominatorScaled - 1n) / denominatorScaled
|
|
154
|
+
|
|
155
|
+
return assertU64(quotient + roundedRemainder, label)
|
|
163
156
|
}
|
|
164
157
|
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
158
|
+
const computeAddDeltas = (params: { node: Tick; shareDeltaRaw: bigint; supply: bigint }): [bigint, bigint] => {
|
|
159
|
+
const ptDelta = mulNumberRatioCeilU64(
|
|
160
|
+
params.node.principalPt,
|
|
161
|
+
params.shareDeltaRaw,
|
|
162
|
+
params.supply,
|
|
163
|
+
"PT equalization in",
|
|
170
164
|
)
|
|
171
|
-
const syDelta =
|
|
172
|
-
|
|
173
|
-
|
|
165
|
+
const syDelta = mulNumberRatioCeilU64(
|
|
166
|
+
params.node.principalSy,
|
|
167
|
+
params.shareDeltaRaw,
|
|
168
|
+
params.supply,
|
|
169
|
+
"SY equalization in",
|
|
174
170
|
)
|
|
175
171
|
|
|
176
172
|
return [ptDelta, syDelta]
|
|
@@ -183,14 +179,8 @@ const summarizePlan = (plan: CrossingEqualizationPlanStep[]): ExistingPositionEq
|
|
|
183
179
|
let ptReleased = 0n
|
|
184
180
|
|
|
185
181
|
for (const step of plan) {
|
|
186
|
-
|
|
187
|
-
|
|
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")
|
|
182
|
+
sySpent = checkedAddU64(sySpent, step.syDelta, "equalization sy spent")
|
|
183
|
+
ptSpent = checkedAddU64(ptSpent, step.ptDelta, "equalization pt spent")
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
return {
|
|
@@ -225,15 +215,16 @@ export function computeExistingPositionEqualization(
|
|
|
225
215
|
if (lpBalance === lActual) {
|
|
226
216
|
return summarizePlan([])
|
|
227
217
|
}
|
|
218
|
+
if (lpBalance < lActual) {
|
|
219
|
+
throw invariantError("active crossing split is overfilled")
|
|
220
|
+
}
|
|
228
221
|
if (lActual === 0n) {
|
|
229
222
|
throw invariantError("active crossing split has zero actual liquidity")
|
|
230
223
|
}
|
|
231
224
|
|
|
232
|
-
const direction: CrossingEqualizationDirection =
|
|
233
|
-
const liquidityDelta = lpBalance
|
|
225
|
+
const direction: CrossingEqualizationDirection = "add"
|
|
226
|
+
const liquidityDelta = lpBalance - lActual
|
|
234
227
|
const plan: CrossingEqualizationPlanStep[] = []
|
|
235
|
-
let sawCrossingRangeShare = false
|
|
236
|
-
let skippedCrossingShareDueToDust = false
|
|
237
228
|
|
|
238
229
|
for (let shareIndex = 0; shareIndex < position.shareTrackers.length; shareIndex++) {
|
|
239
230
|
const share = position.shareTrackers[shareIndex]
|
|
@@ -241,43 +232,24 @@ export function computeExistingPositionEqualization(
|
|
|
241
232
|
continue
|
|
242
233
|
}
|
|
243
234
|
|
|
244
|
-
sawCrossingRangeShare = true
|
|
245
235
|
const node = getTickByIndex(ticks, share.tickIdx)
|
|
246
236
|
const supply = fastFloorU128(node.principalShareSupply)
|
|
247
237
|
if (supply === 0n) {
|
|
248
|
-
skippedCrossingShareDueToDust = true
|
|
249
238
|
continue
|
|
250
239
|
}
|
|
251
240
|
|
|
252
|
-
|
|
253
|
-
if (shareFloor === 0n) {
|
|
254
|
-
skippedCrossingShareDueToDust = true
|
|
241
|
+
if (share.lpShare === 0n) {
|
|
255
242
|
continue
|
|
256
243
|
}
|
|
257
244
|
|
|
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
245
|
const shareDeltaRaw = fastMulRatioRaw(share.lpShare, liquidityDelta, lActual, "share delta")
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
246
|
+
const [ptDelta, syDelta] = computeAddDeltas({
|
|
247
|
+
node,
|
|
248
|
+
shareDeltaRaw,
|
|
249
|
+
supply,
|
|
250
|
+
})
|
|
278
251
|
|
|
279
252
|
if (shareDeltaRaw === 0n && ptDelta === 0n && syDelta === 0n) {
|
|
280
|
-
skippedCrossingShareDueToDust = true
|
|
281
253
|
continue
|
|
282
254
|
}
|
|
283
255
|
|
|
@@ -291,9 +263,6 @@ export function computeExistingPositionEqualization(
|
|
|
291
263
|
})
|
|
292
264
|
}
|
|
293
265
|
|
|
294
|
-
if (plan.length === 0 && sawCrossingRangeShare && skippedCrossingShareDueToDust) {
|
|
295
|
-
return summarizePlan([])
|
|
296
|
-
}
|
|
297
266
|
if (plan.length === 0) {
|
|
298
267
|
throw invariantError("active crossing split has no equalizable crossing-range shares")
|
|
299
268
|
}
|
|
@@ -338,8 +307,8 @@ export function computeExistingPositionBudgetEffect(params: {
|
|
|
338
307
|
}
|
|
339
308
|
|
|
340
309
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
310
|
+
* Expands a desired post-equalization budget into user-facing max token inputs.
|
|
311
|
+
* This is the algebraic inverse of `computeExistingPositionBudgetEffect`.
|
|
343
312
|
*/
|
|
344
313
|
export function computeRequiredUserMaxForExistingPositionEqualization(params: {
|
|
345
314
|
ticks: Ticks
|
package/src/index.ts
CHANGED
|
@@ -48,8 +48,6 @@ export {
|
|
|
48
48
|
simulateAddLiquidity,
|
|
49
49
|
computeLiquidityTargetAndTokenNeeds,
|
|
50
50
|
computeTokenNeedsWithCrossing,
|
|
51
|
-
getCrossingTickStateFromTicks,
|
|
52
|
-
scaleCrossingTickInputs,
|
|
53
51
|
calculateLpOut,
|
|
54
52
|
estimateBalancedDeposit,
|
|
55
53
|
calcDepositSyAndPtFromBaseAmount,
|
|
@@ -84,8 +82,6 @@ export type {
|
|
|
84
82
|
AddLiquidityArgs,
|
|
85
83
|
AddLiquidityOutcome,
|
|
86
84
|
LiquidityNeeds,
|
|
87
|
-
CrossingTickState,
|
|
88
|
-
CrossingScaleParams,
|
|
89
85
|
MarketThreeState,
|
|
90
86
|
} from "./types"
|
|
91
87
|
export { SwapDirection } from "./types"
|
package/src/swap.ts
CHANGED
|
@@ -309,6 +309,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
309
309
|
amountOut: amountOutNet,
|
|
310
310
|
lpFeeChargedOutToken: feeLpOut,
|
|
311
311
|
protocolFeeChargedOutToken: feeProtocolOut,
|
|
312
|
+
boundaryRemainderChargedOutToken: 0,
|
|
312
313
|
finalSpotPrice: currentPriceSpot,
|
|
313
314
|
finalTickIndex: currentLeftBoundaryIndex,
|
|
314
315
|
}
|
package/src/swapV2.ts
CHANGED
|
@@ -16,6 +16,27 @@ 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 boundaryPrincipalRemainder(
|
|
20
|
+
computedGrossOut: number,
|
|
21
|
+
fullOutgoingPrincipal: number,
|
|
22
|
+
reachesBoundary: boolean,
|
|
23
|
+
): number {
|
|
24
|
+
if (!reachesBoundary) return 0
|
|
25
|
+
return Math.max(0, fullOutgoingPrincipal - Math.min(computedGrossOut, fullOutgoingPrincipal))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitFeeWithBoundaryRemainderAsLpFee(
|
|
29
|
+
totalFeeOut: number,
|
|
30
|
+
boundaryRemainder: number,
|
|
31
|
+
protocolFeeBps: number,
|
|
32
|
+
): { lpFeeOut: number; protocolFeeOut: number } {
|
|
33
|
+
const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
|
|
34
|
+
return {
|
|
35
|
+
lpFeeOut: totalFeeOut - protocolFeeOut + boundaryRemainder,
|
|
36
|
+
protocolFeeOut,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
41
|
* Simulate a swap on the CLMM market (V2 - mirrors Rust closely)
|
|
21
42
|
* This is a pure function that does not mutate the market state
|
|
@@ -62,6 +83,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
62
83
|
let amountOutNetU64 = 0
|
|
63
84
|
let feeLpOutU64 = 0
|
|
64
85
|
let feeProtocolOutU64 = 0
|
|
86
|
+
let boundaryRemainderChargedOutToken = 0
|
|
65
87
|
let amountInLeft = args.amountIn
|
|
66
88
|
|
|
67
89
|
if (DEBUG) {
|
|
@@ -227,10 +249,16 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
227
249
|
const anchorUNew = currentPriceSpot + duActual
|
|
228
250
|
const cEffNew = snapshot.getEffectivePrice(anchorUNew)
|
|
229
251
|
const syOutGrossF = lTradeF64 * (cEffOld - cEffNew)
|
|
252
|
+
const reachesRightBoundary = Math.abs(anchorURight - anchorUNew) <= eps
|
|
230
253
|
|
|
231
254
|
// Clamp by SY principal
|
|
232
255
|
let syOutGrossU64 = Math.floor(syOutGrossF)
|
|
233
256
|
syOutGrossU64 = Math.min(syOutGrossU64, Number(principalSy))
|
|
257
|
+
const syRemainderAtBoundary = boundaryPrincipalRemainder(
|
|
258
|
+
syOutGrossU64,
|
|
259
|
+
Number(principalSy),
|
|
260
|
+
reachesRightBoundary,
|
|
261
|
+
)
|
|
234
262
|
|
|
235
263
|
if (DEBUG) {
|
|
236
264
|
console.log(` ptInSegment=${ptInSegment}, syOutGrossU64=${syOutGrossU64}`)
|
|
@@ -238,7 +266,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
238
266
|
|
|
239
267
|
// Fees in token_out (SY)
|
|
240
268
|
// For flash swaps, fee is based on YT value = (pt - sy × sy_exchange_rate) / sy_exchange_rate (in SY terms)
|
|
241
|
-
if (syOutGrossU64 > 0) {
|
|
269
|
+
if (syOutGrossU64 > 0 || syRemainderAtBoundary > 0) {
|
|
242
270
|
let totalFeeOut: number
|
|
243
271
|
if (args.isCurrentFlashSwap) {
|
|
244
272
|
const syOutBase = syOutGrossU64 * args.syExchangeRate
|
|
@@ -248,21 +276,25 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
248
276
|
} else {
|
|
249
277
|
totalFeeOut = getFeeFromAmount(syOutGrossU64, lpFeeRate)
|
|
250
278
|
}
|
|
251
|
-
const protocolFeeOut =
|
|
252
|
-
|
|
279
|
+
const { lpFeeOut, protocolFeeOut } = splitFeeWithBoundaryRemainderAsLpFee(
|
|
280
|
+
totalFeeOut,
|
|
281
|
+
syRemainderAtBoundary,
|
|
282
|
+
protocolFeeBps,
|
|
283
|
+
)
|
|
253
284
|
const syOutNet = syOutGrossU64 - totalFeeOut
|
|
254
285
|
|
|
255
286
|
// Accumulate to user
|
|
256
287
|
amountOutNetU64 += syOutNet
|
|
257
288
|
feeLpOutU64 += lpFeeOut
|
|
258
289
|
feeProtocolOutU64 += protocolFeeOut
|
|
290
|
+
boundaryRemainderChargedOutToken += syRemainderAtBoundary
|
|
259
291
|
|
|
260
292
|
// Mirror on-chain principal mutation for this interval:
|
|
261
293
|
// +PT input (gross), -SY output (gross)
|
|
262
294
|
ticksWrapper.setPrincipals(
|
|
263
295
|
currentLeftBoundaryKey,
|
|
264
296
|
principalPt + BigInt(ptInSegment),
|
|
265
|
-
principalSy - BigInt(syOutGrossU64),
|
|
297
|
+
reachesRightBoundary ? 0n : principalSy - BigInt(syOutGrossU64),
|
|
266
298
|
)
|
|
267
299
|
|
|
268
300
|
// Consume input and advance state
|
|
@@ -320,6 +352,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
320
352
|
// New effective price and spot after consuming ΔC
|
|
321
353
|
const cEffNew = cEffOld + deltaCActual
|
|
322
354
|
const spotPriceNew = snapshot.spotPriceFromEffectivePrice(cEffNew)
|
|
355
|
+
const reachesLeftBoundary = Math.abs(cEffNew - cEffLeft) <= eps || Math.abs(spotPriceNew - anchorULeft) <= eps
|
|
323
356
|
|
|
324
357
|
// Token flows
|
|
325
358
|
const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
|
|
@@ -329,6 +362,11 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
329
362
|
// Clamp gross PT by available principal
|
|
330
363
|
let ptOutGrossU64 = Math.floor(ptOutGrossF)
|
|
331
364
|
ptOutGrossU64 = Math.min(ptOutGrossU64, Number(principalPt))
|
|
365
|
+
const ptRemainderAtBoundary = boundaryPrincipalRemainder(
|
|
366
|
+
ptOutGrossU64,
|
|
367
|
+
Number(principalPt),
|
|
368
|
+
reachesLeftBoundary,
|
|
369
|
+
)
|
|
332
370
|
// ceil for input to protect pool
|
|
333
371
|
const syInSegmentU64 = Math.ceil(syInSegmentF)
|
|
334
372
|
|
|
@@ -338,7 +376,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
338
376
|
}
|
|
339
377
|
|
|
340
378
|
// Match on-chain behavior: if no PT can be taken from this interval, cross and continue.
|
|
341
|
-
if (ptOutGrossU64 === 0) {
|
|
379
|
+
if (ptOutGrossU64 === 0 && ptRemainderAtBoundary === 0) {
|
|
342
380
|
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
343
381
|
currentLeftBoundaryKey,
|
|
344
382
|
currentPriceSpot,
|
|
@@ -353,7 +391,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
353
391
|
continue
|
|
354
392
|
}
|
|
355
393
|
|
|
356
|
-
if (ptOutGrossU64 > 0) {
|
|
394
|
+
if (ptOutGrossU64 > 0 || ptRemainderAtBoundary > 0) {
|
|
357
395
|
// Fees in token_out (PT)
|
|
358
396
|
// For flash swaps, fee is based on YT value = pt - sy × sy_exchange_rate
|
|
359
397
|
let totalFeeOut: number
|
|
@@ -364,20 +402,24 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
364
402
|
} else {
|
|
365
403
|
totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
|
|
366
404
|
}
|
|
367
|
-
const protocolFeeOut =
|
|
368
|
-
|
|
405
|
+
const { lpFeeOut, protocolFeeOut } = splitFeeWithBoundaryRemainderAsLpFee(
|
|
406
|
+
totalFeeOut,
|
|
407
|
+
ptRemainderAtBoundary,
|
|
408
|
+
protocolFeeBps,
|
|
409
|
+
)
|
|
369
410
|
const ptOutNet = ptOutGrossU64 - totalFeeOut
|
|
370
411
|
|
|
371
412
|
// Accumulate to user
|
|
372
413
|
amountOutNetU64 += ptOutNet
|
|
373
414
|
feeLpOutU64 += lpFeeOut
|
|
374
415
|
feeProtocolOutU64 += protocolFeeOut
|
|
416
|
+
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
375
417
|
|
|
376
418
|
// Mirror on-chain principal mutation for this interval:
|
|
377
419
|
// +SY input (gross), -PT output (gross)
|
|
378
420
|
ticksWrapper.setPrincipals(
|
|
379
421
|
currentLeftBoundaryKey,
|
|
380
|
-
principalPt - BigInt(ptOutGrossU64),
|
|
422
|
+
reachesLeftBoundary ? 0n : principalPt - BigInt(ptOutGrossU64),
|
|
381
423
|
principalSy + BigInt(syInSegmentU64),
|
|
382
424
|
)
|
|
383
425
|
}
|
|
@@ -414,6 +456,7 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
|
|
|
414
456
|
amountOut: amountOutNetU64,
|
|
415
457
|
lpFeeChargedOutToken: feeLpOutU64,
|
|
416
458
|
protocolFeeChargedOutToken: feeProtocolOutU64,
|
|
459
|
+
boundaryRemainderChargedOutToken,
|
|
417
460
|
finalSpotPrice: currentPriceSpot,
|
|
418
461
|
finalTickKey: currentLeftBoundaryKey, // This is now the key, not array index
|
|
419
462
|
postMarketState,
|
|
@@ -457,6 +500,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
457
500
|
amountOut: 0,
|
|
458
501
|
lpFeeChargedOutToken: 0,
|
|
459
502
|
protocolFeeChargedOutToken: 0,
|
|
503
|
+
boundaryRemainderChargedOutToken: 0,
|
|
460
504
|
finalSpotPrice: currentPriceSpot,
|
|
461
505
|
finalTickKey: currentLeftBoundaryKey,
|
|
462
506
|
postMarketState: marketState,
|
|
@@ -468,6 +512,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
468
512
|
let amountInConsumed = 0
|
|
469
513
|
let feeLpOutU64 = 0
|
|
470
514
|
let feeProtocolOutU64 = 0
|
|
515
|
+
let boundaryRemainderChargedOutToken = 0
|
|
471
516
|
|
|
472
517
|
let iterations = 0
|
|
473
518
|
const MAX_ITERATIONS = 1000
|
|
@@ -498,8 +543,15 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
498
543
|
const cEffOld = snapshot.getEffectivePrice(currentPriceSpot)
|
|
499
544
|
const cEffLeft = snapshot.getEffectivePrice(anchorULeft)
|
|
500
545
|
const deltaCToLeftBoundary = cEffLeft - cEffOld
|
|
546
|
+
const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentLeftBoundaryKey)
|
|
501
547
|
|
|
502
548
|
if (!Number.isFinite(deltaCToLeftBoundary) || deltaCToLeftBoundary <= eps) {
|
|
549
|
+
if (Number.isFinite(deltaCToLeftBoundary) && deltaCToLeftBoundary <= eps && principalPt > 0n) {
|
|
550
|
+
const ptRemainderAtBoundary = Number(principalPt)
|
|
551
|
+
feeLpOutU64 += ptRemainderAtBoundary
|
|
552
|
+
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
553
|
+
ticksWrapper.setPrincipals(currentLeftBoundaryKey, 0n, principalSy)
|
|
554
|
+
}
|
|
503
555
|
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
504
556
|
currentLeftBoundaryKey,
|
|
505
557
|
currentPriceSpot,
|
|
@@ -514,8 +566,6 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
514
566
|
continue
|
|
515
567
|
}
|
|
516
568
|
|
|
517
|
-
const { principalPt, principalSy } = ticksWrapper.getPrincipals(currentLeftBoundaryKey)
|
|
518
|
-
|
|
519
569
|
const duToLeft = currentPriceSpot - anchorULeft
|
|
520
570
|
if (!Number.isFinite(duToLeft) || deltaCToLeftBoundary <= eps) {
|
|
521
571
|
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
@@ -585,6 +635,12 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
585
635
|
ptOutGrossBoundaryU64 = Math.min(ptOutGrossBoundaryU64, Number(principalPt))
|
|
586
636
|
|
|
587
637
|
if (ptOutGrossBoundaryU64 === 0) {
|
|
638
|
+
const ptRemainderAtBoundary = boundaryPrincipalRemainder(0, Number(principalPt), true)
|
|
639
|
+
feeLpOutU64 += ptRemainderAtBoundary
|
|
640
|
+
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
641
|
+
if (ptRemainderAtBoundary > 0) {
|
|
642
|
+
ticksWrapper.setPrincipals(currentLeftBoundaryKey, 0n, principalSy)
|
|
643
|
+
}
|
|
588
644
|
const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
|
|
589
645
|
currentLeftBoundaryKey,
|
|
590
646
|
currentPriceSpot,
|
|
@@ -633,17 +689,26 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
633
689
|
|
|
634
690
|
amountInConsumed += syInBoundary
|
|
635
691
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
692
|
+
const ptRemainderAtBoundary = boundaryPrincipalRemainder(
|
|
693
|
+
boundarySegment.ptOutGrossActual,
|
|
694
|
+
Number(principalPt),
|
|
695
|
+
true,
|
|
696
|
+
)
|
|
697
|
+
if (boundarySegment.totalFeeOut > 0 || ptRemainderAtBoundary > 0) {
|
|
698
|
+
const { lpFeeOut, protocolFeeOut } = splitFeeWithBoundaryRemainderAsLpFee(
|
|
699
|
+
boundarySegment.totalFeeOut,
|
|
700
|
+
ptRemainderAtBoundary,
|
|
701
|
+
protocolFeeBps,
|
|
702
|
+
)
|
|
639
703
|
feeLpOutU64 += lpFeeOut
|
|
640
704
|
feeProtocolOutU64 += protocolFeeOut
|
|
705
|
+
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
641
706
|
}
|
|
642
707
|
|
|
643
708
|
if (syInBoundary > 0 || boundarySegment.ptOutGrossActual > 0) {
|
|
644
709
|
ticksWrapper.setPrincipals(
|
|
645
710
|
currentLeftBoundaryKey,
|
|
646
|
-
|
|
711
|
+
0n,
|
|
647
712
|
principalSy + BigInt(syInBoundary),
|
|
648
713
|
)
|
|
649
714
|
}
|
|
@@ -740,14 +805,26 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
740
805
|
amountOutAccum += netActual
|
|
741
806
|
amountInConsumed += syInSegmentU64
|
|
742
807
|
|
|
743
|
-
const
|
|
744
|
-
|
|
808
|
+
const reachesLeftBoundary =
|
|
809
|
+
Math.abs(snapshot.getEffectivePrice(segment.spotPriceNew) - cEffLeft) <= eps ||
|
|
810
|
+
segment.ptOutGrossActual === ptOutGrossBoundaryU64
|
|
811
|
+
const ptRemainderAtBoundary = boundaryPrincipalRemainder(
|
|
812
|
+
segment.ptOutGrossActual,
|
|
813
|
+
Number(principalPt),
|
|
814
|
+
reachesLeftBoundary,
|
|
815
|
+
)
|
|
816
|
+
const { lpFeeOut, protocolFeeOut } = splitFeeWithBoundaryRemainderAsLpFee(
|
|
817
|
+
segment.totalFeeOut,
|
|
818
|
+
ptRemainderAtBoundary,
|
|
819
|
+
protocolFeeBps,
|
|
820
|
+
)
|
|
745
821
|
feeLpOutU64 += lpFeeOut
|
|
746
822
|
feeProtocolOutU64 += protocolFeeOut
|
|
823
|
+
boundaryRemainderChargedOutToken += ptRemainderAtBoundary
|
|
747
824
|
|
|
748
825
|
ticksWrapper.setPrincipals(
|
|
749
826
|
currentLeftBoundaryKey,
|
|
750
|
-
principalPt - BigInt(segment.ptOutGrossActual),
|
|
827
|
+
reachesLeftBoundary ? 0n : principalPt - BigInt(segment.ptOutGrossActual),
|
|
751
828
|
principalSy + BigInt(syInSegmentU64),
|
|
752
829
|
)
|
|
753
830
|
|
|
@@ -786,6 +863,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
|
|
|
786
863
|
amountOut: amountOutAccum,
|
|
787
864
|
lpFeeChargedOutToken: feeLpOutU64,
|
|
788
865
|
protocolFeeChargedOutToken: feeProtocolOutU64,
|
|
866
|
+
boundaryRemainderChargedOutToken,
|
|
789
867
|
finalSpotPrice: currentPriceSpot,
|
|
790
868
|
finalTickKey: currentLeftBoundaryKey,
|
|
791
869
|
postMarketState,
|
package/src/types.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface SwapOutcome {
|
|
|
53
53
|
lpFeeChargedOutToken: number
|
|
54
54
|
/** Protocol fee charged in out token */
|
|
55
55
|
protocolFeeChargedOutToken: number
|
|
56
|
+
/** Boundary principal remainder distributed to LPs in out token */
|
|
57
|
+
boundaryRemainderChargedOutToken: number
|
|
56
58
|
/** Final spot price after swap */
|
|
57
59
|
finalSpotPrice: number
|
|
58
60
|
/** Final tick index after swap */
|
|
@@ -68,6 +70,8 @@ export interface SwapOutcomeV2 {
|
|
|
68
70
|
lpFeeChargedOutToken: number
|
|
69
71
|
/** Protocol fee charged in out token */
|
|
70
72
|
protocolFeeChargedOutToken: number
|
|
73
|
+
/** Boundary principal remainder distributed to LPs in out token */
|
|
74
|
+
boundaryRemainderChargedOutToken: number
|
|
71
75
|
/** Final spot price after swap */
|
|
72
76
|
finalSpotPrice: number
|
|
73
77
|
/** Final tick key after swap */
|
|
@@ -109,27 +113,4 @@ export interface LiquidityNeeds {
|
|
|
109
113
|
priceSplitForNeed: number
|
|
110
114
|
/** Tick index of the split point */
|
|
111
115
|
priceSplitTickIdx: number
|
|
112
|
-
/** Original max SY before CLMM trimming (for crossing tick scaling) */
|
|
113
|
-
originalMaxSy: number
|
|
114
|
-
/** Original max PT before CLMM trimming (for crossing tick scaling) */
|
|
115
|
-
originalMaxPt: number
|
|
116
|
-
/** PT distribution extent: current_spot - lower_price */
|
|
117
|
-
duLeftTotal: number
|
|
118
|
-
/** SY distribution extent: eff_price_current - eff_price_upper */
|
|
119
|
-
deltaCRightTotal: number
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** State of the crossing (current) tick for proportional distribution */
|
|
123
|
-
export interface CrossingTickState {
|
|
124
|
-
principalPt: number
|
|
125
|
-
principalSy: number
|
|
126
|
-
principalShareSupply: number
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Parameters for scaling crossing tick inputs from original max values */
|
|
130
|
-
export interface CrossingScaleParams {
|
|
131
|
-
originalMaxSy: number
|
|
132
|
-
originalMaxPt: number
|
|
133
|
-
duLeftTotal: number
|
|
134
|
-
deltaCRightTotal: number
|
|
135
116
|
}
|
package/src/utilsV2.ts
CHANGED
|
@@ -273,11 +273,11 @@ export class TicksWrapper {
|
|
|
273
273
|
apyBasePoints: key,
|
|
274
274
|
principalShareSupply: 0n,
|
|
275
275
|
farms: templateTick ? templateTick.farms.map(() => ({ lastSeenIndex: 0 })) : [],
|
|
276
|
-
emissions: templateTick
|
|
277
|
-
? templateTick.emissions.map(() => ({ lastSeenIndex: 0, lastPositionIndex: 0 }))
|
|
278
|
-
: [],
|
|
276
|
+
emissions: templateTick ? templateTick.emissions.map(() => ({ lastSeenIndex: 0, lastPositionIndex: 0 })) : [],
|
|
279
277
|
lastSplitEpoch: 0n,
|
|
278
|
+
splitParentEpoch: 0n,
|
|
280
279
|
frozenLiquidity: 0n,
|
|
280
|
+
splitParentIdx: 0xffffffff,
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
this.baseTicksTree.push(newTick)
|
|
@@ -285,6 +285,10 @@ export class TicksWrapper {
|
|
|
285
285
|
this.sortedKeys.push(key)
|
|
286
286
|
this.sortedKeys.sort((a, b) => a - b)
|
|
287
287
|
|
|
288
|
+
if (newTick.impliedRate <= this.currentSpotPrice && key > this.currentTickKey) {
|
|
289
|
+
this.currentTickKey = key
|
|
290
|
+
}
|
|
291
|
+
|
|
288
292
|
this.splitPrincipalsAfterInsert(key, snap)
|
|
289
293
|
}
|
|
290
294
|
|
|
@@ -299,7 +303,8 @@ export class TicksWrapper {
|
|
|
299
303
|
const currentTickIdx = this.baseTicksTree.findIndex((tick) => tick.apyBasePoints === currentTickKey)
|
|
300
304
|
const fallbackTickKey = this.findTickKeyBySpotPrice(currentSpotPrice)
|
|
301
305
|
const fallbackTickIdx = this.baseTicksTree.findIndex((tick) => tick.apyBasePoints === fallbackTickKey)
|
|
302
|
-
const resolvedCurrentTick =
|
|
306
|
+
const resolvedCurrentTick =
|
|
307
|
+
currentTickIdx >= 0 ? currentTickIdx + 1 : fallbackTickIdx >= 0 ? fallbackTickIdx + 1 : 0
|
|
303
308
|
|
|
304
309
|
return {
|
|
305
310
|
currentTick: resolvedCurrentTick,
|