Skip to content

fix(payout): reject malformed amounts and drop float conversion (#41) - #43

Open
devorun wants to merge 1 commit into
circlefin:masterfrom
devorun:fix/payout-strict-amount-parsing
Open

fix(payout): reject malformed amounts and drop float conversion (#41)#43
devorun wants to merge 1 commit into
circlefin:masterfrom
devorun:fix/payout-strict-amount-parsing

Conversation

@devorun

@devorun devorun commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Fixes #41.

The payout endpoint parsed user-supplied amounts with parseFloat, which accepts a valid numeric prefix and ignores the rest — so "1USDC" became 1 and "1.5abc" became 1.5, passing validation and moving a value the request body never wrote. The same parseFloat(...) * 1_000_000 + Math.floor conversion also silently truncated anything beyond six decimals.

Changes

  • lib/circle/usdc-amount.ts (new): strict parseUsdcAmountToAtomicUnits / formatUsdcAtomicUnits helpers. This logic already existed privately in lib/circle/unified-balance-payout.ts; it is lifted into a shared module so there is a single implementation instead of two copies.
  • app/api/payout/route.ts: replace the parseFloat-based convertToSmallestUnit and the parseFloat amount check with strict parsing. Malformed input ("1USDC", "1.5abc", "1e6", more than six decimals, empty/negative) now returns 400 Invalid amount instead of silently proceeding. Every amount passed downstream to the App Kit / Gateway SDK is derived from the validated atomic units, so no re-parsed float can drift from the validated value.
  • lib/circle/unified-balance-payout.ts: consume the shared helpers (identical logic, behavior unchanged).

Tests

  • New tests/usdc-amount.test.ts covers the reported cases ("1USDC", "1.5abc"), excess-decimal rejection, whitespace tolerance, signed/empty input, and format round-tripping.
  • npx vitest run passes except for a pre-existing, unrelated failure in tests/spend-arc-gateway-usdc.test.ts (missing ../scripts/spend-arc-gateway-usdc.mjs), which fails identically on master.
  • ESLint clean on the changed files; tsc --noEmit reports no new errors.

Note (out of scope)

app/api/bridge/rebalance/route.ts uses the same parseFloat / amountNum.toString() pattern and would benefit from the shared helper too. Left out to keep this PR focused on #41.

The payout endpoint parsed amounts with parseFloat, which accepts a
valid numeric prefix and ignores the rest, so "1USDC" became 1 and
"1.5abc" became 1.5 -- passing validation and moving a value the
request body never wrote. The parseFloat * 1_000_000 + Math.floor
conversion also silently truncated anything beyond six decimals.

Replace it with strict parsing: lift the existing (previously private)
parseUsdcAmountToAtomicUnits / formatUsdcAtomicUnits helpers out of
unified-balance-payout.ts into a shared lib/circle/usdc-amount.ts and
use them in the payout route. Malformed, negative, empty, or
over-precise amounts now return 400 Invalid amount, and every amount
handed to the App Kit / Gateway SDK is derived from the validated
atomic units instead of a re-parsed float.

Fixes circlefin#41
@devorun
devorun force-pushed the fix/payout-strict-amount-parsing branch from eaca7df to 164d2ab Compare August 6, 2026 18:44
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.

Payout API accepts partially numeric amount strings and silently changes the requested value

1 participant