- Authors
Coin vs Token
Coin
A coin is a native asset of its own blockchain network. Simply put, it's the "default currency" of that blockchain.
Characteristics
- Has its own blockchain: Operates independent blockchain infrastructure
- Pays network fees: Used to pay transaction fees (gas) on that blockchain
- Protocol level: Built into the blockchain protocol itself
Examples
- Bitcoin (BTC): Native coin of the Bitcoin blockchain
- Ether (ETH): Native coin of the Ethereum blockchain
- Solana (SOL): Native coin of the Solana blockchain
Token
A token is a digital asset created via smart contracts on top of an existing blockchain. It doesn't have its own blockchain—it borrows infrastructure from another blockchain.
Characteristics
- Uses existing blockchain: No need to build a new blockchain
- Implemented via smart contracts: Operates according to rules defined in code
- Quick to deploy: Create a new token with just a few lines of code
Examples (Ethereum-based)
- USDT: Tether stablecoin
- USDC: USD Coin stablecoin
- UNI: Uniswap governance token
- LINK: Chainlink oracle token
On Ethereum, ERC-20 is the most widely used token standard.
Key Differences
| Category | Coin | Token |
|---|---|---|
| Blockchain | Has its own blockchain | Uses existing blockchain |
| Creation method | Built into blockchain protocol | Implemented via smart contract |
| Storage | Stored directly in account | Stored in contract |
| Gas fees | Paid in that coin | Paid in base blockchain's coin |
| Examples | BTC, ETH, SOL | USDT, UNI, LINK |
What is ERC-20?
Fungible Tokens
ERC-20 tokens are fungible digital assets. Being fungible means each token is indistinguishable from another and has the same value.
For example:
- **100 for Bob's $100 causes no loss = Fungible
- ERC-20 tokens: Swapping Alice's 100 USDT for Bob's 100 USDT causes no loss = Fungible
The Birth of ERC-20 Standard
ERC-20 (Ethereum Request for Comment 20) is a token standard proposed by Fabian Vogelsteller in 2015.
Why we need a standard
- All tokens operate with the same interface
- Wallets like MetaMask can automatically support all ERC-20 tokens
- Exchanges like Uniswap can easily add new tokens
Where Are Coins and Tokens Stored?
Where Coins (ETH) Are Stored
ETH is stored directly in each account's State.
The Ethereum blockchain stores all account information in a State Database. This database is a giant map that stores account addresses as keys and account information as values.
Each account's state includes the following information:
- nonce: Number of transactions sent from the account
- balance: ETH balance — stored here!
- storageRoot: Hash of smart contract storage
- codeHash: Hash of smart contract code
ETH is managed directly at the blockchain protocol level, without a separate contract.
Where Tokens Are Stored
Tokens are stored in smart contracts, not in wallets.
An ERC-20 token contract is like a bank ledger. Inside the contract, there's a data structure called mapping that records how many tokens each address owns:
// Inside ERC-20 contract
mapping(address => uint256) private _balances;
// Example:
// _balances[0x1234...] = 1,000 ← Alice owns 1,000 tokens
// _balances[0x5678...] = 500 ← Bob owns 500 tokens
Tokens are not in your wallet. They're recorded in the contract's storage, and your wallet only has the authority to move those tokens.
Each token has its own contract and manages its own ledger. The same address can own 1,000 tokens in the USDT contract and 50 tokens in the UNI contract.
How Do Wallet Services Display Tokens?
When you see tokens in a wallet like MetaMask, it's actually querying multiple contracts:
What the wallet service does:
1. My address: 0x1234...
2. Query USDT contract: balanceOf(0x1234...)
→ Response: 1,000 USDT
3. Query UNI contract: balanceOf(0x1234...)
→ Response: 50 UNI
4. Query LINK contract: balanceOf(0x1234...)
→ Response: 200 LINK
5. Display on screen: "USDT: 1,000 | UNI: 50 | LINK: 200"
Wallet services query these contracts on your behalf and display them nicely. That's why lesser-known tokens don't appear automatically—you need to manually enter the contract address. The wallet needs to know the token's contract address to query it.
Core ERC-20 Functions
1. View Functions
View functions only read blockchain state without modifying it. They don't cost gas.
// Returns total token supply
// Sum of all tokens owned by all addresses
function totalSupply() public view returns (uint256)
// Returns token balance of a specific address
// Wallets call this function to display tokens
function balanceOf(address account) public view returns (uint256)
// Returns amount of tokens owner approved for spender
// Check the limit that spender can transfer on behalf
function allowance(address owner, address spender) public view returns (uint256)
2. Transaction Functions
Transaction functions modify blockchain state. They create transactions and require gas.
// Caller transfers amount tokens to recipient
// The most basic transfer method
function transfer(address recipient, uint256 amount) public returns (bool)
// Approves spender to transfer amount tokens on caller's behalf
// Essential function in DeFi
function approve(address spender, uint256 amount) public returns (bool)
// Transfers tokens from sender to recipient within approved amount
// Used together with approve
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool)
3. Events
Events log to the blockchain, allowing external systems to track token movements.
// Emitted whenever tokens are transferred
// Emitted on transfer() and transferFrom() calls
event Transfer(address indexed from, address indexed to, uint256 value)
// Emitted when token spending is approved
// Emitted on approve() call
event Approval(address indexed owner, address indexed spender, uint256 value)
ERC-20 Contract Implementation Example
Here's how an actual ERC-20 token contract is implemented:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleERC20 {
// Token information
string public name = "My Token";
string public symbol = "MTK";
uint8 public decimals = 18;
// Total supply
uint256 private _totalSupply;
// Mapping to store each address's balance (this is the ledger!)
mapping(address => uint256) private _balances;
// Mapping to store approval records
mapping(address => mapping(address => uint256)) private _allowances;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// Constructor: mint initial tokens
constructor(uint256 initialSupply) {
_totalSupply = initialSupply * 10**decimals;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
// Query total supply
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
// Query balance
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
// Transfer tokens
function transfer(address recipient, uint256 amount) public returns (bool) {
require(recipient != address(0), "Invalid recipient");
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
// Approve
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "Invalid spender");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// Query allowance
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
// Delegated transfer
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
require(sender != address(0), "Invalid sender");
require(recipient != address(0), "Invalid recipient");
require(_balances[sender] >= amount, "Insufficient balance");
require(_allowances[sender][msg.sender] >= amount, "Allowance exceeded");
_balances[sender] -= amount;
_balances[recipient] += amount;
_allowances[sender][msg.sender] -= amount;
emit Transfer(sender, recipient, amount);
return true;
}
}
Core Mechanics
1. Token Transfer (transfer)
The most basic token transfer method. What actually happens when Alice sends tokens to Bob:
- Alice calls the
transfer()function from her wallet - Contract checks if Alice has sufficient balance
- Contract updates the
_balancesmapping values Transferevent is emitted
Tokens don't move directly from account to account. The contract updates its own ledger (mapping).
// Alice transfers 100 tokens to Bob
token.transfer(bobAddress, 100);
// What happens inside the contract:
require(_balances[alice] >= 100, "Insufficient balance");
_balances[alice] -= 100; // Alice: 1000 → 900
_balances[bob] += 100; // Bob: 500 → 600
emit Transfer(alice, bob, 100);
Gas fees are paid in ETH during this process. Even when sending tokens, transaction fees are paid in ETH, the native coin of the Ethereum network.
2. Approval and Delegated Transfer (approve & transferFrom)
The most important pattern in DeFi. Used when a smart contract needs to transfer tokens on behalf of a user.
Example of Alice trading tokens on Uniswap:
- approve(): Alice approves the Uniswap contract: "You can use up to 1000 of my tokens"
- transferFrom(): Uniswap contract transfers Alice's tokens on her behalf within the approved limit
This two-step process is necessary because smart contracts cannot directly take tokens from users. Users must grant permission first before contracts can move tokens.
// Step 1: Alice approves Uniswap contract to use 1000 tokens
token.approve(uniswapAddress, 1000);
// Inside the contract:
_allowances[alice][uniswap] = 1000; // Record approval
emit Approval(alice, uniswap, 1000);
// Step 2: Uniswap contract moves Alice's tokens to the pool
token.transferFrom(alice, uniswapPoolAddress, 100);
// Inside the contract:
require(_allowances[alice][uniswap] >= 100, "Allowance exceeded");
require(_balances[alice] >= 100, "Insufficient balance");
_balances[alice] -= 100;
_balances[uniswapPool] += 100;
_allowances[alice][uniswap] -= 100; // Deduct from allowance
emit Transfer(alice, uniswapPool, 100);
What this pattern enables:
- DEXs (Decentralized Exchanges): Uniswap, Sushiswap automatically process token swaps
- Lending Protocols: Aave, Compound automatically handle token deposits/withdrawals
- NFT Marketplaces: OpenSea automatically processes ERC-20 token payments
- Staking: Protocols automatically distribute reward tokens
The amount approved via approve() can be taken by that contract at any time. Never approve untrusted contracts.
Decimals
ERC-20 tokens use a decimals value to support decimal places.
Since Solidity doesn't support floating-point numbers, all values are stored as integers and only converted using decimals when displaying. For example, when minting 1.0 tokens with decimals = 18, the actual stored value is 1 × 10^18 = 1000000000000000000.
This approach allows for precise calculations without floating-point errors. USDT and USDC use decimals = 6, while most ERC-20 tokens use decimals = 18, same as ETH.
Creating ERC-20 Tokens with OpenZeppelin
When writing ERC-20 contracts, it's recommended to use OpenZeppelin ERC-20's audited implementation. You can easily create secure and efficient tokens.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
// OpenZeppelin ERC20 uses decimals = 18 by default
// To mint 1,000,000 tokens, pass 1000000 * 10^18
_mint(msg.sender, 1000000 * 10**18);
}
// decimals() is already implemented in ERC20 (default: 18)
// To use a different value, override it:
// function decimals() public view virtual override returns (uint8) {
// return 6; // Change to 6 like USDT, USDC
// }
}