EVM 토큰 소수점 처리
묵시적 소수점 불일치는 오류 없이 잔액이나 USD 가치가 수십 배 차이 나는 버그를 발생시키는 가장 쉬운 원인 중 하나입니다.
사용 시기
- 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()를 조회하십시오 - 심볼이 아닌 체인과 토큰 주소로 캐싱하십시오
- float 대신
Decimal,BigInt또는 동등한 정확한 수학을 사용하십시오 - 브리징 또는 래퍼 변경 후 소수점을 다시 조회하십시오
- 비교 또는 가격 책정 전에 내부 회계를 일관되게 정규화하십시오