From 4ffc37cf9972f5e4c4358bbf01056c64cad4026a Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 12:13:45 +0400 Subject: [PATCH 1/9] feat(pegged-swap): introduce PeggedPrice --- .../concentrate/bigint-sqrt.spec.ts | 26 ---------- .../instructions/concentrate/bigint-sqrt.ts | 24 --------- .../concentrate-grow-liquidity-2d-args.ts | 2 +- .../concentrate-liquidity-math.test.ts | 2 +- .../concentrate-liquidity-math.ts | 2 +- .../swap-vm/instructions/concentrate/index.ts | 2 +- .../instructions/concentrate/price/index.ts | 2 +- .../instructions/concentrate/price/price.ts | 4 +- .../truncate-human-decimal-string.test.ts | 45 ---------------- .../price/truncate-human-decimal-string.ts | 52 ------------------- .../swap-vm/src/swap-vm/instructions/index.ts | 1 + .../swap-vm/instructions/pegged-swap/index.ts | 7 +++ .../pegged-swap/pegged-swap-args.ts | 4 +- .../pegged-swap/rate-resolver.test.ts | 18 +++---- .../instructions/pegged-swap/rate-resolver.ts | 4 +- 15 files changed, 28 insertions(+), 167 deletions(-) delete mode 100644 typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.spec.ts delete mode 100644 typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.ts delete mode 100644 typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.test.ts delete mode 100644 typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.ts diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.spec.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.spec.ts deleted file mode 100644 index 619dcd9..0000000 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 - -import { UINT_256_MAX } from '@1inch/byte-utils' -import { bigintSqrt } from './bigint-sqrt' - -describe('bigintSqrt', () => { - it('correct for 0-1000', () => { - for (let i = 0; i <= 1000; i++) { - expect(bigintSqrt(BigInt(i))).toBe(BigInt(Math.floor(Math.sqrt(i)))) - } - }) - - describe('correct for all even powers of 2', () => { - for (let i = 0; i < 256; i++) { - it(`2^${i * 2}`, () => { - const root = 2n ** BigInt(i) - const rootSquared = root * root - expect(bigintSqrt(rootSquared)).toBe(root) - }) - } - }) - - it('correct for UINT_256_MAX', () => { - expect(bigintSqrt(UINT_256_MAX)).toBe(BigInt('340282366920938463463374607431768211455')) - }) -}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.ts deleted file mode 100644 index f961a65..0000000 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/bigint-sqrt.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 - -export function bigintSqrt(value: bigint): bigint { - if (value < 0n) { - throw new Error('square root of negative numbers is not supported') - } - - if (value < 2n) { - return value - } - - if (value <= 9007199254740991n) { - return BigInt(Math.floor(Math.sqrt(Number(value)))) - } - - let x0 = value - let x1 = (value / x0 + x0) >> 1n - while (x1 < x0) { - x0 = x1 - x1 = (value / x0 + x0) >> 1n - } - - return x0 -} diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-grow-liquidity-2d-args.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-grow-liquidity-2d-args.ts index bb8826e..b97a390 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-grow-liquidity-2d-args.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-grow-liquidity-2d-args.ts @@ -4,7 +4,7 @@ import type { HexString } from '@1inch/sdk-core' import { UINT_256_MAX } from '@1inch/byte-utils' import assert from 'assert' import { ConcentrateGrowLiquidity2DArgsCoder } from './concentrate-grow-liquidity-2d-args-coder' -import { bigintSqrt } from './bigint-sqrt' +import { bigintSqrt } from '../utils/bigint-sqrt' import type { IArgsCoder, IArgsData } from '../types' export const ONE_E18: bigint = 10n ** 18n diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.test.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.test.ts index bb36c06..947cf39 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.test.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.test.ts @@ -9,7 +9,7 @@ import { computeLiquidityFromAmounts, } from './concentrate-liquidity-math' import { ONE_E18 } from '../concentrate-grow-liquidity-2d-args' -import { bigintSqrt } from '../bigint-sqrt' +import { bigintSqrt } from '../../utils/bigint-sqrt' describe('concentrate-liquidity-math', () => { describe('computeLiquidityAndPrice', () => { diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.ts index 45e4f26..00b8e22 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/concentrate-liquidity-math/concentrate-liquidity-math.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 import { UINT_256_MAX } from '@1inch/byte-utils' -import { bigintSqrt } from '../bigint-sqrt' +import { bigintSqrt } from '../../utils/bigint-sqrt' const ONE = 10n ** 18n diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/index.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/index.ts index 2175139..94c334f 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/index.ts @@ -20,4 +20,4 @@ export type { PriceBounds, ConcentratedLiquidityInfo, } from './concentrate-liquidity-calculator/types' -export * from './bigint-sqrt' +export { bigintSqrt } from '../utils/bigint-sqrt' diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/index.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/index.ts index 1e62e06..4107fb9 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/index.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 export { Price } from './price' -export { truncateHumanDecimalString } from './truncate-human-decimal-string' +export { truncateHumanDecimalString } from '../../utils/truncate-human-decimal-string' export type { PriceJSON, PricePair, PriceToken } from './types' diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/price.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/price.ts index bb5741b..de47680 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/price.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/price.ts @@ -4,8 +4,8 @@ import { Address } from '@1inch/sdk-core' import { formatUnits, parseUnits } from 'viem' import assert from 'assert' import type { PriceJSON, PricePair, PriceToken } from './types' -import { truncateHumanDecimalString } from './truncate-human-decimal-string' -import { bigintSqrt } from '../bigint-sqrt' +import { bigintSqrt } from '../../utils/bigint-sqrt' +import { truncateHumanDecimalString } from '../../utils/truncate-human-decimal-string' const ONE_E18 = 10n ** 18n diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.test.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.test.ts deleted file mode 100644 index c4d3262..0000000 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 - -import { describe, expect, it } from 'vitest' -import { truncateHumanDecimalString } from './truncate-human-decimal-string' - -describe('truncateHumanDecimalString', () => { - it('returns integer-only when maxFrac is 0 (half-up on first fractional digit)', () => { - expect(truncateHumanDecimalString('123.456', 0)).toBe('123') - expect(truncateHumanDecimalString('123.567', 0)).toBe('124') - expect(truncateHumanDecimalString('123', 0)).toBe('123') - }) - - it('returns unchanged when there is no decimal point', () => { - expect(truncateHumanDecimalString('2500', 6)).toBe('2500') - expect(truncateHumanDecimalString('0', 18)).toBe('0') - }) - - it('rounds half-up at maxFrac (first dropped digit >= 5 increments last kept digit)', () => { - expect(truncateHumanDecimalString('1.9999', 2)).toBe('2') - expect(truncateHumanDecimalString('1.994', 2)).toBe('1.99') - expect(truncateHumanDecimalString('2000.000000000000000000131782', 6)).toBe('2000') - }) - - it('strips trailing zeros after rounding', () => { - expect(truncateHumanDecimalString('2000.000000', 6)).toBe('2000') - expect(truncateHumanDecimalString('1.2300', 4)).toBe('1.23') - }) - - it('keeps non-zero fractional digits up to maxFrac (half-up)', () => { - expect(truncateHumanDecimalString('0.000499999999999999999999', 18)).toBe('0.0005') - expect(truncateHumanDecimalString('3.141592653589793', 4)).toBe('3.1416') - }) - - it('does not pad when fractional part is shorter than maxFrac', () => { - expect(truncateHumanDecimalString('1.5', 6)).toBe('1.5') - }) - - it('handles maxFrac larger than available fractional digits', () => { - expect(truncateHumanDecimalString('10.12', 10)).toBe('10.12') - }) - - it('handles empty fractional after strip (integer)', () => { - expect(truncateHumanDecimalString('42.000000', 6)).toBe('42') - }) -}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.ts b/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.ts deleted file mode 100644 index 6b2a35c..0000000 --- a/typescript/swap-vm/src/swap-vm/instructions/concentrate/price/truncate-human-decimal-string.ts +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 - -/** - * Round a decimal string to `maxFrac` fractional digits (half-up: if the first dropped digit - * is `5`–`9`, round the last kept digit up), then strip trailing zeros after the dot. - * - * @param s Decimal string as produced by e.g. `formatUnits` (no scientific notation). - * @param maxFrac Maximum number of digits after `.`; `0` means integer only (round using the - * first fractional digit). - */ -export function truncateHumanDecimalString(s: string, maxFrac: number): string { - if (maxFrac < 0) { - throw new Error('maxFrac must be non-negative') - } - - const dot = s.indexOf('.') - - if (dot === -1) { - return s - } - - const intPartStr = s.slice(0, dot) || '0' - const fracFull = s.slice(dot + 1) - - if (maxFrac === 0) { - let intPart = BigInt(intPartStr) - const first = fracFull[0] - - if (first !== undefined && first >= '5' && first <= '9') { - intPart += 1n - } - - return intPart.toString() - } - - const fracPadded = (fracFull + '0'.repeat(maxFrac)).slice(0, maxFrac) - const nextDigit = fracFull.length > maxFrac ? fracFull[maxFrac] : undefined - const roundUp = nextDigit !== undefined && nextDigit >= '5' && nextDigit <= '9' - - const scale = 10n ** BigInt(maxFrac) - let scaled = BigInt(intPartStr) * scale + BigInt(fracPadded || '0') - - if (roundUp) { - scaled += 1n - } - - const intOut = scaled / scale - let fracOut = (scaled % scale).toString().padStart(maxFrac, '0') - fracOut = fracOut.replace(/0+$/, '') - - return fracOut.length > 0 ? `${intOut}.${fracOut}` : intOut.toString() -} diff --git a/typescript/swap-vm/src/swap-vm/instructions/index.ts b/typescript/swap-vm/src/swap-vm/instructions/index.ts index 3360fd3..d36f8da 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/index.ts @@ -20,6 +20,7 @@ import type { IArgsData } from './types' import * as peggedSwap from './pegged-swap' export * from './types' +export { bigintSqrt, truncateHumanDecimalString } from './utils' export { EMPTY_OPCODE } from './empty' export * as balances from './balances' export * as controls from './controls' diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts index 83bc09c..04a30a9 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts @@ -3,3 +3,10 @@ export * from './opcodes' export { PeggedSwapArgs } from './pegged-swap-args' export type { PeggedTokenInfo } from './types' +export { PeggedPrice } from './price' +export type { + PeggedPricePair, + PeggedReservesInput, + PeggedTokenRef, + PeggedTokenReserve, +} from './price' diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts index 4667899..58247cc 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts @@ -51,8 +51,8 @@ export class PeggedSwapArgs implements IArgsData { tokenB: PeggedTokenInfo, linearWidth: bigint, ): PeggedSwapArgs { - const tokenARate = resolveRate(tokenA.decimals, tokenB.decimals) - const tokenBRate = resolveRate(tokenB.decimals, tokenA.decimals) + const tokenARate = resolveRate(BigInt(tokenA.decimals), BigInt(tokenB.decimals)) + const tokenBRate = resolveRate(BigInt(tokenB.decimals), BigInt(tokenA.decimals)) if (BigInt(tokenA.address.toString()) < BigInt(tokenB.address.toString())) { return new PeggedSwapArgs( diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts index 881568d..96bfb70 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts @@ -5,20 +5,20 @@ import { resolveRate } from './rate-resolver' describe('resolveRate', () => { it('should return 1 when decimals are equal', () => { - expect(resolveRate(18, 18)).toBe(1n) - expect(resolveRate(6, 6)).toBe(1n) - expect(resolveRate(0, 0)).toBe(1n) + expect(resolveRate(18n, 18n)).toBe(1n) + expect(resolveRate(6n, 6n)).toBe(1n) + expect(resolveRate(0n, 0n)).toBe(1n) }) it('should return 1 when tokenA has more decimals than tokenB', () => { - expect(resolveRate(18, 6)).toBe(1n) - expect(resolveRate(18, 0)).toBe(1n) - expect(resolveRate(12, 6)).toBe(1n) + expect(resolveRate(18n, 6n)).toBe(1n) + expect(resolveRate(18n, 0n)).toBe(1n) + expect(resolveRate(12n, 6n)).toBe(1n) }) it('should return 10^(B-A) when tokenA has fewer decimals than tokenB', () => { - expect(resolveRate(6, 18)).toBe(10n ** 12n) - expect(resolveRate(0, 18)).toBe(10n ** 18n) - expect(resolveRate(6, 12)).toBe(10n ** 6n) + expect(resolveRate(6n, 18n)).toBe(10n ** 12n) + expect(resolveRate(0n, 18n)).toBe(10n ** 18n) + expect(resolveRate(6n, 12n)).toBe(10n ** 6n) }) }) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts index b0b9460..0af2e0a 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 -export function resolveRate(tokenADecimals: number, tokenBDecimals: number): bigint { +export function resolveRate(tokenADecimals: bigint, tokenBDecimals: bigint): bigint { if (tokenADecimals === tokenBDecimals) { return 1n } @@ -9,5 +9,5 @@ export function resolveRate(tokenADecimals: number, tokenBDecimals: number): big return 1n } - return 10n ** BigInt(tokenBDecimals - tokenADecimals) + return 10n ** (tokenBDecimals - tokenADecimals) } From e5408947d619b34acfd9d47bf3794543ecb4520d Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 12:18:53 +0400 Subject: [PATCH 2/9] fix: not commited files --- .../pegged-swap-math/pegged-swap-math.test.ts | 60 +++++ .../pegged-swap-math/pegged-swap-math.ts | 58 +++++ .../instructions/pegged-swap/price/index.ts | 9 + .../pegged-swap/price/pegged-price.test.ts | 233 ++++++++++++++++++ .../pegged-swap/price/pegged-price.ts | 141 +++++++++++ .../instructions/pegged-swap/price/types.ts | 24 ++ .../instructions/utils/bigint-sqrt.test.ts | 27 ++ .../swap-vm/instructions/utils/bigint-sqrt.ts | 24 ++ .../src/swap-vm/instructions/utils/index.ts | 4 + .../truncate-human-decimal-string.test.ts | 45 ++++ .../utils/truncate-human-decimal-string.ts | 52 ++++ 11 files changed, 677 insertions(+) create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.test.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.test.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/utils/index.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.test.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.ts diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.test.ts new file mode 100644 index 0000000..ae225ac --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.test.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import { describe, expect, it } from 'vitest' +import { + normalizeReserve, + peggedSwapMarginalGtPerLtE18, + peggedSwapMarginalWeight, + PEGGED_SWAP_ONE, +} from './pegged-swap-math' +import { bigintSqrt } from '../../utils' + +const LINEAR_WIDTH = 8n * 10n ** 26n + +describe('peggedSwapMath', () => { + it('normalizeReserve is ONE when current equals initial', () => { + const initial = 10n ** 18n + expect(normalizeReserve(initial, initial)).toBe(PEGGED_SWAP_ONE) + }) + + it('marginal weight at u = ONE', () => { + const sqrtCoord = bigintSqrt(PEGGED_SWAP_ONE * PEGGED_SWAP_ONE) + const weight = peggedSwapMarginalWeight(sqrtCoord, LINEAR_WIDTH) + expect(weight).toBe((PEGGED_SWAP_ONE * PEGGED_SWAP_ONE) / (2n * sqrtCoord) + LINEAR_WIDTH) + }) + + it('marginal price at center equals y0·rateLt/(x0·rateGt) in 1e18', () => { + const x0 = 10n ** 18n + const y0 = 2n * 10n ** 18n + const rateLt = 1n + const rateGt = 1n + const marginal = peggedSwapMarginalGtPerLtE18(x0, y0, x0, y0, LINEAR_WIDTH, rateLt, rateGt) + expect(marginal).toBe((y0 * rateLt * 10n ** 18n) / (x0 * rateGt)) + }) + + it('marginal price at center with lt 6 / gt 18 decimals', () => { + const x0 = 10n ** 18n + const y0 = 2n * 10n ** 18n + const rateLt = 10n ** 12n + const rateGt = 1n + const marginal = peggedSwapMarginalGtPerLtE18(x0, y0, x0, y0, LINEAR_WIDTH, rateLt, rateGt) + expect(marginal).toBe(2n * 10n ** 30n) + }) + + it('marginal price differs when current reserves differ from initial', () => { + const x0 = 10n ** 18n + const y0 = 10n ** 18n + const atCenter = peggedSwapMarginalGtPerLtE18(x0, y0, x0, y0, LINEAR_WIDTH, 1n, 1n) + const offCenter = peggedSwapMarginalGtPerLtE18( + 993_000_000_000_000_000n, + 999_360_128_962_949_073n, + x0, + y0, + LINEAR_WIDTH, + 1n, + 1n, + ) + expect(offCenter).not.toBe(atCenter) + expect(offCenter).toBeGreaterThan(atCenter) + }) +}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts new file mode 100644 index 0000000..6e09e92 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import assert from 'assert' +import { bigintSqrt } from '../../utils' + +/** Matches `PeggedSwapMath.ONE` in swap-vm. */ +export const PEGGED_SWAP_ONE = 10n ** 27n + +const MARGINAL_PRICE_ONE = 10n ** 18n + +/** + * Spot price tokenGt per tokenLt (raw) in 1e18 fixed-point. + * + * P = (Y₀/X₀) · (1/(2√u) + A) / (1/(2√v) + A) · (rateLt/rateGt) + * + * where u = x·ONE/X₀, v = y·ONE/Y₀, x/y are rate-adjusted Lt/Gt balances, A = `linearWidth`. + */ +export function peggedSwapMarginalGtPerLtE18( + balanceLtNorm: bigint, + balanceGtNorm: bigint, + x0: bigint, + y0: bigint, + linearWidth: bigint, + rateLt: bigint, + rateGt: bigint, +): bigint { + const u = normalizeReserve(balanceLtNorm, x0) + const v = normalizeReserve(balanceGtNorm, y0) + + assert(u !== 0n && v !== 0n, 'PeggedSwapMath: reserves cannot be zero') + + const slopeLt = peggedSwapMarginalWeight(bigintSqrt(u * PEGGED_SWAP_ONE), linearWidth) + const slopeGt = peggedSwapMarginalWeight(bigintSqrt(v * PEGGED_SWAP_ONE), linearWidth) + + return (y0 * slopeLt * rateLt * MARGINAL_PRICE_ONE) / (x0 * slopeGt * rateGt) +} + +/** + * u = x·ONE/X₀, v = y·ONE/Y₀ (x, y are rate-adjusted reserves). + */ +export function normalizeReserve(currentReserve: bigint, initialReserve: bigint): bigint { + return mulDiv(currentReserve, PEGGED_SWAP_ONE, initialReserve) +} + +/** + * Marginal weight `1/(2√u) + A` (Lt side) or `1/(2√v) + A` (Gt side), A = `linearWidth`. + */ +export function peggedSwapMarginalWeight(sqrtCoord: bigint, linearWidth: bigint): bigint { + return mulDiv(PEGGED_SWAP_ONE, PEGGED_SWAP_ONE, 2n * sqrtCoord) + linearWidth +} + +function mulDiv(a: bigint, b: bigint, c: bigint): bigint { + if (c === 0n) { + throw new Error('mulDiv: division by zero') + } + + return (a * b) / c +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts new file mode 100644 index 0000000..58aa209 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +export { PeggedPrice } from './pegged-price' +export type { + PeggedPricePair, + PeggedReservesInput, + PeggedTokenRef, + PeggedTokenReserve, +} from './types' diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts new file mode 100644 index 0000000..4f3b434 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import { describe, expect, it } from 'vitest' +import { Address } from '@1inch/sdk-core' +import { PeggedPrice } from './pegged-price' +import type { PeggedPricePair, PeggedTokenReserve } from './types' + +const TOKEN_A = new Address('0x0000000000000000000000000000000000000001') +const TOKEN_B = new Address('0x0000000000000000000000000000000000000002') +const LINEAR_WIDTH = 8n * 10n ** 26n + +const reserveA: PeggedTokenReserve = { + address: TOKEN_A, + decimals: 18n, + initialReserve: 1000n * 10n ** 18n, + currentReserve: 1000n * 10n ** 18n, +} + +const reserveB: PeggedTokenReserve = { + address: TOKEN_B, + decimals: 18n, + initialReserve: 999n * 10n ** 18n - 1n, + currentReserve: 999n * 10n ** 18n - 1n, +} + +const pairGtQuoteLtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_B, decimals: 18n }, + baseToken: { address: TOKEN_A, decimals: 18n }, +} + +const pairLtQuoteGtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_A, decimals: 18n }, + baseToken: { address: TOKEN_B, decimals: 18n }, +} + +describe('PeggedPrice', () => { + it('fromReserves derives lt/gt from addresses', () => { + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA, + reserveB, + }) + expect(price.toHuman(TOKEN_B)).toBe('0.998999999999999999') + }) + + it('fromReserves accepts tokens in either order', () => { + const forward = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA, + reserveB, + }) + const reversed = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveB, + reserveB: reserveA, + }) + expect(reversed.equals(forward)).toBe(true) + }) + + it('fromHuman round-trips gt quote', () => { + const price = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) + expect(price.toHuman(TOKEN_B)).toBe('1.5') + }) + + it('fromHuman round-trips lt quote', () => { + const price = PeggedPrice.fromHuman('0.5', pairLtQuoteGtBase) + expect(price.toHuman(TOKEN_A)).toBe('0.5') + }) + + describe('mixed decimals (lt 6, gt 18)', () => { + const pairGtQuoteLtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_B, decimals: 18n }, + baseToken: { address: TOKEN_A, decimals: 6n }, + } + + const pairLtQuoteGtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_A, decimals: 6n }, + baseToken: { address: TOKEN_B, decimals: 18n }, + } + + const reserveLt = { + address: TOKEN_A, + decimals: 6n, + initialReserve: 1000n * 10n ** 6n, + currentReserve: 1000n * 10n ** 6n, + } + + const reserveGt = { + address: TOKEN_B, + decimals: 18n, + initialReserve: 999n * 10n ** 18n - 1n, + currentReserve: 999n * 10n ** 18n - 1n, + } + + it('fromReserves at center is 2 gt per 1 lt', () => { + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveLt, + reserveB: reserveGt, + }) + expect(price.toHuman(TOKEN_B)).toBe('0.998999999999999999') + }) + + it('fromHuman round-trips gt quote', () => { + const price = PeggedPrice.fromHuman('2', pairGtQuoteLtBase) + expect(price.toHuman(TOKEN_B)).toBe('2') + }) + + it('fromHuman round-trips lt quote', () => { + const price = PeggedPrice.fromHuman('0.5', pairLtQuoteGtBase) + expect(price.toHuman(TOKEN_A)).toBe('0.5') + }) + }) + + describe('mixed decimals (lt 18, gt 6)', () => { + const pairGtQuoteLtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_B, decimals: 6n }, + baseToken: { address: TOKEN_A, decimals: 18n }, + } + + const pairLtQuoteGtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_A, decimals: 18n }, + baseToken: { address: TOKEN_B, decimals: 6n }, + } + + const reserveLt = { + address: TOKEN_A, + decimals: 18n, + initialReserve: 1000n * 10n ** 18n, + currentReserve: 1000n * 10n ** 18n, + } + + const reserveGt = { + address: TOKEN_B, + decimals: 6n, + initialReserve: 999n * 10n ** 6n - 1n, + currentReserve: 999n * 10n ** 6n - 1n, + } + + it('fromReserves at center is 2 gt per 1 lt', () => { + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveLt, + reserveB: reserveGt, + }) + expect(price.toHuman(TOKEN_B)).toBe('0.998999') + }) + + it('fromHuman round-trips gt quote', () => { + const price = PeggedPrice.fromHuman('2', pairGtQuoteLtBase) + expect(price.toHuman(TOKEN_B)).toBe('2') + }) + + it('fromHuman round-trips lt quote', () => { + const price = PeggedPrice.fromHuman('0.5', pairLtQuoteGtBase) + expect(price.toHuman(TOKEN_A)).toBe('0.5') + }) + }) + + describe('off-center reserves (current ≠ initial)', () => { + it('equal 18/18 decimals', () => { + const initial = 10n ** 18n + const reserveAOff = { + address: TOKEN_A, + decimals: 18n, + initialReserve: initial, + currentReserve: 993_000_000_000_000_000n, + } + const reserveBOff = { + address: TOKEN_B, + decimals: 18n, + initialReserve: initial, + currentReserve: 999_360_128_962_949_073n, + } + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveAOff, + reserveB: reserveBOff, + }) + expect(price.toHuman(TOKEN_B)).toBe('1.001229999999999999') + }) + + it('lt 6 / gt 18 decimals', () => { + const reserveAOff = { + address: TOKEN_A, + decimals: 6n, + initialReserve: 1_000_000n, + currentReserve: 993_000n, + } + const reserveBOff = { + address: TOKEN_B, + decimals: 18n, + initialReserve: 10n ** 18n, + currentReserve: 999_360_128_962_949_073n, + } + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveAOff, + reserveB: reserveBOff, + }) + expect(price.toHuman(TOKEN_B)).toBe('1.001229999999999999') + }) + + it('lt 18 / gt 6 decimals', () => { + const reserveAOff = { + address: TOKEN_A, + decimals: 18n, + initialReserve: 10n ** 18n, + currentReserve: 993_000_000_000_000_000n, + } + const reserveBOff = { + address: TOKEN_B, + decimals: 6n, + initialReserve: 1_000_000n, + currentReserve: 999_360n, + } + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: reserveAOff, + reserveB: reserveBOff, + }) + expect(price.toHuman(TOKEN_B)).toBe('1.001229') + }) + + it('fromHuman round-trips gt quote (18/18)', () => { + const price = PeggedPrice.fromHuman('1.00123', pairGtQuoteLtBase) + expect(price.toHuman(TOKEN_B)).toBe('1.00123') + }) + }) +}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts new file mode 100644 index 0000000..c6bffc4 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import type { Address } from '@1inch/sdk-core' +import { formatUnits, parseUnits } from 'viem' +import assert from 'assert' +import type { PeggedPricePair, PeggedReservesInput, PeggedTokenRef } from './types' +import { peggedSwapMarginalGtPerLtE18 } from '../pegged-swap-math/pegged-swap-math' +import { truncateHumanDecimalString } from '../../utils' +import { resolveRate } from '../rate-resolver' + +const ONE_E18 = 10n ** 18n + +export class PeggedPrice { + private constructor( + private readonly gtPerLtRaw: bigint, + private readonly tokenLt: PeggedTokenRef, + private readonly tokenGt: PeggedTokenRef, + ) { + assert(gtPerLtRaw > 0n, 'price must be positive') + assert(tokenLt.address.lt(tokenGt.address), 'internal pair order violated') + } + + /** + * Spot price from per-token `initialReserve` / `currentReserve` (raw, not rate-scaled) and `linearWidth`. + * Use currentReserve = initialReserve to calculate the spot price before the strategy was deployed + */ + static fromReserves(input: PeggedReservesInput): PeggedPrice { + assert( + input.reserveA.currentReserve > 0n && input.reserveB.currentReserve > 0n, + 'current reserves should be positive', + ) + + assert( + input.reserveA.initialReserve > 0n && input.reserveB.initialReserve > 0n, + 'initial reserves should be positive', + ) + + const zeroForOne = input.reserveA.address.lt(input.reserveB.address) + const reserveLt = zeroForOne ? input.reserveA : input.reserveB + const reserveGt = zeroForOne ? input.reserveB : input.reserveA + + const rateLt = resolveRate(reserveLt.decimals, reserveGt.decimals) + const rateGt = resolveRate(reserveGt.decimals, reserveLt.decimals) + + const initialLtNorm = reserveLt.initialReserve * rateLt + const initialGtNorm = reserveGt.initialReserve * rateGt + + const marginalE18 = peggedSwapMarginalGtPerLtE18( + reserveLt.currentReserve * rateLt, + reserveGt.currentReserve * rateGt, + initialLtNorm, + initialGtNorm, + input.linearWidth, + rateLt, + rateGt, + ) + + return PeggedPrice.fromGtPerLtE18(marginalE18, reserveLt, reserveGt) + } + + /** + * Human decimal string for **quote per 1 base**. + */ + static fromHuman(price: string, pair: PeggedPricePair): PeggedPrice { + assert( + !pair.quoteToken.address.equal(pair.baseToken.address), + 'quote and base must be different tokens', + ) + + const quoteToBase = pair.quoteToken.address.lt(pair.baseToken.address) + + const tokenLt = quoteToBase ? pair.quoteToken : pair.baseToken + const tokenGt = quoteToBase ? pair.baseToken : pair.quoteToken + + const parsed = parseUnits(price.trim(), Number(pair.quoteToken.decimals)) + + const ltDecimals = tokenLt.decimals + const gtDecimals = tokenGt.decimals + + const marginalE18 = quoteToBase + ? 10n ** (gtDecimals + 18n + ltDecimals) / (parsed * 10n ** gtDecimals) + : (parsed * ONE_E18) / 10n ** ltDecimals + + return PeggedPrice.fromGtPerLtE18(marginalE18, tokenLt, tokenGt) + } + + private static fromGtPerLtE18( + marginalGtPerLtE18: bigint, + tokenLt: PeggedTokenRef, + tokenGt: PeggedTokenRef, + ): PeggedPrice { + assert(marginalGtPerLtE18 > 0n, 'marginal rate must be positive') + + const scale = tokenLt.decimals + tokenGt.decimals + const gtPerLtRaw = (marginalGtPerLtE18 * 10n ** scale) / ONE_E18 + + return new PeggedPrice(gtPerLtRaw, tokenLt, tokenGt) + } + + equals(other: PeggedPrice): boolean { + return ( + this.gtPerLtRaw === other.gtPerLtRaw && + this.tokenLt.address.equal(other.tokenLt.address) && + this.tokenGt.address.equal(other.tokenGt.address) && + this.tokenLt.decimals === other.tokenLt.decimals && + this.tokenGt.decimals === other.tokenGt.decimals + ) + } + + /** + * Decimal string for **quote per 1 base**; rounded half-up to quote token decimals. + */ + toHuman(quoteToken: Address): string { + assert( + quoteToken.equal(this.tokenLt.address) || quoteToken.equal(this.tokenGt.address), + 'quote token must be one of the pair tokens', + ) + + const isQuoteLt = quoteToken.equal(this.tokenLt.address) + + const quoteDecimals = isQuoteLt ? this.tokenLt.decimals : this.tokenGt.decimals + const ltDecimals = this.tokenLt.decimals + const gtDecimals = this.tokenGt.decimals + const marginalE18 = this.toGtPerLtE18() + + const scaled = quoteToken.equal(this.tokenGt.address) + ? (marginalE18 * 10n ** ltDecimals) / ONE_E18 + : 10n ** (gtDecimals + 18n + ltDecimals) / (marginalE18 * 10n ** gtDecimals) + + const full = formatUnits(scaled, Number(quoteDecimals)) + + return truncateHumanDecimalString(full, Number(quoteDecimals)) + } + + /** Marginal gt-per-lt rate in 1e18 fixed-point. */ + toGtPerLtE18(): bigint { + const scale = this.tokenLt.decimals + this.tokenGt.decimals + + return (this.gtPerLtRaw * ONE_E18) / 10n ** scale + } +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts new file mode 100644 index 0000000..534e03a --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import type { Address } from '@1inch/sdk-core' + +export type PeggedTokenRef = { + address: Address + decimals: bigint +} + +export type PeggedPricePair = { + quoteToken: PeggedTokenRef + baseToken: PeggedTokenRef +} + +export type PeggedTokenReserve = PeggedTokenRef & { + initialReserve: bigint + currentReserve: bigint +} + +export type PeggedReservesInput = { + reserveA: PeggedTokenReserve + reserveB: PeggedTokenReserve + linearWidth: bigint +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.test.ts b/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.test.ts new file mode 100644 index 0000000..22410aa --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.test.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import { UINT_256_MAX } from '@1inch/byte-utils' +import { describe, expect, it } from 'vitest' +import { bigintSqrt } from './bigint-sqrt' + +describe('bigintSqrt', () => { + it('correct for 0-1000', () => { + for (let i = 0; i <= 1000; i++) { + expect(bigintSqrt(BigInt(i))).toBe(BigInt(Math.floor(Math.sqrt(i)))) + } + }) + + describe('correct for all even powers of 2', () => { + for (let i = 0; i < 256; i++) { + it(`2^${i * 2}`, () => { + const root = 2n ** BigInt(i) + const rootSquared = root * root + expect(bigintSqrt(rootSquared)).toBe(root) + }) + } + }) + + it('correct for UINT_256_MAX', () => { + expect(bigintSqrt(UINT_256_MAX)).toBe(BigInt('340282366920938463463374607431768211455')) + }) +}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.ts b/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.ts new file mode 100644 index 0000000..f961a65 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/utils/bigint-sqrt.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +export function bigintSqrt(value: bigint): bigint { + if (value < 0n) { + throw new Error('square root of negative numbers is not supported') + } + + if (value < 2n) { + return value + } + + if (value <= 9007199254740991n) { + return BigInt(Math.floor(Math.sqrt(Number(value)))) + } + + let x0 = value + let x1 = (value / x0 + x0) >> 1n + while (x1 < x0) { + x0 = x1 + x1 = (value / x0 + x0) >> 1n + } + + return x0 +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/utils/index.ts b/typescript/swap-vm/src/swap-vm/instructions/utils/index.ts new file mode 100644 index 0000000..c6ed25f --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/utils/index.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +export { bigintSqrt } from './bigint-sqrt' +export { truncateHumanDecimalString } from './truncate-human-decimal-string' diff --git a/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.test.ts b/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.test.ts new file mode 100644 index 0000000..c4d3262 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.test.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import { describe, expect, it } from 'vitest' +import { truncateHumanDecimalString } from './truncate-human-decimal-string' + +describe('truncateHumanDecimalString', () => { + it('returns integer-only when maxFrac is 0 (half-up on first fractional digit)', () => { + expect(truncateHumanDecimalString('123.456', 0)).toBe('123') + expect(truncateHumanDecimalString('123.567', 0)).toBe('124') + expect(truncateHumanDecimalString('123', 0)).toBe('123') + }) + + it('returns unchanged when there is no decimal point', () => { + expect(truncateHumanDecimalString('2500', 6)).toBe('2500') + expect(truncateHumanDecimalString('0', 18)).toBe('0') + }) + + it('rounds half-up at maxFrac (first dropped digit >= 5 increments last kept digit)', () => { + expect(truncateHumanDecimalString('1.9999', 2)).toBe('2') + expect(truncateHumanDecimalString('1.994', 2)).toBe('1.99') + expect(truncateHumanDecimalString('2000.000000000000000000131782', 6)).toBe('2000') + }) + + it('strips trailing zeros after rounding', () => { + expect(truncateHumanDecimalString('2000.000000', 6)).toBe('2000') + expect(truncateHumanDecimalString('1.2300', 4)).toBe('1.23') + }) + + it('keeps non-zero fractional digits up to maxFrac (half-up)', () => { + expect(truncateHumanDecimalString('0.000499999999999999999999', 18)).toBe('0.0005') + expect(truncateHumanDecimalString('3.141592653589793', 4)).toBe('3.1416') + }) + + it('does not pad when fractional part is shorter than maxFrac', () => { + expect(truncateHumanDecimalString('1.5', 6)).toBe('1.5') + }) + + it('handles maxFrac larger than available fractional digits', () => { + expect(truncateHumanDecimalString('10.12', 10)).toBe('10.12') + }) + + it('handles empty fractional after strip (integer)', () => { + expect(truncateHumanDecimalString('42.000000', 6)).toBe('42') + }) +}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.ts b/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.ts new file mode 100644 index 0000000..6b2a35c --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/utils/truncate-human-decimal-string.ts @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +/** + * Round a decimal string to `maxFrac` fractional digits (half-up: if the first dropped digit + * is `5`–`9`, round the last kept digit up), then strip trailing zeros after the dot. + * + * @param s Decimal string as produced by e.g. `formatUnits` (no scientific notation). + * @param maxFrac Maximum number of digits after `.`; `0` means integer only (round using the + * first fractional digit). + */ +export function truncateHumanDecimalString(s: string, maxFrac: number): string { + if (maxFrac < 0) { + throw new Error('maxFrac must be non-negative') + } + + const dot = s.indexOf('.') + + if (dot === -1) { + return s + } + + const intPartStr = s.slice(0, dot) || '0' + const fracFull = s.slice(dot + 1) + + if (maxFrac === 0) { + let intPart = BigInt(intPartStr) + const first = fracFull[0] + + if (first !== undefined && first >= '5' && first <= '9') { + intPart += 1n + } + + return intPart.toString() + } + + const fracPadded = (fracFull + '0'.repeat(maxFrac)).slice(0, maxFrac) + const nextDigit = fracFull.length > maxFrac ? fracFull[maxFrac] : undefined + const roundUp = nextDigit !== undefined && nextDigit >= '5' && nextDigit <= '9' + + const scale = 10n ** BigInt(maxFrac) + let scaled = BigInt(intPartStr) * scale + BigInt(fracPadded || '0') + + if (roundUp) { + scaled += 1n + } + + const intOut = scaled / scale + let fracOut = (scaled % scale).toString().padStart(maxFrac, '0') + fracOut = fracOut.replace(/0+$/, '') + + return fracOut.length > 0 ? `${intOut}.${fracOut}` : intOut.toString() +} From d5977e0f8f8d1841d79ab5f4ae2fa47e4bac96a7 Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 15:24:12 +0400 Subject: [PATCH 3/9] chore: refactor --- .../pegged-swap/pegged-swap-args.ts | 4 ++-- .../pegged-swap/price/pegged-price.ts | 12 ++++++------ .../instructions/pegged-swap/price/types.ts | 2 +- .../pegged-swap/rate-resolver.test.ts | 18 +++++++++--------- .../instructions/pegged-swap/rate-resolver.ts | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts index 58247cc..4667899 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-args.ts @@ -51,8 +51,8 @@ export class PeggedSwapArgs implements IArgsData { tokenB: PeggedTokenInfo, linearWidth: bigint, ): PeggedSwapArgs { - const tokenARate = resolveRate(BigInt(tokenA.decimals), BigInt(tokenB.decimals)) - const tokenBRate = resolveRate(BigInt(tokenB.decimals), BigInt(tokenA.decimals)) + const tokenARate = resolveRate(tokenA.decimals, tokenB.decimals) + const tokenBRate = resolveRate(tokenB.decimals, tokenA.decimals) if (BigInt(tokenA.address.toString()) < BigInt(tokenB.address.toString())) { return new PeggedSwapArgs( diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts index c6bffc4..9df2afe 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts @@ -74,8 +74,8 @@ export class PeggedPrice { const parsed = parseUnits(price.trim(), Number(pair.quoteToken.decimals)) - const ltDecimals = tokenLt.decimals - const gtDecimals = tokenGt.decimals + const ltDecimals = BigInt(tokenLt.decimals) + const gtDecimals = BigInt(tokenGt.decimals) const marginalE18 = quoteToBase ? 10n ** (gtDecimals + 18n + ltDecimals) / (parsed * 10n ** gtDecimals) @@ -91,7 +91,7 @@ export class PeggedPrice { ): PeggedPrice { assert(marginalGtPerLtE18 > 0n, 'marginal rate must be positive') - const scale = tokenLt.decimals + tokenGt.decimals + const scale = BigInt(tokenLt.decimals + tokenGt.decimals) const gtPerLtRaw = (marginalGtPerLtE18 * 10n ** scale) / ONE_E18 return new PeggedPrice(gtPerLtRaw, tokenLt, tokenGt) @@ -119,8 +119,8 @@ export class PeggedPrice { const isQuoteLt = quoteToken.equal(this.tokenLt.address) const quoteDecimals = isQuoteLt ? this.tokenLt.decimals : this.tokenGt.decimals - const ltDecimals = this.tokenLt.decimals - const gtDecimals = this.tokenGt.decimals + const ltDecimals = BigInt(this.tokenLt.decimals) + const gtDecimals = BigInt(this.tokenGt.decimals) const marginalE18 = this.toGtPerLtE18() const scaled = quoteToken.equal(this.tokenGt.address) @@ -134,7 +134,7 @@ export class PeggedPrice { /** Marginal gt-per-lt rate in 1e18 fixed-point. */ toGtPerLtE18(): bigint { - const scale = this.tokenLt.decimals + this.tokenGt.decimals + const scale = BigInt(this.tokenLt.decimals + this.tokenGt.decimals) return (this.gtPerLtRaw * ONE_E18) / 10n ** scale } diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts index 534e03a..9f71c05 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts @@ -4,7 +4,7 @@ import type { Address } from '@1inch/sdk-core' export type PeggedTokenRef = { address: Address - decimals: bigint + decimals: number } export type PeggedPricePair = { diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts index 96bfb70..881568d 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.test.ts @@ -5,20 +5,20 @@ import { resolveRate } from './rate-resolver' describe('resolveRate', () => { it('should return 1 when decimals are equal', () => { - expect(resolveRate(18n, 18n)).toBe(1n) - expect(resolveRate(6n, 6n)).toBe(1n) - expect(resolveRate(0n, 0n)).toBe(1n) + expect(resolveRate(18, 18)).toBe(1n) + expect(resolveRate(6, 6)).toBe(1n) + expect(resolveRate(0, 0)).toBe(1n) }) it('should return 1 when tokenA has more decimals than tokenB', () => { - expect(resolveRate(18n, 6n)).toBe(1n) - expect(resolveRate(18n, 0n)).toBe(1n) - expect(resolveRate(12n, 6n)).toBe(1n) + expect(resolveRate(18, 6)).toBe(1n) + expect(resolveRate(18, 0)).toBe(1n) + expect(resolveRate(12, 6)).toBe(1n) }) it('should return 10^(B-A) when tokenA has fewer decimals than tokenB', () => { - expect(resolveRate(6n, 18n)).toBe(10n ** 12n) - expect(resolveRate(0n, 18n)).toBe(10n ** 18n) - expect(resolveRate(6n, 12n)).toBe(10n ** 6n) + expect(resolveRate(6, 18)).toBe(10n ** 12n) + expect(resolveRate(0, 18)).toBe(10n ** 18n) + expect(resolveRate(6, 12)).toBe(10n ** 6n) }) }) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts index 0af2e0a..b0b9460 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/rate-resolver.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 -export function resolveRate(tokenADecimals: bigint, tokenBDecimals: bigint): bigint { +export function resolveRate(tokenADecimals: number, tokenBDecimals: number): bigint { if (tokenADecimals === tokenBDecimals) { return 1n } @@ -9,5 +9,5 @@ export function resolveRate(tokenADecimals: bigint, tokenBDecimals: bigint): big return 1n } - return 10n ** (tokenBDecimals - tokenADecimals) + return 10n ** BigInt(tokenBDecimals - tokenADecimals) } From 5db3aacdf4b24f26fc5211ba65efbf451d6455f5 Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 15:32:02 +0400 Subject: [PATCH 4/9] chore: add fromJSON and toJSON --- .../swap-vm/instructions/pegged-swap/index.ts | 1 + .../instructions/pegged-swap/price/index.ts | 1 + .../pegged-swap/price/pegged-price.test.ts | 76 +++++++++++++------ .../pegged-swap/price/pegged-price.ts | 39 +++++++++- .../instructions/pegged-swap/price/types.ts | 7 ++ 5 files changed, 96 insertions(+), 28 deletions(-) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts index 04a30a9..9edfaa3 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts @@ -5,6 +5,7 @@ export { PeggedSwapArgs } from './pegged-swap-args' export type { PeggedTokenInfo } from './types' export { PeggedPrice } from './price' export type { + PeggedPriceJSON, PeggedPricePair, PeggedReservesInput, PeggedTokenRef, diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts index 58aa209..8f46e1c 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/index.ts @@ -2,6 +2,7 @@ export { PeggedPrice } from './pegged-price' export type { + PeggedPriceJSON, PeggedPricePair, PeggedReservesInput, PeggedTokenRef, diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts index 4f3b434..bf63462 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.test.ts @@ -11,26 +11,26 @@ const LINEAR_WIDTH = 8n * 10n ** 26n const reserveA: PeggedTokenReserve = { address: TOKEN_A, - decimals: 18n, + decimals: 18, initialReserve: 1000n * 10n ** 18n, currentReserve: 1000n * 10n ** 18n, } const reserveB: PeggedTokenReserve = { address: TOKEN_B, - decimals: 18n, + decimals: 18, initialReserve: 999n * 10n ** 18n - 1n, currentReserve: 999n * 10n ** 18n - 1n, } const pairGtQuoteLtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_B, decimals: 18n }, - baseToken: { address: TOKEN_A, decimals: 18n }, + quoteToken: { address: TOKEN_B, decimals: 18 }, + baseToken: { address: TOKEN_A, decimals: 18 }, } const pairLtQuoteGtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_A, decimals: 18n }, - baseToken: { address: TOKEN_B, decimals: 18n }, + quoteToken: { address: TOKEN_A, decimals: 18 }, + baseToken: { address: TOKEN_B, decimals: 18 }, } describe('PeggedPrice', () => { @@ -57,6 +57,34 @@ describe('PeggedPrice', () => { expect(reversed.equals(forward)).toBe(true) }) + it('toJSON should yield bigint-safe JSON', () => { + const price = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) + const json = price.toJSON() + + expect(json.gtPerLtRaw).toBe('1500000000000000000000000000000000000') + expect(json.tokenLt.decimals).toBe('18') + expect(json.tokenGt.decimals).toBe('18') + expect(JSON.stringify(price)).toBe(JSON.stringify(json)) + }) + + it('fromJSON should round-trip toJSON', () => { + const price = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) + + expect(PeggedPrice.fromJSON(price.toJSON()).equals(price)).toBe(true) + }) + + it('fromJSON should require canonical tokenLt < tokenGt order', () => { + const price = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) + const json = price.toJSON() + const bad = { + ...json, + tokenLt: json.tokenGt, + tokenGt: json.tokenLt, + } + + expect(() => PeggedPrice.fromJSON(bad)).toThrow('tokenLt address must be less than tokenGt') + }) + it('fromHuman round-trips gt quote', () => { const price = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) expect(price.toHuman(TOKEN_B)).toBe('1.5') @@ -69,25 +97,25 @@ describe('PeggedPrice', () => { describe('mixed decimals (lt 6, gt 18)', () => { const pairGtQuoteLtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_B, decimals: 18n }, - baseToken: { address: TOKEN_A, decimals: 6n }, + quoteToken: { address: TOKEN_B, decimals: 18 }, + baseToken: { address: TOKEN_A, decimals: 6 }, } const pairLtQuoteGtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_A, decimals: 6n }, - baseToken: { address: TOKEN_B, decimals: 18n }, + quoteToken: { address: TOKEN_A, decimals: 6 }, + baseToken: { address: TOKEN_B, decimals: 18 }, } const reserveLt = { address: TOKEN_A, - decimals: 6n, + decimals: 6, initialReserve: 1000n * 10n ** 6n, currentReserve: 1000n * 10n ** 6n, } const reserveGt = { address: TOKEN_B, - decimals: 18n, + decimals: 18, initialReserve: 999n * 10n ** 18n - 1n, currentReserve: 999n * 10n ** 18n - 1n, } @@ -114,25 +142,25 @@ describe('PeggedPrice', () => { describe('mixed decimals (lt 18, gt 6)', () => { const pairGtQuoteLtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_B, decimals: 6n }, - baseToken: { address: TOKEN_A, decimals: 18n }, + quoteToken: { address: TOKEN_B, decimals: 6 }, + baseToken: { address: TOKEN_A, decimals: 18 }, } const pairLtQuoteGtBase: PeggedPricePair = { - quoteToken: { address: TOKEN_A, decimals: 18n }, - baseToken: { address: TOKEN_B, decimals: 6n }, + quoteToken: { address: TOKEN_A, decimals: 18 }, + baseToken: { address: TOKEN_B, decimals: 6 }, } const reserveLt = { address: TOKEN_A, - decimals: 18n, + decimals: 18, initialReserve: 1000n * 10n ** 18n, currentReserve: 1000n * 10n ** 18n, } const reserveGt = { address: TOKEN_B, - decimals: 6n, + decimals: 6, initialReserve: 999n * 10n ** 6n - 1n, currentReserve: 999n * 10n ** 6n - 1n, } @@ -162,13 +190,13 @@ describe('PeggedPrice', () => { const initial = 10n ** 18n const reserveAOff = { address: TOKEN_A, - decimals: 18n, + decimals: 18, initialReserve: initial, currentReserve: 993_000_000_000_000_000n, } const reserveBOff = { address: TOKEN_B, - decimals: 18n, + decimals: 18, initialReserve: initial, currentReserve: 999_360_128_962_949_073n, } @@ -184,13 +212,13 @@ describe('PeggedPrice', () => { it('lt 6 / gt 18 decimals', () => { const reserveAOff = { address: TOKEN_A, - decimals: 6n, + decimals: 6, initialReserve: 1_000_000n, currentReserve: 993_000n, } const reserveBOff = { address: TOKEN_B, - decimals: 18n, + decimals: 18, initialReserve: 10n ** 18n, currentReserve: 999_360_128_962_949_073n, } @@ -206,13 +234,13 @@ describe('PeggedPrice', () => { it('lt 18 / gt 6 decimals', () => { const reserveAOff = { address: TOKEN_A, - decimals: 18n, + decimals: 18, initialReserve: 10n ** 18n, currentReserve: 993_000_000_000_000_000n, } const reserveBOff = { address: TOKEN_B, - decimals: 6n, + decimals: 6, initialReserve: 1_000_000n, currentReserve: 999_360n, } diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts index 9df2afe..febd2c3 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts @@ -1,9 +1,9 @@ // SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 -import type { Address } from '@1inch/sdk-core' +import { Address } from '@1inch/sdk-core' import { formatUnits, parseUnits } from 'viem' import assert from 'assert' -import type { PeggedPricePair, PeggedReservesInput, PeggedTokenRef } from './types' +import type { PeggedPriceJSON, PeggedPricePair, PeggedReservesInput, PeggedTokenRef } from './types' import { peggedSwapMarginalGtPerLtE18 } from '../pegged-swap-math/pegged-swap-math' import { truncateHumanDecimalString } from '../../utils' import { resolveRate } from '../rate-resolver' @@ -84,6 +84,23 @@ export class PeggedPrice { return PeggedPrice.fromGtPerLtE18(marginalE18, tokenLt, tokenGt) } + static fromJSON(input: PeggedPriceJSON): PeggedPrice { + const tokenLt: PeggedTokenRef = { + address: new Address(input.tokenLt.address), + decimals: Number(input.tokenLt.decimals), + } + const tokenGt: PeggedTokenRef = { + address: new Address(input.tokenGt.address), + decimals: Number(input.tokenGt.decimals), + } + assert( + tokenLt.address.lt(tokenGt.address), + 'tokenLt address must be less than tokenGt (canonical order)', + ) + + return new PeggedPrice(BigInt(input.gtPerLtRaw), tokenLt, tokenGt) + } + private static fromGtPerLtE18( marginalGtPerLtE18: bigint, tokenLt: PeggedTokenRef, @@ -102,8 +119,8 @@ export class PeggedPrice { this.gtPerLtRaw === other.gtPerLtRaw && this.tokenLt.address.equal(other.tokenLt.address) && this.tokenGt.address.equal(other.tokenGt.address) && - this.tokenLt.decimals === other.tokenLt.decimals && - this.tokenGt.decimals === other.tokenGt.decimals + BigInt(this.tokenLt.decimals) === BigInt(other.tokenLt.decimals) && + BigInt(this.tokenGt.decimals) === BigInt(other.tokenGt.decimals) ) } @@ -138,4 +155,18 @@ export class PeggedPrice { return (this.gtPerLtRaw * ONE_E18) / 10n ** scale } + + toJSON(): PeggedPriceJSON { + return { + gtPerLtRaw: this.gtPerLtRaw.toString(), + tokenLt: { + address: this.tokenLt.address.toString(), + decimals: String(this.tokenLt.decimals), + }, + tokenGt: { + address: this.tokenGt.address.toString(), + decimals: String(this.tokenGt.decimals), + }, + } + } } diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts index 9f71c05..f875491 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/types.ts @@ -22,3 +22,10 @@ export type PeggedReservesInput = { reserveB: PeggedTokenReserve linearWidth: bigint } + +/** Snapshot from `PeggedPrice.prototype.toJSON` (bigints as decimal strings). */ +export type PeggedPriceJSON = { + gtPerLtRaw: string + tokenLt: { address: string; decimals: string } + tokenGt: { address: string; decimals: string } +} From c22720b21c9202ebdf036a17a0dd27e976fd1096 Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 17:00:53 +0400 Subject: [PATCH 5/9] feat: add PeggedSwapCalculator --- .../swap-vm/instructions/pegged-swap/index.ts | 2 + .../PEGGED-SWAP-CALCULATOR.md | 146 ++++++++++++++++ .../pegged-swap-calculator/index.ts | 4 + .../pegged-swap-calculator.test.ts | 164 ++++++++++++++++++ .../pegged-swap-calculator.ts | 63 +++++++ .../pegged-swap-calculator/types.ts | 13 ++ .../pegged-swap/price/pegged-price.ts | 11 +- 7 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/PEGGED-SWAP-CALCULATOR.md create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/index.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.test.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.ts create mode 100644 typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/types.ts diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts index 9edfaa3..bc616dd 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/index.ts @@ -3,7 +3,9 @@ export * from './opcodes' export { PeggedSwapArgs } from './pegged-swap-args' export type { PeggedTokenInfo } from './types' +export { PeggedSwapCalculator } from './pegged-swap-calculator' export { PeggedPrice } from './price' +export type { PeggedInitialBalances, PeggedSwapCalculatorArgs } from './pegged-swap-calculator' export type { PeggedPriceJSON, PeggedPricePair, diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/PEGGED-SWAP-CALCULATOR.md b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/PEGGED-SWAP-CALCULATOR.md new file mode 100644 index 0000000..6f99d71 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/PEGGED-SWAP-CALCULATOR.md @@ -0,0 +1,146 @@ +# PeggedSwapCalculator + +Helper for **initial pool balances before deployment** of a PeggedSwap strategy (`peggedSwapGrowPriceRange2D`). It answers: “I know the target spot price and how much of one token I want to deposit — how much of the other token do I need so the pool starts on-peg?” + +This calculator only covers the **pre-launch** case where `currentReserve = initialReserve` (normalized coordinates `u = v = 1` on the curve). It does not model reserves after trading or off-center spot. + +## Overview + +- **Token ordering**: Lt = lower address, Gt = higher address (same as on-chain `PeggedSwapMath`). `tokenA` / `tokenB` order in the constructor does not matter; use `tokenLt` / `tokenGt` getters if needed. +- **Price convention**: Use `PeggedPrice` — human string is **quote per 1 base** (same as `Price` in concentrate). Internally the marginal rate is **tokenGt per tokenLt** in 1e18 fixed-point. +- **At deployment center**: Spot does not depend on `linearWidth`; with equal normalization, `reserveGt ≈ reserveLt × spot` (in raw units, via `toGtPerLtE18()`). `linearWidth` still goes into `PeggedSwapArgs` and affects swaps after launch. + +## On-chain curve (context) + +PeggedSwap invariant (see [PeggedSwap.sol](https://github.com/1inch/swap-vm/blob/main/src/instructions/PeggedSwap.sol)): + +``` +√(x/X₀) + √(y/Y₀) + A(x/X₀ + y/Y₀) = 1 + A +``` + +`X₀` / `Y₀` in args are **rate-scaled** initial balances: `x0 = initialLt × rateLt`, `y0 = initialGt × rateGt` (see `PeggedSwapArgs.fromTokens`). + +## Types + +### `PeggedSwapCalculatorArgs` + +| Field | Type | Description | +|-------|------|-------------| +| `tokenA` | `PeggedTokenRef` | First token (`address`, `decimals`). | +| `tokenB` | `PeggedTokenRef` | Second token. | + +### `PeggedInitialBalances` + +| Field | Type | Description | +|-------|------|-------------| +| `reserveLt` | `bigint` | Raw initial balance for Lt (lower address). | +| `reserveGt` | `bigint` | Raw initial balance for Gt (higher address). | + +## API + +Exported from `instructions.peggedSwap` on `@1inch/swap-vm-sdk`: + +```ts +import { instructions } from '@1inch/swap-vm-sdk' +import { parseUnits } from 'viem' + +const { PeggedSwapCalculator, PeggedPrice, PeggedSwapArgs } = instructions.peggedSwap +``` + +### Creating a calculator + +```ts +const USDC = { address: usdcAddress, decimals: 6 } +const USDT = { address: usdtAddress, decimals: 6 } + +const calculator = PeggedSwapCalculator.new({ tokenA: USDC, tokenB: USDT }) +// calculator.tokenLt → USDC (lower address) +// calculator.tokenGt → USDT +``` + +### `computeFixedAllocation(spotPrice, fixedReserveForToken, fixedReserve)` + +Given a target **`PeggedPrice`** and a **raw** amount for one token, returns both initial reserves in Lt/Gt order. + +**Use case**: “Deposit 1M USDC; how much USDT at 0.998 USDT/USDC before the strategy ships?” + +```ts +const spot = PeggedPrice.fromHuman('0.998', { + quoteToken: { address: usdtAddress, decimals: 6 }, + baseToken: { address: usdcAddress, decimals: 6 }, +}) + +const fixedUsdc = parseUnits('1000000', 6) +const balances = calculator.computeFixedAllocation(spot, usdcAddress, fixedUsdc) + +// balances.reserveLt — USDC amount (raw) +// balances.reserveGt — USDT amount (raw) +``` + +Fix the other side instead: + +```ts +const balances = calculator.computeFixedAllocation(spot, usdtAddress, parseUnits('998000', 6)) +``` + +### Building `PeggedSwapArgs` + +Pass the computed raw reserves into `PeggedSwapArgs.fromTokens` (rates and Lt/Gt ordering are applied inside): + +```ts +const linearWidth = 8n * 10n ** 26n // A = 0.8 + +const args = PeggedSwapArgs.fromTokens( + { address: usdcAddress, decimals: 6, reserve: balances.reserveLt }, + { address: usdtAddress, decimals: 6, reserve: balances.reserveGt }, + linearWidth, +) +``` + +Use `args` with `peggedSwapGrowPriceRange2D` in your program builder (e.g. `AquaPeggedAmmStrategy`). + +## Spot price helpers (`PeggedPrice`) + +The calculator does not compute spot from reserves; use **`PeggedPrice`**: + +| Method | Purpose | +|--------|---------| +| `PeggedPrice.fromHuman(price, pair)` | Parse target quote-per-base (e.g. `'0.998'`, `'1.002'`). | +| `PeggedPrice.fromReserves({ reserveA, reserveB, linearWidth })` | Marginal spot from initial/current reserves; set `currentReserve = initialReserve` before launch. | +| `price.toHuman(quoteToken)` | Format for display. | + +**Verify** allocation (optional): + +```ts +const check = PeggedPrice.fromReserves({ + linearWidth, + reserveA: { ...usdc, initialReserve: balances.reserveLt, currentReserve: balances.reserveLt }, + reserveB: { ...usdt, initialReserve: balances.reserveGt, currentReserve: balances.reserveGt }, +}) + +expect(check.toHuman(usdtAddress)).toBe('0.998') +``` + +## Examples + +### USDC / USDT @ 0.998 + +Lt = USDC, Gt = USDT. Quote = USDT, base = USDC → 0.998 USDT per 1 USDC. + +1M USDC fixed → 998_000 USDT (6 decimals each). + +### DAI / USDC @ 1.002 + +Lt = DAI (18 dec), Gt = USDC (6 dec). Quote = USDC, base = DAI → 1.002 USDC per 1 DAI. + +1M DAI fixed → 1_002_000 USDC (raw `6` decimals). + +## Relation to SwapVM + +| Piece | Role | +|-------|------| +| `PeggedSwapCalculator` | Initial Lt/Gt raw balances from spot + one fixed leg. | +| `PeggedSwapArgs` | Encoded `x0`, `y0`, `linearWidth`, `rateLt`, `rateGt` for the instruction. | +| `PeggedPrice` | Human and reserve-based marginal price (gt per lt). | + +After deployment, spot moves with reserves; use `PeggedPrice.fromReserves` with **current** balances, not this calculator. diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/index.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/index.ts new file mode 100644 index 0000000..080d331 --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/index.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +export { PeggedSwapCalculator } from './pegged-swap-calculator' +export type { PeggedInitialBalances, PeggedSwapCalculatorArgs } from './types' diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.test.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.test.ts new file mode 100644 index 0000000..fe6d3df --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.test.ts @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import { describe, expect, it } from 'vitest' +import { Address } from '@1inch/sdk-core' +import { PeggedSwapCalculator } from './pegged-swap-calculator' +import { PeggedPrice } from '../price/pegged-price' +import type { PeggedPricePair } from '../price/types' + +const TOKEN_A = new Address('0x0000000000000000000000000000000000000001') +const TOKEN_B = new Address('0x0000000000000000000000000000000000000002') +const LINEAR_WIDTH = 8n * 10n ** 26n + +const USDC = new Address('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48') +const USDT = new Address('0xdAC17F958D2ee523a2206206994597C13D831ec7') +const DAI = new Address('0x6B175474E89094C44Da98b954EedeAC495271d0F') + +const pairGtQuoteLtBase: PeggedPricePair = { + quoteToken: { address: TOKEN_B, decimals: 18 }, + baseToken: { address: TOKEN_A, decimals: 18 }, +} + +describe('PeggedSwapCalculator', () => { + const calculator = PeggedSwapCalculator.new({ + tokenA: { address: TOKEN_A, decimals: 18 }, + tokenB: { address: TOKEN_B, decimals: 18 }, + }) + + it('computeFixedAllocation derives initial Gt from fixed Lt', () => { + const spot = PeggedPrice.fromHuman('1.5', pairGtQuoteLtBase) + const fixedLt = 10n ** 18n + + const balances = calculator.computeFixedAllocation(spot, TOKEN_A, fixedLt) + + expect(balances.reserveLt).toBe(fixedLt) + expect(balances.reserveGt).toBe(15n * 10n ** 17n) + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: { + address: TOKEN_A, + decimals: 18, + initialReserve: balances.reserveLt, + currentReserve: balances.reserveLt, + }, + reserveB: { + address: TOKEN_B, + decimals: 18, + initialReserve: balances.reserveGt, + currentReserve: balances.reserveGt, + }, + }) + + expect(price.toHuman(TOKEN_B)).toBe('1.5') + }) + + it('computeFixedAllocation derives initial Lt from fixed Gt (6 / 18 decimals)', () => { + const calc = PeggedSwapCalculator.new({ + tokenA: { address: TOKEN_A, decimals: 6 }, + tokenB: { address: TOKEN_B, decimals: 18 }, + }) + + const spot = PeggedPrice.fromHuman('2', { + quoteToken: { address: TOKEN_B, decimals: 18 }, + baseToken: { address: TOKEN_A, decimals: 6 }, + }) + + const balances = calc.computeFixedAllocation(spot, TOKEN_B, 2n * 10n ** 18n) + + expect(balances.reserveGt).toBe(2n * 10n ** 18n) + expect(balances.reserveLt).toBe(1n * 10n ** 6n) + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: { + address: TOKEN_A, + decimals: 6, + initialReserve: balances.reserveLt, + currentReserve: balances.reserveLt, + }, + reserveB: { + address: TOKEN_B, + decimals: 18, + initialReserve: balances.reserveGt, + currentReserve: balances.reserveGt, + }, + }) + + expect(price.toHuman(TOKEN_B)).toBe('2') + }) + + describe('stablecoin pairs', () => { + it('USDC/USDT: 0.998 USDT per 1 USDC with 1M USDC fixed', () => { + const calculator = PeggedSwapCalculator.new({ + tokenA: { address: USDC, decimals: 6 }, + tokenB: { address: USDT, decimals: 6 }, + }) + + const spot = PeggedPrice.fromHuman('0.998', { + quoteToken: { address: USDT, decimals: 6 }, + baseToken: { address: USDC, decimals: 6 }, + }) + + const fixedUsdc = 1_000_000n * 10n ** 6n + const balances = calculator.computeFixedAllocation(spot, USDC, fixedUsdc) + + expect(balances.reserveLt).toBe(fixedUsdc) + expect(balances.reserveGt).toBe(998_000n * 10n ** 6n) + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: { + address: USDC, + decimals: 6, + initialReserve: balances.reserveLt, + currentReserve: balances.reserveLt, + }, + reserveB: { + address: USDT, + decimals: 6, + initialReserve: balances.reserveGt, + currentReserve: balances.reserveGt, + }, + }) + + expect(price.toHuman(USDT)).toBe('0.998') + }) + + it('DAI/USDC: 1.002 USDC per 1 DAI with 1M DAI fixed', () => { + const calculator = PeggedSwapCalculator.new({ + tokenA: { address: DAI, decimals: 18 }, + tokenB: { address: USDC, decimals: 6 }, + }) + + const spot = PeggedPrice.fromHuman('1.002', { + quoteToken: { address: USDC, decimals: 6 }, + baseToken: { address: DAI, decimals: 18 }, + }) + + const fixedDai = 1_000_000n * 10n ** 18n + const balances = calculator.computeFixedAllocation(spot, DAI, fixedDai) + + expect(balances.reserveLt).toBe(fixedDai) + expect(balances.reserveGt).toBe(1_002_000n * 10n ** 6n) + + const price = PeggedPrice.fromReserves({ + linearWidth: LINEAR_WIDTH, + reserveA: { + address: DAI, + decimals: 18, + initialReserve: balances.reserveLt, + currentReserve: balances.reserveLt, + }, + reserveB: { + address: USDC, + decimals: 6, + initialReserve: balances.reserveGt, + currentReserve: balances.reserveGt, + }, + }) + + expect(price.toHuman(USDC)).toBe('1.002') + }) + }) +}) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.ts new file mode 100644 index 0000000..1cd72db --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/pegged-swap-calculator.ts @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import type { Address } from '@1inch/sdk-core' +import assert from 'assert' +import type { PeggedInitialBalances, PeggedSwapCalculatorArgs } from './types' +import type { PeggedPrice, PeggedTokenRef } from '../price' + +const MARGINAL_PRICE_ONE = 10n ** 18n + +export class PeggedSwapCalculator { + constructor( + private readonly tokenA: PeggedTokenRef, + private readonly tokenB: PeggedTokenRef, + ) { + assert(!tokenA.address.equal(tokenB.address), 'tokens must be different') + } + + get tokenLt(): PeggedTokenRef { + return this.tokenA.address.lt(this.tokenB.address) ? this.tokenA : this.tokenB + } + + get tokenGt(): PeggedTokenRef { + return this.tokenA.address.lt(this.tokenB.address) ? this.tokenB : this.tokenA + } + + static new(args: PeggedSwapCalculatorArgs): PeggedSwapCalculator { + return new PeggedSwapCalculator(args.tokenA, args.tokenB) + } + + /** + * Initial balances before deployment (`currentReserve = initialReserve`, u = v = 1). + * Given spot price and one raw initial reserve, returns the other. + */ + computeFixedAllocation( + spotPrice: PeggedPrice, + fixedReserveForToken: Address, + fixedReserve: bigint, + ): PeggedInitialBalances { + assert(fixedReserve > 0n, 'fixed reserve must be positive') + assert( + spotPrice.matchesTokens(this.tokenLt.address, this.tokenGt.address), + 'spot price must match calculator token pair', + ) + + const marginalE18 = spotPrice.toGtPerLtE18() + + if (fixedReserveForToken.equal(this.tokenLt.address)) { + return { + reserveLt: fixedReserve, + reserveGt: (fixedReserve * marginalE18) / MARGINAL_PRICE_ONE, + } + } + + if (fixedReserveForToken.equal(this.tokenGt.address)) { + return { + reserveLt: (fixedReserve * MARGINAL_PRICE_ONE) / marginalE18, + reserveGt: fixedReserve, + } + } + + throw new Error('fixedReserveForToken token must be one of the two pair tokens') + } +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/types.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/types.ts new file mode 100644 index 0000000..5cb2e8c --- /dev/null +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-calculator/types.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Degensoft-SwapVM-1.1 + +import type { PeggedTokenRef } from '../price/types' + +export type PeggedSwapCalculatorArgs = { + tokenA: PeggedTokenRef + tokenB: PeggedTokenRef +} + +export type PeggedInitialBalances = { + reserveLt: bigint + reserveGt: bigint +} diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts index febd2c3..d78f92b 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/price/pegged-price.ts @@ -13,8 +13,8 @@ const ONE_E18 = 10n ** 18n export class PeggedPrice { private constructor( private readonly gtPerLtRaw: bigint, - private readonly tokenLt: PeggedTokenRef, - private readonly tokenGt: PeggedTokenRef, + public readonly tokenLt: PeggedTokenRef, + public readonly tokenGt: PeggedTokenRef, ) { assert(gtPerLtRaw > 0n, 'price must be positive') assert(tokenLt.address.lt(tokenGt.address), 'internal pair order violated') @@ -114,6 +114,13 @@ export class PeggedPrice { return new PeggedPrice(gtPerLtRaw, tokenLt, tokenGt) } + matchesTokens(tokenA: Address, tokenB: Address): boolean { + return ( + (tokenA.equal(this.tokenLt.address) && tokenB.equal(this.tokenGt.address)) || + (tokenA.equal(this.tokenGt.address) && tokenB.equal(this.tokenLt.address)) + ) + } + equals(other: PeggedPrice): boolean { return ( this.gtPerLtRaw === other.gtPerLtRaw && From 47c85bb31196b5689a4eb74a660d91dec9b72d6f Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 17:41:18 +0400 Subject: [PATCH 6/9] chore: use new classes in e2e tests --- typescript/swap-vm/tests/swap-vm.spec.ts | 32 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/typescript/swap-vm/tests/swap-vm.spec.ts b/typescript/swap-vm/tests/swap-vm.spec.ts index f954575..b8f0033 100644 --- a/typescript/swap-vm/tests/swap-vm.spec.ts +++ b/typescript/swap-vm/tests/swap-vm.spec.ts @@ -25,6 +25,7 @@ import { Opcode } from '../src/swap-vm/instructions/opcode.js' import type { PricePair } from '../src/swap-vm/instructions/concentrate' import { Price, PriceRange, TokenReserve } from '../src/swap-vm/instructions/concentrate' +import { PeggedPrice, PeggedSwapCalculator } from '../src/swap-vm/instructions/pegged-swap' describe('SwapVM', () => { let forkNode: ReadyEvmFork @@ -978,30 +979,47 @@ describe('SwapVM', () => { const USDC = new Address(ADDRESSES.USDC) const DAI = new Address(ADDRESSES.DAI) + const LINEAR_WIDTH = 8n * 10n ** 26n + + const peggedCalculator = PeggedSwapCalculator.new({ + tokenA: { address: DAI, decimals: 18 }, + tokenB: { address: USDC, decimals: 6 }, + }) + + const spot = PeggedPrice.fromHuman('1', { + quoteToken: { address: USDC, decimals: 6 }, + baseToken: { address: DAI, decimals: 18 }, + }) + + const initialBalances = peggedCalculator.computeFixedAllocation( + spot, + DAI, + parseUnits('100000', 18), + ) const amountsAndTokens = [ { - amount: parseUnits('100000', 6), + amount: initialBalances.reserveGt, token: USDC, }, { - amount: parseUnits('100000', 18), + amount: initialBalances.reserveLt, token: DAI, }, ] const program = AquaPeggedAmmStrategy.new({ tokenA: { - address: amountsAndTokens[0].token, + address: USDC, decimals: 6, - reserve: amountsAndTokens[0].amount, + reserve: initialBalances.reserveGt, }, tokenB: { - address: amountsAndTokens[1].token, + address: DAI, decimals: 18, - reserve: amountsAndTokens[1].amount, + reserve: initialBalances.reserveLt, }, - linearWidth: 8n * 10n ** 26n, + linearWidth: LINEAR_WIDTH, }) .withFeeTokenIn(1) .build() From 15d63e5908033ddb90cc3543c27bdef5313ff0d9 Mon Sep 17 00:00:00 2001 From: CI/CD Bot Date: Thu, 21 May 2026 13:48:56 +0000 Subject: [PATCH 7/9] chore(release): - project: swap-vm 0.1.2 --- typescript/swap-vm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/swap-vm/package.json b/typescript/swap-vm/package.json index 2ed473b..f5923ae 100644 --- a/typescript/swap-vm/package.json +++ b/typescript/swap-vm/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/swap-vm-sdk", - "version": "0.1.2-rc.7", + "version": "0.1.2", "description": "1inch Swap VM SDK", "author": "@1inch", "license": "LicenseRef-Degensoft-SwapVM-1.1", From a7d4d3350563b8ae7e4b11239464c395faefaae9 Mon Sep 17 00:00:00 2001 From: Kirill Kuznetcov Date: Thu, 21 May 2026 17:56:46 +0400 Subject: [PATCH 8/9] fix: --isolatedDeclarations --- .../pegged-swap/pegged-swap-math/pegged-swap-math.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts index 6e09e92..cff8a89 100644 --- a/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts +++ b/typescript/swap-vm/src/swap-vm/instructions/pegged-swap/pegged-swap-math/pegged-swap-math.ts @@ -4,7 +4,7 @@ import assert from 'assert' import { bigintSqrt } from '../../utils' /** Matches `PeggedSwapMath.ONE` in swap-vm. */ -export const PEGGED_SWAP_ONE = 10n ** 27n +export const PEGGED_SWAP_ONE: bigint = 10n ** 27n const MARGINAL_PRICE_ONE = 10n ** 18n From ee04bbcccbe291670e6f58e1f09bdfc9d917c783 Mon Sep 17 00:00:00 2001 From: CI/CD Bot Date: Thu, 21 May 2026 14:15:41 +0000 Subject: [PATCH 9/9] chore(release): - project: swap-vm 0.1.3 --- typescript/swap-vm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/swap-vm/package.json b/typescript/swap-vm/package.json index f5923ae..760ac92 100644 --- a/typescript/swap-vm/package.json +++ b/typescript/swap-vm/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/swap-vm-sdk", - "version": "0.1.2", + "version": "0.1.3", "description": "1inch Swap VM SDK", "author": "@1inch", "license": "LicenseRef-Degensoft-SwapVM-1.1",