Series: 30 Days of Solidity
Topic: Implementing ERC-20 Token Standard with Foundry
Difficulty: Beginner β Intermediate
Estimated Time: 45 mins
π§© Introduction
Todayβs challenge: Letβs create our own digital currency!
Weβre going to implement an ERC-20 Token β the most widely used token standard in the Ethereum ecosystem. Whether itβs your favorite DeFi protocol, a DAOβs governance token, or an in-game currency, most fungible assets follow the ERC-20 interface.
This project will teach you how to design, build, and test your own ERC-20 token from scratch using Foundry, the blazing-fast Solidity development toolkit.
π What Weβll Learn
By the end of this article, youβll understand:
- β What makes a token ERC-20 compliant
- β
The purpose of each standard function (
transfer,approve,transferFrom, etc.) - β How to manage balances and allowances
- β How to mint and burn tokens safely
- β How to test your token using Foundryβs Forge
π οΈ Tech Stack
| Tool | Purpose |
|---|---|
| Foundry (Forge) | For building, testing, and deploying smart contracts |
| Solidity (v0.8.19) | Smart contract programming language |
| Anvil | Local Ethereum test node |
| Cast | CLI for contract interaction |
β‘ No Hardhat. No JavaScript. Just pure Solidity and Rust-speed Foundry magic.
π§± File Structure
day-12-erc20-token/ ββ src/ β ββ DayToken.sol ββ script/ β ββ DeployDayToken.s.sol ββ test/ β ββ DayToken.t.sol ββ foundry.toml ββ README.md π¦ Step 1: Initialize a Foundry Project
Start fresh with a new Foundry workspace:
forge init day-12-erc20-token cd day-12-erc20-token Remove the sample files (optional):
rm -rf src/Counter.sol test/Counter.t.sol π‘ Step 2: Create the ERC-20 Token Contract
Weβll create our token β DayToken (DAY) β with minting and burning capabilities, plus basic ownership control.
π File: src/DayToken.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; /// @title DayToken - A minimal ERC20 implementation built with Foundry /// @author /// @notice This contract demonstrates how to create and manage ERC20 tokens manually contract DayToken { string public name = "DayToken"; string public symbol = "DAY"; uint8 public constant decimals = 18; uint256 public totalSupply; address public owner; mapping(address => uint256) private balances; mapping(address => mapping(address => uint256)) private allowances; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } constructor(uint256 initialSupply) { owner = msg.sender; _mint(msg.sender, initialSupply * 10 ** uint256(decimals)); } function balanceOf(address account) external view returns (uint256) { return balances[account]; } function transfer(address to, uint256 amount) external returns (bool) { require(to != address(0), "Invalid address"); require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; emit Transfer(msg.sender, to, amount); return true; } function approve(address spender, uint256 amount) external returns (bool) { allowances[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function allowance(address _owner, address spender) external view returns (uint256) { return allowances[_owner][spender]; } function transferFrom(address from, address to, uint256 amount) external returns (bool) { uint256 allowed = allowances[from][msg.sender]; require(allowed >= amount, "Allowance exceeded"); require(balances[from] >= amount, "Insufficient balance"); balances[from] -= amount; balances[to] += amount; allowances[from][msg.sender] = allowed - amount; emit Transfer(from, to, amount); return true; } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } function burn(address from, uint256 amount) external onlyOwner { require(balances[from] >= amount, "Insufficient balance"); balances[from] -= amount; totalSupply -= amount; emit Transfer(from, address(0), amount); } function _mint(address to, uint256 amount) internal { require(to != address(0), "Invalid address"); balances[to] += amount; totalSupply += amount; emit Transfer(address(0), to, amount); } } π§ͺ Step 3: Write Unit Tests with Foundry
π File: test/DayToken.t.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../src/DayToken.sol"; contract DayTokenTest is Test { DayToken token; address alice = address(0x1); address bob = address(0x2); function setUp() public { token = new DayToken(1000); } function testInitialSupply() public { uint256 expected = 1000 * 10 ** token.decimals(); assertEq(token.totalSupply(), expected); } function testTransfer() public { token.transfer(alice, 100 ether); assertEq(token.balanceOf(alice), 100 ether); } function testApproveAndTransferFrom() public { token.approve(alice, 200 ether); vm.prank(alice); token.transferFrom(address(this), bob, 200 ether); assertEq(token.balanceOf(bob), 200 ether); } function testMintOnlyOwner() public { token.mint(alice, 50 ether); assertEq(token.balanceOf(alice), 50 ether); } function testFailMintNotOwner() public { vm.prank(alice); token.mint(bob, 10 ether); // should revert } function testBurn() public { uint256 supplyBefore = token.totalSupply(); token.burn(address(this), 100 ether); assertEq(token.totalSupply(), supplyBefore - 100 ether); } } Run tests:
forge test -vv π Step 4: Deploy Script
π File: script/DeployDayToken.s.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Script.sol"; import "../src/DayToken.sol"; contract DeployDayToken is Script { function run() external { uint256 deployerKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerKey); DayToken token = new DayToken(1_000_000); console.log("Token deployed at:", address(token)); vm.stopBroadcast(); } } Deploy on a local node (Anvil):
anvil Then in another terminal:
forge script script/DeployDayToken.s.sol --rpc-url http://localhost:8545 --private-key <YOUR_PRIVATE_KEY> --broadcast π Security Considerations
- Owner-only mint/burn: Prevents unauthorized token inflation.
- Avoid public minting: Never expose
mint()to arbitrary callers. - Zero address checks: Ensures tokens arenβt sent to dead accounts.
- Use modifiers carefully: Restrict ownership and sensitive actions.
For production-grade deployments, use OpenZeppelinβs ERC-20 implementation to reduce security risks and ensure full compliance.
π§ Key Takeaways
| Concept | Description |
|---|---|
| ERC-20 | Standard for fungible tokens on Ethereum |
| Foundry (Forge) | Tool for testing, scripting, and deploying Solidity contracts |
| Allowances | Mechanism allowing third parties to spend tokens |
| Minting/Burning | Controls total supply |
| Events | Emitted for every Transfer and Approval for on-chain visibility |
π Next Steps
Now that youβve created your ERC-20 token:
- Try deploying it to a testnet (Sepolia or Base)
- Integrate it into a simple frontend wallet
- Add extensions like burnable, pausable, or governance features
π Wrap Up
Youβve just built your own digital currency from scratch using Foundry! π
This exercise not only reinforces your understanding of Solidity standards but also gives you a real foundation for DeFi, DAOs, and beyond.
π Read more posts in my #30DaysOfSolidity journey here π
https://dev.to/sauravkumar8178/
Top comments (0)