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/

