Skip to content

Aggregate asset totals lose precision for large raw amounts (Number() instead of BigInt) #47

Description

@batuhankocyigit

Packages versions

guardian-dashboard v0.1.0, commit 85929c0 (main, 2026-08-21)

Bug description

normalizeAmount() in lib/token-registry.ts converts the raw on-chain amount
string with Number(rawAmount) before dividing by 10**decimals:

export function normalizeAmount(faucetId: string, rawAmount: string): number {
  const n = Number(rawAmount);
  ...
  return n / Math.pow(10, decimals);
}

Number() silently rounds any integer string above
Number.MAX_SAFE_INTEGER (2^53 - 1 = 9,007,199,254,740,992). A realistic raw
balance is well past that threshold for common decimal counts (e.g. 10,000
units of an 18-decimal token is "10000000000000000000000"), so aggregate
totals can be silently wrong by a non-trivial amount with no error, no
warning, and no test catching it.

This function is the sole totalizer for the dashboard-wide asset sums
(overview/stats) and for the "Total assets (USD)" column in the CSV export
(lib/format.ts, accountsToCsv) — both derive from
lib/account-cache.ts:260, which calls normalizeAmount() per asset and sums
the results with plain floating-point +.

The codebase already has the correct pattern one file away:
components/accounts/AccountDetail.tsx:357 renders the same raw amount field
via BigInt(asset.amount).toLocaleString(), which does not lose precision.
So a single account's detail page can show the exact figure while the
dashboard-wide total / CSV export derived from the same underlying data is
silently off.

How can this be reproduced?

  1. In a Node/TS REPL, import normalizeAmount (or inline the function) and
    call it with a raw amount at or above 2^53 in base units, e.g.:

    normalizeAmount("faucetX", "9007199254740993") // decimals default 6

    Compare the internal Number("9007199254740993") step against the input:
    it silently becomes 9007199254740992 before any division happens.

  2. In the running app: seed/mock a Guardian account whose fungible asset
    amount field is a large base-unit integer (e.g. "10000000000000000000000"
    for a token registered with 18 decimals in GUARDIAN_TOKEN_DECIMALS), load
    the Accounts overview or export the CSV, and compare the displayed/exported
    total against the account's own detail page (which uses BigInt and is
    correct).

Relevant log output

No exception/log is produced — that's the bug. The failure is a silently
wrong number, not a crash. The only observable signal is the discrepancy
between AccountDetail.tsx's BigInt-based per-account figure and the
Number()-based aggregate/CSV total for the same account.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions