EVM トークン Decimals
小数点の暗黙的な不一致は、エラーを発生させずに残高や USD 価格が桁違いにずれる原因となる最も簡単な方法のひとつです。
使用するタイミング
- Python、TypeScript、または Solidity で ERC-20 残高を読み取る場合
- オンチェーン残高から法定通貨相当額を計算する場合
- 複数の EVM チェーンにわたってトークン量を比較する場合
- ブリッジされた資産を扱う場合
- ポートフォリオトラッカー、ボット、またはアグリゲーターを構築する場合
仕組み
ステーブルコインがどこでも同じ decimals を持つと想定しないでください。実行時に decimals() をクエリし、(chain_id, token_address) でキャッシュし、decimal セーフな計算を使用して値を計算します。
例
実行時に decimals をクエリ
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 桁の decimals を持つからといって、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);
}
TypeScript (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);
}
クイックオンチェーンチェック
cast call <token_address> "decimals()(uint8)" --rpc-url <rpc>
ルール
- 必ず実行時に
decimals()をクエリする - シンボルではなく、チェーンとトークンアドレスでキャッシュする
Decimal、BigInt、または同等の正確な計算を使用し、float は使用しない- ブリッジやラッパーの変更後は decimals を再クエリする
- 内部会計を比較や価格設定の前に一貫して正規化する