Décimales des tokens EVM
Les décalages silencieux de décimales sont l'un des moyens les plus faciles d'obtenir des soldes ou des valeurs en USD décalés de plusieurs ordres de grandeur sans qu'aucune erreur ne soit levée.
Quand l'utiliser
- Lecture des soldes ERC-20 en Python, TypeScript ou Solidity
- Calcul des valeurs fiduciaires à partir des soldes on-chain
- Comparaison des montants de tokens sur plusieurs chaînes EVM
- Manipulation d'actifs pontés (bridged assets)
- Création de trackers de portefeuille, bots ou 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 décimales dans 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 hardcodez pas 1_000_000 parce qu'un symbole a habituellement 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 non standards de manière défensive
try:
decimals = contract.functions.decimals().call()
except Exception:
logging.warning(
"decimals() a échoué sur %s (chaîne %s), valeur par défaut à 18",
token_address,
chain_id,
)
decimals = 18
Consignez la valeur de repli dans les logs et gardez-la visible. Les tokens anciens ou non standards existent encore.
Normaliser 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
- Toujours interroger
decimals()à l'exécution - Mettre en cache par chaîne ET adresse du token, pas par symbole
- Utiliser
Decimal,BigIntou un entier exact équivalent, pas de float - Ré-interroger les décimales après un pont ou un changement de wrapper
- Normaliser la comptabilité interne de manière cohérente avant comparaison ou tarification