evm-token-decimals

evm-token-decimals

热门

防止跨EVM链的静默小数位数不匹配错误。涵盖运行时小数查询、链感知缓存、桥接代币精度漂移以及为机器人、仪表盘和DeFi工具提供的安全归一化。

23万Star
3.5万Fork
更新于 2026/7/23
SKILL.md
readonly只读
name
evm-token-decimals
description

防止跨EVM链的静默小数位数不匹配错误。涵盖运行时小数查询、链感知缓存、桥接代币精度漂移以及为机器人、仪表盘和DeFi工具提供的安全归一化。

version
1.0.0

EVM Token Decimals

静默小数位数不匹配是导致余额或美元值相差几个数量级而不报错的最常见原因之一。

何时使用

  • 在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)

不要硬编码 1_000_000,因为某个代币在其他地方通常有6位小数。

按链和代币缓存

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 或等效的精确数学,不要使用浮点数
  • 在桥接或包装器更改后重新查询小数位数
  • 在比较或定价之前,一致地归一化内部记账