Sample ERC20 Contract
The following is an example of an ERC-20 smart contract that mints the total token supply to the account that deploys the contract.
This example is adapted from OpenZeppelin ERC20.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
// OpenZeppelin Ownable module for owner-based access control
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title Sample20Token
* @dev A custom ERC20 token contract implementing essential ERC20 functionality.
* - Implements standard functions: transfer, approve, transferFrom
* - Only the owner can call mint and burn
* - Uses OpenZeppelin's Ownable for access control (deployer as owner)
*/
contract Sample20Token is Ownable {
// ----------------------
// ░░░ Custom Errors ░░░
// ----------------------
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); // Insufficient balance
error ERC20InvalidSender(address sender); // Invalid sender address
error ERC20InvalidReceiver(address receiver); // Invalid receiver address
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); // Allowance too low
error ERC20InvalidApprover(address approver); // Invalid approver address
error ERC20InvalidSpender(address spender); // Invalid spender address
// ----------------------
// ░░░ Events ░░░
// ----------------------
event Transfer(address indexed from, address indexed to, uint256 value); // Emitted on token transfer
event Approval(address indexed owner, address indexed spender, uint256 value); // Emitted on approval
// ----------------------
// ░░░ State Variables ░░░
// ----------------------
mapping(address => uint256) private _balances; // Token balance by address
mapping(address => mapping(address => uint256)) private _allowances; // owner → spender → allowance
uint256 public totalSupply; // Total token supply
string public name; // Token name
string public symbol; // Token symbol
uint8 public immutable decimals; // Number of decimals
// ----------------------
// ░░░ Constructor ░░░
// ----------------------
/**
* @param name_ Token name
* @param symbol_ Token symbol
* @param decimals_ Number of decimals (commonly 18)
*/
constructor(string memory name_, string memory symbol_, uint8 decimals_) Ownable(msg.sender) {
name = name_;
symbol = symbol_;
decimals = decimals_;
}
// ----------------------
// ░░░ Owner-Only Functions ░░░
// ----------------------
/// @notice Mints new tokens to a specified address.
/// @dev Transfers from address(0) to `to`, emits Transfer event.
/// @param to Recipient address
/// @param value Amount to mint
function mint(address to, uint256 value) external onlyOwner returns (bool) {
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), to, value);
return true;
}
/// @notice Burns tokens from a specified address.
/// @dev Transfers from `from` to address(0), emits Transfer event.
/// @param from Address to burn from
/// @param value Amount to burn
function burn(address from, uint256 value) external onlyOwner returns (bool) {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(from, address(0), value);
return true;
}
// ----------------------
// ░░░ ERC20 Standard Functions ░░░
// ----------------------
/// @notice Returns the token balance of a specific account.
/// @param account Address to query
/// @return Token balance
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
/// @notice Transfers tokens from the caller to a recipient.
/// @param to Recipient address
/// @param value Amount to transfer
/// @return Success flag
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/// @notice Transfers tokens from `from` to `to` using allowance.
/// @param from Token owner
/// @param to Recipient
/// @param value Amount to transfer
/// @return Success flag
function transferFrom(address from, address to, uint256 value) public returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/// @notice Returns the remaining allowance from owner to spender.
/// @param owner Token owner
/// @param spender Spender address
/// @return Remaining allowance
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
/// @notice Approves a spender to use tokens on behalf of the caller.
/// @param spender Address to approve
/// @param value Allowance amount
/// @return Success flag
function approve(address spender, uint256 value) public returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value, true);
return true;
}
// ----------------------
// ░░░ Internal Logic ░░░
// ----------------------
/// @dev Internal approval logic.
/// @param owner Token owner
/// @param spender Spender address
/// @param value Allowance amount
/// @param emitEvent Whether to emit Approval event
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/// @dev Deducts allowance from the spender.
/// @param owner Token owner
/// @param spender Caller (spender)
/// @param value Amount to spend
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
/// @dev Internal transfer logic.
/// @param from Sender address
/// @param to Recipient address
/// @param value Amount to transfer
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/// @dev Unified logic for mint, burn, and transfer.
/// @param from Sender address (mint: 0x0)
/// @param to Recipient address (burn: 0x0)
/// @param value Token amount to process
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Mint: increase total supply
totalSupply += value;
} else {
// Decrease sender balance
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
// Burn: decrease total supply
unchecked {
totalSupply -= value;
}
} else {
// Increase recipient balance
unchecked {
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
}
Updated 9 days ago