Beginners Guide to Building a MEME Coin on Base

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:

  1. Fungible Tokens

  2. Non-Fungible Tokens (NFTs)

  3. 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

  1. Create a Folder

  2. 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:

  1. Main Token Functions:
  • sendTokens(address to, uint256 amount): Direct way to send tokens to another address

  • checkBalance(address account): View the token balance of any wallet address

  1. Multi-User Features:
  • multiSend(address[] recipients, uint256[] amounts): Send different amounts of tokens to multiple addresses in one transaction
  1. Supply Management:
  • mint(address to, uint256 amount): Only owner can create new tokens up to max supply

  • burn(uint256 amount): Anyone can burn their own tokens, reducing total supply

  1. Information Functions:
  • getTokenInfo(): Returns token name, symbol, decimals, and current supply in one function call

  • name(): 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 call

  • name(): 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 transferred

  • TokensBurned: When tokens are burned

  • TokensMinted: When new tokens are created

  1. 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

  1. Open the deployment plugin and select Injected Provider - Metamask as the environment.

  2. When Metamask pops up, switch the network to Base.

  3. 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.

  4. The constructor for the contract will be invoked. Enter the token name and symbol when prompted.

  5. After that, click Transact to deploy your contract.

  1. 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