ERC-20 Tokens: Creating Your Own Cryptocurrency
In this tutorial, you'll learn the ERC-20 token standard including total supply, transfers, allowances, and deploying your own fungible token on Ethereum. Why it matters: ERC-20 is the most widely adopted token standard, powering thousands of cryptocurrencies, stablecoins, and DeFi tokens with trillions in market capitalization. By the end, you'll deploy your own ERC-20 token with OpenZeppelin.
ERC-20 is a technical standard for fungible tokens on Ethereum, defining a common interface that all token contracts must implement to ensure compatibility with wallets, exchanges, and DeFi protocols.
Understanding the ERC-20 Standard
The ERC-20 interface defines six required functions and two events that every compliant token must implement.
| Function | Description | Returns |
|---|---|---|
totalSupply() |
Total token supply | uint256 |
balanceOf(address) |
Token balance of an address | uint256 |
transfer(address, uint256) |
Send tokens to an address | bool |
allowance(address, address) |
Amount spender can use from owner | uint256 |
approve(address, uint256) |
Authorize spender to use tokens | bool |
transferFrom(address, address, uint256) |
Transfer on behalf of owner | bool |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// Minimal ERC-20 implementation (for learning)
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
flowchart TD A[ERC-20 Token Contract] --> B[State Variables] A --> C[Core Functions] A --> D[Events] B --> E[_totalSupply] B --> F[_balances mapping] B --> G[_allowances mapping] C --> H[totalSupply] C --> I[balanceOf] C --> J[transfer] C --> K[approve] C --> L[allowance] C --> M[transferFrom] D --> N[Transfer event] D --> O[Approval event]
Creating a Token with OpenZeppelin
The safest way to create an ERC-20 token is using OpenZeppelin's audited and battle-tested contracts.
npm install @openzeppelin/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens
constructor() ERC20("MyToken", "MTK") {
// Mint initial supply to deployer
_mint(msg.sender, 100_000_000 * 10**18); // 100 million
}
function mint(address _to, uint256 _amount) external onlyOwner {
require(totalSupply() + _amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(_to, _amount);
}
function burn(uint256 _amount) external {
_burn(msg.sender, _amount);
}
// Override decimals to 18 (default)
function decimals() public pure override returns (uint8) {
return 18;
}
}
Deploy this contract and interact with it. Expected behavior:
- Token name: "MyToken", symbol: "MTK", decimals: 18
- Initial supply: 100 million MTK minted to deployer
- Owner can mint up to MAX_SUPPLY (1 billion)
- Anyone can burn their own tokens
Testing Token Transfers
Test the transfer, approve, and transferFrom workflow.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
let token, owner, spender, recipient;
beforeEach(async function () {
[owner, spender, recipient] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
token = await MyToken.deploy();
await token.waitForDeployment();
});
it("should have correct name and symbol", async function () {
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
it("should assign initial supply to owner", async function () {
const ownerBalance = await token.balanceOf(owner.address);
expect(ownerBalance).to.equal(ethers.parseEther("100000000"));
});
it("should transfer tokens between accounts", async function () {
await token.connect(owner).transfer(recipient.address, ethers.parseEther("1000"));
expect(await token.balanceOf(recipient.address)).to.equal(ethers.parseEther("1000"));
});
it("should handle approve and transferFrom", async function () {
// Owner approves spender to use 500 tokens
await token.connect(owner).approve(spender.address, ethers.parseEther("500"));
expect(await token.allowance(owner.address, spender.address))
.to.equal(ethers.parseEther("500"));
// Spender transfers from owner to recipient
await token.connect(spender).transferFrom(
owner.address,
recipient.address,
ethers.parseEther("500")
);
expect(await token.balanceOf(recipient.address)).to.equal(ethers.parseEther("500"));
expect(await token.allowance(owner.address, spender.address)).to.equal(0);
});
});
Expected test output:
MyToken
✔ should have correct name and symbol (56ms)
✔ should assign initial supply to owner (42ms)
✔ should transfer tokens between accounts (38ms)
✔ should handle approve and transferFrom (65ms)
4 passing (312ms)
Adding Token Features
Extend your token with additional features like pausing, capped minting, and snapshots.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
contract AdvancedToken is ERC20Capped, ERC20Burnable, ERC20Pausable, Ownable {
constructor(uint256 _cap)
ERC20("AdvancedToken", "ADV")
ERC20Capped(_cap * 10**18)
Ownable(msg.sender)
{}
function mint(address _to, uint256 _amount) external onlyOwner {
_mint(_to, _amount);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Capped, ERC20Pausable)
{
super._update(from, to, value);
}
}
Expected behavior: Tokens can only be minted up to the cap. Pausing prevents all transfers. Burning permanently removes tokens from circulation.
Deploying to a Testnet
Deploy your token to Sepolia testnet and verify on Etherscan.
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying token with account:", deployer.address);
const AdvancedToken = await ethers.getContractFactory("AdvancedToken");
const cap = 1_000_000_000; // 1 billion cap
const token = await AdvancedToken.deploy(cap);
await token.waitForDeployment();
const address = await token.getAddress();
console.log("Token deployed to:", address);
// Auto-verify on Etherscan
if (network.name !== "hardhat") {
await token.deploymentTransaction().wait(5);
await hre.run("verify:verify", {
address: address,
constructorArguments: [cap],
});
console.log("Contract verified on Etherscan");
}
}
main().catch(console.error);
Expected output:
Deploying token with account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Token deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Contract verified on Etherscan
Common Errors and Misunderstandings
1. Decimal Mismatch
Token decimals default to 18. Frontends must use the same decimals for display. Different decimals (6 for USDC) require parseUnits and formatUnits.
2. Approve + TransferFrom Order
Users must approve before a contract can transferFrom. The approve sets the allowance, then the contract calls transferFrom within its own transaction.
3. Infinite Approval
Many dApps request max uint256 approval to avoid repeated approve transactions. While convenient, this grants unlimited spending power. Audit the contract before granting.
4. Transfer to Contract
Sending tokens to a contract that doesn't implement ERC-20 handling results in permanent loss. Use safeTransfer or check the contract supports tokens.
5. Reentrancy in transferFrom
While ERC-20 transfers don't make external calls by default, custom implementations or hooks (ERC-777) can introduce reentrancy. Always use checks-effects-interactions.
Practice Questions
What six functions must an ERC-20 token implement?
totalSupply(),balanceOf(),transfer(),allowance(),approve(), andtransferFrom(). These form the core ERC-20 interface.How does the approve + transferFrom workflow work? Token owner calls
approve(spender, amount)to authorize spending. The spender then callstransferFrom(owner, recipient, amount)within the allowed limit. The allowance decreases by the transferred amount.What is the purpose of the decimal field? Decimals define the smallest unit of the token. 18 decimals (default) means 1 token = 10^18 smallest units. USDC uses 6 decimals for efficiency.
Why use OpenZeppelin's ERC-20 instead of writing from scratch? OpenZeppelin contracts are audited, battle-tested, and handle edge cases correctly. Writing from scratch risks bugs, overflow errors, and security vulnerabilities.
What happens when you transfer tokens to the zero address? It permanently burns the tokens. The ERC-20 standard doesn't prevent this. Most implementations explicitly forbid transfers to
address(0).
Challenge
Create a custom ERC-20 token with a tax mechanism: every transfer deducts 1% as a fee that goes to a treasury address, implements a maximum wallet holding limit of 2% of supply, and includes a cooldown period between transfers. Test all edge cases.
Real-World Task
Launch a community token for a fictional project using OpenZeppelin and Hardhat. Deploy to Sepolia testnet, verify on Etherscan, add liquidity on a testnet Uniswap instance, create a token vesting contract for team members, and build a simple React frontend showing balances and transfers.
Frequently Asked Questions
Next Steps
After deploying your ERC-20 token, explore NFTs (ERC-721 and ERC-1155) for non-fungible tokens, then dive into DeFi protocols for lending and swapping.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro