From 3d536b3175d3107ff503dd6cac575380bbd3561d Mon Sep 17 00:00:00 2001 From: neverm1ndthat Date: Tue, 6 Oct 2026 17:55:26 +0800 Subject: [PATCH] fix(amm): BigInt-safe ratio guard for estimateSwapOutput Issue #1007 - the maxRatio guard compared Number(amountIn)/Number(reserveIn). With values exceeding Number.MAX_VALUE, both become Infinity and Infinity/Infinity=NaN, silently bypassing the 30% liquidity ceiling. Fix: replace with ratioExceedsLimit() that cross-multiplies arbitrary-size BigInts against the numeric limit (parsed into numerator/denominator). Preserves existing behavior for finite callers and non-finite limit edge cases. Existing 12 AMM tests pass. --- src/ammCalculator.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/ammCalculator.ts b/src/ammCalculator.ts index 4ae3d42..7197fdf 100644 --- a/src/ammCalculator.ts +++ b/src/ammCalculator.ts @@ -83,9 +83,11 @@ export function estimateSwapOutput( }; } - // Check against max ratio threshold - const ratio = Number(amountIn) / Number(reserveIn); - if (ratio > maxRatio) { + // Issue #1007 — Check against max ratio threshold without coercing + // arbitrary-size BigInts to Number. Both operands can exceed + // Number.MAX_VALUE, where Infinity / Infinity would otherwise become NaN + // and silently bypass this guard. + if (ratioExceedsLimit(amountIn, reserveIn, maxRatio)) { throw new InsufficientLiquidityError( `Input amount exceeds ${(maxRatio * 100).toFixed(0)}% of pool reserves`, inputReserve.amount, @@ -409,6 +411,31 @@ function formatScaled(value: bigint, decimals: number): string { return `${intPart}.${fracPart.toString().padStart(decimals, "0")}`; } +// Issue #1007 — cross-multiply BigInts against the limit ratio so that +// arbitrary-size inputs (e.g. 10^400) are not coerced to Infinity and then +// NaN by Number(). This replaces the prior `Number(amountIn) / Number(reserveIn) > maxRatio`. +function ratioExceedsLimit(amount: bigint, reserve: bigint, limit: number): boolean { + if (!Number.isFinite(limit)) { + // Preserve the prior comparison semantics for non-finite caller values: + // NaN/+Infinity never reject, while -Infinity rejects every positive ratio. + return limit === -Infinity; + } + + const [coefficient, exponentText] = limit.toString().toLowerCase().split("e"); + const [integerPart, fractionalPart = ""] = coefficient!.split("."); + let numerator = BigInt(`${integerPart}${fractionalPart}`); + let denominator = 10n ** BigInt(fractionalPart.length); + const exponent = Number(exponentText ?? "0"); + + if (exponent > 0) { + numerator *= 10n ** BigInt(exponent); + } else if (exponent < 0) { + denominator *= 10n ** BigInt(-exponent); + } + + return amount * denominator > reserve * numerator; +} + function computeSpotPrice(reserveIn: bigint, reserveOut: bigint): string { // spotPrice = reserveOut / reserveIn as a decimal string if (reserveIn === 0n) return "0";