| name | evm-decimal-validation |
| description | Validate and configure token decimals for EVM-compatible blockchain deployments. Use when working with ERC20 tokens, multi-chain deployments, token amount conversions, or when decimals might vary across chains. |
EVM Token Decimal Validation
Ensure token decimals are correctly configured for each blockchain deployment to prevent amount calculation errors.
Quick Start
- Check current configuration - Review network configs for decimal fields
- Validate against contracts - Query on-chain decimals() for each deployment
- Update configuration - Add per-chain decimals to network config
- Test conversions - Verify FromWei/ToWei work correctly
Token Decimal Standards
Common Decimal Values
| Decimals | Use Case | Examples |
|---|
| 18 | Standard ERC20 (most tokens) | WETH, DAI, USDC (on some chains) |
| 6 | Stablecoins | USDC, USDT (on Ethereum) |
| 8 | Bitcoin-wrapped | WBTC |
| 2 | Fiat-backed (cents) | IDRX, some CBDCs |
| 0 | Raw integers | Some wrapped tokens |
Chain-Specific Variations
The same token contract deployed on different chains may use different decimals:
Token A on Ethereum: 18 decimals
Token A on Polygon: 0 decimals (different deployment)
Token A on BNB Chain: 0 decimals (different deployment)
Validation Checklist
Step 1: Audit Current Configuration
grep -rn "18)" --include="*.go" | grep -i "decimal\|wei\|amount"
grep -rn "Decimals" blockchain/networks.go
Step 2: Query On-Chain Values
decimals, err := contract.Decimals(nil)
if err != nil {
return fmt.Errorf("failed to get decimals: %w", err)
}
Step 3: Update Network Config
type NetworkConfig struct {
Decimals uint8
}
BaseMainnet: {
Decimals: 2,
},
Step 4: Add Helper Function
func GetDecimals(chainID uint64) uint8 {
config, _, exists := GetNetworkConfigByChainID(chainID)
if !exists {
return 2
}
return config.Decimals
}
Step 5: Replace Hardcoded Values
return FromWei(balance, 18), nil
return FromWei(balance, int32(GetDecimals(chainID))), nil
Common Pitfalls
Pitfall 1: Assuming All Chains Use Same Decimals
amount := FromWei(balance, 18)
amount := FromWei(balance, int32(GetDecimals(chainID)))
Pitfall 2: Missing Fallback Value
return config.Decimals, nil
if !exists {
return 2
}
return config.Decimals
Pitfall 3: Not Validating Config Against Chain
func ValidateDecimals() error {
for name, config := range SupportedNetworks {
onChainDecimals, err := queryOnChain(config)
if err != nil {
continue
}
if config.Decimals != onChainDecimals {
return fmt.Errorf("%s: config=%d, on-chain=%d",
name, config.Decimals, onChainDecimals)
}
}
return nil
}
Test Patterns
func TestGetDecimals(t *testing.T) {
testCases := map[uint64]uint8{
8453: 2,
137: 0,
56: 0,
}
for chainID, expected := range testCases {
got := GetDecimals(chainID)
if got != expected {
t.Errorf("Chain %d: expected %d, got %d", chainID, expected, got)
}
}
}
func TestGetDecimalsFallback(t *testing.T) {
got := GetDecimals(999999)
if got != 2 {
t.Errorf("Unknown chain should return fallback, got %d", got)
}
}
Integration Points
When adding decimal validation, check these areas:
| Area | What to Check |
|---|
| Balance queries | BalanceOf() uses per-chain decimals |
| Total supply | TotalSupply() uses per-chain decimals |
| Transfers | ParseTokenAmount() receives correct decimals |
| Bridge operations | Both source and dest chains considered |
| Fee calculations | Platform fees use correct decimals |
| API responses | Human-readable amounts properly converted |
Token Decimals by Chain
See decimals-by-chain.md for a reference guide of common tokens and their decimals across different EVM chains.
Sources