Creating Your First ERC-20 Token

Want to understand how USDC, LINK, or UNI work? Build one yourself.
In the next 60-90 minutes, you'll deploy a working ERC-20 token to Ethereum's test network. A real token that works with MetaMask, can be traded on Uniswap, and follows the same standard securing $100+ billion in value. More importantly, you'll learn why developers make specific design choices and how those choices affect security, control, and user experience.
What You'll Learn
You'll understand the architecture behind every ERC-20 token:
Why standards eliminate thousands of hours of integration work - Your token automatically works with every compatible wallet and exchange. No partnerships needed.
How security audits protect hundreds of millions - OpenZeppelin's contracts secure over $40 billion because thousands of developers found and fixed vulnerabilities over years.
Why testing happens in two different programming languages - Solidity tests catch logic errors. TypeScript tests catch integration problems. Professional teams use both.
Where control lives in "decentralized" systems - That owner address controlling minting? It's the centralization point regulators care about.
What You Need
MetaMask wallet
Holds test funds and signs the deployment transaction that creates your contract
Sepolia test ETH
Pays for gas fees (~$0 in real value, but required for test network)
Project Setup With Hardhat
Hardhat is a professional development environment that simplifies building on Ethereum. We'll use its interactive initializer to set up our project structure and dependencies.
1. Running Hardhat Init
Open your terminal and run the following command to start the setup process:
2. Selecting Version
You'll see the Hardhat welcome screen. Use the latest stable version for access to the newest features and security updates.
3. Folder Selection
Next, Hardhat will ask where to create the project.
4. Type of Project
We will use Viem, a modern and lightweight TypeScript interface for Ethereum that makes interacting with smart contracts intuitive and type-safe.
5. Install Necessary Hardhat Dependencies
The initializer will list all required packages. Confirm the installation to proceed.
6. Install OpenZeppelin Contracts
To build a secure and compliant ERC-20 token without reinventing the wheel, we'll use the audited OpenZeppelin Contracts library.
Why OpenZeppelin? Building smart contracts from scratch is risky. A tiny mistake can lead to catastrophic financial loss. OpenZeppelin provides modular, reusable, and community-audited smart contracts that follow established standards like ERC-20. Using them substantially reduces the risk of common vulnerabilities.
Understanding What You're About to Build
Before writing code, let's establish what makes this contract a "simplified stablecoin."
Traditional stablecoins like USDC aim to maintain their $1 peg through two mechanisms: centralized minting and redemption. Circle (the company behind USDC) mints new USDC tokens when users deposit dollars and burns tokens when users redeem for dollars. One entity controls the supply tap.
Your token replicates the minting side of this model. The deploying address becomes the only account that can create new tokens. This design pattern appears throughout crypto: Tether with USDT, Binance with BUSD (before its shutdown), and dozens of other fiat-backed stablecoins.
Why build centralized control into a decentralized system? Some problems require it. Stablecoins need mechanisms to respond to market demand. If everyone wants to buy USDC, Circle needs to mint more. If everyone's redeeming, they burn supply. Pure algorithmic alternatives (Terra/UST, remember?) failed catastrophically because they lacked this direct control.
The trade-off is obvious: users trust the owner won't abuse minting privileges. Regulatory compliance helps here. Circle submits to audits and regulatory oversight. Your test token? It demonstrates the technical mechanism, not the trust infrastructure.
Now let's implement this pattern in code.
Smart Contract Development
We'll define our token's logic by inheriting from two core OpenZeppelin contracts: ERC20 (for the standard token functionality) and Ownable (for administrative control).
1. Create the Contract File
First, create a new Solidity file in the contracts folder.
2. Initial Imports and Inheritance
We start by defining the Solidity compiler version, setting the license, and importing the necessary OpenZeppelin contracts. Our contract, MyStablecoin, will inherit all the functionality from both ERC20 and Ownable.
3. Implementing the Constructor
The constructor runs once when the contract is deployed. We use it to set the token's initial state.
solidity
ERC20("My Stablecoin", "MS"): We initialize the underlyingERC20contract, setting the public name and symbol for our token.Ownable(msg.sender): We initialize theOwnablecontract, designatingmsg.sender(the address deploying the contract) as the owner._mint(msg.sender, initialSupply): We call the internal_mintfunction from theERC20contract to create the initial supply of tokens and assign them to the owner's address. Note that amounts are handled in their smallest unit (like cents to a dollar). With 18 decimals, 1 token is represented as1 * 10^18.
4. Implementing Mint Functionality
This function exposes the internal _mint capability, but with a restriction: only the owner can call it. This is the core of our "stablecoin" model, allowing the owner to increase the total supply at will.
The onlyOwner modifier, inherited from Ownable, automatically adds a check to ensure that msg.sender is the contract's owner. If anyone else tries to call this function, the transaction will fail.
5. Implementing Burn Functionality
To complete our supply management, we'll add a burn function. This allows any user to permanently destroy their own tokens, removing them from circulation and decreasing the total supply.
The internal _burn(msg.sender, amount) function from OpenZeppelin's ERC20 contract handles the logic securely, ensuring a user can only burn tokens they possess.
Why Testing Isn't Optional
Your contract is 50 lines of code. Simple? Not when it controls value.
Those 50 lines control value. Maybe just testnet tokens worth nothing today. But the same patterns secure USDC's $25 billion, LINK's oracle network feeding data to $14 billion in DeFi, and countless other tokens. One logic error means lost funds. No customer service to call. No transactions to reverse. Just a permanent bug in immutable code.
Smart contract testing differs from traditional software testing in one way: the cost of bugs. A bug in a web app might crash the page. A bug in a smart contract might drain millions before anyone notices. The Poly Network hack extracted $600 million through a tiny authorization flaw. The DAO hack stole $60 million from what everyone assumed was carefully reviewed code.
This is why professional developers test obsessively and why we're using two completely different testing approaches:
Solidity tests via Foundry run directly on the EVM (Ethereum Virtual Machine). They execute fastβthousands of tests per secondβand let you test edge cases cheaply. Can a user burn more tokens than they hold? Does the minting function properly reject non-owners? Solidity tests answer these questions by checking individual functions in isolation.
TypeScript tests via Viem simulate real-world usage from start to finish. They connect to a local blockchain fork, deploy your contract using actual wallet clients, and execute multi-step interactions just like a user would. This catches integration issues: Does the deployment work? Can different accounts interact correctly? Do events emit properly?
You need both. Unit tests (Solidity) catch logic errors in functions. Integration tests (TypeScript) catch workflow problems between functions.
Let's write tests that prove your contract works.
Writing Tests
Testing is not optional in smart contract development; it's essential. Hardhat v3 integrates Foundry, allowing us to write tests in both Solidity (for fast, low-level unit tests) and TypeScript (for end-to-end integration tests).
Solidity Unit Tests
We'll use Foundry-style Solidity tests to check individual functions in isolation. This verifies specific logic and edge cases.
1. Setup
Next, add the boilerplate. The setUp function runs before each test, deploying a fresh instance of our contract to ensure tests don't interfere with each other.
2. Test Constructor
This test verifies that the contract is deployed with the correct initial state values: name, symbol, decimals, owner, and total supply.
3. Test Mint
Here, we test both the "happy path" (successful minting by the owner) and the "sad path" (failed attempt by a non-owner).
vm.prank(notOwner): This is a "cheat code" from Foundry that lets us simulate a transaction coming from any address we want.vm.expectRevert(...): This tells the test runner to expect the next call to fail with a specific error message, ensuring our onlyOwner modifier works correctly.
4. Test Burn
We test for successful burning and for the case where a user tries to burn more tokens than they have.
What These Tests Verify
Look at what we've built. Five test functions that check specific behaviors:
Constructor test verifies the initial state matches expectations. If this fails, something broke during deploymentβwrong name, wrong supply, wrong owner. These are the values the entire token economy depends on.
Mint success test confirms the happy path: owner mints tokens, recipient receives them, total supply increases correctly. This is the core functionality enabling supply management.
Mint failure test proves the security mechanism works: non-owners can't create tokens from thin air. This single test stands between controlled supply and infinite inflation.
Burn success test validates supply reduction. When someone burns tokens, both their balance and total supply decrease by exactly the same amount. No tokens disappearing into the void, no accounting errors.
Burn failure test ensures users can't burn tokens they don't have. Sounds obvious, but without this check, a user could trigger integer underflow and accidentally give themselves billions of tokens.
Each test follows the same pattern: set up initial state, execute action, verify results match expectations. This is how professional teams build confidence in their code before risking real value.
5. Run Solidity Tests
Execute the tests using the following command. You should see all tests passing.
TypeScript End-to-End Test
While Solidity tests work well for unit logic, TypeScript tests simulate real-world user interactions from deployment to multiple function calls.
1. Setup
Create the TypeScript test file:
And add the initial boilerplate to connect to the Hardhat network.
2. Implement E2E Flow
This single test will cover the entire lifecycle: deploy the contract, verify its initial state, have the owner mint tokens to another account, verify the new state, have the owner burn some of their tokens, and verify the final state. This ensures all parts of the contract work together as expected.
typescript
3. Run TypeScript Tests
Now, run the end-to-end test.
From Test Network to Public Blockchain
Everything you've built so far runs on your local computer. Hardhat creates a simulated blockchain that resets every time you restart. It's perfect for testing but insufficient for anyone else to use. Now we're changing that.
Deploying to Sepolia (Ethereum's test network) means your contract lives on a blockchain that thousands of nodes maintain. It becomes permanent. It becomes public. It becomes realβor as real as a test network gets.
This is where theory meets practice. You're about to interact with a public blockchain network, broadcast a transaction that thousands of computers will validate, and create a permanent record that anyone with internet access can verify. The process mirrors exactly what you'd do on Ethereum mainnet, except Sepolia ETH is free and mistakes cost nothing.
Three things need to happen:
1. Your code needs to connect to the Ethereum network - That's what the Alchemy RPC URL provides. Think of it as your gateway to blockchain infrastructure. Without it, your computer can't talk to Ethereum nodes.
2. You need to prove you're authorizing the deployment - That's what your private key does. It cryptographically signs the transaction creating your contract, proving you have the authority to spend the gas fees.
3. The deployment needs to be repeatable and verifiable - That's what Hardhat Ignition provides. It creates deployment artifacts that prove exactly what code was deployed with what parameters.
Before we proceed: a serious note about security.
Setting Up for Deployment
With our contract written and thoroughly tested, it's time to deploy it to a public network. We'll use the Sepolia testnet.
1. Setting Up Environment Variables
To deploy, you need to provide your wallet's private key (to sign the transaction) and an RPC URL (to communicate with the network). Storing these directly in your code is a major security risk. Hardhat provides a secure keystore to manage these secrets.
Set SEPOLIA_RPC_URL: Run the command below, set a password for your keystore, and paste in your Alchemy RPC URL.
Set SEPOLIA_PRIVATE_KEY: Do the same for your wallet's private key. WARNING: Your private key controls your funds. Never share it with anyone or commit it to a public repository.
2. Implementing the Deployment Script
We will use hardhat-ignition, a powerful and declarative deployment system that makes deployments reliable and repeatable.
Create the deployment file:
Setup the deployment script: This script tells Ignition what contract to deploy (MyStablecoin) and what arguments to pass to its constructor (the initialSupply).
3. Deploy Your Stablecoin!
Run the deployment command, targeting the Sepolia network. You will be prompted for your keystore password.
After confirming, Ignition will deploy your contract. If successful, you'll see the contract address printed in your terminal.
Congratulations! You have successfully deployed your own ERC-20 token to the Ethereum Sepolia testnet.
Conclusion
You've successfully journeyed from an empty folder to a live ERC-20 token on a public testnet. By following this guide, you have:
Set up a professional development environment using Hardhat.
Written a secure ERC-20 smart contract by inheriting from OpenZeppelin's audited base contracts.
Implemented custom logic for a simplified stablecoin with
mintandburnfunctionalities controlled by an owner.Mastered a dual-testing strategy, writing both granular unit tests in Solidity and comprehensive end-to-end tests in TypeScript.
Securely managed secrets using Hardhat's keystore and deployed your contract to the Sepolia testnet using Hardhat Ignition.
Last updated