Lesson 10 / 25
ERC-20 Fungible Tokens
Understand the ERC-20 interface, approvals and safe usage.
The fungible token standard
ERC-20 defines fungible tokens, where every unit is interchangeable: stablecoins, governance tokens and in-app points. The interface includes totalSupply(), balanceOf(account), transfer(to, amount), approve(spender, amount), allowance(owner, spender) and transferFrom(from, to, amount), plus the Transfer and Approval events; name, symbol and decimals (usually 18, but USDC and USDT use 6) are optional metadata. The approve-then-transferFrom pattern lets contracts such as exchanges pull tokens you have authorised. Unlimited approvals are convenient but dangerous if the approved contract is compromised, so approve only what is needed and revoke unused approvals. EIP-2612 permit lets users approve with an off-chain signature, saving a transaction. Some real tokens deviate from the standard (missing return values, fees on transfer, rebasing balances, blocklists), so integrations should use OpenZeppelin's SafeERC20 (safeTransfer, safeTransferFrom) and avoid assuming the received amount equals the sent amount. Creating a token is technically easy; the hard parts are distribution, legal and regulatory compliance, and real utility.
Approve and transferFrom
The owner approves a spender, which can then pull up to the approved amount.
A capped ERC-20 token and a safe integration
OpenZeppelin v5 base contracts and SafeERC20.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract CampusPoints is ERC20, ERC20Permit, Ownable {
uint256 public constant MAX_SUPPLY = 10_000_000 * 1e18;
constructor(address initialOwner)
ERC20("Campus Points", "CPT")
ERC20Permit("Campus Points")
Ownable(initialOwner)
{}
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "cap exceeded");
_mint(to, amount);
}
}
contract TuitionDesk {
using SafeERC20 for IERC20;
IERC20 public immutable token;
mapping(address => uint256) public paid;
constructor(IERC20 token_) { token = token_; }
function payFees(uint256 amount) external {
uint256 before = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount); // requires prior approve
uint256 received = token.balanceOf(address(this)) - before; // handles fee-on-transfer tokens
paid[msg.sender] += received;
}
}Decimals are not always 18
USDC and USDT use 6 decimals, so 1 USDC is 1_000_000. Always read decimals() or configure it per token; mixing units is a common and costly integration bug.
Quick check: Why use SafeERC20 when interacting with arbitrary tokens?
- It makes transfers free
- It converts tokens to ether
- Some tokens do not follow the standard exactly (for example, no return value), and SafeERC20 handles these cases and reverts on failure
- It is required to compile
Answer
Some tokens do not follow the standard exactly (for example, no return value), and SafeERC20 handles these cases and reverts on failure — SafeERC20 wraps calls to handle non-standard tokens safely.