Skills affaan-m ecc

evm-token-decimals

Use it when:
You read ERC-20 balances or price tokens across EVM chains, or a balance or USD value is off by orders of magnitude.
It does:
Has the agent query decimals() on-chain, cache it per chain and token address, and normalize amounts with exact math.
You get:
Code in Python with web3.py, TypeScript with ethers and Solidity, plus a cast command to check a token's decimals.
Why:
The same stablecoin can use different decimals on different chains, and the bug throws no error.

TL;DR

A short rule set from ECC for a bug that fails silently: assuming a token's decimals. A symbol with 6 decimals on one chain can have a different count elsewhere, so it never hardcodes them. The agent queries decimals() at runtime, caches the result by chain ID and token address rather than by symbol, and does value math with Decimal, BigInt or another exact type instead of floats. For old or non-standard tokens whose decimals() call reverts, it falls back to 18 and logs a visible warning. In Solidity it normalizes amounts to 18 decimals (a WAD) before comparing or pricing. Examples cover a web3.py balance read and a cached decimals lookup, ethers' formatUnits in TypeScript and a Foundry cast call for a quick on-chain check. It also says to query decimals again after bridging or a wrapper change.

Summarizes the skill at d2ef268.

npx skills add https://github.com/affaan-m/ecc --skill evm-token-decimals
Weekly Downloads week to Nov 9, 2026
438
+73% in 8 weeks

More weekly downloads than 55% of skills

Weekly values
Week toInstalls
Sep 21253
Sep 28159
Oct 5136
Oct 12229
Oct 19405
Oct 26397
Nov 2465
Nov 9438
Total Downloads on skills.sh
5,300

Fewer downloads than 91% of skills

Repo stars affaan-m/ecc
299,776

More stars than 97% of repos

Age published May 19, 2026
5 months

Newer than 66% of skills

Category

Test-first work, implementation from specs, git hygiene and scope control.

Everything for building software: planning, coding, debugging, design, frameworks, cloud, agents and skills.

Type

Knowledge and rules the agent applies while it works: best practices, design principles, API references.

Effect

Adds no side effects of its own. It shapes how the agent thinks, writes or reviews. Guidelines and routers are always effect:none, even when the task they support edits code.

Needs

Instructions only. Works in any agent that supports skills.

Publisher

Everyone else. Unverified vendor claims default to community.

Topics
Stack

SKILL.md

From affaan-m/ecc at d2ef268. Checked daily, last changed Nov 6, 2026.

Frontmatter
name: evm-token-decimals
description: Prevent silent decimal mismatch bugs across EVM chains. Covers runtime decimal lookup, chain-aware caching, bridged-token precision drift, and safe normalization for bots, dashboards, and DeFi tools. Use when handling token amounts across EVM chains, or when a balance, price, or transfer amount is off by orders of magnitude.
metadata:
  version: "1.0.0"
  origin: ECC direct-port adaptation

EVM Token Decimals

Silent decimal mismatches are one of the easiest ways to ship balances or USD values that are off by orders of magnitude without throwing an error.

When to Use

  • Reading ERC-20 balances in Python, TypeScript, or Solidity
  • Calculating fiat values from on-chain balances
  • Comparing token amounts across multiple EVM chains
  • Handling bridged assets
  • Building portfolio trackers, bots, or aggregators

How It Works

Never assume stablecoins use the same decimals everywhere. Query decimals() at runtime, cache by (chain_id, token_address), and use decimal-safe math for value calculations.

Examples

Query decimals at runtime
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)

Do not hardcode 1_000_000 because a symbol usually has 6 decimals somewhere else.

Cache by chain and 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()
Handle odd tokens defensively
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

Log the fallback and keep it visible. Old or non-standard tokens still exist.

Normalize to 18-decimal WAD in Solidity
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 with 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);
}
Quick on-chain check
cast call <token_address> "decimals()(uint8)" --rpc-url <rpc>

Rules

  • Always query decimals() at runtime
  • Cache by chain plus token address, not symbol
  • Use Decimal, BigInt, or equivalent exact math, not float
  • Re-query decimals after bridging or wrapper changes
  • Normalize internal accounting consistently before comparison or pricing