Skip to content

fix: validate amount with isFinite and fix float precision in USDC conversion - #30

Open
memosr wants to merge 2 commits into
circlefin:masterfrom
memosr:fix/nan-float-amount-parsing
Open

fix: validate amount with isFinite and fix float precision in USDC conversion#30
memosr wants to merge 2 commits into
circlefin:masterfrom
memosr:fix/nan-float-amount-parsing

Conversation

@memosr

@memosr memosr commented Apr 12, 2026

Copy link
Copy Markdown

Problem

Two related bugs in deposit/route.ts and transfer/route.ts:

1. NaN bypasses validation and crashes BigInt

const parsedAmount = parseFloat(amount); // "abc" → NaN
if (parsedAmount <= 0) ...               // NaN <= 0 is false — check passes!
BigInt(Math.floor(NaN * 1_000_000))      // RangeError crash

2. Float precision loss in financial math

BigInt(Math.floor(parseFloat("0.1") * 1_000_000)) // → 99999, not 100000

IEEE 754 float multiplication causes precision loss for USDC amounts.

Fix

+ if (!isFinite(parsedAmount)) {
+   return NextResponse.json({ error: "Amount must be a valid number" }, { status: 400 });
+ }

- const amountInAtomicUnits = BigInt(Math.floor(parsedAmount * 1_000_000));
+ const [intPart, decPart = ""] = parsedAmount.toFixed(6).split(".");
+ const amountInAtomicUnits = BigInt(intPart) * 1_000_000n + BigInt(decPart);

toFixed(6) produces a stable string like "1.100000" which we split and recombine as integer arithmetic — no float multiplication, no precision loss.

Impact

  • Correctness: Prevents RangeError crash on invalid input
  • Financial precision: USDC amounts now convert correctly (e.g. 0.1 USDC → 100000 not 99999)
  • Risk: Low — input validation added, conversion logic made more precise

Comment thread app/api/gateway/deposit/route.ts Outdated
@@ -58,6 +58,14 @@ export async function POST(req: NextRequest) {
// Convert amount to bigint (amount should be in USDC, multiply by 1_000_000)
const parsedAmount = parseFloat(amount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amount comes directly from req.json(), so this still accepts malformed money input before the finite check:

parseFloat("1abc") === 1
Number.isFinite(parseFloat("1abc")) === true

That means amount: "1abc" passes validation on this deposit endpoint and is treated as 1 USDC. parsedAmount.toFixed(6) also silently rounds over-precision input (0.1234567 -> 0.123457) instead of rejecting it.

The new BigInt conversion fixes the old Math.floor(parsedAmount * 1_000_000) precision path, but the raw input needs a strict decimal gate before parseFloat:

if (typeof amount !== "string" || !/^\d+(?:\.\d{1,6})?$/.test(amount)) {
  return NextResponse.json(
    { error: "Amount must be a valid USDC amount" },
    { status: 400 }
  );
}

Then keep the existing parsedAmount <= 0 check. The same pattern exists in app/api/gateway/transfer/route.ts, so the guard should be applied there too.

@memosr

memosr commented May 12, 2026

Copy link
Copy Markdown
Author

Thanks.

Both issues addressed in dbb9acb:

Added strict regex validation /^\d+(?:\.\d{1,6})?$/ at the string level before parseFloat
Removed isFinite check (now redundant since regex guarantees a valid number)
Replaced parsedAmount.toFixed(6).split(".") with amount.split(".") + padEnd(6, "0") — splits the original validated string directly, eliminating the silent rounding for over-precision input
Same guard applied to both deposit/route.ts and transfer/route.ts

Now "1abc", "1.1234567" (7 decimal places), empty strings, and non-strings are all properly rejected with a 400 response.

@Cassxbt

Cassxbt commented May 12, 2026

Copy link
Copy Markdown

Nice, this addresses the issue cleanly.

I re-checked the update on dbb9acb. The original parsing issues are resolved in both deposit/route.ts and transfer/route.ts: the raw amount value is now string-gated before parseFloat, malformed input like "1abc" is rejected, over-precision input like "1.1234567" is rejected instead of rounded, and atomic units are built from the validated original string.

I also checked representative cases ("1", "1.2", "1.000001", "0.000001", "0", "1abc", "1.1234567", non-string input), and the conversion/rejection behavior matches the intended USDC 6-decimal handling.

No further concerns from my earlier review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants