Decimais de Token EVM
Incompatibilidades silenciosas de decimais são uma das maneiras mais fáceis de gerar saldos ou valores em USD que estão errados por ordens de magnitude sem gerar um erro.
Quando Usar
- Lendo saldos ERC-20 em Python, TypeScript ou Solidity
- Calculando valores fiduciários a partir de saldos on-chain
- Comparando quantidades de tokens em múltiplas cadeias EVM
- Lidando com ativos bridgeados
- Construindo rastreadores de portfólio, bots ou agregadores
Como Funciona
Nunca presuma que stablecoins usam os mesmos decimais em todos os lugares. Consulte decimals() em tempo de execução, armazene em cache por (chain_id, token_address) e use matemática segura para decimais em cálculos de valor.
Exemplos
Consultar decimais em tempo de execução
from decimal import Decimal
from web3 import Web3
ERC20_ABI = [
{"name": "decimals", "type": "function", "inputs": [],
"outputs": [{"type": "uint8"}], "stateMutability": "view"},
{"name": "balanceOf", "type": "function",
"inputs": [{"name": "account", "type": "address"}],
"outputs": [{"type": "uint256"}], "stateMutability": "view"},
]
def get_token_balance(w3: Web3, token_address: str, wallet: str) -> Decimal:
contract = w3.eth.contract(
address=Web3.to_checksum_address(token_address),
abi=ERC20_ABI,
)
decimals = contract.functions.decimals().call()
raw = contract.functions.balanceOf(Web3.to_checksum_address(wallet)).call()
return Decimal(raw) / Decimal(10 ** decimals)
Não hardcode 1_000_000 só porque um símbolo geralmente tem 6 decimais em outro lugar.
Cache por cadeia e token
from functools import lru_cache
@lru_cache(maxsize=512)
def get_decimals(chain_id: int, token_address: str) -> int:
w3 = get_web3_for_chain(chain_id)
contract = w3.eth.contract(
address=Web3.to_checksum_address(token_address),
abi=ERC20_ABI,
)
return contract.functions.decimals().call()
Lidar defensivamente com tokens incomuns
try:
decimals = contract.functions.decimals().call()
except Exception:
logging.warning(
"decimals() reverteu em %s (cadeia %s), usando 18 como padrão",
token_address,
chain_id,
)
decimals = 18
Registre o fallback e mantenha-o visível. Tokens antigos ou não padronizados ainda existem.
Normalizar para WAD de 18 decimais em Solidity
interface IERC20Metadata {
function decimals() external view returns (uint8);
}
function normalizeToWad(address token, uint256 amount) internal view returns (uint256) {
uint8 d = IERC20Metadata(token).decimals();
if (d == 18) return amount;
if (d < 18) return amount * 10 ** (18 - d);
return amount / 10 ** (d - 18);
}
TypeScript com ethers
import { Contract, formatUnits } from 'ethers';
const ERC20_ABI = [
'function decimals() view returns (uint8)',
'function balanceOf(address) view returns (uint256)',
];
async function getBalance(provider: any, tokenAddress: string, wallet: string): Promise<string> {
const token = new Contract(tokenAddress, ERC20_ABI, provider);
const [decimals, raw] = await Promise.all([
token.decimals(),
token.balanceOf(wallet),
]);
return formatUnits(raw, decimals);
}
Verificação rápida on-chain
cast call <token_address> "decimals()(uint8)" --rpc-url <rpc>
Regras
- Sempre consulte
decimals()em tempo de execução - Armazene em cache por cadeia mais endereço do token, não pelo símbolo
- Use
Decimal,BigIntou matemática exata equivalente, não float - Consulte novamente os decimais após bridge ou mudanças de wrapper
- Normalize a contabilidade interna de forma consistente antes de comparação ou precificação