evm-token-decimals

evm-token-decimals

熱門

防止跨 EVM 鏈的無聲小數位不匹配錯誤。涵蓋執行時期小數位查詢、鏈感知快取、跨鏈代幣精度偏移,以及為機器人、儀表板和 DeFi 工具提供安全的正規化。

23萬星標
3.5萬分支
更新於 2026/7/23
SKILL.md
readonlyread-only
name
evm-token-decimals
description

防止跨 EVM 鏈的無聲小數位不匹配錯誤。涵蓋執行時期小數位查詢、鏈感知快取、跨鏈代幣精度偏移,以及為機器人、儀表板和 DeFi 工具提供安全的正規化。

version
1.0.0

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() 在 %s (鏈 %s) 上回退,預設為 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()
  • 按鏈和代幣地址快取,而不是按代幣符號
  • 使用 DecimalBigInt 或等效的精確數學運算,不要使用浮點數
  • 在跨鏈或包裝器變更後重新查詢小數位
  • 在比較或定價之前,一致地正規化內部會計