How to build ERC 20 Token and Faucets Dapp

thumbnail

Hello friends, Today in this post we will learn to build our own ERC20 token and a faucet Dapp(Decentralized App) using which we can interact with our contract. So we will learn to develop an ERC20 smart contract and a faucet contract which will help in requesting tokens and we will create our frontend using Next JS and use ethers library for interacting with the contract. So let’s get started.

A Short Brief on ERC20

ERC20 is also a smart contract that follows an EIP-20 standard, so in order to create our own token we need the following functionalities to be in your smart contract as defined in the standards.

EIP-20: Token Standard

So just like our normal currency, the ERC20 token also has a name and symbol. Additionally, we can generate tokens, transfer them and even allow smart contracts or other account to transfer on our behalf.

Let’s get into the ERC20 functions

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)

name — to return the name of the token
symbol— to return the symbol of the token
decimals— to return the decimals. Since solidity does not support decimal points we need to provide the number of decimals such that the number of zeros will be added at the end. For eg: if we have 18 decimals then for 1000000000000000000 will be equal to 1 Token and 10000000000000000 will be equal to 0.01 Token.
balanceOf— returns the token balance of the address passed.
totalSupply— returns the number of tokens generated.
⁠transfer(toAddress, amount)— to transfer tokens from our account to the address specified.
transferFrom(from, to, amount)— Used to transfer tokens on behalf of the user and returns true if successful. For security purposes, the fromAddress should call the approve method with toAddress and amount.
approve(spenderAddress, amount)—To make transferFrom work, we need to approve the spender address who will transfer our tokens on our behalf. For eg: If account#0 wants account#1 to transfer 10 tokens on its behalf to itself, then account#0 should call approve(account#1, 10) after approving now account#1 can call transferFrom(account#0, account#1,10)
allowances(ownerAddress, spenderAddress) — To get the number of tokens approved to spenderAddress from ownerAddress.For eg: After we call the approve function we have one mapping (uint256 => mapping(uint256 => amount) ) which stores who can send how many tokens to whom.

Now we got know about the ERC20 lets write the smart contract.

Project Setup

Create a project directory I will name it ERC20-DAPP inside it let's have one folder named smart-contracts and initialize a hardhat javascript project

npx hardhat
npm install — save-dev “hardhat@².12.3” “@nomicfoundation/hardhat-toolbox@².0.0”

Again in our project directory let's initialize a NEXT js project.

npx create-next-app frontend

Building Smart Contracts

We are going to use openzeppelin/contracts package for the token creation, so we will install using

npm i @openzeppelin/contracts

Contracts Wizard - OpenZeppelin Docs

Let's create a new contract file CWMToken.sol inside the contracts folder and start writing our contract. The first line would be the license identifier I will define it as MIT. Next, we will mention our solidity version using pragma solidity. We will import ERC20.sol from @openzeppelin/contracts. Now let's define our contract which will inherit ERC20 so that we can use the functions inside that ERC20.sol. In the constructor, part initializes the ERC20 with the name and symbol of our token.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

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

contract CWMToken is ERC20 {
    constructor() ERC20("CodeWithMarish", "CWM") {}
}

Inside the constructor, let's generate some tokens and send them to our account. For that, we will use the _mint function of ERC20 which generates token and transfer them to the account which we specify.

Also, create a mint function to generate tokens whenever we needed, will name it a mint with parameters address, and amount inside that we will call _mint function. One additional thing we need we don’t want someone else to generate our tokens and want only the owner of the contract to generate the tokens. We can import an Ownable contract from openzeppelin contracts and make our contract inherit it. Now lets add modifier onlyOwner which is defined in Ownable Contract to mint function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// Uncomment this line to use console.log
// import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CWMToken is ERC20, Ownable {
    constructor() ERC20("CodeWithMarish", "CWM") {
        _mint(msg.sender, 100 ether);
    }

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

For deploying let’s import one more package hardhat-deploy

npm i -D hardhat-deploy
npm install — save-dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers

Now let’s write our deploy script, create a new folder deploy inside that create a file 00-cwm.js. Inside it, we need an export function that gives hardhat ethers as a parameter from which we can extract getNamedAccounts and deployments. The deployments is an object which gives us deploy and log using this deploy we can deploy our contract and log for printing statements to the console. Using namedAccounts we can list accounts for our local hardhat network. For this namedAccounts to work in hardhat.config.js we will add one more property namedAccounts inside that deployer and it will default to 0 so that when we use getNamedAccounts we will get an account for the deployer.

./smart-contracts/hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.17",
  namedAccounts:{
    deployer:{
      default: 0
    }
  }
};

./smart-contracts/deploy/00-cwm.js

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();

  await deploy("CWMToken", {
    from: deployer,
    args: [],
    waitConfirmations: 1,
    log: true,
  });
};

To deploy run the below command it will deploy all scripts inside the deploy folder in the order they are placed in the folder make sure you number it if you have multiple scripts.

npx hardhat deploy

Now let’s write some tests for our contract, create a new file 01-cwm.test.js inside the test folder to make sure everything works fine, for that we will use the chai package which comes by default in hardhat. We need two functions assert and expect to import from chai to validate conditions. We need our contract to be deployed before each test. We can call**deployments.fixture()**which will internally call npx hardhat deploy to deploy our contracts. We will have a global variable for contract, accounts using ethers.getContract(“contract name”) we will get the contract deployed and ethers.getSigners() to get the list of accounts connected to the network.

Now for the constructor test we can check whether the deployer balance is 100 by using the balanceOf function of the token. We can also check name and symbols by calling name() and symbol() functions.

Next, we can test the token transfer functionality by transferring some tokens from account0 to account1 after that we can check the balance with assert to pass the test.

Finally, we need to test whether the contract or account can transfer tokens from our account on our behalf if we approve them. For that we will approve account1 for sending 10 token on behalf of account0, next we will connect our token contract with account1 and use transferFrom function.

To run the test

npx hardhat test

./smart-contracts/test/01-cwm.test.js

const { expect, assert } = require("chai");
const { deployments, ethers } = require("hardhat");

describe("CWM", function () {
  let cwm, accounts;
  beforeEach(async () => {
    await deployments.fixture();
    cwm = await ethers.getContract("CWMToken");
    accounts = await ethers.getSigners();
  });

  describe("Starting Test", () => {
    describe("Constructor check", () => {
      it("Constructor check for deployer gets 100 token and check the name symbol to be CodeWithMarish and CWM", async () => {
        assert.equal(
          (await cwm.balanceOf(accounts[0].address)).toString(),
          ethers.utils.parseEther("100").toString()
        );

        assert.equal(await cwm.name(), "CodeWithMarish");
        assert.equal(await cwm.symbol(), "CWM");
      });
      it("Transfer 10 token from account 0 to account 1", async () => {
        await cwm.transfer(accounts[1].address, ethers.utils.parseEther("10"));
        assert.equal(
          (await cwm.balanceOf(accounts[0].address)).toString(),
          ethers.utils.parseEther("90").toString()
        );
        assert.equal(
          (await cwm.balanceOf(accounts[1].address)).toString(),
          ethers.utils.parseEther("10").toString()
        );
      });

      it("Approve contract to take tokens from sender account to another account else we will get not enough allowances error", async () => {
        await expect(
          cwm.transferFrom(
            accounts[0].address,
            accounts[1].address,
            ethers.utils.parseEther("10")
          )
        ).to.be.revertedWith("ERC20: insufficient allowance");
        let tx = await cwm.approve(
          accounts[1].address,
          ethers.utils.parseEther("10")
        );
        await tx.wait();
        const cwm1 = await cwm.connect(accounts[1]);
        let tx1 = await cwm1.transferFrom(
          accounts[0].address,
          accounts[1].address,
          ethers.utils.parseEther("10")
        );
        await tx1.wait();
        assert.equal(
          (await cwm.balanceOf(accounts[1].address)).toString(),
          ethers.utils.parseEther("10").toString()
        );
      });
    });
  });
});

That’s it for our token contract.

Now let’s code the faucet contract, our faucet contract should provide 10 tokens when the user requests but the contract provides only after a specific time configured for example the user can request only once per day. So we need a map to store the user address and its next buy time and getter for the buy time limit. We will initialize a token contract and buylimittime since we don't want to change these values further after initializing we will mark them as immutable to save some gas fee and we will initialize them in the constructor, after that we cannot change them. Now for requestToken function, we can check whether we are not sending to a zero address, block.timestamp(returns current block time) is greater than the last user request time using require(). If these two condition passes then we call the transfer function to the requestor address using msg.sender. Here 10 ether does not refer to ethers it is just a value as 1 ether = 10000000000000000 we can also provide the value but it is a shortcut.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "./CWMToken.sol";
contract Faucets {
    mapping(address => uint256) userNextBuyTime;
    uint256 private immutable buyTimeLimit;
    CWMToken private immutable cwmToken;

    constructor(address tokenAddress, uint256 _buyTimeLimit) {
        cwmToken = CWMToken(tokenAddress);
        buyTimeLimit = _buyTimeLimit;
    }
    function requestTokens() public {
        require(msg.sender != address(0), "Cannot send token zero address");
        require(
            block.timestamp > userNextBuyTime[msg.sender],
            "Your next request time is not reached yet"
        );
        require(
            cwmToken.transfer(msg.sender, 10 ether),
            "requestTokens(): Failed to Transfer"
        );
        userNextBuyTime[msg.sender] = block.timestamp + buyTimeLimit;
    }
    function getNextBuyTime() public view returns (uint256) {
        return userNextBuyTime[msg.sender];
    }
}

Deploy script would be slightly different than CWMToken deploy script since we need to pass constructor arguments. We need to get the deployed CWMToken contract and inside the args list we pass the address of token and 1800(30 minutes).

./smart-contracts/deploy/01-faucets.js

const { ethers } = require("hardhat");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const cwm = await ethers.getContract("CWMToken");
  await deploy("Faucets", {
    from: deployer,
    args: [cwm.address, 1800],
    waitConfirmations: 1,
    log: true,
  });
};

We will now write a test 02-faucets.test.js to check out the functionality like how we did for a token but we need to test whether we are able to purchase tokens after buy time passes we can increase the network time by using the network from hardhat.

./smart-contracts/test/02-faucets.test.js

const { expect, assert } = require("chai");
const { deployments, ethers, network } = require("hardhat");

describe("Faucets", function () {
  let cwm, faucets, accounts, buyTimeLimit;
  beforeEach(async () => {
    await deployments.fixture();
    cwm = await ethers.getContract("CWMToken");
    faucets = await ethers.getContract("Faucets");
    accounts = await ethers.getSigners();
    buyTimeLimit = await faucets.getBuyLimitTime();
    await cwm.transfer(faucets.address, ethers.utils.parseEther("50"));
  });

  describe("Starting Test", () => {
    it("Request Tokens and receive 10 tokens from contract, fails last purchase less buytimeLimit and we can request tokens after buylimit time exceeds", async () => {
      let tx = await faucets.requestTokens();
      await tx.wait();
      assert.equal(
        (await cwm.balanceOf(accounts[0].address)).toString(),
        ethers.utils.parseEther("60").toString()
      );

      await expect(faucets.requestTokens()).to.be.revertedWith(
        "Your next request time is not reached yet"
      );
      //   console.log(buyTimeLimit.toNumber());
      await network.provider.send("evm_increaseTime", [
        buyTimeLimit.toNumber() + 1,
      ]);
      await network.provider.request({ method: "evm_mine", params: [] });

      let tx1 = await faucets.requestTokens();
      await tx1.wait();
      assert.equal(
        (await cwm.balanceOf(accounts[0].address)).toString(),
        ethers.utils.parseEther("70").toString()
      );
    });
  });
});

Now we are done with our Faucet contract let’s build our frontend.

Frontend

Let's build our frontend, initiate by generating a next js app using

npx create-next-app frontend

Install ethers
npm i ethers or yarn add ethers

To run
npm run dev or yarn dev

We will use Context API for statement management as in our previous we have built a metamask connect with next js we will utilize that, you can refer here

How to add Web3 Authentication using NEXT JS & MetaMask?

Now let’s create a function that will help in interacting with the contracts. Before writing the function we will need the contract address and contract Abi. Let us run a local hardhat network using

npx hardhat node

This will deploy and start a local running environment where we can test with our front end. Copy the address of the deployed contract and inside the smart contracts/artifacts/contracts folder there will be CWMToken.sol and Faucets.sol inside this folder there will be CWMToken.json and Faucets.json. Inside this JSON file, there will be a property abi you need to copy the list. Now inside the frontend folder create utils/constants.js, we can create variables for contract addresses and abi and paste the values for each variable.

./frontend/utils/constants.js

export const cwmContractAddress="0x5Fb....aa3";
export const faucetsContractAddress="0xe7f...512";
export const cwmAbi=[{...}];
export const faucetsAbi=[{...}];

To get the contract object we need the provider, signer, contractAddress, and contract Abi.
provider — We will get the provider once we connect to the network
signer — ethers.getSigner returns a signer

Now on the component mount just after connecting to our account, we will call this function to loadContract and store it in a state variable. All functions in a contract are asynchronous, to call a view function, we can directly call await contract.functionName(). For a non-view function, it is 2 step process first we need to call the function next we need to call the wait function to wait for the transaction to complete. Also, add try-catch block to catch errors.

./frontend/context/AppContext.js

import { ethers } from "ethers";
import React, { createContext, useEffect, useState } from "react";
import {
  cwmAbi,
  cwmContractAddress,
  faucetsAbi,
  faucetsContractAddress,
} from "../utils/constants";

export const AppContext = createContext();

const { ethereum } = typeof window !== "undefined" ? window : {};
const POSSIBLE_ERRORS = [
  "ERC20: insufficient allowance",
  "Your next request time is not reached yet",
  "ERC20: transfer amount exceeds balance",
  "requestTokens(): Failed to Transfer",
];

const createContract = (address, contractName) => {
  console.log(address);
  const provider = new ethers.providers.JsonRpcProvider();
  const signer = provider.getSigner(address);
  let contract;
  if (contractName == "coin")
    contract = new ethers.Contract(cwmContractAddress, cwmAbi, signer);
  else
    contract = new ethers.Contract(faucetsContractAddress, faucetsAbi, signer);
  console.log(contract);
  return contract;
};

const AppProvider = ({ children }) => {
  const [account, setAccount] = useState("");
  const [refresh, setRefresh] = useState(0);
  const [message, setMessage] = useState("");
  const [balance, setBalance] = useState(0);
  const [nextBuyTime, setNextBuyTime] = useState(0);
  const [cwmContract, setCwmContract] = useState();
  const [faucetContract, setfaucetContract] = useState();

  const checkErrors = (err) => {
    const error = POSSIBLE_ERRORS.find((e) => err.includes(e));
    console.log(error);
    if (error) {
      setMessage({
        title: "error",
        description: error,
      });
    }
  };

  const checkEthereumExists = () => {
    if (!ethereum) {
      return false;
    }
    return true;
  };
  const getConnectedAccounts = async () => {
    try {
      const accounts = await ethereum.request(
        {
          method: "eth_accounts",
        },
        []
      );
      setAccount(accounts[0]);
    } catch (err) {
      setMessage({ title: "error", description: err.message.split("(")[0] });
    }
  };
  const connectWallet = async () => {
    if (checkEthereumExists()) {
      try {
        const accounts = await ethereum.request(
          {
            method: "eth_requestAccounts",
          },
          []
        );
        console.log(accounts);
        setAccount(accounts[0]);
      } catch (err) {
        setMessage({ title: "error", description: err.message.split("(")[0] });
      }
    }
  };

  const callContract = async (cb) => {
    if (checkEthereumExists() && account) {
      try {
        await cb();
      } catch (err) {
        console.log(err);
        checkErrors(err.message);
      }
    }
  };

  const getBalance = () => {
    callContract(async () => {
      console.log(account, cwmContract);
      let bal = await cwmContract.balanceOf(account);
      console.log(balance);
      setBalance(bal);
    });
  };

  const transfer = (address, amount) => {
    callContract(async () => {
      console.log(account, cwmContract, address, amount);
      let tx = await cwmContract.transfer(
        address,
        ethers.utils.parseEther(amount)
      );
      await tx.wait();
      setMessage({ title: "success", description: "Transferred Successfully" });
      setRefresh((prev) => prev + 1);
    });
  };
  const approve = async (address, amount) => {
    callContract(async () => {
      //0xfb..26 approved to take money on my behalf
      let tx = await cwmContract.approve(
        address,
        ethers.utils.parseEther(amount)
      );
      await tx.wait();
      setMessage({
        title: "success",
        description: "Approved tokens successfully",
      });
    });
  };

  const requestTokens = async () => {
    callContract(async () => {
      let tx = await faucetContract.requestTokens();
      await tx.wait();

      setMessage({
        title: "success",
        description: "Tokens Received successfully",
      });
      setRefresh((prev) => prev + 1);
    });
  };

  const getNextBuyTime = async () => {
    callContract(async () => {
      let nextBuyTime = await faucetContract.getNextBuyTime();
      setNextBuyTime(nextBuyTime.toNumber() * 1000);
    });
  };

  const loadContract = async () => {
    let cc = await createContract(account, "coin");
    let fc = await createContract(account);

    setCwmContract(cc);
    setfaucetContract(fc);
  };

  useEffect(() => {
    if (cwmContract) getBalance();
    if (faucetContract) getNextBuyTime();
  }, [refresh, cwmContract]);
  useEffect(() => {
    console.log(account);
    if (account) {
      loadContract();
    }
  }, [account]);

  useEffect(() => {
    if (checkEthereumExists()) {
      ethereum.on("accountsChanged", getConnectedAccounts);
      getConnectedAccounts();
    }
    return () => {
      if (checkEthereumExists()) {
        ethereum.removeListener("accountsChanged", getConnectedAccounts);
      }
    };
  }, []);
  return (
    <AppContext.Provider
      value={{
        account,
        connectWallet,
        balance,
        transfer,
        approve,
        requestTokens,
        nextBuyTime,
        message,
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export default AppProvider;

Now let’s build the UI we will use the tailwind CSS to speed up. We will have header component that will show a connect button or an account number if connected also using useContext hook we can get function values passed from the AppProvider.

./frontend/components/Header.js

import Link from "next/link";
import React, { useContext } from "react";
import { AppContext } from "../context/AppContext";

const Header = () => {
  const { account, connectWallet } = useContext(AppContext);

  return (
    <header className=" border-b-2 border-black">
      <div className="container py-4 max-w-3xl flex items-center justify-between">
        <h1 className="font-bold text-3xl">CWM Faucets</h1>

        {account ? (
          <p className="font-semibold text-3xl">{`${account.substring(
            0,
            5
          )}..${account.slice(-3)}`}</p>
        ) : (
          <button className="btn bg-amber-200" onClick={connectWallet}>
            Connect
          </button>
        )}
      </div>
    </header>
  );
};

export default Header;

Now, in index.js we will have buttons and input to call out functions created inside the AppContext. BigNumber.from is used to convert string to big number and ethers.formatEther is used to convert bignumber to ether formatted string for eg: 10000000000000000 returns “0.01”. The time value returned for nextBuyTime will be in unix seconds epoch so we have already multiplied with 1000 to change to millisecond epoch after that we can pass in new Date(millisecond) to get the datetime.

import { useContext, useEffect, useState } from "react";
import { AppContext } from "../context/AppContext";
import { ethers, BigNumber } from "ethers";
import Head from "next/head";
export default function Home() {
  const { balance, message, transfer, approve, nextBuyTime, requestTokens } =
    useContext(AppContext);
  const [data, setData] = useState({});

  const handleChange = (e) => {
    setData({
      ...data,
      [e.target.name]: e.target.value,
    });
  };

  return (
    <div className="bg-white-300 flex-1">
      <Head>
        <title>CWM Faucets</title>
      </Head>
      <div className="container max-w-3xl py-10">
        <div className="shadow-border bg-blue-200 p-6">
          <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
            <div className="border-4 bg-yellow-200 flex flex-col p-4 border-black">
              <h2 className="text-4xl md:text-5xl font-bold">{`${ethers.utils.formatEther(
                BigNumber.from(balance)
              )} CWM`}</h2>
              <div className="flex-1" />
              <p className="text-lg font-bold">Balance</p>
            </div>

            <button
              onClick={
                new Date() - new Date(nextBuyTime) > 0 ? requestTokens : null
              }
              className="btn  bg-green-200 md:text-2xl"
            >
              {new Date() - new Date(nextBuyTime) < 0 ? (
                <>{`Next Request at ${new Date(
                  nextBuyTime
                ).toLocaleString()}`}</>
              ) : (
                <>
                  Request <span className="block">Tokens</span>
                </>
              )}
            </button>
            <div className="sm:col-span-2 bg-gray-700 h-0.5" />
            <input
              placeholder="Address"
              className="border-4 border-black p-2"
              type={"text"}
              name="address"
              onChange={handleChange}
            />
            <input
              placeholder="Amount"
              className="border-4 border-black p-2"
              type={"text"}
              name="amount"
              onChange={handleChange}
            />

            <button
              onClick={() => {
                console.log(data);
                transfer(data.address, data.amount);
              }}
              className="btn bg-green-200 border-4 md:text-2xl"
            >
              Transfer <span className="sm:block">Tokens</span>
            </button>

            <button
              onClick={() => approve(data.address, data.amount)}
              className="btn md:text-2xl bg-orange-200 border-4"
            >
              Approve <span className="sm:block">Tokens</span>
            </button>

            <div className="col-span-2 bg-amber-200 border-4 border-black p-4">
              {message.description ? (
                <p className="text-xl capitalize">{`${message.title}: ${message.description}`}</p>
              ) : (
                <p className="text-xl">{`No Message`}</p>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
       

Now we can check out the UI and test the functionalities, you can create a function and interface to mint tokens.

Thanks for reading this post, if you found this post helpful please share maximum. Will be back with amazing content on Web3. Stay tuned.

Find this code on Github

ERC20 Faucets Dapp

If you are facing any issues please contact us from our contact section.

Contact Us | CodeWithMarish

Also please don’t forget to subscribe to our youtube channel codewithmarish for all web development-related challenges.

Code With Marish | Youtube

Related Posts