A Practical Guide to Building an ERC20 Token on Ethereum

·

The ERC20 token standard has been a cornerstone of the Ethereum ecosystem since its widespread adoption in 2017-2018. These digital assets have facilitated early-stage funding and liquidity for thousands of projects, with their elegant interface design quickly becoming the industry standard recognized by exchanges and wallet applications worldwide. Even today, many projects choose Ethereum for their initial development due to the programming convenience of creating ERC20-compliant tokens.

In this guide, we'll build an ERC20 token contract from scratch, incorporating compilation techniques and testing strategies to demonstrate how contracts managing digital assets for thousands of users evolve from concept to production.

Setting Up the Development Environment

Instead of downloading pre-made Truffle Boxes, we'll create a new blank project to build our token from the ground up.

Create a new directory for our project:

mkdir erc20-test
cd erc20-test/
truffle init

This gives us a basic project structure:

erc20-test/
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js

Since we'll be using a local Ganache/Test-RPC client for testing, we need to modify truffle.js to include our test network configuration:

module.exports = {
  networks: {
    development: {
      host: "localhost",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};

With our development environment configured, we can begin implementing the ERC20 token contract.

Understanding the ERC20 Basic Interface

The original ERC20 standard included only essential functions and public attributes, inspired by the EIP179 proposal. This foundational interface consists of three functions and one event:

pragma solidity ^0.4.24;

contract ERC20Basic {
  function totalSupply() public view returns (uint256);
  function balanceOf(address _who) public view returns (uint256);
  function transfer(address _to, uint256 _value) public returns (bool);
  
  event Transfer(
    address indexed from,
    address indexed to,
    uint256 value
  );
}

Let's examine each component:

The contract also defines a Transfer event that records sender, recipient, and token amount. Crucially, the transfer() function must verify the sender has sufficient balance to prevent unauthorized token transfers.

Expanding to the Full ERC20 Interface

The complete ERC20 standard extends the basic interface with additional functionality that enables more sophisticated token management patterns. This enhanced interface introduces an "allowance" system that enables delegated spending:

pragma solidity ^0.4.24;
import "./ERC20Basic.sol";

contract ERC20 is ERC20Basic {
  function allowance(address _owner, address _spender) public view returns (uint256);
  function transferFrom(address _from, address _to, uint256 _value) public returns (bool);
  function approve(address _spender, uint256 _value) public returns (bool);
  
  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );
}

The inheritance relationship is established with "contract ERC20 is ERC20Basic". The expanded interface includes:

The Approval event records authorization actions, capturing owner, spender, and approved amount.

Implementing SafeMath for Secure Arithmetic

Ethereum lacks built-in integer bounds checking, making overflow/underflow vulnerabilities a significant concern. Without proper safeguards, values can wrap around unexpectedly (e.g., maximum value + 1 becomes minimum value), potentially causing substantial financial losses.

The OpenZeppelin SafeMath library provides secure arithmetic operations for our ERC20 implementation:

pragma solidity ^0.4.24;

library SafeMath {
  function mul(uint256 _a, uint256 _b) internal pure returns (uint256 c) {
    if (_a == 0) {
      return 0;
    }
    c = _a * _b;
    assert(c / _a == _b);
    return c;
  }
  
  function div(uint256 _a, uint256 _b) internal pure returns (uint256) {
    return _a / _b;
  }
  
  function sub(uint256 _a, uint256 _b) internal pure returns (uint256) {
    assert(_b <= _a);
    return _a - _b;
  }
  
  function add(uint256 _a, uint256 _b) internal pure returns (uint256 c) {
    c = _a + _b;
    assert(c >= _a);
    return c;
  }
}

The SafeMath library operates as follows:

All functions use internal pure modifiers, indicating they're inherited utility functions that don't modify or read blockchain state.

Building the CAT Token Implementation

With our foundation in place, we can now create our example token - "CAT Token" - that implements the full ERC20 standard with SafeMath protection.

Our implementation follows this inheritance structure:

The project structure now includes:

erc20-test/
├── contracts
│   ├── Cat.sol
│   ├── ERC20.sol
│   ├── ERC20Basic.sol
│   ├── Migrations.sol
│   └── SafeMath.sol
├── migrations
│   └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js

The CAT token implementation begins with necessary imports and variable declarations:

pragma solidity ^0.4.24;
import "./ERC20.sol";
import "./SafeMath.sol";

contract CAT is ERC20 {
  using SafeMath for uint256;
  
  string public constant name = "CAT Token";
  string public constant symbol = "CAT";
  uint8 public constant decimals = 18;
  uint256 internal totalSupply_;
  
  mapping(address => uint256) public balances;
  mapping (address => mapping (address => uint256)) internal allowed;

The "using SafeMath for uint256" directive enables safe arithmetic operations on all uint256 variables. We've defined:

The constructor initializes our token supply:

constructor() public {
  totalSupply_ = 1 * (10 ** 10) * (10 ** 18); // 10,000,000,000 tokens
  balances[msg.sender] = totalSupply_;
  emit Transfer(0, msg.sender, totalSupply_);
}

This creates 10 billion CAT tokens, each divisible to 18 decimal places, and assigns the entire supply to the contract creator.

The basic ERC20 functions are implemented with safety checks:

function totalSupply() public view returns (uint256) {
  return totalSupply_;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
  return balances[_owner];
}

function transfer(address _to, uint256 _value) public returns (bool) {
  require(_to != address(0), "Cannot send to zero address.");
  require(_value <= balances[msg.sender], "Insufficient balance.");
  
  balances[msg.sender] = balances[msg.sender].sub(_value);
  balances[_to] = balances[_to].add(_value);
  emit Transfer(msg.sender, _to, _value);
  return true;
}

The transfer function includes critical safety checks:

  1. Prevents transfers to the zero address
  2. Verifies sufficient sender balance
  3. Uses SafeMath operations for arithmetic
  4. Emits appropriate Transfer event

The approval system functions complete our implementation:

function allowance(address _owner, address _spender) public view returns (uint256) {
  return allowed[_owner][_spender];
}

function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
  require(_value <= balances[_from], "Insufficient _from balance.");
  require(_value <= allowed[_from][msg.sender], "Allowance exceeded.");
  require(_to != address(0), "Cannot send to zero address.");
  
  balances[_from] = balances[_from].sub(_value);
  balances[_to] = balances[_to].add(_value);
  allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
  emit Transfer(_from, _to, _value);
  return true;
}

function approve(address _spender, uint256 _value) public returns (bool) {
  allowed[msg.sender][_spender] = _value;
  emit Approval(msg.sender, _spender, _value);
  return true;
}

The transferFrom implementation includes three safety checks and properly reduces the allowance after transfer. Notably, the approve function doesn't require the approver to have sufficient balance at approval time—this check occurs during the actual transfer.

With our complete ERC20 implementation, we're ready to proceed with testing and deployment. 👉 Explore more deployment strategies

Frequently Asked Questions

What is the main purpose of the ERC20 standard?
ERC20 provides a standardized interface for fungible tokens on Ethereum, ensuring compatibility across wallets, exchanges, and dApps. This standardization eliminates the need for custom implementations for each new token, significantly reducing development time and increasing interoperability within the Ethereum ecosystem.

Why is SafeMath important in token contracts?
SafeMath prevents arithmetic overflow/underflow vulnerabilities that could lead to massive token losses or unauthorized creation of tokens. These security issues are particularly dangerous in financial contracts where exact arithmetic is critical for maintaining proper accounting of token balances and transfers.

How does the allowance system work in ERC20?
The allowance system enables delegated spending, where token owners can authorize third parties to transfer specific amounts on their behalf. This creates flexible spending patterns similar to setting limits on credit cards, where the owner maintains control while granting limited spending privileges to others.

What are the key security considerations for ERC20 contracts?
Important security measures include preventing transfers to zero addresses, checking for sufficient balances before transfers, using safe arithmetic operations, and properly managing approval amounts. Additionally, contracts should implement reentrancy protection and follow the checks-effects-interactions pattern to avoid common vulnerabilities.

Can ERC20 tokens have different decimal precision?
Yes, the decimals field allows tokens to specify their divisibility, with 18 being common but values from 0-18 all being valid. This flexibility allows token creators to match the divisibility requirements of their specific use case, whether they need whole units or highly divisible tokens.

What's the difference between transfer and transferFrom?
Transfer moves tokens from the sender's address, while transferFrom moves tokens from an address that has approved the sender to spend on their behalf. This distinction enables sophisticated financial interactions where users can delegate spending authority without transferring ownership of their tokens.