EVM 代幣小數位數 是做什麼的?
靜默的小數位數不匹配是最容易導致餘額或美元價值出現數量級偏差,卻又不會拋出錯誤的問題之一。
使用時機
- 在 Python、TypeScript 或 Solidity 中讀取 ERC-20 餘額
- 根據鏈上餘額計算法幣價值
- 比較跨越多條 EVM 鏈的代幣數量
- 處理跨鏈橋接資產
- 構建投資組合追蹤器、機器人或聚合器
工作原理
切勿假設穩定幣在各處都使用相同的小數位數。應在運行時查詢 decimals(),按 (chain_id, token_address) 進行快取,並使用小數安全運算進行數值計算。
範例
在運行時查詢小數位數
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)
不要因為某個代幣通常有 6 位小數,就硬編碼 1_000_000。
按鏈和代幣進行快取
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()
防禦性處理異常代幣
try:
decimals = contract.functions.decimals().call()
except Exception:
logging.warning(
"decimals() reverted on %s (chain %s), defaulting to 18",
token_address,
chain_id,
)
decimals = 18
記錄這個回退行為並讓它保持可見。老舊或非標準的代幣依然存在。
在 Solidity 中標準化為 18 位小數的 WAD
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);
}
使用 ethers 的 TypeScript
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);
}
快速鏈上查詢
cast call <token_address> "decimals()(uint8)" --rpc-url <rpc>
規則
- 始終在運行時查詢
decimals() - 按鏈加代幣地址進行快取,而不是按代幣符號
- 使用
Decimal、BigInt或等效的精確數學運算,不要使用浮點數 - 在跨鏈橋接或包裝器變更後重新查詢小數位數
- 在比較或定價之前,始終一致地標準化內部記賬