Decimal Scaling
Why every confidential wrapper is 6 decimals, and the exact rule for shield vs. unshield amounts.
This is the most common source of bugs when integrating Zama FHE tokens. Read it carefully — it is short.
FHE operates on euint64 — a 64-bit unsigned integer with a maximum of ~1.84 × 10¹⁹. A standard 18-decimal ERC-20 represents 1.0 token as 10¹⁸; multiplied by any meaningful amount that overflows 64 bits quickly.
Therefore all ERC-7984 wrapper tokens use 6 decimals, regardless of the underlying token's precision. The wrapper scales amounts automatically during shielding and unshielding.
Critical rule: when calling
useShield, parse the amount with the underlying token's decimals. When calling useUnshield, always use 6 decimals (the wrapper decimals). When displaying a confidential balance, always format with 6.Decision table
| Operation | Decimals to use | Example (1.0 WETH) |
|---|---|---|
| Shield (wrap) | parseUnits(amount, underlyingDecimals) | parseUnits('1', 18) → 10¹⁸ |
| Unshield (unwrap) | parseUnits(amount, 6) | parseUnits('1', 6) → 10⁶ |
| Display balance | formatUnits(balance, 6) | formatUnits(1_000_000n, 6)→ '1.0' |
decimal-scaling.ts
import { parseUnits, formatUnits } from 'viem';
// USDC (6 decimals) — same decimals as wrapper, no confusion
parseUnits('100', 6) // for shield: 100_000_000n ✓
parseUnits('100', 6) // for unshield: 100_000_000n ✓ — same!
// WETH (18 decimals underlying, 6 wrapper)
parseUnits('1', 18) // for shield: 1_000_000_000_000_000_000n ✓
parseUnits('1', 6) // for unshield: 1_000_000n ✓
// Display: always use 6 (wrapper decimals)
formatUnits(1_000_000n, 6) // "1.0" ✓
formatUnits(1_000_000n, 18) // "0.000000000001" ✗ ← the zero-balance bug!