--- name: "ERC NFT Standards Guide (ERC-721, ERC-1155, ERC-2981, ERC-4907)" description: Use when implementing NFT contracts, multi-token systems, royalties, rentable NFTs, or soulbound tokens. Covers 721 vs 1155 trade-offs and all major NFT extensions. ecosystem: ethereum type: standard-guide source: official confidence: high version: 1.0.0 time_sensitivity: evergreen tags: - ethereum - erc - standard - erc-721 - erc-1155 - erc-2981 - erc-4907 - erc-5192 - erc-6372 - nft - royalty - soulbound - rental updated_at: 2026-03-26T00:00:00.000Z --- # ERC NFT Standards Guide (ERC-721, ERC-1155, ERC-2981, ERC-4907) ## Overview This guide covers the core NFT standards on Ethereum and their extensions. Understanding when to use ERC-721 vs ERC-1155 and how royalties/rental/soulbound extensions work is critical for any NFT or gaming dApp. ## Standards Comparison Table | Feature | ERC-721 | ERC-1155 | ERC-2981 | ERC-4907 | ERC-5192 | ERC-6372 | |---------|---------|---------|---------|---------|---------|---------| | Purpose | Unique NFT | Multi-token | Royalty info | Rentable NFT | Soulbound | Contract clock | | Token model | 1 token = 1 ID | Multiple tokens per ID | Extension | Extension | Extension | Extension | | Batch support | Limited | Native | N/A | N/A | N/A | N/A | | Gas (batch mint) | High | Low | N/A | N/A | N/A | N/A | | Use case | Art, PFPs | Gaming items, SFTs | Marketplace royalties | Rental markets | SBTs, credentials | Governance timing | ## ERC-721 — Non-Fungible Token Standard ERC-721 is the foundation of NFTs. Each token has a unique `tokenId` and a single owner. ```solidity interface IERC721 { // Core transfers function transferFrom(address from, address to, uint256 tokenId) external; function safeTransferFrom(address from, address to, uint256 tokenId) external; function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; // Approvals function approve(address to, uint256 tokenId) external; function setApprovalForAll(address operator, bool approved) external; function getApproved(uint256 tokenId) external view returns (address operator); function isApprovedForAll(address owner, address operator) external view returns (bool); // Queries function ownerOf(uint256 tokenId) external view returns (address owner); function balanceOf(address owner) external view returns (uint256 balance); // 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); } ``` ### ERC-721 Metadata Extension ```solidity interface IERC721Metadata is IERC721 { function name() external view returns (string memory); function symbol() external view returns (string memory); function tokenURI(uint256 tokenId) external view returns (string memory); } ``` **tokenURI best practices:** - Return JSON conforming to OpenSea metadata standard - Use IPFS URIs for decentralization (`ipfs://Qm...`) - For on-chain SVG: encode as `data:application/json;base64,...` ### Safe Transfer Receiver ```solidity interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); // Must return 0x150b7a02 } ``` **Critical**: Always use `safeTransferFrom` when sending to a contract. `transferFrom` to a non-receiver contract will lock the NFT permanently. ### Common ERC-721 Implementation (OpenZeppelin) ```solidity import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; contract MyNFT is ERC721URIStorage { uint256 private _nextTokenId; constructor() ERC721("MyNFT", "MNFT") {} function mint(address to, string calldata uri) external returns (uint256) { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); return tokenId; } } ``` ## ERC-1155 — Multi-Token Standard ERC-1155 handles both fungible and non-fungible tokens in one contract. Each `id` can have multiple copies (SFT — Semi-Fungible Token). ```solidity interface IERC1155 { // Single transfers function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes calldata data ) external; // Batch transfers (gas efficient) function safeBatchTransferFrom( address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data ) external; // Queries function balanceOf(address account, uint256 id) external view returns (uint256); function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); // Approvals (operator-level only, no per-token approval) function setApprovalForAll(address operator, bool approved) external; function isApprovedForAll(address account, address operator) external view returns (bool); } ``` ### ERC-721 vs ERC-1155 Decision Guide | Scenario | Use | |---------|-----| | 1-of-1 art, unique items | ERC-721 | | Limited edition prints (100 copies) | ERC-1155 | | Game items (100 swords, 50 shields) | ERC-1155 | | PFP collection (unique traits per token) | ERC-721 | | Batch minting 10,000 NFTs cost-efficiently | ERC-1155 | | Token needs per-token approval | ERC-721 | | Mix of fungible + non-fungible in one contract | ERC-1155 | **Gas savings with ERC-1155**: Batch transfer of 10 tokens costs ~30% less gas than 10 individual ERC-721 transfers. ## ERC-2981 — NFT Royalty Standard ERC-2981 provides a standardized way for marketplaces to query and honor royalty payments. ```solidity interface IERC2981 { function royaltyInfo( uint256 tokenId, uint256 salePrice ) external view returns ( address receiver, uint256 royaltyAmount ); } ``` ### Implementation ```solidity import "@openzeppelin/contracts/token/common/ERC2981.sol"; contract MyNFT is ERC721, ERC2981 { constructor() ERC721("MyNFT", "MNFT") { // Set 5% royalty to deployer for all tokens _setDefaultRoyalty(msg.sender, 500); // 500 = 5% (basis points) } // Override for ERC-165 function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC2981) returns (bool) { return super.supportsInterface(interfaceId); } } ``` **Important**: ERC-2981 only standardizes *reporting* royalties. Enforcement depends on marketplace compliance. On-chain royalty enforcement requires custom transfer hooks or wrapper contracts. ## ERC-4907 — Rental NFT Standard ERC-4907 extends ERC-721 with a `user` role separate from `owner`, enabling time-limited usage rights (rental). ```solidity interface IERC4907 { event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires); // Set a user and expiry (owner or approved can set) function setUser(uint256 tokenId, address user, uint64 expires) external; // Get current user (returns address(0) if expired) function userOf(uint256 tokenId) external view returns (address); // Get expiry timestamp function userExpires(uint256 tokenId) external view returns (uint256); } ``` ### Rental Pattern ```solidity // Renting out an NFT for 7 days uint64 expires = uint64(block.timestamp + 7 days); IERC4907(nftContract).setUser(tokenId, renterAddress, expires); // Check if currently rented address currentUser = IERC4907(nftContract).userOf(tokenId); bool isRented = currentUser != address(0); ``` **Use case**: In-game item rental, metaverse land access, subscription-based NFT utilities. ## ERC-5192 — Soulbound Tokens (SBT) ERC-5192 marks NFTs as "locked" — non-transferable. Used for credentials, attestations, and identity tokens. ```solidity interface IERC5192 { event Locked(uint256 tokenId); event Unlocked(uint256 tokenId); // Returns true if the token is locked (soulbound) function locked(uint256 tokenId) external view returns (bool); } ``` ### SBT Implementation ```solidity contract SoulboundToken is ERC721, IERC5192 { mapping(uint256 => bool) private _locked; function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal override { // Allow minting (from == address(0)) but block transfers if (from != address(0) && _locked[tokenId]) { revert("SBT: token is soulbound"); } super._beforeTokenTransfer(from, to, tokenId, batchSize); } function locked(uint256 tokenId) external view returns (bool) { return _locked[tokenId]; } } ``` **SBT use cases**: University degrees, KYC credentials, DAO membership, achievement badges. ## ERC-6372 — Contract Clock ERC-6372 standardizes how contracts report their internal "clock" — block number or timestamp. Critical for governance systems where voting power snapshots need a consistent time reference. ```solidity interface IERC6372 { // Returns current clock value (block.number or block.timestamp) function clock() external view returns (uint48); // Returns "mode" string: "timestamp" or "blocknumber" function CLOCK_MODE() external view returns (string memory); } ``` **Why it matters**: Governance tokens (like OpenZeppelin's ERC20Votes) need a consistent clock. Using `block.number` on L2 chains (where block times vary) can cause issues — use `block.timestamp` instead. ## ERC-4906 — Metadata Update Event ERC-4906 adds an event for signaling that NFT metadata has changed. Marketplaces listen for this to refresh cached metadata. ```solidity interface IERC4906 is IERC165, IERC721 { event MetadataUpdate(uint256 _tokenId); event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); } ``` ## Common NFT Pitfalls 1. **Lost NFTs**: Sending ERC-721 with `transferFrom` (not `safeTransferFrom`) to a contract that doesn't implement `onERC721Received` permanently locks the token. 2. **Royalty bypass**: ERC-2981 royalties are not enforced on-chain — most marketplaces can choose to ignore them. 3. **tokenId = 0**: Many implementations have bugs with tokenId 0. Start tokenIds from 1 or handle 0 explicitly. 4. **Gas estimation for batch minting**: ERC-721 batch mints using loops can hit block gas limits. Consider lazy minting or ERC-1155. 5. **Soulbound + approvals**: ERC-5192 doesn't automatically block `approve` — you must also override approval functions.