ERC20 — fungible tokens
One contract represents one fungible asset. Balances are plain uint256 maps. Transfers move units between accounts. Decimals are display-only — on-chain math uses the smallest unit (wei-style). DeFi, governance, and rewards almost always start here.
- Core functions: transfer, approve, transferFrom, balanceOf, totalSupply.
- Mint/burn are not in the base standard — add OpenZeppelin ERC20Mintable or _mint internally.
- Fee-on-transfer tokens break naive accounting — document if you add fees.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(address initialOwner)
ERC20("My Token", "MTK")
Ownable(initialOwner)
{
_mint(initialOwner, 1_000_000 * 10 ** decimals());
}
}ERC721 — unique tokens
Each tokenId is distinct. ownerOf maps ids to holders. tokenURI points to metadata (often IPFS). Transfers must use safeTransferFrom when the receiver is a contract — prevents stuck NFTs in contracts that cannot handle them.
- Enumerable adds tokenByIndex — expensive on-chain enumeration.
- ERC2981 royalties are separate — marketplaces optionally honor them.
- Consecutive mint batching (OZ extension) saves gas for large drops.
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, Ownable {
constructor(address initialOwner)
ERC721("My NFT", "MNFT")
Ownable(initialOwner)
{}
function safeMint(address to, uint256 tokenId) external onlyOwner {
_safeMint(to, tokenId);
}
}ERC1155 — multi-token
One contract manages many token ids, each with its own supply. Batch transfers amortize gas. Ideal for game items with duplicates, semi-fungible editions, or redemptions where 721-per-item would be wasteful.
- balanceOf(account, id) replaces separate contracts per item type.
- safeBatchTransferFrom moves many ids in one call.
- URI can be per-id or shared via ERC1155URIStorage patterns.
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GameItems is ERC1155, Ownable {
constructor(address initialOwner)
ERC1155("")
Ownable(initialOwner)
{}
function mint(address to, uint256 id, uint256 amount) external onlyOwner {
_mint(to, id, amount, "");
}
}Votes and governance extensions
Governance needs historical balances — ERC20Votes checkpoints balance at each block. Holders must delegate to themselves or a delegatee before votes count. Plain ERC20 balances do not automatically confer voting power.
- ERC20Votes + Governor + Timelock is the OpenZeppelin DAO stack.
- Delegation is on-chain — passive holders often have zero votes until they delegate.
- Clock mode (block number vs timestamp) must match Governor configuration.
Permit and gasless approvals
ERC20Permit (EIP-2612) lets holders sign approvals off-chain. Integrators call permit() then transferFrom in one transaction. Reduces friction for DEX and staking onboarding. Not all tokens support it — check the interface before relying on permit in your UI.
- DOMAIN_SEPARATOR binds signatures to chain id and contract address.
- Nonces prevent replay of signed permits.
- OpenZeppelin ERC20Permit mixin adds permit to your token in one import.
Choosing a standard
Fungible currency or governance → ERC20. Unique collectibles or memberships → ERC721. Games, coupons, or multi-edition drops → ERC1155. You can combine: ERC20 for currency, ERC721 for membership, staking contract linking both.
- Marketplace templates expect 721 or 1155 — match your NFT standard.
- Staking templates typically take an ERC20 staking token.
- Avoid custom transfer hooks unless you understand ERC777-style risks.
