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-2612 permit pattern 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 the amount parameter.
  • Decimals are cosmetic: decimals() is optional and purely for UI rendering. Internally, all arithmetic operates on raw uint256 values. Mishandling this is a frequent source of pricing bugs in DEX integrations.
  • Return-value inconsistency: Some tokens (notably USDT on mainnet) do not return bool from transfer/approve. OpenZeppelin's SafeERC20 library 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

  • safeTransferFrom re-entrancy: The onERC721Received callback 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: ERC721Enumerable adds tokenOfOwnerByIndex and totalSupply tracking. 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 exposes locked() 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 safeBatchTransferFrom iterates 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). Only setApprovalForAll exists. 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 safeTransferFrom and safeBatchTransferFrom call onERC1155Received or onERC1155BatchReceived on receiving contracts. Failure to implement these in a recipient contract causes reverts—a common integration pitfall.
  • No transferFrom without safe check: Unlike ERC-721's dual transferFrom/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.