Hands-On Guide: Building an ERC20 Token Contract on Ethereum

·

Let's set aside the MetaCoin project for now and start building our own contract from scratch. A perfect example is the ERC20 token contract—one of the most popular DApp standards on Ethereum during 2017-2018. This elegant interface design became the de facto standard adopted by exchanges and wallet applications, facilitating early-stage fundraising and liquidity for thousands of projects.

Project Initialization

We'll create a new blank project without using any Truffle Box templates:

$ mkdir erc20-test
$ cd erc20-test/
$ truffle init

The basic directory structure will look like this:

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

Configure truffle.js for local Ganache/Test-RPC testing:

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

ERC20 Basic Contract Interface

The original ERC20 Basic contract supports 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
  );
}

Key components:

Enhanced ERC20 Interface

The expanded ERC20 contract adds critical functionality:

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
  );
}

New features:

Secure Mathematics with SafeMath

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;
  }
}

Security features:

Implementing CAT Token

Our complete CAT token implementation:

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;

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

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

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

  function transfer(address _to, uint256 _value) public returns (bool) {
    require(_to != address(0), "Invalid recipient 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;
  }

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

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

  function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
    require(_value <= balances[_from], "Insufficient source balance");
    require(_value <= allowed[_from][msg.sender], "Allowance exceeded");
    require(_to != address(0), "Invalid recipient 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;
  }
}

Testing and Deployment

👉 Learn about deploying smart contracts with Truffle's comprehensive testing framework. The complete test suite should verify:

  1. Initial token distribution
  2. Basic transfer functionality
  3. Allowance mechanisms
  4. Edge cases and security checks

Frequently Asked Questions

What makes ERC20 tokens special?

ERC20 provides a standardized interface that ensures compatibility across wallets, exchanges, and other Ethereum applications. This standardization reduces development effort and increases interoperability.

How many decimal places should my token use?

Most tokens use 18 decimals to match Ethereum's ether denomination, but you can choose any value between 0-18 based on your tokenomics.

What's the difference between transfer() and transferFrom()?

transfer() moves tokens directly from the sender's balance, while transferFrom() allows an approved spender to move tokens on behalf of the owner.

👉 Discover more advanced token features like minting, burning, and pausable contracts to enhance your token's functionality.

How do I ensure my token contract is secure?

Conclusion

This guide has walked you through creating a production-ready ERC20 token contract from scratch. By implementing proper security measures, clear interfaces, and thorough testing, you can deploy tokens that will safely handle millions of dollars in value.