Token Standards

A from-scratch implementation (educational):

30.1 ERC-20: fungible tokens

Function / Event balance of an account transfer(to, amount) move caller's tokens approve(spender, amount) allow spender to move caller's tokens allowance(owner, spender) transferFrom(from, to, amount) spender moves tokens event Transfer(from, to, value) mint (from = 0), burn (to = 0), transfer event Approval(owner, spender, value)

Optional: name(), symbol(), decimals() metadata (decimals usually 18; USDC uses 6)

A from-scratch implementation (educational):

ERC-20 gotchas: the approve race condition (change allowance to 0 first, or use permit/increaseAllowance-style flows); non-standard tokens (USDT doesn't return bool; fee-on-transfer and rebasing tokens change balances unexpectedly) — interact with arbitrary tokens through OpenZeppelin SafeERC20 (safeTransfer, safeTransferFrom, forceApprove) and measure balance deltas when fees are possible.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleERC20 {
    string public name;
    string public symbol;
    uint8 public constant decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    error InsufficientBalance();
    error InsufficientAllowance();
    error ZeroAddress();

    constructor(string memory _name, string memory _symbol, uint256 initialSupply) {
        name = _name;
        symbol = _symbol;
        _mint(msg.sender, initialSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external retur
…

30.2 ERC-721: non-fungible tokens

Function / Event ownerOf(tokenId), balanceOf(owner) safeTransferFrom(from, to, id[, data]) transfer; calls onERC721Received if to is a contract transferFrom(from, to, id) transfer without receiver check approve(to, id), getApproved(id)

setApprovalForAll(operator, bool), isApprovedForAll operator approval (marketplaces) tokenURI(id) (metadata extension) URL/data URI to JSON metadata

Events: Transfer, Approval, ApprovalForAll Receiving contracts must implement onERC721Received(address operator, address from, uint256 tokenId, bytes data) returning its selector. Related: ERC-2981 (royalty info), ERC-4907 (rentals), ERC-721A (gas-cheap batch mints), ERC-5192 (soulbound).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, Ownable {
    uint256 public nextId;
    uint256 public constant MAX_SUPPLY = 10_000;
    uint256 public constant PRICE = 0.01 ether;
    string private baseURI;

    error SoldOut();
    error WrongPrice();

    constructor(string memory base) ERC721("My NFT", "MNFT") Ownable(msg.sender) {
        baseURI = base;
    }

    function mint() external payable returns (uint256 id) {
        if (nextId >= MAX_SUPPLY) revert SoldOut();
        if (msg.value != PRICE) revert WrongPrice();
        id = nextId++;
        _safeMint(msg.sender, id);          // reentrancy surface: calls onERC721Received
    }

    function _baseURI() internal view override returns (string memory) { return baseURI; }

    function withdraw() external onlyOwner {
        (bool ok, ) = owner().call{value: address(this).balance}("");
        require(ok);
    }
}

30.3 ERC-1155: multi-token

One contract manages many token IDs, each fungible or non-fungible: balanceOf(account, id), balanceOfBatch, safeTransferFrom(from, to, id, amount, data), safeBatchTransferFrom, setApprovalForAll, uri(id); events TransferSingle, TransferBatch, ApprovalForAll, URI. Ideal for game items and editions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract GameItems is ERC1155, Ownable {
    uint256 public constant GOLD = 0;      // fungible
    uint256 public constant SWORD = 1;     // semi-fungible
    uint256 public constant CROWN = 2;     // unique (supply 1)

    constructor() ERC1155("https://game.example/api/item/{id}.json") Ownable(msg.sender) {
        _mint(msg.sender, GOLD, 1_000_000, "");
        _mint(msg.sender, SWORD, 100, "");
        _mint(msg.sender, CROWN, 1, "");
    }

    function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts) external onlyOwner {
        _mintBatch(to, ids, amounts, "");
    }
}

30.4 ERC-165: interface detection

ERC-721 (0x80ac58cd) and ERC-1155 (0xd9b67a26) require ERC-165. When inheriting multiple OZ contracts that implement it, override supportsInterface and call super.supportsInterface(id).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IGreeter { function greet() external view returns (string memory); }

contract Greeter is IERC165, IGreeter {
    function greet() external pure returns (string memory) { return "hi"; }

    function supportsInterface(bytes4 id) external pure returns (bool) {
        return id == type(IERC165).interfaceId     // 0x01ffc9a7
            || id == type(IGreeter).interfaceId;
    }
}