DEFI LIBRARY FOUNDATIONAL CONCEPTS

From Tokens to Contracts Learning ERC-20 Fundamentals

4 min read
#Ethereum #Smart Contracts #Token Standards #ERC-20 #Tokenomics
From Tokens to Contracts Learning ERC-20 Fundamentals

In the world of decentralized finance, the first step that many developers and entrepreneurs take is learning how to create and manage tokens. Tokens are the building blocks of any blockchain application that needs a form of digital value. Whether you want to launch a new cryptocurrency, create a utility token for a service, or issue rewards for a community, you will almost certainly end up interacting with the ERC‑20 standard. This article walks you through the key concepts, the practical steps to implement an ERC‑20 contract, and the best practices that keep your token safe and compliant.


Why ERC‑20 Is Still the Cornerstone

The Ethereum network was designed for programmable contracts, and over the years a handful of standards emerged. ERC‑20 was the first that gained mass adoption because it established a simple, consistent interface for fungible tokens. That consistency matters for several reasons:

  • Wallet support – Every Ethereum wallet knows how to display ERC‑20 balances and transfer them.
  • Exchange listings – Most decentralized and centralized exchanges accept ERC‑20 tokens because they can query balances and execute trades automatically.
  • Inter‑contract compatibility – Many smart contracts (e.g., lending platforms, staking protocols, and marketplaces) expect ERC‑20 tokens to follow the standard to interact correctly.

Because of this widespread compatibility, mastering ERC‑20 is a prerequisite for any serious work on Ethereum.


The Core Functions of an ERC‑20 Token

At its heart, an ERC‑20 token is just a set of functions and events defined in the Ethereum Virtual Machine. Below is a quick reference of the essential pieces:

Component Purpose
totalSupply() Returns the total number of tokens in existence.
balanceOf(address) Returns the token balance of a given address.
transfer(address, uint256) Moves tokens from the caller’s address to another.
allowance(address, address) Returns how many tokens the owner allowed a spender to transfer.
approve(address, uint256) Authorizes a spender to transfer up to a specified amount.
transferFrom(address, address, uint256) Moves tokens on behalf of the owner, as authorized.
Transfer event Emitted whenever a transfer occurs.
Approval event Emitted when an allowance is set or updated.

These functions form a minimal interface that all ERC‑20 tokens must implement. Additional features—such as pausing transfers, adding minting or burning capabilities, or integrating with ERC‑721—can be added on top, but the base contract should expose the items listed above.


A Step‑by‑Step Guide to Writing Your First ERC‑20

Below is a concise tutorial that takes you from a blank file to a verified, deployable token. All code is written in Solidity 0.8.x, the latest stable release that includes built‑in overflow protection.

1. Set Up Your Development Environment

  1. Install Node.js (v20+ recommended).
  2. Create a new project folder and run npm init -y.
  3. Install Hardhat, a popular Ethereum development framework:
npm install --save-dev hardhat
  1. Initialize Hardhat with npx hardhat. Choose “Create an empty hardhat.config.js.”

You now have a clean repository ready to compile, test, and deploy contracts.

2. Write the ERC‑20 Contract

Create contracts/MyToken.sol and paste the following code:

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @title MyToken
/// @notice A simple ERC‑20 token with a capped supply and optional minting.
contract MyToken is ERC20 {
    uint256 private _cap;

    constructor(string memory name_, string memory symbol_, uint256 initialSupply_, uint256 cap_) ERC20(name_, symbol_) {
        require(cap_ > 0, "Cap must be greater than zero");
        _cap = cap_;
        _mint(msg.sender, initialSupply_);
    }

    /// @notice Returns the cap on total supply.
    function cap() public view returns (uint256) {
        return _cap;
    }

    /// @notice Mints new tokens, respecting the cap.
    /// @dev Only the contract owner can call this function.
    function mint(address to, uint256 amount) public onlyOwner {
        require(totalSupply() + amount <= _cap, "Cap exceeded");
        _mint(to, amount);
    }

    // Override _mint to enforce cap
    function _mint(address account, uint256 amount) internal virtual override {
        require(totalSupply() + amount <= _cap, "Cap exceeded");
        super._mint(account, amount);
    }
}

Key Points

  • OpenZeppelin Imports – Using OpenZeppelin’s audited ERC‑20 base contract reduces risk.
  • Cap Logic – A cap variable limits the maximum number of tokens that can ever exist.
  • Owner‑Only Minting – The onlyOwner modifier (inherited from Ownable) ensures only the deployer can mint new tokens.

3. Compile the Contract

npx hardhat compile

If the compiler reports no errors, you’re ready for tests.

4. Write Unit Tests

Create test/MyToken.js:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function () {
  let Token, token, owner, addr1, addr2;

  beforeEach(async function () {
    Token = await ethers.getContractFactory("MyToken");
    [owner, addr1, addr2] = await ethers.getSigners();
    token = await Token.deploy("MyToken", "MTK", ethers.parseEther("1000"), ethers.parseEther("2000"));
    await token.waitForDeployment();
  });

  it("Should set the right total supply", async function () {
    expect(await token.totalSupply()).to.equal(ethers.parseEther("1000"));
  });

  it("Should mint new tokens within the cap", async function () {
    await token.mint(addr1.address, ethers.parseEther("500"));
    expect(await token.balanceOf(addr1.address)).to.equal(ethers.parseEther("500"));
    expect(await token.totalSupply()).to.equal(ethers.parseEther("1500"));
  });

  it("Should not exceed cap", async function () {
    await expect(token.mint(addr1.address, ethers.parseEther("1500"))).to.be.revertedWith("Cap exceeded");
  });
});

Run the tests:

npx hardhat test

All tests should pass if the contract behaves as expected.

5. Deploy to a Testnet

Edit hardhat.config.js to include a testnet RPC URL and private key. Then run:

npx hardhat run scripts/deploy.js --network sepolia

The script will deploy your token, and you’ll receive the contract address.


Understanding Gas Costs and Optimization

When you deploy or interact with ERC‑20 contracts, gas consumption is a key concern. The following guidelines help keep costs reasonable:

  1. Avoid Repeated Storage Writes – Each storage slot write costs ~20,000 gas. Store values that rarely change in a single slot.
  2. Use unchecked Blocks – In Solidity 0.8+, overflow checks add overhead. For internal arithmetic that is guaranteed safe, wrap operations in unchecked {}.
  3. Minimize Event Logs – Events are stored on-chain; each log entry costs gas. Emit only essential events (e.g., Transfer and Approval).
  4. Batch Operations – If you need to transfer tokens to many recipients, consider a multi‑transfer function that loops internally but still incurs fewer transaction overheads than individual transfers.

Here is a small example of a gas‑efficient transfer that uses unchecked:

function _transfer(address from, address to, uint256 amount) internal virtual override {
    require(from != address(0) && to != address(0), "Zero address");
    unchecked {
        uint256 fromBalance = balanceOf(from);
        require(fromBalance >= amount, "Insufficient balance");
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;
    }
    emit Transfer(from, to, amount);
}

Common Pitfalls and How to Avoid Them

Pitfall Explanation Mitigation
Not verifying signatures Attackers could manipulate approvals. Use SafeERC20 wrapper from OpenZeppelin to handle safe approvals.
Hardcoding addresses Statically assigned addresses become brittle. Store addresses in a mapping or upgradeable proxy.
Exposing private data Accidentally logging sensitive data. Keep private variables strictly off‑chain and never log them.
Using transfer for external calls transfer has a fixed gas stipend and may fail. Prefer call with a gas limit, and check the return value.
Ignoring receive() and fallback() functions Your contract might revert on unexpected calls. Implement minimal receive() to accept native Ether if needed.

Interacting with Your Token After Deployment

Once the token is live, you can interact with it through any Ethereum wallet that supports ERC‑20, such as MetaMask, Trust Wallet, or Ledger Live. Here are the steps to add a custom token:

  1. Open your wallet and choose “Add Custom Token.”
  2. Input the contract address, token symbol, and decimals (18 for most ERC‑20 tokens).
  3. Confirm and view your balance.

For programmatic interactions, use Web3.js or Ethers.js. Below is an Ethers.js snippet that transfers tokens from a user to another address:

const { ethers } = require("ethers");

const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_KEY");
const signer = provider.getSigner(); // Assume the signer holds the private key

const tokenAddress = "0xYourTokenAddress";
const abi = [
  "function transfer(address to, uint256 amount) external returns (bool)",
];

const tokenContract = new ethers.Contract(tokenAddress, abi, signer);

async function sendTokens(to, amount) {
  const tx = await tokenContract.transfer(to, ethers.parseEther(amount.toString()));
  await tx.wait();
  console.log(`Transferred ${amount} MTK to ${to}`);
}

Governance, Upgradeability, and Future Directions

While a vanilla ERC‑20 contract works for many use cases, more sophisticated projects require dynamic behavior. Here are some strategies:

  1. Proxy Patterns – Use the Transparent or UUPS proxy pattern from OpenZeppelin to upgrade your contract logic without losing state.
  2. Governance Modules – Add a governance contract that allows token holders to vote on parameter changes, such as the cap or minting rights.
  3. ERC‑2612 Permit – Integrate off‑chain signatures to allow approvals without a transaction, reducing gas costs for end users.
  4. EIP‑1400 – Consider a more advanced token standard if you need partitioning or compliance features.

Each added feature introduces complexity, so weigh the benefits against the risks of a larger attack surface.


Testing Edge Cases

A well‑tested token is essential for security. Test the following edge cases:

  • Zero address transfers – Ensure that transfers to or from the zero address revert.
  • Allowance exhaustion – Confirm that transferFrom reverts when the spender attempts to exceed the allowance.
  • Cap enforcement – Verify that no minting can push the total supply above the cap.
  • Reentrancy – Though ERC‑20 itself is non‑stateful, when combined with other contracts (e.g., staking), test reentrancy guards.

Automated test frameworks can run these checks every time you push code, catching regressions early.


Deployment Checklist

Before going live, make sure you have completed the following:

  1. Security Audit – Either perform your own audit or hire an external firm.
  2. Unit and Integration Tests – Run them on a local node and a public testnet.
  3. Verify Contract Source – Publish the source code on Etherscan or a similar explorer.
  4. Liquidity Provision – If you plan to list on an exchange, consider providing initial liquidity on Uniswap or SushiSwap.
  5. Marketing and Community – Prepare a website, whitepaper, and social channels to inform potential users.

Resources for Further Learning

  • OpenZeppelin Documentation – Best practices for secure smart contracts.
  • Ethereum Improvement Proposals (EIPs) – ERC‑20, ERC‑2612, ERC‑1400, etc.
  • Hardhat Documentation – Tips for testing, deployment, and debugging.
  • Solidity Documentation – Language specifics and compiler options.
  • Community Forums – Ethereum StackExchange, r/ethdev, and Discord channels for real‑time help.

Closing Thoughts

ERC‑20 may be one of the earliest token standards, but it remains a core pillar of the Ethereum ecosystem. By mastering its fundamentals—understanding the required functions, writing secure contracts, and implementing best practices—you open the door to building robust DeFi projects, launching your own token, and participating in the broader blockchain economy. Remember that the standard is only the foundation; real innovation comes from how you build on top of it, whether through governance, composability, or integrating with other protocols.

Happy coding and may your tokens transact smoothly!

Emma Varela
Written by

Emma Varela

Emma is a financial engineer and blockchain researcher specializing in decentralized market models. With years of experience in DeFi protocol design, she writes about token economics, governance systems, and the evolving dynamics of on-chain liquidity.

Contents