1. Prerequisites
Before starting this tutorial, you should have:
- Basic understanding of Solidity syntax
- Node.js v18+ installed
- A code editor (VS Code recommended)
- MetaMask wallet with testnet ETH (Sepolia)
Pro Tip
Get free testnet ETH from the Sepolia Faucet
2. What is ERC-20?
ERC-20 is a technical standard for fungible tokens on the Ethereum blockchain. It defines a set of rules that all Ethereum tokens must follow, enabling seamless interoperability between tokens, wallets, and decentralized applications.
Required Functions
| Function | Description |
|---|---|
| totalSupply() | Returns total token supply |
| balanceOf(address) | Returns balance of an account |
| transfer(to, amount) | Transfers tokens to an address |
| approve(spender, amount) | Approves spender to use tokens |
| allowance(owner, spender) | Returns remaining allowance |
| transferFrom(from, to, amount) | Transfers on behalf of owner |
3. Project Setup
We will use Hardhat as our development framework. Create a new project:
mkdir my-token && cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat initInstall OpenZeppelin contracts for secure, audited implementations:
npm install @openzeppelin/contracts4. Basic Token Contract
Create a new file contracts/MyToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) Ownable(msg.sender) {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}Understanding the Code
- ERC20 Import: We inherit from OpenZeppelin's battle-tested ERC20 implementation that handles all standard functionality.
- Ownable: Provides access control so only the contract owner can mint new tokens.
- Constructor: Initializes token with name, symbol, and mints initial supply to deployer.
- decimals(): Returns 18 by default. We multiply initialSupply by 10^18 to account for this.
5. Advanced Features
Let's add some useful features to our token:
// 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/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract AdvancedToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10 ** 18; // 1 billion max
mapping(address => bool) public blacklisted;
event Blacklisted(address indexed account);
event Unblacklisted(address indexed account);
constructor()
ERC20("Advanced Token", "ADV")
Ownable(msg.sender)
ERC20Permit("Advanced Token")
{
_mint(msg.sender, 100_000_000 * 10 ** decimals()); // 100M initial
}
function mint(address to, uint256 amount) public onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function blacklist(address account) public onlyOwner {
blacklisted[account] = true;
emit Blacklisted(account);
}
function unblacklist(address account) public onlyOwner {
blacklisted[account] = false;
emit Unblacklisted(account);
}
function _update(
address from,
address to,
uint256 value
) internal override(ERC20, ERC20Pausable) {
require(!blacklisted[from], "Sender is blacklisted");
require(!blacklisted[to], "Recipient is blacklisted");
super._update(from, to, value);
}
}Security Note
The blacklist feature is controversial in decentralized finance. Consider your use case carefully before implementing centralized controls.
6. Deployment
Create a deployment script at scripts/deploy.js:
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with:", deployer.address);
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(
"My Token", // name
"MTK", // symbol
1000000 // initial supply (1 million)
);
await token.waitForDeployment();
console.log("Token deployed to:", await token.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Configure Hardhat for Sepolia testnet in hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};Deploy to Sepolia:
npx hardhat run scripts/deploy.js --network sepolia7. Contract Verification
Verify your contract on Etherscan to allow users to read the source code:
npx hardhat verify --network sepolia <CONTRACT_ADDRESS> "My Token" "MTK" "1000000"Verification Benefits
Verified contracts build trust with users, enable direct interaction through Etherscan, and make your project appear more professional.
8. Next Steps
Congratulations! You've created your first ERC-20 token. Here's what you can explore next: