@sundaeswap/math 0.5.1 → 0.6.0
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/dist/cjs/PoolMath/ConcentratedLiquidityPool.js +655 -160
- package/dist/cjs/PoolMath/ConcentratedLiquidityPool.js.map +1 -1
- package/dist/esm/PoolMath/ConcentratedLiquidityPool.js +605 -137
- package/dist/esm/PoolMath/ConcentratedLiquidityPool.js.map +1 -1
- package/dist/types/PoolMath/ConcentratedLiquidityPool.d.ts +231 -45
- package/dist/types/PoolMath/ConcentratedLiquidityPool.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/PoolMath/ConcentratedLiquidityPool.ts +712 -212
- package/src/PoolMath/__tests__/ConcentratedLiquidityPool.test.ts +189 -196
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { AssetAmount, IAssetAmountMetadata } from "@sundaeswap/asset";
|
|
2
|
-
import { Fraction
|
|
3
|
-
|
|
4
|
-
import * as ConstantProductPool from "./ConstantProductPool.js";
|
|
2
|
+
import { Fraction } from "@sundaeswap/fraction";
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
|
-
* Holds the calculated outcome of a concentrated-liquidity swap.
|
|
8
|
-
*
|
|
9
|
-
* swap-outcome consumers.
|
|
5
|
+
* Holds the calculated outcome of a concentrated-liquidity swap. It has the
|
|
6
|
+
* same shape as `ConstantProductPool.TSwapOutcome`, so the generic
|
|
7
|
+
* swap-outcome consumers accept it.
|
|
10
8
|
*/
|
|
11
9
|
export type TSwapOutcome = {
|
|
12
10
|
input: bigint;
|
|
@@ -17,259 +15,761 @@ export type TSwapOutcome = {
|
|
|
17
15
|
priceImpact: Fraction;
|
|
18
16
|
};
|
|
19
17
|
|
|
20
|
-
/** A sqrt-price bound from the CL config, as an exact rational `[num, den]`. */
|
|
21
|
-
export type TSqrtPrice = [bigint, bigint];
|
|
22
|
-
|
|
23
18
|
/**
|
|
24
|
-
* Concentrated liquidity
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* pool's liquidity `
|
|
19
|
+
* Concentrated liquidity: Sundae v4's ladder invariant.
|
|
20
|
+
*
|
|
21
|
+
* A pool is a ladder of bands in ascending sqrt-price order. Each band is a
|
|
22
|
+
* concentrated-liquidity arc (`curve` 0) or a constant-sum bin (`curve` 1)
|
|
23
|
+
* with its own share of the pool's liquidity (`weight / weightTotal`) and
|
|
24
|
+
* its own two fees. LP is one fungible token for the whole ladder; a
|
|
25
|
+
* deposit or withdrawal scales every band.
|
|
29
26
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* The chain never stores the price. It stores reserves, and the state is a
|
|
28
|
+
* WITNESS `(X, k)`: the ladder counter `X` (the pool's `total_lp`, spec V12)
|
|
29
|
+
* and the active band `k`, which the reserves must prove. Everything here
|
|
30
|
+
* mirrors `lib/modules/cl_check.ak` and the scooper's ladder
|
|
31
|
+
* math, rounding included; where they disagree the chain wins
|
|
32
|
+
* and the transaction fails. The rounding is not negotiable:
|
|
32
33
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* - `L_k` floors; saturation ceils, in ONE division;
|
|
35
|
+
* - the band fee ceils, per step, on the step's real input;
|
|
36
|
+
* - the prefix sums come from the LADDER INDEX (cumulative coefficients
|
|
37
|
+
* in fixed point at 2^64, spec §2.3), multiplied by the counter and
|
|
38
|
+
* ceiled ONCE. A per-band sum of ceilings is a different number and the
|
|
39
|
+
* chain rejects it.
|
|
37
40
|
*/
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/** An exact rational `num / den`. */
|
|
43
|
+
export type TFrac = { num: bigint; den: bigint };
|
|
44
|
+
|
|
45
|
+
/** One band of the ladder. */
|
|
46
|
+
export type TBand = {
|
|
47
|
+
/** The band's LOWER sqrt-price edge. The next band's `start` is its upper edge. */
|
|
48
|
+
start: TFrac;
|
|
49
|
+
/** The band's share of the ladder's liquidity counter. */
|
|
50
|
+
weight: bigint;
|
|
51
|
+
/** 0 = concentrated-liquidity arc, 1 = constant-sum bin. */
|
|
52
|
+
curve: 0 | 1;
|
|
53
|
+
/** Fee on swaps that buy the pool's first asset (B in). */
|
|
54
|
+
feeBuy: TFrac;
|
|
55
|
+
/** Fee on swaps that sell the pool's first asset (A in). */
|
|
56
|
+
feeSell: TFrac;
|
|
44
57
|
};
|
|
45
58
|
|
|
59
|
+
/** A ladder: bands, the top edge, and the sum of the weights. */
|
|
60
|
+
export type TLadder = {
|
|
61
|
+
bands: TBand[];
|
|
62
|
+
/** The last band's UPPER sqrt-price edge. */
|
|
63
|
+
closing: TFrac;
|
|
64
|
+
weightTotal: bigint;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The witness the chain checks: the ladder counter and the active band. */
|
|
68
|
+
export type TWitness = { x: bigint; k: number };
|
|
69
|
+
|
|
70
|
+
/** 2^64, the fixed-point scale of the ladder index. */
|
|
71
|
+
export const FIXED_ONE = 1n << 64n;
|
|
72
|
+
|
|
46
73
|
/**
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
74
|
+
* The least counter per unit of weight a ladder may be created with.
|
|
75
|
+
* Mirrors `cl_check.min_counter_per_weight` (sundae-v4 #229, SV4-007):
|
|
76
|
+
* Create requires `total_lp >= MIN_COUNTER_PER_WEIGHT * weightTotal`.
|
|
50
77
|
*/
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
export const MIN_COUNTER_PER_WEIGHT = 1000n;
|
|
79
|
+
|
|
80
|
+
/** The least counter (initial `total_lp`) the chain accepts at Create for `ladder`. */
|
|
81
|
+
export const minCreateCounter = (ladder: TLadder): bigint =>
|
|
82
|
+
MIN_COUNTER_PER_WEIGHT * ladder.weightTotal;
|
|
83
|
+
|
|
84
|
+
const ceilDiv = (n: bigint, d: bigint): bigint => (n + d - 1n) / d;
|
|
85
|
+
|
|
86
|
+
/** Band i's upper sqrt-price edge. */
|
|
87
|
+
export const upperEdge = (l: TLadder, i: number): TFrac =>
|
|
88
|
+
i + 1 < l.bands.length ? l.bands[i + 1]!.start : l.closing;
|
|
89
|
+
|
|
90
|
+
/** `D_i = hi.num·lo.den − lo.num·hi.den`: positive exactly when the edges increase. */
|
|
91
|
+
export const delta = (lo: TFrac, hi: TFrac): bigint =>
|
|
92
|
+
hi.num * lo.den - lo.num * hi.den;
|
|
93
|
+
|
|
94
|
+
/** `a_sat_i = ceil(w_i·X·D_i / (W·lo.num·hi.num))`: the A a band above the price holds. */
|
|
95
|
+
export const aSat = (l: TLadder, x: bigint, i: number): bigint => {
|
|
96
|
+
const lo = l.bands[i]!.start;
|
|
97
|
+
const hi = upperEdge(l, i);
|
|
98
|
+
return ceilDiv(
|
|
99
|
+
l.bands[i]!.weight * x * delta(lo, hi),
|
|
100
|
+
l.weightTotal * lo.num * hi.num,
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** `b_sat_i = ceil(w_i·X·D_i / (W·lo.den·hi.den))`: the B a band below the price holds. */
|
|
105
|
+
export const bSat = (l: TLadder, x: bigint, i: number): bigint => {
|
|
106
|
+
const lo = l.bands[i]!.start;
|
|
107
|
+
const hi = upperEdge(l, i);
|
|
108
|
+
return ceilDiv(
|
|
109
|
+
l.bands[i]!.weight * x * delta(lo, hi),
|
|
110
|
+
l.weightTotal * lo.den * hi.den,
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `L_k = floor(w_k·X / W)`. */
|
|
115
|
+
export const liquidityOf = (l: TLadder, x: bigint, k: number): bigint =>
|
|
116
|
+
(l.bands[k]!.weight * x) / l.weightTotal;
|
|
117
|
+
|
|
118
|
+
/** Band i's A-side saturation coefficient: the part of `a_sat_i` independent of X, rounded up. */
|
|
119
|
+
export const coeffA = (l: TLadder, i: number): bigint => {
|
|
120
|
+
const lo = l.bands[i]!.start;
|
|
121
|
+
const hi = upperEdge(l, i);
|
|
122
|
+
return ceilDiv(
|
|
123
|
+
l.bands[i]!.weight * delta(lo, hi) * FIXED_ONE,
|
|
124
|
+
l.weightTotal * lo.num * hi.num,
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** The same on the B side. */
|
|
129
|
+
export const coeffB = (l: TLadder, i: number): bigint => {
|
|
130
|
+
const lo = l.bands[i]!.start;
|
|
131
|
+
const hi = upperEdge(l, i);
|
|
132
|
+
return ceilDiv(
|
|
133
|
+
l.bands[i]!.weight * delta(lo, hi) * FIXED_ONE,
|
|
134
|
+
l.weightTotal * lo.den * hi.den,
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** One index entry: the cumulative coefficients above and below band k. */
|
|
139
|
+
export type TIndexEntry = { ca: bigint; cb: bigint };
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The ladder index `Create` pins and every spend reads: entry k is the pair
|
|
143
|
+
* of cumulative coefficients for the bands strictly above and strictly
|
|
144
|
+
* below band k. `cl_check.build_index`, to the digit.
|
|
145
|
+
*/
|
|
146
|
+
export const buildIndex = (l: TLadder): TIndexEntry[] => {
|
|
147
|
+
const n = l.bands.length;
|
|
148
|
+
const a = Array.from({ length: n }, (_, i) => coeffA(l, i));
|
|
149
|
+
const b = Array.from({ length: n }, (_, i) => coeffB(l, i));
|
|
150
|
+
const out: TIndexEntry[] = [];
|
|
151
|
+
for (let k = 0; k < n; k++) {
|
|
152
|
+
let ca = 0n;
|
|
153
|
+
let cb = 0n;
|
|
154
|
+
for (let i = 0; i < n; i++) {
|
|
155
|
+
if (i > k) ca += a[i]!;
|
|
156
|
+
if (i < k) cb += b[i]!;
|
|
157
|
+
}
|
|
158
|
+
out.push({ ca, cb });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A ladder with its index built once. Every probe of the witness search
|
|
165
|
+
* reads the index for every band, and rebuilding it per probe is most of
|
|
166
|
+
* the cost of a quote.
|
|
167
|
+
*/
|
|
168
|
+
export type TIndexedLadder = TLadder & { index: TIndexEntry[] };
|
|
169
|
+
|
|
170
|
+
export const indexLadder = (l: TLadder): TIndexedLadder =>
|
|
171
|
+
"index" in l && Array.isArray((l as TIndexedLadder).index)
|
|
172
|
+
? (l as TIndexedLadder)
|
|
173
|
+
: { ...l, index: buildIndex(l) };
|
|
174
|
+
|
|
175
|
+
/** `C_A`, `C_B` at (X, k) from the index: one ceiling after the multiply by X. */
|
|
176
|
+
export const prefixes = (
|
|
177
|
+
l: TIndexedLadder,
|
|
178
|
+
x: bigint,
|
|
179
|
+
k: number,
|
|
180
|
+
): { ca: bigint; cb: bigint } => {
|
|
181
|
+
const e = l.index[k]!;
|
|
182
|
+
return { ca: ceilDiv(e.ca * x, FIXED_ONE), cb: ceilDiv(e.cb * x, FIXED_ONE) };
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** `cl_check.f_at_raw`: the CL arc, cross-multiplied. */
|
|
186
|
+
const fAt = (a: bigint, b: bigint, L: bigint, lo: TFrac, hi: TFrac): bigint =>
|
|
187
|
+
(a * hi.num + L * hi.den) * (b * lo.den + L * lo.num) -
|
|
188
|
+
L * L * hi.num * lo.den;
|
|
189
|
+
|
|
190
|
+
/** G at a witness, on whichever curve the active band carries. */
|
|
191
|
+
const gAt = (
|
|
192
|
+
l: TIndexedLadder,
|
|
193
|
+
A: bigint,
|
|
194
|
+
B: bigint,
|
|
195
|
+
x: bigint,
|
|
196
|
+
k: number,
|
|
197
|
+
): bigint => {
|
|
198
|
+
const { ca, cb } = prefixes(l, x, k);
|
|
199
|
+
const lo = l.bands[k]!.start;
|
|
200
|
+
const hi = upperEdge(l, k);
|
|
201
|
+
const ra = A - ca;
|
|
202
|
+
const rb = B - cb;
|
|
203
|
+
const L = liquidityOf(l, x, k);
|
|
204
|
+
return l.bands[k]!.curve === 0
|
|
205
|
+
? fAt(ra, rb, L, lo, hi)
|
|
206
|
+
: ra * (lo.num * hi.num) + rb * (lo.den * hi.den) - L * delta(lo, hi);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** The band proof, P1 through P7. The chain runs exactly this. */
|
|
210
|
+
export const bandProof = (
|
|
211
|
+
l: TIndexedLadder,
|
|
212
|
+
A: bigint,
|
|
213
|
+
B: bigint,
|
|
214
|
+
x: bigint,
|
|
215
|
+
k: number,
|
|
216
|
+
): boolean => {
|
|
217
|
+
if (x < l.weightTotal) return false; // P1
|
|
218
|
+
const { ca, cb } = prefixes(l, x, k);
|
|
219
|
+
const ra = A - ca;
|
|
220
|
+
const rb = B - cb;
|
|
221
|
+
if (ra < 0n || rb < 0n) return false; // P2, P3
|
|
222
|
+
if (ra > aSat(l, x, k) || rb > bSat(l, x, k)) return false; // P4, P5
|
|
223
|
+
if (gAt(l, A, B, x, k) < 0n) return false; // P6
|
|
224
|
+
return gAt(l, A, B, x + 1n, k) < 0n; // P7
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const SCAN = 400n;
|
|
228
|
+
const CAP = 1n << 80n;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The witness in band k, seeded at `hint` when given: the counter only
|
|
232
|
+
* grows along a swap, so the doubling search starts from the previous
|
|
233
|
+
* witness. `lo` is the largest x with non-negative residuals and G ≥ 0,
|
|
234
|
+
* and a witness is such an x whose successor fails G, so the scan walks
|
|
235
|
+
* DOWN from `lo` first. Mirrors the scooper's `find_witness_in_band_from`.
|
|
236
|
+
*/
|
|
237
|
+
export const findWitnessInBand = (
|
|
238
|
+
l: TIndexedLadder,
|
|
239
|
+
A: bigint,
|
|
240
|
+
B: bigint,
|
|
241
|
+
k: number,
|
|
242
|
+
hint?: bigint,
|
|
243
|
+
): TWitness | null => {
|
|
244
|
+
const ok = (x: bigint): boolean => {
|
|
245
|
+
const { ca, cb } = prefixes(l, x, k);
|
|
246
|
+
return A - ca >= 0n && B - cb >= 0n && gAt(l, A, B, x, k) >= 0n;
|
|
247
|
+
};
|
|
248
|
+
const search = (start: bigint): TWitness | null => {
|
|
249
|
+
let lo = start;
|
|
250
|
+
let span = 1n;
|
|
251
|
+
let hi = lo + span;
|
|
252
|
+
while (ok(hi) && hi < CAP) {
|
|
253
|
+
lo = hi;
|
|
254
|
+
span *= 2n;
|
|
255
|
+
hi = lo + span;
|
|
256
|
+
}
|
|
257
|
+
while (lo + 1n < hi) {
|
|
258
|
+
const m = (lo + hi) / 2n;
|
|
259
|
+
if (ok(m)) lo = m;
|
|
260
|
+
else hi = m;
|
|
261
|
+
}
|
|
262
|
+
const from = lo > SCAN ? lo - SCAN : 1n;
|
|
263
|
+
for (let x = lo; x >= from; x--) {
|
|
264
|
+
if (bandProof(l, A, B, x, k)) return { x, k };
|
|
265
|
+
}
|
|
266
|
+
for (let x = lo + 1n; x <= lo + SCAN; x++) {
|
|
267
|
+
if (bandProof(l, A, B, x, k)) return { x, k };
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
if (hint !== undefined && hint > 0n && ok(hint)) {
|
|
272
|
+
const w = search(hint);
|
|
273
|
+
if (w) return w;
|
|
274
|
+
}
|
|
275
|
+
if (!ok(1n)) return null;
|
|
276
|
+
return search(1n);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The witness `(X, k)` for a pool's reserves. A hint (the indexed
|
|
281
|
+
* `bandCounter` / `activeBand`) is checked first and seeds the search in
|
|
282
|
+
* its band; a stale hint falls back to every band. Null when the reserves
|
|
283
|
+
* sit on no point of the ladder — the chain would reject them too.
|
|
284
|
+
*/
|
|
285
|
+
export const findWitness = (
|
|
286
|
+
ladder: TLadder,
|
|
287
|
+
A: bigint,
|
|
288
|
+
B: bigint,
|
|
289
|
+
hint?: TWitness,
|
|
290
|
+
): TWitness | null => {
|
|
291
|
+
const l = indexLadder(ladder);
|
|
292
|
+
if (hint && hint.k >= 0 && hint.k < l.bands.length) {
|
|
293
|
+
if (bandProof(l, A, B, hint.x, hint.k)) return hint;
|
|
294
|
+
const w = findWitnessInBand(l, A, B, hint.k, hint.x);
|
|
295
|
+
if (w) return w;
|
|
296
|
+
}
|
|
297
|
+
for (let k = 0; k < l.bands.length; k++) {
|
|
298
|
+
const w = findWitnessInBand(l, A, B, k);
|
|
299
|
+
if (w) return w;
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** The active band's view at a witness: its residual reserves and liquidity. */
|
|
305
|
+
export type TBandView = {
|
|
306
|
+
ra: bigint;
|
|
307
|
+
rb: bigint;
|
|
308
|
+
l: bigint;
|
|
309
|
+
lo: TFrac;
|
|
310
|
+
hi: TFrac;
|
|
311
|
+
curve: 0 | 1;
|
|
312
|
+
feeBuy: TFrac;
|
|
313
|
+
feeSell: TFrac;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const bandView = (
|
|
317
|
+
l: TIndexedLadder,
|
|
318
|
+
A: bigint,
|
|
319
|
+
B: bigint,
|
|
320
|
+
w: TWitness,
|
|
321
|
+
): TBandView => {
|
|
322
|
+
const { ca, cb } = prefixes(l, w.x, w.k);
|
|
323
|
+
const band = l.bands[w.k]!;
|
|
324
|
+
return {
|
|
325
|
+
ra: A - ca,
|
|
326
|
+
rb: B - cb,
|
|
327
|
+
l: liquidityOf(l, w.x, w.k),
|
|
328
|
+
lo: band.start,
|
|
329
|
+
hi: upperEdge(l, w.k),
|
|
330
|
+
curve: band.curve,
|
|
331
|
+
feeBuy: band.feeBuy,
|
|
332
|
+
feeSell: band.feeSell,
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** The band's fee for a direction: `feeSell` when A is sold, `feeBuy` when A is bought. */
|
|
337
|
+
export const feeFor = (v: TBandView, isAInput: boolean): TFrac =>
|
|
338
|
+
isAInput ? v.feeSell : v.feeBuy;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The band fee on a step's real input `dx`: `ceil(dx * num / den)`.
|
|
342
|
+
* Mirrors `cl_check.band_fee` (sundae-v4 #228, SV4-006). The chain charges
|
|
343
|
+
* it per transcript step and rounds it UP, so a step below `den / num`
|
|
344
|
+
* units still pays one unit. A zero rate charges nothing.
|
|
345
|
+
*/
|
|
346
|
+
export const bandFee = (dx: bigint, fee: TFrac): bigint =>
|
|
347
|
+
(dx * fee.num + fee.den - 1n) / fee.den;
|
|
348
|
+
|
|
349
|
+
/** The output one band pays for `dx` within itself. Mirrors the scooper's `band_output`. */
|
|
350
|
+
export const bandOutput = (
|
|
351
|
+
v: TBandView,
|
|
62
352
|
isAInput: boolean,
|
|
353
|
+
dx: bigint,
|
|
63
354
|
): bigint => {
|
|
64
|
-
const fee = (
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
355
|
+
const fee = feeFor(v, isAInput);
|
|
356
|
+
if (fee.den <= 0n) return 0n;
|
|
357
|
+
const dxEff = dx - bandFee(dx, fee);
|
|
358
|
+
if (dxEff <= 0n) return 0n;
|
|
359
|
+
if (v.curve === 0) {
|
|
360
|
+
const va0 = v.ra * v.hi.num + v.l * v.hi.den;
|
|
361
|
+
const vb0 = v.rb * v.lo.den + v.l * v.lo.num;
|
|
362
|
+
if (isAInput) {
|
|
363
|
+
const dva = dxEff * v.hi.num;
|
|
364
|
+
const den = (va0 + dva) * v.lo.den;
|
|
365
|
+
return den === 0n ? 0n : (vb0 * dva) / den;
|
|
366
|
+
}
|
|
367
|
+
const dvb = dxEff * v.lo.den;
|
|
368
|
+
const den = (vb0 + dvb) * v.hi.num;
|
|
369
|
+
return den === 0n ? 0n : (va0 * dvb) / den;
|
|
73
370
|
}
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
371
|
+
const pn = v.lo.num * v.hi.num;
|
|
372
|
+
const pd = v.lo.den * v.hi.den;
|
|
373
|
+
return isAInput ? (dxEff * pn) / pd : (dxEff * pd) / pn;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/** What the band can pay out in a direction: its residual of the output asset. */
|
|
377
|
+
export const bandCapacity = (v: TBandView, isAInput: boolean): bigint =>
|
|
378
|
+
isAInput ? v.rb : v.ra;
|
|
379
|
+
|
|
380
|
+
/** The largest input whose output stays within the band. */
|
|
381
|
+
export const maxDxInBand = (v: TBandView, isAInput: boolean): bigint => {
|
|
382
|
+
const cap = bandCapacity(v, isAInput);
|
|
383
|
+
if (cap <= 0n) return 0n;
|
|
384
|
+
const fits = (dx: bigint) => bandOutput(v, isAInput, dx) <= cap;
|
|
385
|
+
let hi = 1n;
|
|
386
|
+
for (let guard = 0; fits(hi); guard++) {
|
|
387
|
+
hi *= 2n;
|
|
388
|
+
if (guard > 200) return hi;
|
|
389
|
+
}
|
|
390
|
+
let lo = 0n;
|
|
391
|
+
while (lo + 1n < hi) {
|
|
392
|
+
const m = (lo + hi) / 2n;
|
|
393
|
+
if (fits(m)) lo = m;
|
|
394
|
+
else hi = m;
|
|
395
|
+
}
|
|
396
|
+
return lo;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/** The next band a fill enters: selling A walks down the ladder, buying A walks up. */
|
|
400
|
+
export const nextBand = (
|
|
401
|
+
k: number,
|
|
402
|
+
isAInput: boolean,
|
|
403
|
+
n: number,
|
|
404
|
+
): number | null =>
|
|
405
|
+
isAInput ? (k === 0 ? null : k - 1) : k + 1 >= n ? null : k + 1;
|
|
406
|
+
|
|
407
|
+
/** One band of a fill: what it paid, and the state it left. */
|
|
408
|
+
export type TFillStep = {
|
|
409
|
+
band: number;
|
|
410
|
+
input: bigint;
|
|
411
|
+
output: bigint;
|
|
412
|
+
after: TWitness;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Price `dx` across as many bands as it takes, as the scooper's
|
|
417
|
+
* `swap_steps` does: fill the active band, cross to the next, repeat.
|
|
418
|
+
* Returns the steps, or null when the ladder cannot absorb `dx` (the
|
|
419
|
+
* direction is exhausted, or the remainder pays nothing).
|
|
420
|
+
*/
|
|
421
|
+
export const swapSteps = (
|
|
422
|
+
ladder: TLadder,
|
|
423
|
+
A: bigint,
|
|
424
|
+
B: bigint,
|
|
425
|
+
isAInput: boolean,
|
|
426
|
+
dx: bigint,
|
|
427
|
+
hint?: TWitness,
|
|
428
|
+
): TFillStep[] | null => {
|
|
429
|
+
const l = indexLadder(ladder);
|
|
430
|
+
const n = l.bands.length;
|
|
431
|
+
let ca = A;
|
|
432
|
+
let cb = B;
|
|
433
|
+
let remaining = dx;
|
|
434
|
+
const steps: TFillStep[] = [];
|
|
435
|
+
let known: TWitness | null = findWitness(l, A, B, hint);
|
|
436
|
+
if (!known) return null;
|
|
437
|
+
let prefer: number | null = null;
|
|
438
|
+
let hintX: bigint | undefined;
|
|
439
|
+
for (let guard = 0; remaining > 0n; guard++) {
|
|
440
|
+
if (guard > 2 * n + 2) return null;
|
|
441
|
+
let before: TWitness | null = known;
|
|
442
|
+
known = null;
|
|
443
|
+
if (!before) {
|
|
444
|
+
if (prefer !== null) before = findWitnessInBand(l, ca, cb, prefer, hintX);
|
|
445
|
+
if (!before) before = findWitness(l, ca, cb);
|
|
446
|
+
if (!before) return null;
|
|
447
|
+
}
|
|
448
|
+
const view = bandView(l, ca, cb, before);
|
|
449
|
+
const capDx = maxDxInBand(view, isAInput);
|
|
450
|
+
const stepDx = capDx < remaining ? capDx : remaining;
|
|
451
|
+
const dy = stepDx > 0n ? bandOutput(view, isAInput, stepDx) : 0n;
|
|
452
|
+
if (dy <= 0n) {
|
|
453
|
+
const kNext = nextBand(before.k, isAInput, n);
|
|
454
|
+
if (kNext === null || prefer === kNext) return null;
|
|
455
|
+
prefer = kNext;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const na = isAInput ? ca + stepDx : ca - dy;
|
|
459
|
+
const nb = isAInput ? cb - dy : cb + stepDx;
|
|
460
|
+
const crossing = stepDx < remaining;
|
|
461
|
+
let after: TWitness | null = null;
|
|
462
|
+
if (crossing) {
|
|
463
|
+
const kNext = nextBand(before.k, isAInput, n);
|
|
464
|
+
if (kNext !== null) after = findWitnessInBand(l, na, nb, kNext, before.x);
|
|
465
|
+
} else {
|
|
466
|
+
after = findWitnessInBand(l, na, nb, before.k, before.x);
|
|
467
|
+
}
|
|
468
|
+
if (!after) after = findWitness(l, na, nb);
|
|
469
|
+
if (!after || after.x < before.x) return null;
|
|
470
|
+
steps.push({ band: before.k, input: stepDx, output: dy, after });
|
|
471
|
+
remaining -= stepDx;
|
|
472
|
+
ca = na;
|
|
473
|
+
cb = nb;
|
|
474
|
+
prefer = after.k;
|
|
475
|
+
hintX = after.x;
|
|
476
|
+
known = after;
|
|
477
|
+
}
|
|
478
|
+
return steps;
|
|
78
479
|
};
|
|
79
480
|
|
|
80
481
|
/**
|
|
81
|
-
*
|
|
482
|
+
* The swap output of a concentrated-liquidity pool for `input` of one asset, crossing
|
|
483
|
+
* bands as needed. `aReserve`/`bReserve` are the pool's reserves in
|
|
484
|
+
* canonical order; `hint` is the indexed `bandCounter` / `activeBand`,
|
|
485
|
+
* which seeds the witness search and may be stale.
|
|
82
486
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @param bReserve The pool's raw reserve of asset B.
|
|
87
|
-
* @param totalLp The pool's total LP supply (the virtual liquidity `L`).
|
|
88
|
-
* @param sqrtPriceA The lower sqrt-price bound `[num, den]` from the CL config.
|
|
89
|
-
* @param sqrtPriceB The upper sqrt-price bound `[num, den]` from the CL config.
|
|
90
|
-
* @param fee The liquidity-provider fee (plus protocol fee) as a fraction.
|
|
91
|
-
* @param isAInput True when asset A is the supplied asset (A→B swap).
|
|
92
|
-
* @returns The swap details in the shared {@link TSwapOutcome} shape.
|
|
487
|
+
* The `lpFee` is the fee the bands kept of the input, summed across the
|
|
488
|
+
* steps. `priceImpact` is against the active band's opening marginal
|
|
489
|
+
* price. Throws when the ladder cannot absorb the input.
|
|
93
490
|
*/
|
|
94
491
|
export const getSwapOutput = (
|
|
95
492
|
inputMetadata: IAssetAmountMetadata,
|
|
96
493
|
input: bigint,
|
|
97
494
|
aReserve: bigint,
|
|
98
495
|
bReserve: bigint,
|
|
99
|
-
|
|
100
|
-
sqrtPriceA: TSqrtPrice,
|
|
101
|
-
sqrtPriceB: TSqrtPrice,
|
|
102
|
-
fee: TFractionLike,
|
|
496
|
+
ladder: TLadder,
|
|
103
497
|
isAInput: boolean,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
498
|
+
hint?: TWitness,
|
|
499
|
+
): TSwapOutcome & { steps: TFillStep[] } => {
|
|
500
|
+
if (input <= 0n || aReserve < 0n || bReserve < 0n)
|
|
501
|
+
throw new Error("Input must be positive and reserves non-negative");
|
|
502
|
+
if (ladder.bands.length === 0 || ladder.weightTotal <= 0n)
|
|
503
|
+
throw new Error("The ladder has no bands");
|
|
504
|
+
const l = indexLadder(ladder);
|
|
505
|
+
const steps = swapSteps(l, aReserve, bReserve, isAInput, input, hint);
|
|
506
|
+
if (!steps || steps.length === 0)
|
|
507
|
+
throw new Error("The ladder cannot absorb this input in this direction");
|
|
508
|
+
let output = 0n;
|
|
509
|
+
let lpFee = 0n;
|
|
510
|
+
for (const s of steps) {
|
|
511
|
+
output += s.output;
|
|
512
|
+
const band = l.bands[s.band]!;
|
|
513
|
+
const fee = isAInput ? band.feeSell : band.feeBuy;
|
|
514
|
+
lpFee += bandFee(s.input, fee);
|
|
515
|
+
}
|
|
113
516
|
const inputReserve = isAInput ? aReserve : bReserve;
|
|
114
517
|
const outputReserve = isAInput ? bReserve : aReserve;
|
|
115
|
-
|
|
116
|
-
let output = rawSwapOutput(
|
|
117
|
-
input,
|
|
118
|
-
aReserve,
|
|
119
|
-
bReserve,
|
|
120
|
-
totalLp,
|
|
121
|
-
spaNum,
|
|
122
|
-
spaDen,
|
|
123
|
-
spbNum,
|
|
124
|
-
spbDen,
|
|
125
|
-
feeNum,
|
|
126
|
-
feeDen,
|
|
127
|
-
isAInput,
|
|
128
|
-
);
|
|
129
|
-
// Can't take more than the pool actually holds (virtual reserves can
|
|
130
|
-
// otherwise let the formula exceed the real reserve near the boundary).
|
|
131
518
|
if (output > outputReserve) output = outputReserve;
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const nextOutputReserve = outputReserve - output;
|
|
136
|
-
|
|
137
|
-
// Price impact vs the marginal (dx→0) rate. Marginal output-per-effective-
|
|
138
|
-
// input is vb0·spbNum/(va0·spaDen) for A→B (symmetric for B→A), so the ideal
|
|
139
|
-
// input-per-output is its reciprocal. Display-only.
|
|
140
|
-
const va0 = aReserve * spbNum + totalLp * spbDen;
|
|
141
|
-
const vb0 = bReserve * spaDen + totalLp * spaNum;
|
|
519
|
+
// Price impact against the opening marginal of the first band traded.
|
|
520
|
+
const w0 = findWitness(l, aReserve, bReserve, hint)!;
|
|
521
|
+
const v0 = bandView(l, aReserve, bReserve, w0);
|
|
142
522
|
let priceImpact = Fraction.ZERO;
|
|
143
523
|
if (output > 0n) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
524
|
+
let ideal: Fraction;
|
|
525
|
+
if (v0.curve === 1) {
|
|
526
|
+
const pn = v0.lo.num * v0.hi.num;
|
|
527
|
+
const pd = v0.lo.den * v0.hi.den;
|
|
528
|
+
ideal = isAInput ? new Fraction(pd, pn) : new Fraction(pn, pd);
|
|
529
|
+
} else {
|
|
530
|
+
const va0 = v0.ra * v0.hi.num + v0.l * v0.hi.den;
|
|
531
|
+
const vb0 = v0.rb * v0.lo.den + v0.l * v0.lo.num;
|
|
532
|
+
ideal = isAInput
|
|
533
|
+
? new Fraction(va0 * v0.lo.den, vb0 * v0.hi.num)
|
|
534
|
+
: new Fraction(vb0 * v0.hi.num, va0 * v0.lo.den);
|
|
535
|
+
}
|
|
536
|
+
const actual = new Fraction(input - lpFee, output);
|
|
537
|
+
priceImpact = Fraction.ONE.subtract(ideal.divide(actual));
|
|
149
538
|
}
|
|
150
|
-
|
|
151
539
|
return {
|
|
152
540
|
input,
|
|
153
541
|
output,
|
|
154
|
-
lpFee: new AssetAmount(
|
|
155
|
-
nextInputReserve,
|
|
156
|
-
nextOutputReserve,
|
|
542
|
+
lpFee: new AssetAmount(lpFee, inputMetadata),
|
|
543
|
+
nextInputReserve: inputReserve + input,
|
|
544
|
+
nextOutputReserve: outputReserve - output,
|
|
157
545
|
priceImpact,
|
|
546
|
+
steps,
|
|
158
547
|
};
|
|
159
548
|
};
|
|
160
549
|
|
|
550
|
+
/** What each band holds at the pool's reserves. */
|
|
551
|
+
export type TBandState = {
|
|
552
|
+
/** The band's reserves in canonical order. The bands sum to the pool's reserves. */
|
|
553
|
+
quantities: [bigint, bigint];
|
|
554
|
+
liquidity: bigint;
|
|
555
|
+
active: boolean;
|
|
556
|
+
};
|
|
557
|
+
|
|
161
558
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* absorbs the fee floor and integer rounding so the result is the exact minimal
|
|
166
|
-
* input whose forward swap yields at least `output`.
|
|
559
|
+
* Every band's state at the pool's reserves: a band below the price holds
|
|
560
|
+
* only B, a band above it only A, the active band some of both. Null when
|
|
561
|
+
* the reserves sit on no point of the ladder.
|
|
167
562
|
*/
|
|
168
|
-
export const
|
|
169
|
-
|
|
170
|
-
output: bigint,
|
|
563
|
+
export const bandStates = (
|
|
564
|
+
ladder: TLadder,
|
|
171
565
|
aReserve: bigint,
|
|
172
566
|
bReserve: bigint,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const ceilDiv = (n: bigint, d: bigint) => (n + d - 1n) / d;
|
|
196
|
-
|
|
197
|
-
// Closed-form minimal effective input, then gross up for the fee floor.
|
|
198
|
-
let effMin: bigint;
|
|
199
|
-
if (isAInput) {
|
|
200
|
-
// dvaEff ≥ output·spaDen·va0 / (vb0 − output·spaDen)
|
|
201
|
-
const denom = vb0 - output * spaDen;
|
|
202
|
-
if (denom <= 0n)
|
|
203
|
-
throw new Error("Output not reachable within the pool's range");
|
|
204
|
-
const dvaEffMin = ceilDiv(output * spaDen * va0, denom);
|
|
205
|
-
effMin = ceilDiv(dvaEffMin, spbNum);
|
|
206
|
-
} else {
|
|
207
|
-
const denom = va0 - output * spbNum;
|
|
208
|
-
if (denom <= 0n)
|
|
209
|
-
throw new Error("Output not reachable within the pool's range");
|
|
210
|
-
const dvbEffMin = ceilDiv(output * spbNum * vb0, denom);
|
|
211
|
-
effMin = ceilDiv(dvbEffMin, spaNum);
|
|
567
|
+
hint?: TWitness,
|
|
568
|
+
): TBandState[] | null => {
|
|
569
|
+
const l = indexLadder(ladder);
|
|
570
|
+
const w = findWitness(l, aReserve, bReserve, hint);
|
|
571
|
+
if (!w) return null;
|
|
572
|
+
const out: TBandState[] = [];
|
|
573
|
+
let restA = 0n;
|
|
574
|
+
let restB = 0n;
|
|
575
|
+
for (let i = 0; i < l.bands.length; i++) {
|
|
576
|
+
const liquidity = liquidityOf(l, w.x, i);
|
|
577
|
+
if (i < w.k) {
|
|
578
|
+
const b = bSat(l, w.x, i);
|
|
579
|
+
restB += b;
|
|
580
|
+
out.push({ quantities: [0n, b], liquidity, active: false });
|
|
581
|
+
} else if (i > w.k) {
|
|
582
|
+
const a = aSat(l, w.x, i);
|
|
583
|
+
restA += a;
|
|
584
|
+
out.push({ quantities: [a, 0n], liquidity, active: false });
|
|
585
|
+
} else {
|
|
586
|
+
out.push({ quantities: [0n, 0n], liquidity, active: true });
|
|
587
|
+
}
|
|
212
588
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
rawSwapOutput(
|
|
219
|
-
dx,
|
|
220
|
-
aReserve,
|
|
221
|
-
bReserve,
|
|
222
|
-
totalLp,
|
|
223
|
-
spaNum,
|
|
224
|
-
spaDen,
|
|
225
|
-
spbNum,
|
|
226
|
-
spbDen,
|
|
227
|
-
feeNum,
|
|
228
|
-
feeDen,
|
|
229
|
-
isAInput,
|
|
230
|
-
);
|
|
231
|
-
let guard = 0;
|
|
232
|
-
while (fwd(input) < output && guard++ < 64) input += 1n;
|
|
233
|
-
while (input > 1n && fwd(input - 1n) >= output) input -= 1n;
|
|
234
|
-
|
|
235
|
-
const inputLpFee = (input * feeNum) / feeDen;
|
|
236
|
-
const inputReserve = isAInput ? aReserve : bReserve;
|
|
237
|
-
const nextInputReserve = inputReserve + input;
|
|
238
|
-
const nextOutputReserve = outputReserve - output;
|
|
589
|
+
const ra = aReserve - restA;
|
|
590
|
+
const rb = bReserve - restB;
|
|
591
|
+
out[w.k]!.quantities = [ra < 0n ? 0n : ra, rb < 0n ? 0n : rb];
|
|
592
|
+
return out;
|
|
593
|
+
};
|
|
239
594
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
595
|
+
/** Which band holds a sqrt-price. */
|
|
596
|
+
export const bandOf = (l: TLadder, s: TFrac): number => {
|
|
597
|
+
for (let i = 0; i < l.bands.length; i++) {
|
|
598
|
+
const hi = upperEdge(l, i);
|
|
599
|
+
if (s.num * hi.den < hi.num * s.den) return i;
|
|
600
|
+
}
|
|
601
|
+
return l.bands.length - 1;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The exact reserves a ladder holds at counter `x` with the price at
|
|
606
|
+
* sqrt-price `s`, which must lie in band `k`: what a pool creator deposits
|
|
607
|
+
* for a chosen shape, scale and launch price. The result is floored, so
|
|
608
|
+
* `findWitness` must still be run on it to learn the counter the chain
|
|
609
|
+
* derives — usually `x`, but rounding can move it.
|
|
610
|
+
*/
|
|
611
|
+
export const reservesAt = (
|
|
612
|
+
l: TLadder,
|
|
613
|
+
x: bigint,
|
|
614
|
+
k: number,
|
|
615
|
+
s: TFrac,
|
|
616
|
+
): { A: bigint; B: bigint } => {
|
|
617
|
+
let A = 0n;
|
|
618
|
+
let B = 0n;
|
|
619
|
+
for (let i = 0; i < l.bands.length; i++) {
|
|
620
|
+
if (i > k) {
|
|
621
|
+
A += aSat(l, x, i);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (i < k) {
|
|
625
|
+
B += bSat(l, x, i);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const lo = l.bands[i]!.start;
|
|
629
|
+
const hi = upperEdge(l, i);
|
|
630
|
+
const L = liquidityOf(l, x, i);
|
|
631
|
+
if (l.bands[i]!.curve === 1) {
|
|
632
|
+
// A constant-sum bin prices at the geometric mean; `s` only says how
|
|
633
|
+
// far through the bin the conversion has gone.
|
|
634
|
+
const K = L * delta(lo, hi);
|
|
635
|
+
const frac = (s.num * lo.den - lo.num * s.den) * hi.den;
|
|
636
|
+
const span = (hi.num * lo.den - lo.num * hi.den) * s.den;
|
|
637
|
+
const rb = (K * frac) / (span * (lo.den * hi.den));
|
|
638
|
+
A += (K - rb * (lo.den * hi.den)) / (lo.num * hi.num);
|
|
639
|
+
B += rb;
|
|
640
|
+
} else {
|
|
641
|
+
// a = L·(1/s − 1/hi), b = L·(s − lo), in exact rationals.
|
|
642
|
+
A += (L * (s.den * hi.num - s.num * hi.den)) / (s.num * hi.num);
|
|
643
|
+
B += (L * (s.num * lo.den - lo.num * s.den)) / (s.den * lo.den);
|
|
644
|
+
}
|
|
247
645
|
}
|
|
646
|
+
return { A, B };
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/** A fraction in lowest terms. */
|
|
650
|
+
const reduceFrac = (f: TFrac): TFrac => {
|
|
651
|
+
const gcd = (a: bigint, b: bigint): bigint => (b === 0n ? a : gcd(b, a % b));
|
|
652
|
+
const g = gcd(f.num < 0n ? -f.num : f.num, f.den);
|
|
653
|
+
return g > 1n ? { num: f.num / g, den: f.den / g } : f;
|
|
654
|
+
};
|
|
248
655
|
|
|
656
|
+
/**
|
|
657
|
+
* An equal-weight ladder of `n` bands from `lo` to `hi`, every band a CL
|
|
658
|
+
* arc at `fee` unless named in `curves`. The edges are built over a common
|
|
659
|
+
* denominator and reduced: every rational component of a concentrated-liquidity config
|
|
660
|
+
* must stay below 2^64 (spec V9).
|
|
661
|
+
*/
|
|
662
|
+
export const uniformLadder = (
|
|
663
|
+
n: number,
|
|
664
|
+
lo: TFrac,
|
|
665
|
+
hi: TFrac,
|
|
666
|
+
fee: TFrac,
|
|
667
|
+
curves: (0 | 1)[] = [],
|
|
668
|
+
): TLadder => {
|
|
669
|
+
if (n < 1) throw new Error("A ladder needs at least one band");
|
|
670
|
+
if (lo.num * hi.den >= hi.num * lo.den)
|
|
671
|
+
throw new Error("The lower edge must be strictly below the upper edge");
|
|
672
|
+
const span = hi.num * lo.den - lo.num * hi.den;
|
|
673
|
+
const commonDen = lo.den * hi.den * BigInt(n);
|
|
674
|
+
const bands: TBand[] = [];
|
|
675
|
+
for (let i = 0; i < n; i++) {
|
|
676
|
+
bands.push({
|
|
677
|
+
start: reduceFrac({
|
|
678
|
+
num: lo.num * hi.den * BigInt(n) + span * BigInt(i),
|
|
679
|
+
den: commonDen,
|
|
680
|
+
}),
|
|
681
|
+
weight: 1n,
|
|
682
|
+
curve: curves[i] ?? 0,
|
|
683
|
+
feeBuy: fee,
|
|
684
|
+
feeSell: fee,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
249
687
|
return {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
688
|
+
bands,
|
|
689
|
+
closing: reduceFrac({
|
|
690
|
+
num: lo.num * hi.den * BigInt(n) + span * BigInt(n),
|
|
691
|
+
den: commonDen,
|
|
692
|
+
}),
|
|
693
|
+
weightTotal: BigInt(n),
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/** Band weight profiles. Every weight is at least 1, as `Create` demands. */
|
|
698
|
+
export type TWeightShape =
|
|
699
|
+
| "flat"
|
|
700
|
+
| "concentrated"
|
|
701
|
+
| "skewed-low"
|
|
702
|
+
| "skewed-high"
|
|
703
|
+
| "bimodal";
|
|
704
|
+
|
|
705
|
+
export const bandWeights = (
|
|
706
|
+
n: number,
|
|
707
|
+
shape: TWeightShape = "concentrated",
|
|
708
|
+
opts: { centre?: number; peak?: bigint; tail?: bigint } = {},
|
|
709
|
+
): bigint[] => {
|
|
710
|
+
if (shape === "flat") return Array.from({ length: n }, () => 1n);
|
|
711
|
+
const peak = Number(opts.peak ?? 64n);
|
|
712
|
+
const tail = Number(opts.tail ?? 1n);
|
|
713
|
+
const centre = opts.centre ?? (n - 1) / 2;
|
|
714
|
+
const sigma = Math.max(n / 5, 0.75);
|
|
715
|
+
const bell = (i: number, c: number) => Math.exp(-(((i - c) / sigma) ** 2));
|
|
716
|
+
const profile = (i: number): number => {
|
|
717
|
+
switch (shape) {
|
|
718
|
+
case "concentrated":
|
|
719
|
+
return bell(i, centre);
|
|
720
|
+
case "skewed-low":
|
|
721
|
+
return Math.exp(-i / Math.max(n / 3, 1));
|
|
722
|
+
case "skewed-high":
|
|
723
|
+
return Math.exp(-(n - 1 - i) / Math.max(n / 3, 1));
|
|
724
|
+
case "bimodal":
|
|
725
|
+
return Math.max(bell(i, (n - 1) / 4), bell(i, (3 * (n - 1)) / 4));
|
|
726
|
+
default:
|
|
727
|
+
return 1;
|
|
728
|
+
}
|
|
256
729
|
};
|
|
730
|
+
return Array.from({ length: n }, (_, i) => {
|
|
731
|
+
const w = Math.round(tail + (peak - tail) * profile(i));
|
|
732
|
+
return BigInt(Math.max(1, w));
|
|
733
|
+
});
|
|
257
734
|
};
|
|
258
735
|
|
|
259
736
|
/**
|
|
260
|
-
*
|
|
261
|
-
* constant-
|
|
262
|
-
*
|
|
263
|
-
* `a1·L0 ≥ a0·L1` and `b1·L0 ≥ b0·L1` — and those two bounds multiply to
|
|
264
|
-
* exactly the on-chain CL non-swap invariant `va1·vb1·L0² ≥ va0·vb0·L1²`
|
|
265
|
-
* (`cl_check.ak`). So the proportional deposit is always chain-valid; excess of
|
|
266
|
-
* either asset is refunded via aChange/bChange. This mirrors the scooper's
|
|
267
|
-
* `resolve_cl_deposit` (which likewise reuses the CP pinning).
|
|
737
|
+
* A ladder of `n` bands from `lo` to `hi` with a weight shape, optional
|
|
738
|
+
* constant-sum bins and an optional per-band fee. The edges are
|
|
739
|
+
* `uniformLadder`'s exactly; only the weights, curves and fees differ.
|
|
268
740
|
*/
|
|
269
|
-
export const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
741
|
+
export const shapedLadder = (
|
|
742
|
+
n: number,
|
|
743
|
+
lo: TFrac,
|
|
744
|
+
hi: TFrac,
|
|
745
|
+
fee: TFrac,
|
|
746
|
+
opts: {
|
|
747
|
+
shape?: TWeightShape;
|
|
748
|
+
centre?: number;
|
|
749
|
+
peak?: bigint;
|
|
750
|
+
tail?: bigint;
|
|
751
|
+
/** Zero-based band indices to price as constant-sum bins. */
|
|
752
|
+
csBands?: number[];
|
|
753
|
+
/** Overrides `fee` per band. */
|
|
754
|
+
feeAt?: (i: number) => TFrac;
|
|
755
|
+
} = {},
|
|
756
|
+
): TLadder => {
|
|
757
|
+
const base = uniformLadder(n, lo, hi, fee);
|
|
758
|
+
const weights = bandWeights(n, opts.shape ?? "concentrated", opts);
|
|
759
|
+
const cs = new Set(opts.csBands ?? []);
|
|
760
|
+
const bands: TBand[] = base.bands.map((b, i) => {
|
|
761
|
+
const f = opts.feeAt ? opts.feeAt(i) : fee;
|
|
762
|
+
return {
|
|
763
|
+
...b,
|
|
764
|
+
weight: weights[i]!,
|
|
765
|
+
curve: cs.has(i) ? 1 : 0,
|
|
766
|
+
feeBuy: f,
|
|
767
|
+
feeSell: f,
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
return {
|
|
771
|
+
bands,
|
|
772
|
+
closing: base.closing,
|
|
773
|
+
weightTotal: weights.reduce((s, w) => s + w, 0n),
|
|
774
|
+
};
|
|
775
|
+
};
|