Skip to content
1Claw Academy
Curriculum/Transactions & Treasury2 minAdvanced · Lesson 8 of 16

Lab: send a trillion dollars by mistake

Lab

Convert amounts between human and atomic units, get the decimals wrong in both directions, and read what you actually sent.

The x402 lesson mentioned that amounts are atomic units and that USDC has six decimals. That is a one-line fact with a very wide blast radius, because getting it wrong does not error, it succeeds at the wrong magnitude.

  1. 1

    Write the conversion both ways and make three attempts at sending 1.5 USDC.

    bash
    cat > /tmp/units.py <<'EOF'
    DECIMALS = {"ETH": 18, "USDC": 6}
    to_atomic = lambda amount, sym: int(amount * 10 ** DECIMALS[sym])
    fmt       = lambda atomic, sym: atomic / 10 ** DECIMALS[sym]
    
    print("intent: send 1.5 USDC\n")
    
    correct = to_atomic(1.5, "USDC")
    print(f"  correct:            {correct:>22}  -> {fmt(correct, 'USDC')} USDC")
    
    naive = int(1.5)                        # forgot to convert at all
    print(f"  human value passed: {naive:>22}  -> {fmt(naive, 'USDC')} USDC")
    
    wrong = to_atomic(1.5, "ETH")           # reused the ETH decimals
    print(f"  18 decimals on USDC:{wrong:>22}  -> {fmt(wrong, 'USDC'):,.0f} USDC")
    EOF
    python3 /tmp/units.py
  2. 2

    Two of the three succeed and neither sends what you meant.

    text
    intent: send 1.5 USDC
    
      correct:                          1500000  -> 1.5 USDC
      human value passed:                     1  -> 1e-06 USDC
      18 decimals on USDC:  1500000000000000000  -> 1,500,000,000,000 USDC
  3. 3

    Clean up.

    bash
    rm /tmp/units.py

Neither mistake is a crash. One sends dust, which you notice eventually and which costs a fee; the other attempts to send one and a half trillion USDC, which fails on balance rather than on validation and which would not fail at all if the balance were large enough.

  • Decimals vary per token, not per chain. USDC uses 6, ETH uses 18, WBTC uses 8, and hardcoding one of them is how a working integration breaks on the second asset.
  • A value cap denominated in atomic units has the same problem. A guardrail written for 18 decimals is a thousand billion times looser when applied to a 6-decimal token.
  • Float arithmetic makes it worse. 0.1 + 0.2 is not 0.3, and converting a float to atomic units can round into the wrong integer, which is why production code uses integer or decimal types throughout.
Watch out

Read the amount back in human units before signing and compare it to what you intended. It is one line and it catches every one of these, which is why simulation and typed-data guardrails matter: they let a human or a policy see the amount in a form that can be checked.

Tip

This is also why the x402 challenge validation pins the asset contract rather than the symbol. Two tokens can both call themselves USDC and use different decimals.

Check your understanding

3 questions
1

Why is passing the human amount straight through so hard to catch?

2

What goes wrong when a value cap is written in atomic units for the wrong token?

3

Why does x402 challenge validation pin the asset contract rather than the symbol?