Decimais de Tokens EVM
Incompatibilidades silenciosas de decimais são uma das formas mais fáceis de gerar saldos ou valores USD que estão fora por ordens de magnitude sem lançar 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 bridge
- 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 aritmética exata para 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 codifique 1_000_000 porque um símbolo geralmente tem 6 decimais em outro lugar.
Armazenar em 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 com tokens incomuns de forma defensiva
try:
decimals = contract.functions.decimals().call()
except Exception:
logging.warning(
"decimals() reverteu em %s (cadeia %s), usando padrão 18",
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 por símbolo
- Use
Decimal,BigIntou aritmética exata equivalente, não float - Reconsulte decimais após operações de bridge ou mudanças de wrapper
- Normalize a contabilidade interna de forma consistente antes de comparação ou precificação