100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Building a Simple Token Contract

A hands-on walkthrough of writing an ERC-20 token in Solidity, from the standard's required interface to a minimal from-scratch implementation and the safer OpenZeppelin approach.

PracticeBeginner9 min readJul 10, 2026
Analogies

What an ERC-20 Token Really Is

A fungible token on Ethereum is just a smart contract that tracks who owns how much. The ERC-20 standard defines a common interface so wallets, exchanges, and other contracts can interact with any token the same way. At its core the contract holds a mapping from addresses to balances, plus an allowance mapping that lets one address authorize another to spend on its behalf. There is no physical coin—ownership is simply a number in the contract's storage, and transferring tokens means decrementing one balance and incrementing another while emitting a Transfer event.

🏏

Cricket analogy: An ERC-20 balance mapping is like the official scorebook: no physical runs exist, just numbers next to each batter's name that everyone agrees on, updated ball by ball.

The Required Interface

ERC-20 mandates a specific set of functions and events: totalSupply(), balanceOf(address), transfer(to, amount), approve(spender, amount), allowance(owner, spender), and transferFrom(from, to, amount), plus the Transfer and Approval events. The approve/transferFrom pair enables the allowance pattern, which lets contracts like a DEX move your tokens only up to a limit you set—critical for DeFi composability. Optional metadata functions name(), symbol(), and decimals() (usually 18) tell wallets how to display the token. Adhering exactly to this interface is what makes a token 'just work' across the entire Ethereum ecosystem.

🏏

Cricket analogy: The ERC-20 interface is like the fixed set of signals an umpire must know—out, four, six, wide; approve/transferFrom is the captain pre-authorizing a substitute to field within agreed limits.

A Minimal Implementation From Scratch

Writing ERC-20 by hand is instructive: the constructor mints the initial supply to the deployer, transfer moves tokens between balances after checking the sender has enough, and approve plus transferFrom implement delegated spending. Each state-changing function emits the appropriate event so off-chain tools can index activity. Note the decimals convention: with 18 decimals, one whole token is represented as 1e18 base units, so amounts are always in the smallest unit. This from-scratch version is great for learning, but for anything real you should extend OpenZeppelin's audited ERC20 base instead of trusting your own edge-case handling.

🏏

Cricket analogy: Coding ERC-20 by hand is like learning to keep score manually before trusting the electronic scoreboard—valuable practice, but for a Test match you use the certified system.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleToken {
    string public name = "SimpleToken";
    string public symbol = "SMPL";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(uint256 initialSupply) {
        totalSupply = initialSupply * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function transfer(address to, uint256 value) external returns (bool) {
        require(balanceOf[msg.sender] >= value, "Insufficient balance");
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);
        return true;
    }

    function approve(address spender, uint256 value) external returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        require(balanceOf[from] >= value, "Insufficient balance");
        require(allowance[from][msg.sender] >= value, "Allowance exceeded");
        allowance[from][msg.sender] -= value;
        balanceOf[from] -= value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
        return true;
    }
}

The production-grade approach is a few lines: import OpenZeppelin's ERC20 and mint in the constructor. Example: contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") { _mint(msg.sender, 1_000_000 * 10 ** decimals()); } }. You inherit audited transfer, approve, and allowance logic for free.

A classic pitfall is the approve race condition: changing an existing allowance directly can let a spender front-run and spend both the old and new amounts. Prefer OpenZeppelin's safeIncreaseAllowance/safeDecreaseAllowance, or set the allowance to zero before setting a new value. Also remember amounts are in base units—forgetting the 18-decimal scaling is a common bug.

  • An ERC-20 token is a contract that stores balances and allowances in mappings—no physical coin exists.
  • The standard interface (totalSupply, balanceOf, transfer, approve, allowance, transferFrom + Transfer/Approval events) makes tokens interoperable.
  • The approve/transferFrom allowance pattern enables DeFi composability like DEX swaps.
  • decimals is typically 18, so one whole token equals 1e18 base units; always work in base units.
  • Writing ERC-20 from scratch teaches the mechanics, but production tokens should extend OpenZeppelin's audited ERC20.
  • Watch for the approve race condition and use safeIncreaseAllowance/safeDecreaseAllowance to avoid it.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SolidityStudyNotes#BuildingASimpleTokenContract#Building#Simple#Token#Contract#StudyNotes#SkillVeris#ExamPrep