diff --git a/typescript/swap-vm/package.json b/typescript/swap-vm/package.json index 37a941b..76393fc 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.4", + "version": "0.1.5", "description": "1inch Swap VM SDK", "author": "@1inch", "license": "LicenseRef-Degensoft-SwapVM-1.1", 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 2315f8e..26f5c29 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 @@ -4,7 +4,10 @@ export * from './opcodes' export { PeggedSwapArgs } from './pegged-swap-args' export type { PeggedTokenInfo } from './types' export { PeggedSwapCalculator } from './pegged-swap-calculator' -export { linearWidthFromSymmetricRangePercent } from './pegged-swap-math/pegged-swap-math' +export { + linearWidthFromSymmetricRangePercent, + symmetricRangePercentFromLinearWidth, +} from './pegged-swap-math/pegged-swap-math' export { PeggedPrice } from './price' export type { PeggedInitialBalances, PeggedSwapCalculatorArgs } from './pegged-swap-calculator' export type { 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 index 2311f78..654abe2 100644 --- 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 @@ -8,6 +8,7 @@ import { peggedSwapMarginalGtPerLtE18, peggedSwapMarginalWeight, PEGGED_SWAP_ONE, + symmetricRangePercentFromLinearWidth, } from './pegged-swap-math' import { bigintSqrt } from '../../utils' @@ -38,6 +39,60 @@ describe('linearWidthFromSymmetricRangePercent', () => { }) }) +describe('symmetricRangePercentFromLinearWidth', () => { + it('computes percent = 100 / (2A + 1) (inverse of the forward mapping)', () => { + expect(symmetricRangePercentFromLinearWidth(15n * 10n ** 26n)).toBe(25) + }) + + it('returns 20% at the on-chain maximum linearWidth', () => { + expect(symmetricRangePercentFromLinearWidth(MAX_LINEAR_WIDTH)).toBe(20) + }) + + it('returns 100% when linearWidth is zero', () => { + expect(symmetricRangePercentFromLinearWidth(0n)).toBe(100) + }) + + it('rejects negative linearWidth', () => { + expect(() => symmetricRangePercentFromLinearWidth(-1n)).toThrow(/must be non-negative/) + }) + + it('rejects linearWidth above the on-chain maximum', () => { + expect(() => symmetricRangePercentFromLinearWidth(MAX_LINEAR_WIDTH + 1n)).toThrow( + /exceeds maximum/, + ) + }) + + it('round-trips percent -> linearWidth -> percent', () => { + for (const percent of [20, 25, 25.5, 33.33, 50, 99]) { + const linearWidth = linearWidthFromSymmetricRangePercent(percent) + expect(symmetricRangePercentFromLinearWidth(linearWidth)).toBeCloseTo(percent, 10) + } + }) + + it('round-trips linearWidth -> percent -> linearWidth within rounding', () => { + for (const linearWidth of [ + 10n ** 26n, + 5n * 10n ** 26n, + 10n ** 27n, + 15n * 10n ** 26n, + MAX_LINEAR_WIDTH, + ]) { + const percent = symmetricRangePercentFromLinearWidth(linearWidth) + const recovered = linearWidthFromSymmetricRangePercent(percent) + const tolerance = PEGGED_SWAP_ONE / 10n ** 13n + const delta = recovered > linearWidth ? recovered - linearWidth : linearWidth - recovered + expect(delta).toBeLessThanOrEqual(tolerance) + } + }) + + it('round-trips exactly for clean percents', () => { + for (const percent of [20, 25, 50]) { + const linearWidth = linearWidthFromSymmetricRangePercent(percent) + expect(symmetricRangePercentFromLinearWidth(linearWidth)).toBe(percent) + } + }) +}) + describe('peggedSwapMath', () => { it('normalizeReserve is ONE when current equals initial', () => { const initial = 10n ** 18n 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 cefa152..db98ad4 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 @@ -11,6 +11,9 @@ export const MAX_LINEAR_WIDTH: bigint = 2n * PEGGED_SWAP_ONE const MARGINAL_PRICE_ONE = 10n ** 18n +const PERCENT_PRECISION = 10n ** 13n +const PERCENT_PRECISION_NUMBER = 1e13 + /** * On-chain `linearWidth` from symmetric peg-band half-width as a human percent. * @@ -42,6 +45,27 @@ export function linearWidthFromSymmetricRangePercent(symmetricRangePercent: numb return linearWidth } +/** + * Symmetric peg-band half-width as a human percent from on-chain `linearWidth`. + * + * Inverse of {@link linearWidthFromSymmetricRangePercent}. + * + * @param linearWidth - On-chain `linearWidth` = A · `PEGGED_SWAP_ONE`. + * + * X = 1 / (2A + 1), A = `linearWidth` / `PEGGED_SWAP_ONE`, percent = X · 100. + */ +export function symmetricRangePercentFromLinearWidth(linearWidth: bigint): number { + assert(linearWidth >= 0n, 'PeggedSwapMath: linearWidth must be non-negative') + assert( + linearWidth <= MAX_LINEAR_WIDTH, + `PeggedSwapMath: linearWidth exceeds maximum (${MAX_LINEAR_WIDTH})`, + ) + + const x = mulDiv(PEGGED_SWAP_ONE, PEGGED_SWAP_ONE, 2n * linearWidth + PEGGED_SWAP_ONE) + + return scaledFractionToSymmetricRangePercent(x) +} + /** * Spot price tokenGt per tokenLt (raw) in 1e18 fixed-point. * @@ -84,7 +108,17 @@ export function peggedSwapMarginalWeight(sqrtCoord: bigint, linearWidth: bigint) } function symmetricRangePercentToScaledFraction(percent: number): bigint { - return (BigInt(Math.round(percent * 1e9)) * PEGGED_SWAP_ONE) / (100n * 10n ** 9n) + return ( + (BigInt(Math.round(percent * PERCENT_PRECISION_NUMBER)) * PEGGED_SWAP_ONE) / + (100n * PERCENT_PRECISION) + ) +} + +function scaledFractionToSymmetricRangePercent(scaledFraction: bigint): number { + return ( + Number(mulDiv(scaledFraction, 100n * PERCENT_PRECISION, PEGGED_SWAP_ONE)) / + PERCENT_PRECISION_NUMBER + ) } function mulDiv(a: bigint, b: bigint, c: bigint): bigint {