I Built an ERC-20 Token and React dApp from Scratch Complete Web3 Breakdown

Published: (May 3, 2026 at 02:22 AM EDT)
2 min read
Source: Dev.to

Source: Dev.to

What I Built

HarambeeCoin (HBC) is a custom ERC‑20 token. The dApp lets you:

  • Connect MetaMask
  • Check your balance
  • Send tokens
  • View your full transaction history (sent and received)

The Smart Contract

I wrote HarambeeCoin from scratch without OpenZeppelin to understand every line. The core includes the standard ERC‑20 functions plus custom mint (owner‑only) and burn (any holder).

function transfer(address to, uint256 amount) public returns (bool) {
    require(balanceOf[msg.sender] >= amount, "Insufficient balance");
    balanceOf[msg.sender] -= amount;
    balanceOf[to] += amount;
    emit Transfer(msg.sender, to, amount);
    return true;
}

Testing with Hardhat

Six tests cover the critical paths. Example test:

it("Should transfer tokens between accounts", async function () {
    await harambeeCoin.transfer(addr1.address, 1000);
    expect(await harambeeCoin.balanceOf(addr1.address)).to.equal(1000);
});

All six pass. Testing first let me catch a balance‑check bug before it ever touched a real network.

Connecting React to the Blockchain

ethers.js v6 handles the connection. The following function sets up the entire Web3 layer:

import { ethers } from "ethers";

async function initWeb3() {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const contractInstance = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);
  return { provider, signer, contractInstance };
}

The contractInstance works like a regular JavaScript object—you call its methods and await the results.

Transaction History from Events

Every ERC‑20 transfer emits a Transfer event. ethers.js lets you query past events using filters:

const sentFilter = contract.filters.Transfer(account, null);
const receivedFilter = contract.filters.Transfer(null, account);
const sentEvents = await contract.queryFilter(sentFilter, 0, "latest");
const receivedEvents = await contract.queryFilter(receivedFilter, 0, "latest");

This provides a full history without a backend; the blockchain itself acts as the database.

What Broke

  • MetaMask cache – When the Hardhat node restarts, MetaMask may still think it’s on the old chain. Delete and re‑add the Localhost network in MetaMask settings after each node restart.
  • Chain ID – Hardhat defaults to 31337, not 1337. Verify the chain ID from the node output before configuring MetaMask.
  • ENS resolution – ENS lookups fail on local networks. Ensure any address you paste starts with 0x and is a full 42‑character hex string.

What Is Next

  • Deploy to Sepolia testnet
  • Add a token faucet so anyone can get HBC
  • Extend with NFT minting
  • Implement DAO voting using HBC tokens

Try It

GitHub:

0 views
Back to Blog

Related posts

Read more »