Décimales des Tokens EVM
Les erreurs silencieuses de décimales sont l'un des moyens les plus simples d'expédier des soldes ou des valeurs USD qui sont décalés de plusieurs ordres de grandeur sans lever d'erreur.
Quand l'utiliser
- Lire des soldes ERC-20 en Python, TypeScript ou Solidity
- Calculer des valeurs fiduciaires à partir de soldes on-chain
- Comparer des montants de tokens sur plusieurs chaînes EVM
- Manipuler des actifs pontés
- Construire des trackers de portefeuille, des bots ou des agrégateurs
Comment ça fonctionne
Ne supposez jamais que les stablecoins utilisent les mêmes décimales partout. Interrogez decimals() à l'exécution, mettez en cache par (chain_id, token_address), et utilisez des mathématiques sécurisées pour les calculs de valeur.
Exemples
Interroger les décimales à l'exécution
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)
Ne codez pas en dur 1_000_000 parce qu'un symbole a généralement 6 décimales ailleurs.
Mise en cache par chaîne et 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()
Gérer les tokens étranges de manière défensive
try:
decimals = contract.functions.decimals().call()
except Exception:
logging.warning(
"decimals() a réverté sur %s (chaîne %s), valeur par défaut 18",
token_address,
chain_id,
)
decimals = 18
Enregistrez le repli et gardez-le visible. Les tokens anciens ou non standard existent encore.
Normalisation en WAD (18 décimales) en 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 avec 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);
}
Vérification rapide on-chain
cast call <token_address> "decimals()(uint8)" --rpc-url <rpc>
Règles
- Interrogez toujours
decimals()à l'exécution - Mettez en cache par chaîne et adresse de token, pas par symbole
- Utilisez
Decimal,BigInt, ou un équivalent mathématique exact, pas de float - Re-interrogez les décimales après un pont ou un changement de wrapper
- Normalisez la comptabilité interne de manière cohérente avant toute comparaison ou tarification