@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.
@@ -1,225 +1,218 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { IAssetAmountMetadata } from "@sundaeswap/asset";
3
- import { Fraction } from "@sundaeswap/fraction";
4
3
 
5
4
  import {
6
- calculateLiquidity,
7
- getSwapInput,
5
+ aSat,
6
+ bandOf,
7
+ bandOutput,
8
+ bandProof,
9
+ bandStates,
10
+ bandView,
11
+ buildIndex,
12
+ findWitness,
8
13
  getSwapOutput,
9
- type TSqrtPrice,
14
+ indexLadder,
15
+ liquidityOf,
16
+ prefixes,
17
+ reservesAt,
18
+ shapedLadder,
19
+ swapSteps,
20
+ type TLadder,
21
+ uniformLadder,
10
22
  } from "../ConcentratedLiquidityPool.js";
11
23
 
12
- const tokenA: IAssetAmountMetadata = {
13
- assetId: "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e41",
24
+ const tokenB: IAssetAmountMetadata = {
25
+ assetId: "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e42",
14
26
  decimals: 0,
15
27
  };
16
28
 
17
- const threePct = new Fraction(3n, 1000n);
18
- const zeroFee = new Fraction(0n, 1n);
19
-
20
- // Range [0.5, 2.0] as sqrt-price rationals.
21
- const spa: TSqrtPrice = [1n, 2n];
22
- const spb: TSqrtPrice = [2n, 1n];
23
-
24
- /** Direct evaluation of the on-chain/scooper virtual-reserve swap formula. */
25
- const refOutput = (
26
- dx: bigint,
27
- a: bigint,
28
- b: bigint,
29
- l: bigint,
30
- isAInput: boolean,
31
- feeNum: bigint,
32
- feeDen: bigint,
33
- ): bigint => {
34
- const fee = (dx * feeNum) / feeDen;
35
- const dxEff = dx - fee;
36
- const va0 = a * spb[0] + l * spb[1];
37
- const vb0 = b * spa[1] + l * spa[0];
38
- if (isAInput) {
39
- const dvaEff = dxEff * spb[0];
40
- return (vb0 * dvaEff) / ((va0 + dvaEff) * spa[1]);
41
- }
42
- const dvbEff = dxEff * spa[0];
43
- return (va0 * dvbEff) / ((vb0 + dvbEff) * spb[0]);
29
+ // The eight equal-weight ladder of sundae-v4's lib/tests/unit/cl_check_unit.ak
30
+ // (edges 1.00..1.08 at 1e6, fee 3/1000).
31
+ const fee = { num: 3n, den: 1000n };
32
+ const den = 1_000_000n;
33
+ const eq8: TLadder = {
34
+ bands: Array.from({ length: 8 }, (_, i) => ({
35
+ start: { num: 1_000_000n + 10_000n * BigInt(i), den },
36
+ weight: 1n,
37
+ curve: 0 as const,
38
+ feeBuy: fee,
39
+ feeSell: fee,
40
+ })),
41
+ closing: { num: 1_080_000n, den },
42
+ weightTotal: 8n,
44
43
  };
44
+ const A = 8_637_368n;
45
+ const B = 624_999n;
45
46
 
46
- describe("ConcentratedLiquidityPool.getSwapOutput", () => {
47
- it("matches the hand-computed virtual-reserve formula (A→B, no fee)", () => {
48
- // a=b=L=1000, range [0.5,2]: va0=vb0=3000, dvaEff=200,
49
- // out = 3000·200 / ((3000+200)·2) = 600000/6400 = 93.75 -> 93.
50
- const { output } = getSwapOutput(
51
- tokenA,
52
- 100n,
53
- 1000n,
54
- 1000n,
55
- 1000n,
56
- spa,
57
- spb,
58
- zeroFee,
59
- true,
60
- );
61
- expect(output).toEqual(93n);
47
+ describe("ConcentratedLiquidityPool: the on-chain vectors", () => {
48
+ it("finds the witness the Aiken test names", () => {
49
+ const w = findWitness(eq8, A, B)!;
50
+ expect(w.x).toBe(999_999_813n);
51
+ expect(w.k).toBe(0);
52
+ const l = indexLadder(eq8);
53
+ const p = prefixes(l, w.x, w.k);
54
+ expect(p.ca).toBe(8_021_634n);
55
+ expect(p.cb).toBe(0n);
56
+ expect(liquidityOf(l, w.x, w.k)).toBe(124_999_976n);
57
+ expect(aSat(l, w.x, w.k)).toBe(1_237_624n);
58
+ expect(bandProof(l, A, B, 999_999_813n, 0)).toBe(true);
59
+ expect(bandProof(l, A, B, 71_428_457n, 7)).toBe(false);
62
60
  });
63
61
 
64
- it("matches the hand-computed formula (B→A, no fee)", () => {
65
- // dvbEff=100, out = 3000·100 / ((3000+100)·2) = 300000/6200 = 48.38 -> 48.
66
- const { output } = getSwapOutput(
67
- tokenA,
68
- 100n,
69
- 1000n,
70
- 1000n,
71
- 1000n,
72
- spa,
73
- spb,
74
- zeroFee,
75
- false,
76
- );
77
- expect(output).toEqual(48n);
62
+ it("finds the post-swap witness", () => {
63
+ expect(findWitness(eq8, 8_636_752n, 625_623n)!.x).toBe(999_999_938n);
78
64
  });
79
65
 
80
- it("agrees with the reference evaluator across sizes and fees", () => {
81
- for (const isAInput of [true, false]) {
82
- for (const dx of [1n, 7n, 100n, 5000n, 250000n]) {
83
- const { output } = getSwapOutput(
84
- tokenA,
85
- dx,
86
- 1_000_000n,
87
- 1_000_000n,
88
- 1_000_000n,
89
- spa,
90
- spb,
91
- threePct,
92
- isAInput,
93
- );
94
- expect(output).toEqual(
95
- refOutput(dx, 1_000_000n, 1_000_000n, 1_000_000n, isAInput, 3n, 1000n),
96
- );
97
- }
98
- }
66
+ // sundae-v4 #228 (SV4-006): the band fee rounds UP, so 624 B pays a fee
67
+ // of ceil(624 * 3 / 1000) = 2 and the pair admits 615 A. With the floored
68
+ // fee of 1 it admitted 616. lib/tests/unit/cl_check_unit.ak.
69
+ it("pays 615 A for 624 B, the band fee rounded up to 2", () => {
70
+ const out = getSwapOutput(tokenB, 624n, A, B, eq8, false);
71
+ expect(out.output).toBe(615n);
72
+ expect(out.lpFee.amount).toBe(2n);
73
+ expect(out.steps.length).toBe(1);
74
+ expect(out.steps[0]!.after.x).toBe(1_000_000_062n);
75
+ expect(findWitness(eq8, A - 615n, B + 624n)!.x).toBe(1_000_000_062n);
99
76
  });
100
77
 
101
- it("caps output at the output reserve", () => {
102
- const { output } = getSwapOutput(
103
- tokenA,
104
- 10_000_000n,
105
- 100n,
106
- 5n,
107
- 1_000_000n,
108
- spa,
109
- spb,
110
- zeroFee,
111
- true,
112
- );
113
- expect(output).toBeLessThanOrEqual(5n);
78
+ it("recovers from a stale hint", () => {
79
+ const out = getSwapOutput(tokenB, 624n, A, B, eq8, false, { x: 123n, k: 5 });
80
+ expect(out.output).toBe(615n);
114
81
  });
115
82
 
116
- it("throws on non-positive input/reserves/liquidity", () => {
117
- expect(() =>
118
- getSwapOutput(tokenA, 0n, 10n, 10n, 10n, spa, spb, zeroFee, true),
119
- ).toThrow();
120
- expect(() =>
121
- getSwapOutput(tokenA, 1n, 10n, 10n, 0n, spa, spb, zeroFee, true),
122
- ).toThrow();
83
+ it("charges a fee of one unit on an input below den / num", () => {
84
+ const l = indexLadder(eq8);
85
+ const v = bandView(l, A, B, findWitness(l, A, B)!);
86
+ // ceil(1 * 3 / 1000) = 1, so one unit of input has no effective input.
87
+ expect(bandOutput(v, false, 1n)).toBe(0n);
88
+ // ceil(333 * 3 / 1000) = 1: dx_eff = 332.
89
+ const free = { ...v, feeBuy: { num: 0n, den: 1n } };
90
+ expect(bandOutput(v, false, 333n)).toBe(bandOutput(free, false, 332n));
91
+ // ceil(1000 * 3 / 1000) = 3 exactly: no extra unit when it divides.
92
+ expect(bandOutput(v, false, 1_000n)).toBe(bandOutput(free, false, 997n));
93
+ // A zero rate still charges nothing, whatever its denominator.
94
+ const zero = { ...v, feeBuy: { num: 0n, den: 1_000n } };
95
+ expect(bandOutput(zero, false, 1_000n)).toBe(bandOutput(free, false, 1_000n));
96
+ });
97
+
98
+ it("crosses bands for an input larger than the active band", () => {
99
+ const l = indexLadder(eq8);
100
+ const w = findWitness(l, A, B)!;
101
+ // Band 0 holds a_sat_0 of A; buying more than that crosses into band 1.
102
+ const steps = swapSteps(l, A, B, false, 2_000_000n, w)!;
103
+ expect(steps.length).toBeGreaterThan(1);
104
+ expect(steps[0]!.band).toBe(0);
105
+ expect(steps[1]!.band).toBe(1);
106
+ const total = steps.reduce((s, st) => s + st.output, 0n);
107
+ expect(total).toBeGreaterThan(1_237_624n);
108
+ expect(total).toBeLessThan(A);
123
109
  });
124
- });
125
110
 
126
- describe("ConcentratedLiquidityPool.getSwapInput", () => {
127
- it("returns the minimal input whose forward swap clears the target", () => {
128
- for (const isAInput of [true, false]) {
129
- for (const targetOut of [1n, 40n, 93n, 1000n, 90_000n]) {
130
- const { input } = getSwapInput(
131
- tokenA,
132
- targetOut,
133
- 1_000_000n,
134
- 1_000_000n,
135
- 1_000_000n,
136
- spa,
137
- spb,
138
- threePct,
139
- isAInput,
140
- );
141
- const got = getSwapOutput(
142
- tokenA,
143
- input,
144
- 1_000_000n,
145
- 1_000_000n,
146
- 1_000_000n,
147
- spa,
148
- spb,
149
- threePct,
150
- isAInput,
151
- ).output;
152
- // Reaches the target...
153
- expect(got).toBeGreaterThanOrEqual(targetOut);
154
- // ...and is minimal: one unit less falls short.
155
- const less = getSwapOutput(
156
- tokenA,
157
- input - 1n,
158
- 1_000_000n,
159
- 1_000_000n,
160
- 1_000_000n,
161
- spa,
162
- spb,
163
- threePct,
164
- isAInput,
165
- ).output;
166
- expect(less).toBeLessThan(targetOut);
167
- }
168
- }
111
+ it("refuses an input the ladder cannot absorb", () => {
112
+ // The price sits in band 0: selling A pays out band 0's B residual and
113
+ // then has no band below to walk into.
114
+ const small = getSwapOutput(tokenB, 1_000n, A, B, eq8, true);
115
+ expect(small.output).toBeGreaterThan(0n);
116
+ expect(() => getSwapOutput(tokenB, 1_000_000_000n, A, B, eq8, true)).toThrow();
169
117
  });
170
118
 
171
- it("throws when the output exceeds the reserve", () => {
172
- expect(() =>
173
- getSwapInput(tokenA, 200n, 1000n, 100n, 1000n, spa, spb, threePct, true),
174
- ).toThrow();
119
+ it("splits the reserves across the bands exactly", () => {
120
+ const states = bandStates(eq8, A, B)!;
121
+ expect(states.length).toBe(8);
122
+ expect(states[0]!.active).toBe(true);
123
+ const sumA = states.reduce((s, b) => s + b.quantities[0], 0n);
124
+ const sumB = states.reduce((s, b) => s + b.quantities[1], 0n);
125
+ expect(sumA).toBe(A);
126
+ expect(sumB).toBe(B);
127
+ for (let i = 1; i < 8; i++) expect(states[i]!.quantities[1]).toBe(0n);
175
128
  });
176
129
  });
177
130
 
178
- describe("ConcentratedLiquidityPool.calculateLiquidity", () => {
179
- // The on-chain CL non-swap invariant a proportional deposit must satisfy.
180
- const invariantHolds = (
181
- a0: bigint,
182
- b0: bigint,
183
- l0: bigint,
184
- da: bigint,
185
- db: bigint,
186
- dl: bigint,
187
- ): boolean => {
188
- const va0 = a0 * spb[0] + l0 * spb[1];
189
- const vb0 = b0 * spa[1] + l0 * spa[0];
190
- const a1 = a0 + da;
191
- const b1 = b0 + db;
192
- const l1 = l0 + dl;
193
- const va1 = a1 * spb[0] + l1 * spb[1];
194
- const vb1 = b1 * spa[1] + l1 * spa[0];
195
- return va1 * vb1 * l0 * l0 >= va0 * vb0 * l1 * l1;
196
- };
197
-
198
- it("mints proportionally and the fill satisfies the CL invariant", () => {
199
- const cases: Array<[bigint, bigint, bigint, bigint, bigint]> = [
200
- // [offeredA, offeredB, reserveA, reserveB, totalLp]
201
- [100n, 100n, 1000n, 1000n, 1000n],
202
- [100n, 250n, 1000n, 1000n, 1000n], // B surplus
203
- [500n, 100n, 3333n, 991n, 1777n], // coprime-ish reserves
204
- [12345n, 67890n, 1_000_000n, 5_000_000n, 2_000_000n],
205
- ];
206
- for (const [a, b, ra, rb, lp] of cases) {
207
- const res = calculateLiquidity(a, b, ra, rb, lp);
208
- expect(res.generatedLp).toBeGreaterThan(0n);
209
- // Never consumes more than offered.
210
- expect(res.actualDepositedA).toBeLessThanOrEqual(a);
211
- expect(res.actualDepositedB).toBeLessThanOrEqual(b);
212
- // The resulting deposit is chain-valid under the CL invariant.
213
- expect(
214
- invariantHolds(
215
- ra,
216
- rb,
217
- lp,
218
- res.actualDepositedA,
219
- res.actualDepositedB,
220
- res.generatedLp,
221
- ),
222
- ).toBe(true);
223
- }
131
+ describe("ConcentratedLiquidityPool: building a ladder", () => {
132
+ it("derives reserves at a launch price that the ladder then witnesses", () => {
133
+ const ladder = shapedLadder(8, { num: 95n, den: 100n }, { num: 105n, den: 100n }, fee, {
134
+ shape: "concentrated",
135
+ });
136
+ expect(ladder.weightTotal).toBe(ladder.bands.reduce((s, b) => s + b.weight, 0n));
137
+ const launch = { num: 1n, den: 1n };
138
+ const k = bandOf(ladder, launch);
139
+ const r = reservesAt(ladder, 1_000_000_000n, k, launch);
140
+ const w = findWitness(ladder, r.A, r.B)!;
141
+ expect(w).not.toBeNull();
142
+ // The reserves are floored, so the band the chain derives can sit one
143
+ // below the one asked for; a creator takes the witness's band.
144
+ expect(Math.abs(w.k - k)).toBeLessThanOrEqual(1);
145
+ expect(buildIndex(ladder).length).toBe(8);
146
+ });
147
+
148
+ it("keeps uniform edges in lowest terms and refuses an inverted range", () => {
149
+ const u = uniformLadder(4, { num: 1n, den: 2n }, { num: 2n, den: 1n }, fee);
150
+ expect(u.bands[0]!.start).toEqual({ num: 1n, den: 2n });
151
+ expect(u.closing).toEqual({ num: 2n, den: 1n });
152
+ expect(() => uniformLadder(4, { num: 2n, den: 1n }, { num: 1n, den: 2n }, fee)).toThrow();
224
153
  });
225
154
  });
155
+
156
+ // The scaling bench of sundae-v4 validators/benches/scaling/cl.ak after #228
157
+ // (the band fee rounded up): for N equal-weight bands with edges
158
+ // 1.00 + 0.01*j and fee 3/1000, a B-in swap of 0.1% of the B reserve.
159
+ // Each row is (N, A, B, X, k, A', B', X', budget); the chain admits A - A'.
160
+ const benchRows: [number, bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint][] = [
161
+ [1, 4925865n, 4999999n, 999999826n, 0n, 4920931n, 5004998n, 1000001381n, 1555n],
162
+ [2, 2414759n, 7500000n, 999999999n, 1n, 2407502n, 7507500n, 1000002313n, 2314n],
163
+ [3, 4782629n, 5000000n, 999999899n, 1n, 4777791n, 5005000n, 1000001399n, 1500n],
164
+ [4, 3517823n, 6249999n, 999999799n, 2n, 3511894n, 6256248n, 1000001620n, 1821n],
165
+ [5, 4645760n, 4999999n, 999999749n, 2n, 4641017n, 5004998n, 1000001269n, 1520n],
166
+ [6, 3797891n, 5833333n, 999999799n, 3n, 3792463n, 5839166n, 1000001701n, 1902n],
167
+ [7, 4514876n, 5000000n, 999999766n, 3n, 4510223n, 5005000n, 1000001333n, 1567n],
168
+ [8, 3876484n, 5624999n, 999999799n, 4n, 3871350n, 5630623n, 1000001551n, 1752n],
169
+ [9, 4389622n, 4999999n, 999999674n, 4n, 4385059n, 5004998n, 1000001303n, 1629n],
170
+ [10, 3877638n, 5500000n, 999999786n, 5n, 3872713n, 5505500n, 1000001599n, 1813n],
171
+ [11, 4269672n, 5000000n, 999999879n, 5n, 4265194n, 5005000n, 1000001446n, 1567n],
172
+ [12, 3842499n, 5416666n, 999999799n, 6n, 3837740n, 5422082n, 1000001599n, 1800n],
173
+ [13, 4154721n, 4999999n, 999999649n, 6n, 4150328n, 5004998n, 1000001383n, 1734n],
174
+ [14, 3788541n, 5357142n, 999999799n, 7n, 3783921n, 5362499n, 1000001599n, 1800n],
175
+ [15, 4044489n, 5000000n, 999999848n, 7n, 4040176n, 5005000n, 1000001445n, 1597n],
176
+ [16, 3724376n, 5312499n, 999999799n, 8n, 3719878n, 5317811n, 1000001399n, 1600n],
177
+ [17, 3938713n, 4999999n, 999999712n, 8n, 3934480n, 5004998n, 1000001387n, 1675n],
178
+ [18, 3654687n, 5277777n, 999999643n, 9n, 3650300n, 5283054n, 1000001395n, 1752n],
179
+ [19, 3837151n, 5000000n, 999999903n, 9n, 3832994n, 5005000n, 1000001455n, 1552n],
180
+ [20, 3582202n, 5249999n, 999999799n, 10n, 3577917n, 5255248n, 1000001399n, 1600n],
181
+ [21, 3739575n, 4999999n, 999999692n, 10n, 3735494n, 5004998n, 1000001309n, 1617n],
182
+ [22, 3508584n, 5227272n, 999999799n, 11n, 3504393n, 5232499n, 1000001399n, 1600n],
183
+ [23, 3645776n, 5000000n, 999999854n, 11n, 3641767n, 5005000n, 1000001380n, 1526n],
184
+ [24, 3434886n, 5208333n, 999999754n, 12n, 3430785n, 5213541n, 1000001588n, 1834n],
185
+ [25, 3555555n, 5000000n, 999999791n, 12n, 3551617n, 5005000n, 1000001445n, 1654n],
186
+ [26, 3361787n, 5192307n, 999999655n, 13n, 3357770n, 5197499n, 1000001522n, 1867n],
187
+ [27, 3468729n, 4999999n, 999999792n, 13n, 3464861n, 5004998n, 1000001295n, 1503n],
188
+ [28, 3289730n, 5178571n, 999999799n, 14n, 3285794n, 5183749n, 1000001614n, 1815n],
189
+ [29, 3385125n, 4999999n, 999999735n, 14n, 3381324n, 5004998n, 1000001272n, 1537n],
190
+ [30, 3219003n, 5166666n, 999999799n, 15n, 3215144n, 5171832n, 1000001599n, 1800n],
191
+ [31, 3304583n, 4999999n, 999999653n, 15n, 3300848n, 5004998n, 1000001389n, 1736n],
192
+ [32, 3149791n, 5156250n, 999999799n, 16n, 3146005n, 5161406n, 1000001553n, 1754n],
193
+ ];
194
+
195
+ const benchLadder = (n: number): TLadder => ({
196
+ bands: Array.from({ length: n }, (_, i) => ({
197
+ start: { num: 1_000_000n + 10_000n * BigInt(i), den },
198
+ weight: 1n,
199
+ curve: 0 as const,
200
+ feeBuy: fee,
201
+ feeSell: fee,
202
+ })),
203
+ closing: { num: 1_000_000n + 10_000n * BigInt(n), den },
204
+ weightTotal: BigInt(n),
205
+ });
206
+
207
+ describe("ConcentratedLiquidityPool: the scaling bench vectors", () => {
208
+ for (const [n, a, b, x, k, a1, b1] of benchRows) {
209
+ it(`pays ${a - a1} A for ${b1 - b} B on ${n} bands`, () => {
210
+ const ladder = benchLadder(n);
211
+ const w = findWitness(ladder, a, b)!;
212
+ expect(w.x).toBe(x);
213
+ expect(BigInt(w.k)).toBe(k);
214
+ const out = getSwapOutput(tokenB, b1 - b, a, b, ladder, false, w);
215
+ expect(out.output).toBe(a - a1);
216
+ });
217
+ }
218
+ });