Middle · Blockchain infrastructure

The token amount lost one base unit

A decimal token amount is decoded to float64, multiplied by 10^18 and then converted to big.Int. Why can the result already be wrong?

Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.

EXAMPLE · FRAGMENT UNLESS A COMPLETE PROGRAM IS SHOWN

go
package main

import (
	"fmt"
	"math/big"
	"strings"
)

func units(text string, scale uint8) (*big.Int, error) {
	if scale > 36 || len(text) > 80 {
		return nil, fmt.Errorf("input budget")
	}
	p := strings.Split(text, ".")
	if len(p) > 2 || p[0] == "" {
		return nil, fmt.Errorf("decimal required")
	}
	frac := ""
	if len(p) == 2 {
		frac = p[1]
		if frac == "" {
			return nil, fmt.Errorf("empty fraction")
		}
	}
	if len(frac) > int(scale) {
		return nil, fmt.Errorf("precision exceeds scale")
	}
	digits := p[0] + frac
	for _, c := range digits {
		if c < '0' || c > '9' {
			return nil, fmt.Errorf("digits only")
		}
	}
	value, ok := new(big.Int).SetString(digits+strings.Repeat("0", int(scale)-len(frac)), 10)
	if !ok || value.BitLen() > 256 {
		return nil, fmt.Errorf("amount out of range")
	}
	return value, nil
}
func main() { n, err := units("1.000000000000000001", 18); fmt.Println(n, err) }
Reveal a strong answer
  1. Token amounts frequently exceed the exact integer range of float64, even when the human-readable value looks small. Converting through a float before math/big permanently loses information. Parse text under an explicit decimal-scale policy, reject unsupported precision and preserve the integer base-unit value. Token metadata is input to validate, not an instruction to allocate unbounded memory.

  2. Obtain and verify the correct token contract, network and decimal metadata, then define supported scales and input length. Keep amounts in base units for arithmetic and serialize large integers as exact strings where the wire contract needs them. math/big.Int methods mutate the receiver; use Set for independent copies before sharing values between owners. The sample produces 1000000000000000001 exactly, without a floating-point intermediate. It rejects signs, exponents, excess fractional precision and values outside its stated 256-bit policy. The 36-decimal cap is this example’s application budget, not a claim that every token uses at most that scale.

  3. Symbols are not unique token identifiers, and decimal metadata can be surprising or unsupported. Do not round away an extra fractional digit silently. Test zero, leading zeros, maximum value, too many digits, unsupported scale and the same symbol on different networks. Fees, token transfers and fiat conversions are different quantities with separately defined units and rounding policies.

Common trap

An exact type at the final step does not repair a lossy intermediate representation.

Try it

Parse 1.000000000000000001 directly under an 18-decimal policy and reject excess precision and out-of-range input.

Worked solution

float64 cannot preserve all relevant integers and decimal fractions, so later converting to big.Int cannot restore lost information. Parse validated decimal text directly to exact base units under an explicit scale, range and rounding policy. Verify the chain, token contract and metadata instead of inferring decimals from a symbol. Bound input size and remember that big.Int methods mutate their receiver when values cross ownership boundaries.

Change the constraint

How should the wire API represent large integers for JavaScript clients without silently losing precision?

How did you do?

Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.

Not marked. Progress stays on this browser, separate from course completion.

Primary references

Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.