⚠️ Disclaimer
This article is for educational purposes only.
The token created here is not money, has no intrinsic value, and is not associated with any real-world brand or stablecoin.
Do not impersonate existing projects or tickers. Always use your own unique token name and symbol.
Why I Wrote This Article
Many people entering crypto ask the same questions:
- “Why did a random token appear in my wallet?”
- “Why do I see a balance but can’t sell it?”
- “How are ERC-20 tokens actually created?”
- “Why does a token look real if it’s fake?”
The best way to truly understand this is simple:
Create your own ERC-20 token once.
When you do that, everything suddenly makes sense:
- balances
- wallets
- approvals
- fake tokens
- approvals
This article walks through the entire process from zero to deployment, step by step.
What We Will Build
We will build a clean educational ERC-20 token with:
- OpenZeppelin ERC-20 implementation
- Role-based access control
- Pausable transfers
- Minting
- Deployment with Hardhat
- Adding the token to MetaMask
- Understanding how wallets detect balances
1. What Is ERC-20 (Plain English)
ERC-20 is not a currency.
It’s simply a standard interface — a set of functions that wallets and dApps know how to call.
At minimum, an ERC-20 token has:
- balanceOf(address)
- transfer(address, amount)
- totalSupply()
- name(), symbol(), decimals()
❗ Important
An ERC-20 contract does not know its price.
Price exists only if:
- a market exists
- liquidity exists
- someone is willing to trade
No liquidity → no price → no real value.
2. Environment Setup
Requirements
- Node.js (LTS)
- npm
- MetaMask wallet
- Sepolia test ETH
- RPC provider (Alchemy, Infura, etc.)
Create the Project
mkdir my-token
cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npx hardhat
Choose:
Create a JavaScript project
3. Install OpenZeppelin
OpenZeppelin provides audited, production-grade smart contracts.
npm install @openzeppelin/contract
4. Writing the ERC-20 Token Contract
Create contracts/MyToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*
MyToken — an educational ERC-20 token.
NOT money. NOT a stablecoin.
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
contract MyToken is ERC20, AccessControl, Pausable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
constructor(uint256 initialSupply)
ERC20("My Educational Token", "MYT")
{
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(PAUSER_ROLE, msg.sender);
_mint(msg.sender, initialSupply);
}
function mint(address to, uint256 amount)
external
onlyRole(MINTER_ROLE)
{
_mint(to, amount);
}
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
function unpause() external onlyRole(PAUSER_ROLE) {
_unpause();
}
function _update(address from, address to, uint256 amount)
internal
override
whenNotPaused
{
super._update(from, to, amount);
}
}
Key Concepts Here
- Name and symbol are just strings
- The contract itself stores balances
- Tokens exist only after mint
- If you don’t mint → balance will be zero
5. Hardhat Configuration
Create hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};
6. Environment Variables
Create .env (never commit this):
SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
7. Deployment Script
Create scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying with address:", deployer.address);
const initialSupply = hre.ethers.parseUnits("1000", 18);
const MyToken = await hre.ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(initialSupply);
await token.waitForDeployment();
console.log("Token deployed at:", await token.getAddress());
}
main().catch(console.error);
8. Compile and Deploy
npx hardhat compile
npx hardhat run scripts/deploy.js --network sepolia
You will receive a contract address.
9. Adding the Token to MetaMask
- Switch MetaMask to Sepolia
- Go to Assets → Import token
- Paste the contract address
- MetaMask auto-fills symbol & decimals
If balance shows 0, check:
- correct network
- minted address
- decimals
- wallet cache (reload MetaMask)
10. Why Tokens Can Appear “Out of Nowhere”
Here’s the critical realization:
Wallets do not store tokens.
A wallet simply calls:
balanceOf(yourAddress)
If any contract returns a non-zero number → the wallet displays it.
That means:
- someone can mint tokens to your address
- without asking
- without permission
Your wallet will still show them.
11. Why Name, Symbol, and Icon Mean Nothing
Blockchains have:
- no global registry of names
- no trademark enforcement
- no “official token” flag
Anyone can deploy:
- any name
- any symbol
- any decimals
- any logo The only real identity of a token is:
its contract address
12. The Core Security Lesson
Once you understand this, many scams become obvious:
- “free tokens” → meaningless balances
- fake stablecoins → just ERC-20 contracts
- approval traps → dangerous permissions
Knowledge beats fear.
Conclusion
ERC-20 tokens are just smart contracts.
Balances are just numbers.
Price comes from markets, not from code.
Trust comes from verification, not appearance.
Creating your own token once is the fastest way to understand all of this.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.