ERC20 and ERC721 are the most widely used token standards on Ethereum, representing fungible tokens and non-fungible tokens (NFTs) respectively. Understanding their core implementation principles is essential for developing DeFi and NFT projects.
1. ERC20 Standard in Detail
ERC20 defines the standard interface for fungible tokens where each token is interchangeable.
ERC20 Interface Definition
solidityinterface IERC20 { // Query total supply function totalSupply() external view returns (uint256); // Query account balance function balanceOf(address account) external view returns (uint256); // Query allowance function allowance(address owner, address spender) external view returns (uint256); // Transfer function transfer(address to, uint256 amount) external returns (bool); // Approve function approve(address spender, uint256 amount) external returns (bool); // Transfer from approved account function transferFrom(address from, address to, uint256 amount) external returns (bool); // Events event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); }
Complete ERC20 Implementation
soliditycontract ERC20 is IERC20 { // State variables mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; uint256 private _totalSupply; string private _name; string private _symbol; uint8 private _decimals; // Constructor constructor(string memory name_, string memory symbol_, uint8 decimals_) { _name = name_; _symbol = symbol_; _decimals = decimals_; } // Query functions function name() public view returns (string memory) { return _name; } function symbol() public view returns (string memory) { return _symbol; } function decimals() public view returns (uint8) { return _decimals; } function totalSupply() public view override returns (uint256) { return _totalSupply; } function balanceOf(address account) public view override returns (uint256) { return _balances[account]; } function allowance(address owner, address spender) public view override returns (uint256) { return _allowances[owner][spender]; } // Transfer function function transfer(address to, uint256 amount) public override returns (bool) { address owner = msg.sender; _transfer(owner, to, amount); return true; } // Approve function function approve(address spender, uint256 amount) public override returns (bool) { address owner = msg.sender; _approve(owner, spender, amount); return true; } // Transfer from approved account function transferFrom(address from, address to, uint256 amount) public override returns (bool) { address spender = msg.sender; _spendAllowance(from, spender, amount); _transfer(from, to, amount); return true; } // Internal transfer logic function _transfer(address from, address to, uint256 amount) internal { require(from != address(0), "ERC20: transfer from zero address"); require(to != address(0), "ERC20: transfer to zero address"); uint256 fromBalance = _balances[from]; require(fromBalance >= amount, "ERC20: insufficient balance"); unchecked { _balances[from] = fromBalance - amount; _balances[to] += amount; } emit Transfer(from, to, amount); } // Internal approve logic function _approve(address owner, address spender, uint256 amount) internal { require(owner != address(0), "ERC20: approve from zero address"); require(spender != address(0), "ERC20: approve to zero address"); _allowances[owner][spender] = amount; emit Approval(owner, spender, amount); } // Spend allowance function _spendAllowance(address owner, address spender, uint256 amount) internal { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, "ERC20: insufficient allowance"); unchecked { _approve(owner, spender, currentAllowance - amount); } } } // Mint tokens (internal function) function _mint(address account, uint256 amount) internal { require(account != address(0), "ERC20: mint to zero address"); _totalSupply += amount; unchecked { _balances[account] += amount; } emit Transfer(address(0), account, amount); } // Burn tokens (internal function) function _burn(address account, uint256 amount) internal { require(account != address(0), "ERC20: burn from zero address"); uint256 accountBalance = _balances[account]; require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); unchecked { _balances[account] = accountBalance - amount; _totalSupply -= amount; } emit Transfer(account, address(0), amount); } }
ERC20 Extended Features
solidity// Pausable ERC20 import "@openzeppelin/contracts/security/Pausable.sol"; contract PausableERC20 is ERC20, Pausable { constructor() ERC20("PausableToken", "PTK", 18) {} function _beforeTokenTransfer(address from, address to, uint256 amount) internal override whenNotPaused { super._beforeTokenTransfer(from, to, amount); } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } } // Burnable ERC20 contract BurnableERC20 is ERC20 { constructor() ERC20("BurnableToken", "BTK", 18) {} function burn(uint256 amount) public { _burn(msg.sender, amount); } function burnFrom(address account, uint256 amount) public { _spendAllowance(account, msg.sender, amount); _burn(account, amount); } }
2. ERC721 Standard in Detail
ERC721 defines the standard for non-fungible tokens (NFTs) where each token is unique.
ERC721 Interface Definition
solidityinterface IERC721 { // Query balance function balanceOf(address owner) external view returns (uint256 balance); // Query token owner function ownerOf(uint256 tokenId) external view returns (address owner); // Safe transfer function safeTransferFrom(address from, address to, uint256 tokenId) external; function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; // Transfer function transferFrom(address from, address to, uint256 tokenId) external; // Approve function approve(address to, uint256 tokenId) external; function setApprovalForAll(address operator, bool approved) external; // Query approval function getApproved(uint256 tokenId) external view returns (address operator); function isApprovedForAll(address owner, address operator) external view returns (bool); // Events event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); } // Metadata interface interface IERC721Metadata { function name() external view returns (string memory); function symbol() external view returns (string memory); function tokenURI(uint256 tokenId) external view returns (string memory); }
Complete ERC721 Implementation
soliditycontract ERC721 is IERC721, IERC721Metadata { // Token name string private _name; // Token symbol string private _symbol; // Token ID to owner address mapping mapping(uint256 => address) private _owners; // Owner address to token count mapping mapping(address => uint256) private _balances; // Token ID to approved address mapping mapping(uint256 => address) private _tokenApprovals; // Owner to operator approval mapping mapping(address => mapping(address => bool)) private _operatorApprovals; // Token ID to token URI mapping mapping(uint256 => string) private _tokenURIs; constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; } function name() public view override returns (string memory) { return _name; } function symbol() public view override returns (string memory) { return _symbol; } function tokenURI(uint256 tokenId) public view override returns (string memory) { require(_exists(tokenId), "ERC721: URI query for nonexistent token"); return _tokenURIs[tokenId]; } function balanceOf(address owner) public view override returns (uint256) { require(owner != address(0), "ERC721: balance query for zero address"); return _balances[owner]; } function ownerOf(uint256 tokenId) public view override returns (address) { address owner = _owners[tokenId]; require(owner != address(0), "ERC721: owner query for nonexistent token"); return owner; } function approve(address to, uint256 tokenId) public override { address owner = ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); require( msg.sender == owner || isApprovedForAll(owner, msg.sender), "ERC721: approve caller is not owner nor approved for all" ); _approve(to, tokenId); } function getApproved(uint256 tokenId) public view override returns (address) { require(_exists(tokenId), "ERC721: approved query for nonexistent token"); return _tokenApprovals[tokenId]; } function setApprovalForAll(address operator, bool approved) public override { require(operator != msg.sender, "ERC721: approve to caller"); _operatorApprovals[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function isApprovedForAll(address owner, address operator) public view override returns (bool) { return _operatorApprovals[owner][operator]; } function transferFrom(address from, address to, uint256 tokenId) public override { require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved"); _transfer(from, to, tokenId); } function safeTransferFrom(address from, address to, uint256 tokenId) public override { safeTransferFrom(from, to, tokenId, ""); } function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) public override { require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved"); _safeTransfer(from, to, tokenId, _data); } // Internal functions function _exists(uint256 tokenId) internal view returns (bool) { return _owners[tokenId] != address(0); } function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { require(_exists(tokenId), "ERC721: operator query for nonexistent token"); address owner = ownerOf(tokenId); return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); } function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal { _transfer(from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); } function _transfer(address from, address to, uint256 tokenId) internal { require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); require(to != address(0), "ERC721: transfer to the zero address"); // Clear approval _approve(address(0), tokenId); _balances[from] -= 1; _balances[to] += 1; _owners[tokenId] = to; emit Transfer(from, to, tokenId); } function _approve(address to, uint256 tokenId) internal { _tokenApprovals[tokenId] = to; emit Approval(ownerOf(tokenId), to, tokenId); } // Mint function function _mint(address to, uint256 tokenId) internal { require(to != address(0), "ERC721: mint to the zero address"); require(!_exists(tokenId), "ERC721: token already minted"); _balances[to] += 1; _owners[tokenId] = to; emit Transfer(address(0), to, tokenId); } // Safe mint function _safeMint(address to, uint256 tokenId) internal { _safeMint(to, tokenId, ""); } function _safeMint(address to, uint256 tokenId, bytes memory _data) internal { _mint(to, tokenId); require( _checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer" ); } // Burn function function _burn(uint256 tokenId) internal { address owner = ownerOf(tokenId); _approve(address(0), tokenId); _balances[owner] -= 1; delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); } // Check if receiver implements ERC721Receiver interface function _checkOnERC721Received( address from, address to, uint256 tokenId, bytes memory _data ) private returns (bool) { if (to.code.length > 0) { try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert("ERC721: transfer to non ERC721Receiver implementer"); } else { assembly { revert(add(32, reason), mload(reason)) } } } } return true; } } // ERC721Receiver interface interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); }
3. ERC20 vs ERC721 Comparison
| Feature | ERC20 | ERC721 |
|---|---|---|
| Token type | Fungible | Non-fungible |
| Interchangeability | Interchangeable | Unique, non-interchangeable |
| Balance query | balanceOf(address) | balanceOf(address) + ownerOf(tokenId) |
| Transfer | transfer(to, amount) | transferFrom(from, to, tokenId) |
| Approval | approve(spender, amount) | approve(to, tokenId) + setApprovalForAll(operator, approved) |
| Use cases | Currency, points, governance tokens | Digital art, game items, identity credentials |
4. Practical Application Examples
Complete ERC20 Token
solidityimport "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyToken is ERC20, Ownable { constructor(uint256 initialSupply) ERC20("MyToken", "MTK") { _mint(msg.sender, initialSupply * 10 ** decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }
Complete NFT Contract
solidityimport "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyNFT is ERC721URIStorage, Ownable { uint256 private _tokenIds; uint256 public mintPrice = 0.01 ether; uint256 public maxSupply = 10000; string public baseURI; constructor(string memory _baseURI) ERC721("MyNFT", "MNFT") { baseURI = _baseURI; } function mint(string memory tokenURI) public payable returns (uint256) { require(msg.value >= mintPrice, "Insufficient payment"); require(_tokenIds < maxSupply, "Max supply reached"); _tokenIds++; uint256 newTokenId = _tokenIds; _mint(msg.sender, newTokenId); _setTokenURI(newTokenId, tokenURI); return newTokenId; } function withdraw() public onlyOwner { payable(owner()).transfer(address(this).balance); } function _baseURI() internal view override returns (string memory) { return baseURI; } }
5. Security Considerations
- Integer overflow: Use Solidity 0.8+ or SafeMath library
- Reentrancy attacks: Follow Checks-Effects-Interactions pattern
- Authorization management: Properly handle approve and allowance
- Zero address checks: Prevent tokens from being sent to zero address
- Event emission: All state changes should trigger events
6. Extension Standards
- ERC777: More advanced token standard with hook functions
- ERC1155: Multi-token standard supporting both fungible and non-fungible tokens
- ERC2981: NFT royalty standard
- ERC4907: NFT rental standard