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
- Install Node.js (v20+ recommended).
- Create a new project folder and run
npm init -y. - Install Hardhat, a popular Ethereum development framework:
npm install --save-dev hardhat
- 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
capvariable limits the maximum number of tokens that can ever exist. - Owner‑Only Minting – The
onlyOwnermodifier (inherited fromOwnable) 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:
- Avoid Repeated Storage Writes – Each storage slot write costs ~20,000 gas. Store values that rarely change in a single slot.
- Use
uncheckedBlocks – In Solidity 0.8+, overflow checks add overhead. For internal arithmetic that is guaranteed safe, wrap operations inunchecked {}. - Minimize Event Logs – Events are stored on-chain; each log entry costs gas. Emit only essential events (e.g.,
TransferandApproval). - 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:
- Open your wallet and choose “Add Custom Token.”
- Input the contract address, token symbol, and decimals (18 for most ERC‑20 tokens).
- 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:
- Proxy Patterns – Use the Transparent or UUPS proxy pattern from OpenZeppelin to upgrade your contract logic without losing state.
- Governance Modules – Add a governance contract that allows token holders to vote on parameter changes, such as the cap or minting rights.
- ERC‑2612 Permit – Integrate off‑chain signatures to allow approvals without a transaction, reducing gas costs for end users.
- 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
transferFromreverts 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:
- Security Audit – Either perform your own audit or hire an external firm.
- Unit and Integration Tests – Run them on a local node and a public testnet.
- Verify Contract Source – Publish the source code on Etherscan or a similar explorer.
- Liquidity Provision – If you plan to list on an exchange, consider providing initial liquidity on Uniswap or SushiSwap.
- 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
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.
Random Posts
Exploring Advanced DeFi Projects with Layer Two Scaling and ZK EVM Compatibility
Explore how top DeFi projects merge layer two scaling with zero knowledge EVM compatibility, cutting costs, speeding transactions, and enhancing privacy for developers and users.
8 months ago
Deep Dive Into Advanced DeFi Projects With NFT-Fi GameFi And NFT Rental Protocols
See how NFT, Fi, GameFi and NFT, rental protocols intertwine to turn digital art into yield, add gaming mechanics, and unlock liquidity in advanced DeFi ecosystems.
2 weeks ago
Hedging Smart Contract Vulnerabilities with DeFi Insurance Pools
Discover how DeFi insurance pools hedge smart contract risks, protecting users and stabilizing the ecosystem by pooling capital against bugs and exploits.
5 months ago
Token Bonding Curves Explained How DeFi Prices Discover Their Worth
Token bonding curves power real, time price discovery in DeFi, linking supply to price through a smart, contracted function, no order book needed, just transparent, self, adjusting value.
3 months ago
From Theory to Trading - DeFi Option Valuation, Volatility Modeling, and Greek Sensitivity
Learn how DeFi options move from theory to practice and pricing models, volatility strategies, and Greek sensitivity explained for traders looking to capitalize on crypto markets.
1 week ago
Latest Posts
Foundations Of DeFi Core Primitives And Governance Models
Smart contracts are DeFi’s nervous system: deterministic, immutable, transparent. Governance models let protocols evolve autonomously without central authority.
1 day ago
Deep Dive Into L2 Scaling For DeFi And The Cost Of ZK Rollup Proof Generation
Learn how Layer-2, especially ZK rollups, boosts DeFi with faster, cheaper transactions and uncovering the real cost of generating zk proofs.
1 day ago
Modeling Interest Rates in Decentralized Finance
Discover how DeFi protocols set dynamic interest rates using supply-demand curves, optimize yields, and shield against liquidations, essential insights for developers and liquidity providers.
1 day ago