Introduction
Before diving into ERC-20, we first need to understand what “ERC” means.
An Ethereum Request for Comments (ERC) is a token standard accepted by the Ethereum research community. These standards help developers write smart contracts that follow consistent rules.
New token standards typically begin as an Ethereum Improvement Proposal (EIP), which anyone can submit. After thorough review, an EIP may be accepted as an official ERC.
In Ethereum, there are three primary types of token standards used in smart contracts:
Fungible Tokens
Non-Fungible Tokens (NFTs)
Semi-Fungible Tokens (SFTs)
Fungible Tokens
Fungible tokens are interchangeable and divisible assets. Each token unit is identical to another unit of the same type and can be split into smaller units. A good example of a fungible asset is a dollar. You can break a dollar down into smaller denominations (cents), and any dollar is identical in value to another.
Fungible tokens are commonly represented using the ERC-20 standard.
Non-Fungible Tokens(NFT`s)
Non-fungible tokens represent unique, indivisible assets. Unlike fungible tokens, these cannot be broken down into smaller units, and each token has its own unique identity. A good real-world example is the Mona Lisa. If you try to tear it apart or split it into pieces, its value and uniqueness are lost.
NFTs are commonly represented using the ERC-721 standard.
SEMI-FUNGIBLE TOKENS (SFT`s)
Semi-fungible tokens combine the properties of both fungible and non-fungible tokens. Initially, they behave like fungible tokens—interchangeable and identical in value. However, once they are redeemed or used in a specific context, they transform into unique, non-fungible assets.
A good example is a concert ticket. Before the event, all tickets of the same category (e.g., general admission) are identical and interchangeable. After the event, they become unique collectibles, representing proof of attendance or memorabilia.
SFTs are commonly represented using the ERC-1155 standard.
Step-by-Step Guide to Writing a simple meme coin (ERC-20) Smart Contract
1.Setting up the Development Environment
→Since we are using Remix browser IDE we wont be installing any dependancies or tools.
We will need Metamask to deploy our contract to BASE an Ethereum L2.
If you haven’t set up metamask use this guys instructions to install and set up metamask
→Add Base Network (Both Mainnet and TestNet). Use the following short video i made to add The Base network on your Metamask.
Edit this text
- Head over to → Remix IDE
Create a Folder
Inside the Folder Create a File called “MyToken.sol ” or any name of your prefference.
Enter that File(MyToken.sol) and Proceed to the next stage
2.Writing the Code
We will use openZeppelin library to implement this sample Project as we will be implementing most of the functions.
We will implement the following functions/features:
- Main Token Functions:
sendTokens(address to, uint256 amount)
: Direct way to send tokens to another addresscheckBalance(address account)
: View the token balance of any wallet address
- Multi-User Features:
multiSend(address[] recipients, uint256[] amounts)
: Send different amounts of tokens to multiple addresses in one transaction
- Supply Management:
mint(address to, uint256 amount)
: Only owner can create new tokens up to max supplyburn(uint256 amount)
: Anyone can burn their own tokens, reducing total supply
- Information Functions:
getTokenInfo()
: Returns token name, symbol, decimals, and current supply in one function callname()
: Returns token name ("MemeToken")symbol()
: Returns token symbol ("MEME")decimals()
: Returns number of decimals (18)
//SPDX-License-Identifier:MIT
pragma solidity 0.8.28; //The latest solidity version
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; //We will use most of the OZ functions
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MemeCoin
* @dev A simple meme coin token contract for educational purposes
* @dev Don't use on a commercial scale as it is not audited.
*/
contract MemeCoin is ERC20, Ownable {
}
Now we have our boilerplate and we will write our code/contract logic inside thecurly braces .
Creates the token with name "" and symbol ""
Makes the deployer the owner
Mints initial 21 million tokens to the deployer
Done only once when contract is deployed
//We will emit events whenever important logic has happened ie burns,mints,transfers
event TokensSent(address indexed from, address indexed to, uint256 amount);
event TokensBurned(address indexed burner,uint256 amount);
event TokensMinted(address indexed recipient, uint256 amount;
//We will have a maximum supply of 21 million Tokens
uint256 public constant MAX_SUPPLY = 21000000e18
constructor(string memory name,
string memory sysmbol)
ERC20(name, symbol) Ownable(msg.sender){
_mint(msg.sender, 21000000e18); //Minting initial Supply to the deployer of the token which is me
}
Allows users to send tokens to another address
Checks:
Can't send to zero address
Amount must be greater than zero
Sender must have enough tokens
Emits TokensSent event when successful
/**
* @title send
* @dev Allows users to send tokens to another address
* @param to The address to send tokens to
* @param amount The amount of tokens to send
* @return success Whether the transfer was successful
*/
function send(address payable to,
uint256 amount) public payable returns (bool success){
require( to != address(0), "Can not send to zero address");
require(amount > 0, "Amount must be greater than zero");
require(balanceOf(msg.sender) >= amount, "Not enough Tokens to send");
_transfer(msg.sender, to, amount);
emit TokensSent(msg.sender, to, amount);
return true;
}
Sends tokens to multiple addresses in one transaction
Takes arrays of recipients and amounts
Checks:
Arrays must be same length
Must have at least one recipient
Sender must have enough total tokens
Can't send to zero address
Emits TokensSent event for each transfer
/**
* @title multiSend
* @dev Allows users to send tokens to multiple addresses at once
* @param recipients Array of addresses to send tokens to
* @param amounts Array of token amounts to send
*/
function multiSend(
address payable[] calldata recipients,
uint256[] calldata amounts ) public payable {
require(recipients.length == amounts.length, "Arrays need to be of same length");
require(recipients.length > 0 ,"Must send to atleast one recipient");
uint256 totalAmount = 0;
for( uint256 i = 0 ; i < totalAmount ; i++){
totalAmount += amounts[i];
}
require(balanceOf(msg.sender) >= totalAmounts, "Not enough funds to do the transfer");
for(uint256 i = 0; i < recipients.length; i++){
_transfer(msg.sender, recipients[i],amounts[i]);
emit TokensSent(msg.sender, recipients[i],amounts[i]);
}
}
Lets anyone check the token balance of any address
Returns the number of tokens owned by that address
View function (doesn't modify state)
/**
* @title checkBalance
* @dev Check the token balance of any address
* @param account The address to check
* @return The token balance of the address
*/
function checkBalance(address account) public view returns (uint256){
return balanceOf(account);
}
Only owner can create new tokens
Checks:
Can't mint to zero address
Can't exceed MAX_SUPPLY
Emits TokensMinted event
Increases total supply
/**
* @title mint
* @dev Owner can mint new tokens up to MAX_SUPPLY
* @param to Address to mint tokens to
* @param amount Amount of tokens to mint
*/
function mint(address payable to, uint256 amount) public onlyOwner {
require(to != address(0), "Can not mint to the zero address");
require(amount > 0, "Amount can not be zero");
require(totalSupply() + amount <= MAX_SUPPLY, "YOu cant exceed the MAX supply");
_mint(to,amount);
emit TokensMinted(to, amount);
}
Allows users to destroy their own tokens
Reduces total supply
Checks:
Can't burn zero tokens
Must have enough tokens to burn
Emits TokensBurned event
/**
* @title burn
* @dev Allows users to burn their own tokens
* @param amount The amount of tokens to burn
*/
function burn(uint256 amount) public {
require(amount > 0, "Can not burn zero tokens");
require(balanceOf(msg.sender) >= amounts , "You dont have enough tokens to burn");
_burn(msg.sender, amount);
emit TokensBurned(msg.sender,amount);
}
getTokenInfo()
: Returns token name, symbol, decimals, and current supply in one callname()
: Returns token name ()symbol()
: Returns token symbol ()decimals()
: Returns number of decimals (18)totalSupply()
: Returns the total supply ()
/**
* @dev Get token information
* @return name_ Token name
* @return symbol_ Token symbol
* @return decimals_ Token decimals
* @return supply_ Current total supply
*/
function getTokenInfo() public view returns (
string memory name_,
string memory symbol_,
uint8 decimals_,
uint256 supply_
) {
return (
name(),
symbol(),
decimals(),
totalSupply()
);
}
- Full CodeBase
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MemeCoin
* @dev A simple meme coin token contract for educational purposes
* @dev Don't use on a commercial scale as it is not audited.
*/
contract MemeCoin is ERC20, Ownable {
// Events to track token transfers, burns and mints
event TokensSent(address indexed from, address indexed to, uint256 amount);
event TokensBurned(address indexed burner, uint256 amount);
event TokensMinted(address indexed recipient, uint256 amount);
// Maximum supply of tokens (21 million with 18 decimals)
uint256 public constant MAX_SUPPLY = 21_000_000e18
;
constructor(string memory name,
string memory symbol) ERC20(name,symbol) Ownable(msg.sender) {
// Mint initial supply to contract deployer (2 million tokens)
_mint(msg.sender, 2_000_000e18);
}
/**
* @dev Allows users to send tokens to another address
* @param to The address to send tokens to
* @param amount The amount of tokens to send
* @return success Whether the transfer was successful
*/
function sendTokens(address to, uint256 amount) public returns (bool success) {
require(to != address(0), "Cannot send to zero address");
require(amount > 0, "Amount must be greater than zero");
require(balanceOf(msg.sender) >= amount, "Not enough tokens to send");
_transfer(msg.sender, to, amount);
emit TokensSent(msg.sender, to, amount);
return true;
}
/**
* @dev Allows users to send tokens to multiple addresses at once
* @param recipients Array of addresses to send tokens to
* @param amounts Array of token amounts to send
*/
function multiSend(address[] calldata recipients, uint256[] calldata amounts) public {
require(recipients.length == amounts.length, "Arrays must be same length");
require(recipients.length > 0, "Must send to at least one recipient");
uint256 totalAmount = 0;
for(uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
require(balanceOf(msg.sender) >= totalAmount, "Insufficient balance for multi-send");
for(uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "Cannot send to zero address");
_transfer(msg.sender, recipients[i], amounts[i]);
emit TokensSent(msg.sender, recipients[i], amounts[i]);
}
}
/**
* @dev Check the token balance of any address
* @param account The address to check
* @return The token balance of the address
*/
function checkBalance(address account) public view returns (uint256) {
return balanceOf(account);
}
/**
* @dev Owner can mint new tokens up to MAX_SUPPLY
* @param to Address to mint tokens to
* @param amount Amount of tokens to mint
*/
function mint(address to, uint256 amount) public onlyOwner {
require(to != address(0), "Cannot mint to zero address");
require(totalSupply() + amount <= MAX_SUPPLY, "Would exceed max supply");
_mint(to, amount);
emit TokensMinted(to, amount);
}
/**
* @dev Allows users to burn their own tokens
* @param amount The amount of tokens to burn
*/
function burn(uint256 amount) public {
require(amount > 0, "Cannot burn zero tokens");
require(balanceOf(msg.sender) >= amount, "Not enough tokens to burn");
_burn(msg.sender, amount);
emit TokensBurned(msg.sender, amount);
}
/**
* @dev Get token information
* @return name_ Token name
* @return symbol_ Token symbol
* @return decimals_ Token decimals
* @return supply_ Current total supply
*/
function getTokenInfo() public view returns (
string memory name_,
string memory symbol_,
uint8 decimals_,
uint256 supply_
) {
return (
name(),
symbol(),
decimals(),
totalSupply()
);
}
}
Important Contract Constants:
MAX_SUPPLY
: 21 million tokens (21_000_000e18)Initial supply: 2 million tokens (2_000_000e18)
Events that get emitted:
TokensSent
: When tokens are transferredTokensBurned
: When tokens are burnedTokensMinted
: When new tokens are created
Set the compiler to use 0.8.28 and Click Compile .
It will show a green tick after a successfull Compilation.
4. Deploy Our Contract to Base
Open the deployment plugin and select Injected Provider - Metamask as the environment.
When Metamask pops up, switch the network to Base.
Since you/we didn’t acquire testnet tokens, you/we can proceed to deploy directly to the Base Mainnet. The gas fees on Base are significantly lower compared to Ethereum or Solana, making it a more cost-effective choice.
The constructor for the contract will be invoked. Enter the token name and symbol when prompted.
After that, click Transact to deploy your contract.
-
You can see the total gas fee $0.16
5. View Contract Details on BaseScan
After deploying the contract, you can view its details on BaseScan using the following address:
Contract Address:
0xd2cB6Bb417CeB5cEC1A8A4d69EF5f0c4f3Fbae9f
➡ BaseScan - Unverified
Initially, I encountered issues verifying the contract on BaseScan, so I used alternative explorers:
If you want to learn how to verify contracts using Remix, check out this detailed Twitter thread I wrote.
Thank you for staying till the end.
You can check more of me here .
https://calendly.com/mulinyafadhil/coffee-chat-s-with-fadhil
https://x.com/mulinyafadhil
https://www.linkedin.com/in/fadhil-mulinya-35464b238/
https://warpcast.com/mulinya