Launching an ERC-20 Token
Why Create Your Own Token?
ERC-20 is the most widely adopted token standard on Ethereum. Every major cryptocurrency you interact with — USDT (Tether), UNI (Uniswap), LINK (Chainlink), and thousands more — is an ERC-20 token. Understanding how to create one is the gateway to the entire decentralized finance ecosystem.
Why this matters for your career:
- Tokens are the building blocks of DeFi: every protocol, exchange, and lending platform starts with a token
- Creating and deploying tokens is the most common smart contract development task
- Token economics (tokenomics) is a core skill for Web3 founders and developers
- ERC-20 knowledge transfers directly to other standards (ERC-721 NFTs, ERC-1155 multi-tokens)
What Is the ERC-20 Standard?
ERC-20 defines a uniform interface for fungible tokens on Ethereum. "Fungible" means every token is identical and interchangeable — just like dollars: any $1 bill is worth exactly the same as any other $1 bill.
Core Functions
| Function | Description | Real-World Analogy |
|:---------|:------------|:-------------------|
| totalSupply() | Returns the total number of tokens in existence | The total amount of currency printed by a central bank |
| balanceOf(address) | Returns the token balance of a given address | Checking how much money is in someone's bank account |
| transfer(to, amount) | Sends tokens from your address to another | Handing cash directly to someone |
| approve(spender, amount) | Authorizes another address to spend your tokens | Giving your credit card to a friend for a single purchase |
| transferFrom(from, to, amount) | Transfers tokens on behalf of the owner | A merchant charging your pre-authorized credit card |
| allowance(owner, spender) | Returns the remaining approved spending amount | Checking how much credit limit remains on your card |
Why These Six Functions?
The ERC-20 standard was designed to solve a critical problem: interoperability. Before ERC-20, every token had a different interface — wallets, exchanges, and dApps had to write custom code for each token. With ERC-20, any wallet (MetaMask, Trust Wallet) and any exchange (Uniswap, Binance) can instantly support any ERC-20 token without modification.
How to Create an ERC-20 Token
Step 1: Choose Your Tools
Instead of writing everything from scratch, we use OpenZeppelin Contracts — the industry-standard, battle-tested, audited library of smart contract components. It is used by projects like Compound, Aave, and Chainlink.
Step 2: Design Your Token Features
Before writing code, decide on your token's economics:
| Feature | Our Choice | Why | |:--------|:-----------|:----| | Name & Symbol | "Vibe Token" (VIBE) | Brand identity for your project | | Decimals | 18 (standard) | Matches ETH for seamless exchange integration | | Initial Supply | 1,000,000 VIBE | Enough for testing and initial distribution | | Minting | Owner only | Controlled supply — prevents inflation | | Burning | Anyone can burn their own tokens | Reduces supply, can create scarcity | | Transaction Tax | 3% to treasury | Funds project development and operations | | Pausable | Owner can pause all transfers | Emergency brake for critical vulnerabilities |
Step 3: Write the Smart Contract
// contracts/VibeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
contract VibeToken is ERC20, ERC20Burnable, Ownable, ERC20Pausable {
// Transaction tax rate: 3% = 300 basis points (denominator = 10000)
uint256 public constant TAX_RATE = 300;
uint256 public constant TAX_DENOMINATOR = 10000;
// Treasury collects all transaction taxes
address public treasuryAddress;
// Accounts that are exempt from the transaction tax
mapping(address => bool) public isExcludedFromTax;
// Events for off-chain tracking
event TaxCharged(address indexed from, address indexed to, uint256 amount, uint256 tax);
event TreasuryUpdated(address indexed newTreasury);
event TaxExclusionUpdated(address indexed account, bool excluded);
constructor(
address _treasuryAddress
) ERC20("Vibe Token", "VIBE") Ownable(msg.sender) {
require(_treasuryAddress != address(0), "Treasury address cannot be zero");
treasuryAddress = _treasuryAddress;
// Mint 1,000,000 tokens to the contract owner
uint256 initialSupply = 1_000_000 * 10 ** decimals();
_mint(msg.sender, initialSupply);
// Contract owner is tax-exempt
isExcludedFromTax[msg.sender] = true;
}
// Override decimals to 18 (the ERC-20 standard)
function decimals() public pure virtual override returns (uint8) {
return 18;
}
// Override transfer to include transaction tax
function transfer(
address to,
uint256 amount
) public virtual override returns (bool) {
address owner = _msgSender();
if (isExcludedFromTax[owner] || isExcludedFromTax[to]) {
return super.transfer(to, amount);
}
// Calculate the 3% transaction tax
uint256 tax = (amount * TAX_RATE) / TAX_DENOMINATOR;
uint256 amountAfterTax = amount - tax;
// Send tax to treasury first
super.transfer(treasuryAddress, tax);
emit TaxCharged(owner, to, amountAfterTax, tax);
// Transfer the remaining amount to the recipient
return super.transfer(to, amountAfterTax);
}
// Override transferFrom to include transaction tax
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
if (isExcludedFromTax[from] || isExcludedFromTax[to]) {
return super.transferFrom(from, to, amount);
}
uint256 tax = (amount * TAX_RATE) / TAX_DENOMINATOR;
uint256 amountAfterTax = amount - tax;
super.transferFrom(from, treasuryAddress, tax);
emit TaxCharged(from, to, amountAfterTax, tax);
return super.transferFrom(from, to, amountAfterTax);
}
// Admin: update the treasury address
function updateTreasury(address _newTreasury) external onlyOwner {
require(_newTreasury != address(0), "Treasury cannot be zero address");
treasuryAddress = _newTreasury;
emit TreasuryUpdated(_newTreasury);
}
// Admin: exclude or include an address from tax
function setTaxExclusion(address account, bool excluded) external onlyOwner {
isExcludedFromTax[account] = excluded;
emit TaxExclusionUpdated(account, excluded);
}
// Admin: mint new tokens
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// Emergency: pause all transfers
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Required by OpenZeppelin for multiple inheritance
function _update(
address from,
address to,
uint256 value
) internal override(ERC20, ERC20Pausable) {
super._update(from, to, value);
}
}
Compilation and Deployment
// scripts/deploy_token.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deployer address:", deployer.address);
console.log("Deployer balance:", hre.ethers.formatEther(
await hre.ethers.provider.getBalance(deployer.address)
), "ETH");
// Deploy VibeToken
const VibeToken = await hre.ethers.getContractFactory("VibeToken");
const token = await VibeToken.deploy(deployer.address);
await token.waitForDeployment();
const tokenAddress = await token.getAddress();
console.log("VibeToken deployed at:", tokenAddress);
// Query total supply
const totalSupply = await token.totalSupply();
console.log("Total supply:", hre.ethers.formatEther(totalSupply), "VIBE");
// Query deployer balance
const balance = await token.balanceOf(deployer.address);
console.log("Deployer balance:", hre.ethers.formatEther(balance), "VIBE");
// Save deployment info for the frontend
const fs = require("fs");
const deploymentInfo = {
tokenAddress: tokenAddress,
deployer: deployer.address,
totalSupply: hre.ethers.formatEther(totalSupply),
network: hre.network.name,
};
fs.writeFileSync(
"deployment_info.json",
JSON.stringify(deploymentInfo, null, 2)
);
console.log("Deployment info saved to deployment_info.json");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
# Deploy to local Hardhat network for testing
npx hardhat run scripts/deploy_token.js
# Deploy to Sepolia testnet (get free ETH from a faucet first)
npx hardhat run scripts/deploy_token.js --network sepolia
Verifying Your Contract on Etherscan
Once deployed, verification makes your source code publicly visible on Etherscan. Users can read the contract, inspect functions, see transactions, and interact with it directly:
# Install the Hardhat verification plugin
npm install --save-dev @nomicfoundation/hardhat-verify
# Add your Etherscan API key to hardhat.config.js
// hardhat.config.js
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
# Verify the contract (pass constructor arguments)
npx hardhat verify --network sepolia <CONTRACT_ADDRESS> <TREASURY_ADDRESS>
How to Use Vibe Coding to Launch Your Token
Instead of writing all the Solidity by hand, you can ask AI to generate and refine your token contract. Here is a battle-tested prompt:
🔥 Vibe Coding Prompt for Token Creation "Create an ERC-20 token with the following requirements:
- Token name: MyToken, symbol: MTK, total supply: 1 billion
- A 2% transaction fee that gets burned on every transfer
- The owner can mint additional tokens
- The owner can pause all transfers for emergencies
- Deploy to Sepolia testnet and verify on Etherscan"
ERC-20 Security Checklist
| Check | Why It Matters | How to Verify |
|:------|:---------------|:--------------|
| Ownable is correctly initialized | Prevents unauthorized ownership changes | Check Ownable(msg.sender) in constructor |
| Excessive minting authority | Owner can mint unlimited tokens, destroying value | Review mint logic, consider a supply cap |
| Hidden tax changes | Owner could raise tax to 100% without notice | Always emit events for every configuration change |
| No hardcoded addresses | Contract becomes unusable if treasury key is lost | Use setter functions with onlyOwner and events |
| Pausable is safe | Owner can freeze all user funds | Add a timelock or multisig requirement for pause |
| Contract not verified | Users cannot see or audit the source code | Always verify on Etherscan after deployment |
Common Pitfalls
| Mistake | Why It Is Dangerous | Fix |
|:--------|:-------------------|:----|
| Not verifying the contract | Users can't audit the code — trust is impossible | Always verify on Etherscan |
| Hardcoding the treasury address | If the address is compromised, taxes are lost forever | Use updateTreasury() with onlyOwner |
| No supply cap on minting | Owner can inflate supply infinitely, destroying token value | Cap total supply or add a mint limit |
| Forgetting to exclude owner from tax | Owner pays tax on their own transfers and minting | Set isExcludedFromTax[owner] = true in constructor |
| Misconfigured decimals | Exchanges display the wrong price (e.g. 8 vs 18 decimals) | Always use 18 decimals unless you have a specific reason |
| No events for state changes | Off-chain dashboards can't track activity | Emit events for every state mutation |
| Poor error messages in require() | Users don't know why their transaction failed | Use descriptive revert messages |
Summary
You have now created a fully functional ERC-20 token with production-grade features:
- What you built: A smart contract implementing the ERC-20 standard with transaction tax, minting, burning, pausing, and ownership controls
- Why it matters: ERC-20 tokens power the entire DeFi ecosystem — from stablecoins (USDT, USDC) to governance tokens (UNI, COMP) to meme coins (SHIB, PEPE)
- How it works: OpenZeppelin provides audited base contracts; you inherit and extend them with custom business logic for taxes, minting, and emergency controls
- Deployment pipeline: Write the contract → compile with Hardhat → deploy to a testnet → verify on Etherscan → integrate with your dApp frontend
Key takeaways:
- ERC-20's six mandatory functions ensure every token works with every wallet, exchange, and dApp without custom integration
- OpenZeppelin Contracts eliminate the need to write security-critical code from scratch — they are audited and used by billions of dollars in TVL
- Tokenomics features (tax, mint, burn, pause) are common extensions that make a token production-ready
- Always verify contracts so users can audit the code themselves — an unverified contract is a red flag for any project
- Use Vibe Coding prompts to iterate on token contracts rapidly: describe the features you want in plain English and let AI generate the Solidity
What Is Next: ERC-721 NFTs
Now that you can create fungible tokens where every unit is identical, the next chapter introduces non-fungible tokens (NFTs) using the ERC-721 standard. Unlike ERC-20 where 100 tokens are indistinguishable from each other, ERC-721 makes every token completely unique — perfect for digital art, collectibles, in-game items, music rights, and real-world asset tokenization. You will learn how to mint NFTs with metadata, set royalty fees for secondary sales, and build a complete NFT marketplace contract.