Back to Lessons
Lesson 06

Decimals: Why Tokens Use 18 Decimal Places

+15EFFORT
6 sections
decimals()
01
text

Solidity has no floating-point numbers. There are no fractions, no 0.5, no 3.14. Everything is an integer. So how do you represent '2.5 tokens'? ERC-20 solves this with the decimals() function. Most tokens use 18 decimals, meaning the smallest unit is 10^-18 of one token. When the contract stores '2500000000000000000', that represents 2.5 tokens to a human.

02
code
solidity
1// The decimals function — usually returns 18
2function decimals() public pure returns (uint8) {
3 return 18;
4}
5
6// Internally, all amounts are stored in the smallest unit (like wei)
7// 1 token = 1000000000000000000 (1e18)
8// 0.5 tokens = 500000000000000000 (5e17)
9// 0.01 token = 10000000000000000 (1e16)
10
11// The name(), symbol(), and decimals() form "token metadata"
12function name() public pure returns (string memory) {
13 return "Effort Token";
14}
15
16function symbol() public pure returns (string memory) {
17 return "EFFORT";
18}
03
text

Not all tokens use 18 decimals. USDC and USDT use 6 decimals (matching the cent precision of USD). WBTC uses 8 decimals (matching Bitcoin's satoshi). Some tokens use 0 decimals for non-divisible tokens. This is critical: if you confuse decimals between tokens, you could send 1,000,000x too much or too little. Always check decimals() before doing math with token amounts.

04
note
Key Insight

Common mistake: Sending raw numbers without converting. If you call transfer(recipient, 100) on an 18-decimal token, you are sending 0.0000000000000001 tokens — not 100 tokens. You need to send 100 * 10^18 = 100000000000000000000.

05
code
javascript
1// Converting between human-readable and raw amounts
2import { parseUnits, formatUnits, parseEther, formatEther } from "viem";
3
4// For 18-decimal tokens (most common):
5const rawAmount = parseEther("2.5");
6// Result: 2500000000000000000n (BigInt)
7
8const humanAmount = formatEther(2500000000000000000n);
9// Result: "2.5"
10
11// For tokens with different decimals (e.g., USDC = 6):
12const usdcRaw = parseUnits("100.50", 6);
13// Result: 100500000n
14
15const usdcHuman = formatUnits(100500000n, 6);
16// Result: "100.5"
17
18// DANGER: Using parseEther for USDC would give the wrong amount!
19// parseEther("100") = 100000000000000000000 (way too large for USDC)
06
text

The name(), symbol(), and decimals() functions are technically optional in EIP-20 (they are in the 'optional' section of the spec), but in practice every modern token implements them. These three metadata functions let wallets and dApps display tokens correctly. Without decimals(), a wallet could not know whether '1000000' means one million tokens or one token with 6 decimals.

Complete

Connect your wallet to mark this lesson as complete

Earn 15 EFFORT tokens