Concept

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

OperationDecimals to useExample (1.0 WETH)
Shield (wrap)parseUnits(amount, underlyingDecimals)parseUnits('1', 18) → 10¹⁸
Unshield (unwrap)parseUnits(amount, 6)parseUnits('1', 6) → 10⁶
Display balanceformatUnits(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!