Hashtag Web3 Logo

Understanding ERC-20 Code

9 min
intermediate

Balances are contract state

A token balance is recorded by the token contract. A wallet reads that state and displays the amount associated with an address.

ERC-20 Token = A Mapping Inside a Contract USDC Contract (0x1a2b...) 0xAlice... 100 USDC 0xBob... 2,500 USDC 0xCarol... 50 USDC Alice's Wallet Tokens aren't here! balanceOf[addr] → uint256

An ERC-20 token is a standardized smart contract. Because every ERC-20 contract has the exact same function names (like transfer and balanceOf), wallets like MetaMask know exactly how to interact with all of them.

The following simplified example shows balance storage and transfers. It is not a complete production token implementation.

The Core Data Structure: Mappings

To keep track of balances, Solidity uses a mapping. A mapping is like a dictionary or a hash table.

contract SimpleToken {
 // This creates a dictionary linking an address to a number
 mapping(address => uint256) public balanceOf;

 // Total supply of the token
 uint256 public totalSupply;

 // Token metadata
 string public name = "HashtagToken";
 string public symbol = "HTG";
 uint8 public decimals = 18;
}

In the balanceOf mapping:

  • The Key is an address (e.g., 0xabc...)
  • The Value is a uint256 (an unsigned integer, meaning no negative numbers).

If you call balanceOf[0xabc...], it returns the token balance of that address.

The Transfer Function

How do tokens move? We just subtract from one address in the mapping and add to another.

 // An event we will trigger when tokens move
 event Transfer(address indexed from, address indexed to, uint256 value);

 function transfer(address _to, uint256 _amount) public returns (bool) {
 // 1. Check: Does the sender have enough tokens?
 require(balanceOf[msg.sender] >= _amount, "Not enough tokens");

 // 2. Execute: Subtract from sender
 balanceOf[msg.sender] = balanceOf[msg.sender] - _amount;

 // 3. Execute: Add to receiver
 balanceOf[_to] = balanceOf[_to] + _amount;

 // 4. Log: Emit an event for the outside world
 emit Transfer(msg.sender, _to, _amount);

 return true;
 }

Breaking down the transfer

  1. require: The balance check rejects a transfer larger than the sender's balance. A revert rolls back state changes, but gas already consumed by execution is still charged.
  2. Math: We directly modify the balanceOf mapping. The sender loses tokens, the receiver gains them.
  3. emit Transfer(...): Blockchains are closed systems. To let frontends (like Etherscan or your React app) know that a transfer happened without them having to constantly read the contract state, we emit an event. Apps listen for these events to update UI histories.

The Decimals Problem

Solidity does not support floating point numbers (decimals). You cannot have 1.5 tokens in the code.

To fix this, tokens use very large integers. When you define decimals = 18 in the contract, it tells the frontend (like MetaMask) to move the decimal point 18 places to the left when showing the number to the user.

If you want to transfer exactly 1 token, you must pass the number 1000000000000000000 (1 followed by 18 zeros) to the transfer function. The smart contract handles the massive integer, but MetaMask formats it nicely as "1.00" for the user.

Key takeaways

  • Tokens do not live in wallets; they live in a mapping inside a smart contract.
  • Transferring tokens just means doing math: subtracting from the sender's mapping balance and adding to the receiver's.
  • require statements protect against invalid actions (like spending more than you have).
  • Events are emitted so off-chain apps know when things happen.
  • Solidity has no decimals. It uses massive integers, and the frontend handles the visual formatting.

Quiz: Understanding ERC-20 Code

1 / 5

What data structure does an ERC-20 contract use to keep track of everyone's token balances?