Introduction
Ethereum's composability owes much of its power to well-defined token standards. ERC-20, ERC-721, and ERC-1155 each encode fundamentally different ownership models at the smart-contract level. While the surface-level distinction—fungible vs. non-fungible vs. multi-token—is well-known, advanced builders must understand the storage layouts, gas profiles, security edge cases, and interoperability nuances that determine which standard (or combination) to choose.
ERC-20: Fungible Token Mechanics
Core Interface Recap
`solidity
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
`
Storage is simple: a mapping(address => uint256) for balances and a nested mapping(address => mapping(address => uint256)) for allowances.
Advanced Considerations
- Approval race condition: The classic double-spend vector where a spender front-runs an allowance change. Mitigations include
increaseAllowance/decreaseAllowance(OpenZeppelin) or the ERC-2612permitpattern using EIP-712 signatures, which eliminates the need for a separate approval transaction entirely. - Fee-on-transfer & rebasing tokens: Tokens like USDT (fee variant) or stETH (rebasing) break composability assumptions. Any protocol integrating ERC-20s must measure actual balance deltas (
balanceAfter - balanceBefore) instead of trusting theamountparameter. - Decimals are cosmetic:
decimals()is optional and purely for UI rendering. Internally, all arithmetic operates on rawuint256values. Mishandling this is a frequent source of pricing bugs in DEX integrations. - Return-value inconsistency: Some tokens (notably USDT on mainnet) do not return
boolfromtransfer/approve. OpenZeppelin'sSafeERC20library wraps calls to handle both cases.
ERC-721: Non-Fungible Token Mechanics
Core Interface Recap
`solidity
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function setApprovalForAll(address operator, bool approved) external;
`
Storage maps each tokenId to an owner address and tracks per-token and operator-level approvals.
Advanced Considerations
safeTransferFromre-entrancy: TheonERC721Receivedcallback opens a re-entrancy vector. Contracts minting or transferring NFTs must follow checks-effects-interactions or use re-entrancy guards. This was exploited in multiple early NFT mints.- Enumerable extension cost:
ERC721EnumerableaddstokenOfOwnerByIndexandtotalSupplytracking. This requires extra storage writes on every transfer (updating index arrays), adding ~20-40k gas per transfer. ERC721A (Azuki) optimized sequential minting by assuming contiguous token IDs and lazily initializing ownership slots—reducing batch mint costs dramatically. - Metadata immutability debate:
tokenURI()can point to IPFS (content-addressed, immutable), Arweave, or an HTTP endpoint. Advanced projects use on-chain SVG generation or store metadata in contract storage for full decentralization, at the cost of higher deployment gas. - Soulbound tokens (ERC-5192): An extension that emits
Locked(uint256 tokenId)events and exposeslocked()to signal non-transferability, useful for credentials and reputation.
ERC-1155: Multi-Token Standard
Core Interface Recap
`solidity
function balanceOf(address account, uint256 id) external view returns (uint256);
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
`
Storage uses mapping(uint256 => mapping(address => uint256))—a balance per (tokenId, address) pair. Approvals are operator-only (setApprovalForAll), with no per-token approve.
Advanced Considerations
- Gas efficiency for batch operations: A single
safeBatchTransferFromiterates internally but saves on base transaction overhead (21k gas) and calldata compared to N separate ERC-20 or ERC-721 transfers. For gaming inventories or airdrop scenarios, savings can be 40-60%. - Mixed fungibility: Token IDs with max supply of 1 behave as NFTs; IDs with supply > 1 behave as fungibles. This hybrid model requires careful metadata handling—
uri(uint256 id)uses{id}substitution per the spec, where clients replace the hex-encoded token ID in the URI template. - No per-token approval: Unlike ERC-721, there is no
approve(address, uint256). OnlysetApprovalForAllexists. This is a deliberate design simplification but forces all-or-nothing delegation. Protocols needing granular permissions must implement wrapper logic or use ERC-1155 extensions. - Receiver hooks are mandatory: Both
safeTransferFromandsafeBatchTransferFromcallonERC1155ReceivedoronERC1155BatchReceivedon receiving contracts. Failure to implement these in a recipient contract causes reverts—a common integration pitfall. - No
transferFromwithout safe check: Unlike ERC-721's dualtransferFrom/safeTransferFrom, ERC-1155 only exposes safe variants, enforcing receiver checks always.
Comparative Analysis
| Dimension | ERC-20 | ERC-721 | ERC-1155 |
|---|---|---|---|
| Fungibility | Fully fungible | Unique per token | Per-ID configurable |
| Contracts per collection | 1 per asset | 1 per collection | 1 for all asset types |
| Batch transfers | No native support | No native support | Native batch |
| Approval granularity | Per-spender amount | Per-token + operator | Operator-only |
| Receiver callback | None (ERC-20) | onERC721Received | onERC1155Received |
| Storage per transfer | 2 SSTORE | 3-5 SSTORE | 2 SSTORE per ID |
| Metadata | Optional (name, symbol) | tokenURI(id) | uri(id) with template |
When to Use What: Design Heuristics
- ERC-20: Governance tokens, stablecoins, LP shares—anything where every unit is interchangeable. Pair with ERC-2612 permits for gasless UX.
- ERC-721: Unique assets requiring per-token provenance—PFP collections, real-world asset deeds, ENS names. Consider ERC-721A for large-scale mints.
- ERC-1155: Games with mixed item types, edition-based art (100 prints of one artwork), or platforms managing hundreds of asset classes in a single contract. The reduction in deployment costs alone can be significant.
Interoperability & Bridging Concerns
Cross-standard wrapping is common: wrapping an ERC-20 as an ERC-1155 ID to unify marketplace interfaces, or fractionalizing an ERC-721 into ERC-20 shares (e.g., Fractional/Tessera pattern). When bridging to L2s or other chains, the standard must be preserved or mapped correctly—bridge contracts need to implement the appropriate receiver hooks and handle metadata propagation.
Conclusion
Choosing between ERC-20, ERC-721, and ERC-1155 is rarely just about fungibility. Gas optimization, approval security models, receiver hook implications, and downstream composability with DeFi protocols and marketplaces all factor into the decision. The best Ethereum architects understand these standards not as rigid categories but as building blocks that can be composed, extended, and wrapped to fit precise application requirements.