@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.
- package/dist/cjs/PoolMath/ConstantSumPool.js +106 -1
- package/dist/cjs/PoolMath/ConstantSumPool.js.map +1 -1
- package/dist/cjs/PoolMath/V4StableswapPool.js +529 -0
- package/dist/cjs/PoolMath/V4StableswapPool.js.map +1 -0
- package/dist/cjs/PoolMath/index.js +3 -1
- package/dist/cjs/PoolMath/index.js.map +1 -1
- package/dist/esm/PoolMath/ConstantSumPool.js +81 -0
- package/dist/esm/PoolMath/ConstantSumPool.js.map +1 -1
- package/dist/esm/PoolMath/V4StableswapPool.js +495 -0
- package/dist/esm/PoolMath/V4StableswapPool.js.map +1 -0
- package/dist/esm/PoolMath/index.js +1 -0
- package/dist/esm/PoolMath/index.js.map +1 -1
- package/dist/types/PoolMath/ConstantSumPool.d.ts +43 -0
- package/dist/types/PoolMath/ConstantSumPool.d.ts.map +1 -1
- package/dist/types/PoolMath/V4StableswapPool.d.ts +221 -0
- package/dist/types/PoolMath/V4StableswapPool.d.ts.map +1 -0
- package/dist/types/PoolMath/index.d.ts +1 -0
- package/dist/types/PoolMath/index.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/PoolMath/ConstantSumPool.ts +131 -0
- package/src/PoolMath/V4StableswapPool.ts +638 -0
- package/src/PoolMath/__tests__/ConstantSumPool.test.ts +167 -6
- package/src/PoolMath/__tests__/V4StableswapPool.test.ts +688 -0
- package/src/PoolMath/index.ts +1 -0
|
@@ -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:
|
|
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(() =>
|
|
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(() =>
|
|
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
|
+
});
|