evm-gotchaslisted
Install: claude install-skill Mixard/fable-pack
# EVM Gotchas
## Token decimals vary per chain and bridge
The same symbol does not imply the same decimals across chains or bridge deployments. Hardcoding a divisor (e.g. `1_000_000` because a stablecoin has 6 decimals on one chain) produces balances and USD values off by orders of magnitude with no error thrown. Bridged and wrapped tokens can change precision relative to the origin asset.
Rules:
- Query `decimals()` at runtime, never assume by symbol
- Cache keyed by `(chain_id, token_address)`, not by symbol
- Use exact math (`Decimal`, `BigInt`), not floats
- Re-query decimals after bridging or wrapper changes
- Normalize internal accounting to one precision before comparison or pricing
### Runtime lookup (Python / web3.py)
```python
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)
```
### Cache by (cha