@sundaeswap/math 0.3.0 → 0.5.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.
@@ -6,13 +6,15 @@ import {
6
6
  calculateDepositN,
7
7
  calculateLiquidity,
8
8
  calculatePinnedDeposit,
9
+ calculateZap,
9
10
  getSwapInput,
10
11
  getSwapOutput,
11
12
  pinnedDepositFromAnchor,
12
13
  } from "../ConstantSumPool.js";
13
14
 
14
15
  const tokenA: IAssetAmountMetadata = {
15
- assetId: "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e41",
16
+ assetId:
17
+ "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e41",
16
18
  decimals: 0,
17
19
  };
18
20
 
@@ -103,9 +105,13 @@ describe("ConstantSumPool.getSwapOutput", () => {
103
105
 
104
106
  describe("ConstantSumPool.getSwapInput", () => {
105
107
  it("throws on non-positive output, reserves, or prices", () => {
106
- expect(() => getSwapInput(tokenA, 0n, 10n, 10n, 1n, 1n, threePct)).toThrow();
108
+ expect(() =>
109
+ getSwapInput(tokenA, 0n, 10n, 10n, 1n, 1n, threePct),
110
+ ).toThrow();
107
111
  expect(() => getSwapInput(tokenA, 1n, 0n, 10n, 1n, 1n, threePct)).toThrow();
108
- expect(() => getSwapInput(tokenA, 1n, 10n, 10n, 1n, 0n, threePct)).toThrow();
112
+ expect(() =>
113
+ getSwapInput(tokenA, 1n, 10n, 10n, 1n, 0n, threePct),
114
+ ).toThrow();
109
115
  });
110
116
 
111
117
  it("throws when the output exceeds the output reserve, but allows draining it", () => {
@@ -272,9 +278,7 @@ describe("ConstantSumPool.calculateLiquidity", () => {
272
278
  // Deltas never exceed the offers, and match the validator's ceil pin.
273
279
  result.deltas.forEach((delta, i) => {
274
280
  expect(delta).toBeLessThanOrEqual(offered[i]);
275
- expect(delta).toEqual(
276
- (reserves[i] * result.targetDeltaV + vB - 1n) / vB,
277
- );
281
+ expect(delta).toEqual((reserves[i] * result.targetDeltaV + vB - 1n) / vB);
278
282
  });
279
283
  expect(result.generatedLp).toEqual(
280
284
  (totalLp * (vB + result.targetDeltaV)) / vB - totalLp,
@@ -320,3 +324,160 @@ describe("ConstantSumPool.calculateLiquidity", () => {
320
324
  ).toThrow();
321
325
  });
322
326
  });
327
+
328
+ describe("ConstantSumPool.calculateZap", () => {
329
+ // ADA/MINT-shaped pool at prices [2, 5]: 1M A + 400k B = 4M value, balanced.
330
+ const prices = [2n, 5n];
331
+ const reserves = [1_000_000n, 400_000n];
332
+ const totalLp = 4_000_000n;
333
+ const fee = new Fraction(30n, 10_000n);
334
+
335
+ /** The scooper's fill for an arbitrary dx — reference for the optimality scan. */
336
+ const fillAt = (
337
+ dx: bigint,
338
+ offered: bigint[],
339
+ res: bigint[],
340
+ px: bigint[],
341
+ lp: bigint,
342
+ f: Fraction,
343
+ ): bigint => {
344
+ const [x, y] = [0, 1];
345
+ const inputValue = dx * px[x];
346
+ const feeValue = (inputValue * f.numerator) / f.denominator;
347
+ const dy = (inputValue - feeValue) / px[y];
348
+ const basket = [offered[x] - dx, offered[y] + dy];
349
+ const next = [res[x] + dx, res[y] - dy];
350
+ if (basket.some((a) => a <= 0n) || next.some((r) => r <= 0n)) return 0n;
351
+ try {
352
+ return calculatePinnedDeposit(basket, next, px, lp).generatedLp;
353
+ } catch {
354
+ return 0n;
355
+ }
356
+ };
357
+
358
+ it("rejects anything but two assets, bad prices or reserves, and empty or negative offers", () => {
359
+ expect(() =>
360
+ calculateZap([1n, 1n, 1n], [1n, 1n, 1n], [1n, 1n, 1n], 1n, fee),
361
+ ).toThrow("exactly two assets");
362
+ expect(() =>
363
+ calculateZap([1n, 0n], reserves, [0n, 5n], totalLp, fee),
364
+ ).toThrow("Prices");
365
+ expect(() =>
366
+ calculateZap([1n, 0n], [0n, 1n], prices, totalLp, fee),
367
+ ).toThrow("Reserves");
368
+ expect(() =>
369
+ calculateZap([0n, 0n], reserves, prices, totalLp, fee),
370
+ ).toThrow("at least one positive");
371
+ expect(() =>
372
+ calculateZap([-1n, 1n], reserves, prices, totalLp, fee),
373
+ ).toThrow();
374
+ expect(() => calculateZap([1n, 1n], reserves, prices, 0n, fee)).toThrow(
375
+ "liquidity",
376
+ );
377
+ expect(() =>
378
+ calculateZap([1n, 1n], reserves, prices, totalLp, Fraction.ONE),
379
+ ).toThrow("fee");
380
+ });
381
+
382
+ it("is a plain pinned deposit when the basket already matches the reserves", () => {
383
+ const offered = [100_000n, 40_000n];
384
+ const zap = calculateZap(offered, reserves, prices, totalLp, fee);
385
+ const pinned = calculatePinnedDeposit(offered, reserves, prices, totalLp);
386
+
387
+ expect(zap.swapIndex).toBeUndefined();
388
+ expect(zap.swapInput).toBe(0n);
389
+ expect(zap.swapOutput).toBe(0n);
390
+ expect(zap.nextReserves).toEqual(reserves);
391
+ expect(zap.depositBasket).toEqual(offered);
392
+ expect(zap.generatedLp).toBe(pinned.generatedLp);
393
+ expect(zap.change).toEqual([0n, 0n]);
394
+ });
395
+
396
+ it("single-sided A: swaps the closed-form dx, deposits the rest, leaves dust change", () => {
397
+ const zap = calculateZap([100_000n, 0n], reserves, prices, totalLp, fee);
398
+
399
+ // dx = (a_X·r_Y)·feeDen·p_Y / ((feeDen−feeNum)·p_X·(r_X+a_X) + feeDen·p_Y·r_Y)
400
+ // = 2e15 / 41_934_000_000 = 47_693
401
+ expect(zap.swapIndex).toBe(0);
402
+ expect(zap.swapInput).toBe(47_693n);
403
+ // getSwapOutput semantics: floor((95_386 − floor(95_386·0.003)) / 5)
404
+ expect(zap.swapOutput).toBe(19_020n);
405
+ expect(zap.nextReserves).toEqual([1_047_693n, 380_980n]);
406
+ expect(zap.depositBasket).toEqual([52_307n, 19_020n]);
407
+
408
+ // The pin is the already-tested function applied to that basket.
409
+ const pinned = calculatePinnedDeposit(
410
+ zap.depositBasket,
411
+ zap.nextReserves,
412
+ prices,
413
+ totalLp,
414
+ );
415
+ expect(zap.deltas).toEqual(pinned.deltas);
416
+ expect(zap.generatedLp).toBe(pinned.generatedLp);
417
+ expect(zap.change).toEqual([
418
+ zap.depositBasket[0] - pinned.deltas[0],
419
+ zap.depositBasket[1] - pinned.deltas[1],
420
+ ]);
421
+ // Rounding leaves at most a unit or two behind.
422
+ expect(zap.change[0] + zap.change[1]).toBeLessThanOrEqual(2n);
423
+
424
+ // A 200k-value basket into a 4M pool with 4M LP is 200k LP before fees;
425
+ // half of it swaps at 0.3%, so the loss is ~0.15%.
426
+ expect(zap.generatedLp).toBeLessThan(200_000n);
427
+ expect(zap.generatedLp).toBeGreaterThan(199_400n);
428
+ });
429
+
430
+ it("swap leg agrees with getSwapOutput for the same dx", () => {
431
+ const zap = calculateZap([100_000n, 0n], reserves, prices, totalLp, fee);
432
+ const swap = getSwapOutput(
433
+ tokenA,
434
+ zap.swapInput,
435
+ reserves[0],
436
+ reserves[1],
437
+ prices[0],
438
+ prices[1],
439
+ fee,
440
+ );
441
+ expect(zap.swapOutput).toBe(swap.output);
442
+ expect(zap.nextReserves).toEqual([
443
+ swap.nextInputReserve,
444
+ swap.nextOutputReserve,
445
+ ]);
446
+ });
447
+
448
+ it("single-sided B swaps in the other direction", () => {
449
+ const zap = calculateZap([0n, 40_000n], reserves, prices, totalLp, fee);
450
+ expect(zap.swapIndex).toBe(1);
451
+ expect(zap.swapInput).toBeGreaterThan(0n);
452
+ expect(zap.depositBasket[0]).toBeGreaterThan(0n);
453
+ expect(zap.depositBasket[1]).toBeLessThan(40_000n);
454
+ expect(zap.nextReserves[0]).toBeLessThan(reserves[0]);
455
+ expect(zap.nextReserves[1]).toBeGreaterThan(reserves[1]);
456
+ });
457
+
458
+ it("closed-form dx maximizes minted LP over every integer dx", () => {
459
+ // Small enough to scan exhaustively; skewed prices and reserves so the
460
+ // ratio work is non-trivial.
461
+ const px = [3n, 7n];
462
+ const res = [5_000n, 2_000n];
463
+ const lp = 29_000n;
464
+ const f = new Fraction(25n, 10_000n);
465
+ const offered = [1_200n, 50n];
466
+
467
+ const zap = calculateZap(offered, res, px, lp, f);
468
+ expect(zap.swapIndex).toBe(0);
469
+
470
+ let best = 0n;
471
+ for (let dx = 0n; dx <= offered[0]; dx++) {
472
+ const got = fillAt(dx, offered, res, px, lp, f);
473
+ if (got > best) best = got;
474
+ }
475
+ expect(zap.generatedLp).toBe(best);
476
+ });
477
+
478
+ it("throws when the offer is too small to survive the swap", () => {
479
+ expect(() =>
480
+ calculateZap([1n, 0n], reserves, prices, totalLp, fee),
481
+ ).toThrow("too small");
482
+ });
483
+ });