Hashtag Web3 Logo

Understanding ERC-20 Code

9 min
intermediate

Tokens are just spreadsheets

When people say "I have 100 USDC in my wallet," they are technically wrong. The USDC is not in their wallet. The USDC smart contract simply has a spreadsheet, and next to their wallet address, the number is 100.

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.

Let's look at the core logic of a token contract.

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: This is our security check. If the caller (msg.sender) tries to send 100 tokens but only has 50, the require statement fails. The transaction reverts, no gas is spent on execution, and no balances change.
  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?