Skip to content
Learni
View all tutorials
Blockchain

How to Create an ERC-20 Token in Solidity in 2026

Lire en français

Introduction

In 2026, ERC-20 tokens remain the gold standard for fungible cryptocurrencies on Ethereum and its Layer-2 networks. This intermediate tutorial walks you through creating a custom ERC-20 token in Solidity 0.8.26, leveraging OpenZeppelin libraries for top-tier security. You'll implement core functions (transfer, approval), advanced extensions (controlled minting, burning, emergency pause), and deploy to a testnet using Remix IDE.

Why this contract? Unlike a basic ERC-20, ours includes safeguards like ownership to prevent common hacks (reentrancy, overflow). Think of it as a fortified house: solid foundations (ERC-20 core) plus extra locks (Ownable, Pausable). By the end, you'll have a deployed, testable token ready for Uniswap or any DEX. Estimated time: 30 minutes. Ready to mint your first million tokens?

Prerequisites

  • Basic Solidity knowledge (variables, modifiers, events)
  • MetaMask account with ETH on Sepolia testnet (faucet: sepoliafaucet.com)
  • Remix IDE open in your browser (remix.ethereum.org)
  • Familiarity with ERC-20 standards (totalSupply, balanceOf, transfer)

Basic ERC-20 Skeleton

MyTokenBasic.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyTokenBasic is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
}

This contract inherits from OpenZeppelin's ERC20, automatically implementing transfer, approve, and balanceOf. The constructor mints an initial supply to the deployer. Pitfall: Forgetting *10**decimals() undercounts units (18 by default). Copy to Remix and compile with Solidity 0.8.26.

Adding Ownership

For admin controls (renounce ownership, transfer), extend with Ownable. This prevents contracts from being frozen after deployment. OpenZeppelin handles the onlyOwner modifier. Test in Remix console: deploy with 1000000, check balanceOf(deployer).

ERC-20 with Ownable

MyTokenOwnable.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyTokenOwnable is ERC20, Ownable {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") Ownable(msg.sender) {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

Ownable adds onlyOwner and owner(). New mint function to issue more tokens. Security: Mint restricted to owner, preventing runaway inflation. Deploy and call mint(friend, 1000e18) as owner.

Emergency Management with Pausable

Hacks like Ronin (2022) highlight the need for pausing. Pausable blocks transfers in emergencies using the whenNotPaused modifier. Perfect for post-launch audits.

Pausable and Burnable ERC-20

MyTokenAdvanced.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

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/utils/Pausable.sol";

contract MyTokenAdvanced is ERC20, ERC20Burnable, ERC20Pausable, Ownable {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") Ownable(msg.sender) {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Pausable) {
        super._update(from, to, amount);
    }
}

Multiple inheritance: Burnable (self-burn), Pausable (pause/unpause). Override _update to integrate pausing with transfers. Test: pause(), try transfer (reverts), unpause(). Burning permanently reduces supply.

Solidity Test Script

MyTokenTest.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "./MyTokenAdvanced.sol";

contract MyTokenTest {
    MyTokenAdvanced public token;

    constructor() {
        token = new MyTokenAdvanced(1000000);
    }

    function testTransfer(address to, uint256 amount) public {
        token.transfer(to, amount);
    }

    function testMint(address to, uint256 amount) public {
        token.mint(to, amount);
    }
}

Test contract to validate without JavaScript. Deploy and call testTransfer(0x..., 1e18). Check events/logs in Remix. Great for simulating interactions before mainnet deployment.

Deploying to Sepolia

Remix Steps:

  1. Copy MyTokenAdvanced.sol to a new workspace.
  2. Compile (Solidity 0.8.26, auto-import OpenZeppelin).
  3. Deploy & Run > Injected Provider (MetaMask Sepolia).
  4. Deploy with initialSupply=1000000.
  5. Note the contract address, verify on Sepolia Etherscan.

Add liquidity on Uniswap testnet for trading.

ABI for Frontend Verification

MyTokenABI.json
[
  {
    "inputs": [
      {"internalType": "address", "name": "to", "type": "address"},
      {"internalType": "uint256", "name": "amount", "type": "uint256"}
    ],
    "name": "mint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  }
]

ABI excerpt for Web3.js/ethers.js integration. Copy the full ABI from Remix (Compilation Details > ABI). Use to call mint from React or Vue.

Best Practices

  • Always use OpenZeppelin: Audited, protects against reentrancy/overflow (SafeMath built-in since 0.8).
  • Renounce ownership after minting: Call renounceOwnership() for true decentralization.
  • Test thoroughly: Use Foundry/Hardhat for fuzzing, Slither for static audits.
  • Gas optimization: Immutables for constants, stick to <0.8.26 to avoid costly checks.
  • NatSpec documentation: Add /// @notice for Etherscan verification.

Common Errors to Avoid

  • Forgetting decimals(): Minting 1e6 gives 1e6 wei, not millions of tokens.
  • Inheritance without override: Missing _update breaks Pausable (transfer reverts).
  • Unlimited mint without cap: Add maxSupply to prevent hyperinflation.
  • Deploying without testnet: Wastes mainnet gas; always test on Sepolia first.

Next Steps

Master ERC-20 hooks (transfer hooks for fees). Explore ERC-4626 (vaults). Resources:


Check out our Learni Solidity & DeFi courses for pro-level skills: audits, L2 deployment, flashloans.