跳转至

EVM Token 小数位

静默的小数位不一致是最容易导致余额或 USD 值出现数量级错误的方式之一,且不会引发错误。

何时使用

  • 使用 Python、TypeScript 或 Solidity 读取 ERC-20 余额时
  • 从链上余额计算法定货币价值时
  • 在多个 EVM 链之间比较 Token 数量时
  • 处理桥接资产时
  • 构建投资组合跟踪器、机器人或聚合器时

工作原理

不要假设稳定币使用相同的小数位。在运行时查询 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。

按链和 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()

防御性地处理特殊 Token

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

记录回退日志以便可视化。旧版或非标准 Token 仍然存在。

在 Solidity 中规范化为 WAD(18 位小数)

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()
  • 按链和 Token 地址缓存,而不是按符号缓存
  • 使用 Decimal、BigInt 或同等精确的数学运算,而不是 float
  • 在桥接或包装器变更后重新查询小数位
  • 在比较或价格计算之前,始终一致地规范化内部会计

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC