← Back to the library
Web3Core · 6 min

Token amounts are off by orders of magnitude

“A 100 USDC balance shows as 0.0000000001 or as 100 trillion, a transfer sends a millionth of what the user typed, or a price looks fine on one chain and absurd on another.”

ERROR CODES YOU MAY SEE

A code is a clue. Use its meaning and surrounding evidence to narrow the cause.

NUMERIC_FAULTethers v6 · library

An operation would produce incorrect arithmetic output, such as overflow, division by zero, or precision loss (underflow).

From parseUnits, fault "underflow" means the input has more fractional digits than the token's decimals. Check which decimals value was passed and whether the input was already in base units.

Official reference for NUMERIC_FAULT (opens in new tab)Code definition reviewed

THE PRINCIPLE

Amounts are integers with a unit. Never let a number lose track of which token, which chain, and which decimals it belongs to.

FIRST MOVES

  1. Find where 18 is assumed. USDC and USDT on Ethereum use 6 decimals, while the same symbol can differ elsewhere: Binance-Peg USDC on BNB Smart Chain uses 18.
  2. Key token metadata by (chainId, address), read decimals() once and cache it, and store raw base-unit amounts alongside the decimals in your database.
  3. Keep amounts as bigint (or NUMERIC/DECIMAL in SQL). JavaScript numbers lose integer precision above 2^53 - 1, which a token amount with 18 decimals passes at about 0.009 tokens.
  4. Parse user input with parseUnits(input, decimals) and render with formatUnits(raw, decimals). Check the fractional digit count first: ethers throws NUMERIC_FAULT on extra digits, but viem rounds them silently. Never feed a formatted string back into math.
  5. When converting between tokens or computing prices, scale explicitly by 10n ** BigInt(decimals) and multiply before dividing.

TOOLS: THEN → NOW

bignumber.js or BN.js wrappersNative bigint with viem or ethers v6 parseUnits / formatUnits; decimal libraries remain useful for display math
Hard-coded token listsToken lists still help with discovery; confirm decimals on-chain per chain and address

PATTERN SNAPSHOT

import { erc20Abi, formatUnits, parseUnits } from "viem";

const decimals = await client.readContract({ address: token, abi: erc20Abi, functionName: "decimals" });

const amount = parseUnits("12.5", decimals);   // 12500000n when decimals is 6
const shown = formatUnits(balance, decimals);  // display only, never back into math

// Convert a 6-decimal amount to an 18-decimal one: multiply first, stay in bigint.
const to18 = (amount * 10n ** 18n) / 10n ** BigInt(decimals);

CLOSE THE AI. EXPLAIN THIS.

Your price math works for DAI but shows USDC as worth 10^12 times too much. Which decimals assumption is wrong, and where should the conversion live?

SOURCES

Guide reviewed