akash11

ERC-721: The Token Standard That Powers NFTs

A technical standard for creating unique, non-interchangeable tokens on Ethereum where each token has distinct properties and cannot be split into smaller units.

Published on 21 November, 2025

Categories:

SolidityToken

What Makes ERC-721 Different

ERC-721 tokens are non-fungible. This means each token is unique and cannot be exchanged on a one-to-one basis like regular currencies. Think of them as digital certificates of ownership for unique items.
In ERC-20, all tokens are identical. 100 USDC tokens are the same as any other 100 USDC tokens. But with ERC-721, token #1 is completely different from token #2. Each has its own identity, properties, and value.

Core Contract Interface

The ERC-721 standard defines specific functions that every compliant contract must implement:
interface IERC721 { function balanceOf(address owner) external view returns (uint256); function ownerOf(uint256 tokenId) external view returns (address); function transferFrom(address from, address to, uint256 tokenId) external; function approve(address to, uint256 tokenId) external; function getApproved(uint256 tokenId) external view returns (address); function setApprovalForAll(address operator, bool approved) external; function isApprovedForAll(address owner, address operator) external view returns (bool); }
Breaking down the important functions:
balanceOf: Returns how many NFTs an address owns. Simple count.
ownerOf: Takes a token ID and returns who owns it. Every token has exactly one owner.
transferFrom: Moves a token from one address to another. The caller must have permission.
approve: Allows another address to transfer a specific token on your behalf.
setApprovalForAll: Grants an operator permission to manage all your tokens.

Building a Basic NFT Contract

Here's a simple implementation that shows the core mechanics:
contract SimpleNFT { mapping(uint256 => address) private owners; mapping(address => uint256) private balances; mapping(uint256 => address) private tokenApprovals; uint256 private nextTokenId; function mint(address to) external returns (uint256) { uint256 tokenId = nextTokenId; nextTokenId++; owners[tokenId] = to; balances[to]++; return tokenId; } function ownerOf(uint256 tokenId) external view returns (address) { address owner = owners[tokenId]; require(owner != address(0), "Token does not exist"); return owner; } function transferFrom(address from, address to, uint256 tokenId) external { require(owners[tokenId] == from, "Not the owner"); require(msg.sender == from || tokenApprovals[tokenId] == msg.sender, "Not approved"); balances[from]--; balances[to]++; owners[tokenId] = to; delete tokenApprovals[tokenId]; } }
The contract uses three mappings. One tracks which address owns each token ID. Another counts how many tokens each address owns. The third stores approvals for individual tokens.

Token Metadata

ERC-721 includes an optional metadata extension. This lets you attach information to each token:
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); }
The tokenURI function returns a link to JSON metadata. This JSON typically follows a standard format:
{ "name": "My NFT #123", "description": "A unique digital item", "image": "ipfs://QmX7...", "attributes": [ { "trait_type": "Background", "value": "Blue" }, { "trait_type": "Rarity", "value": "Common" } ] }
Most projects store this metadata on IPFS to ensure it stays accessible and unchanged. The smart contract only stores the hash or reference.

Transfer Flow

When transferring an NFT, the standard checks several conditions:
Transfer Request
      |
      v
Token exists?
      |
      v
Caller is owner OR approved?
      |
      v
Update balances
      |
      v
Update owner
      |
      v
Clear approvals
      |
      v
Emit Transfer event
The contract must verify ownership before any transfer happens. It also clears individual token approvals after transfer to prevent unauthorized access with old permissions.

Safe Transfer Functions

The standard includes safe transfer methods that check if the recipient can handle NFTs:
function safeTransferFrom(address from, address to, uint256 tokenId) external;
These functions call back to the recipient if it's a contract. The recipient must implement a specific function to confirm it can receive NFTs:
interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); }
If the recipient is a contract and doesn't implement this interface, the transfer fails. This prevents tokens from getting stuck in contracts that can't handle them.

Events

ERC-721 requires three 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);
These events let external applications track ownership changes without constantly querying the blockchain. Marketplaces and wallets rely heavily on these events.

Practical Implementation Considerations

Token ID Strategy: You can use sequential IDs (1, 2, 3...) or random ones. Sequential IDs are simpler. Random IDs can prevent sniping during mints.
Gas Optimization: Minting costs gas. Batch minting multiple tokens in one transaction saves money compared to individual mints.
Enumeration: The optional enumeration extension lets you list all tokens or all tokens owned by an address. This costs extra gas but provides useful functionality:
interface IERC721Enumerable { function totalSupply() external view returns (uint256); function tokenByIndex(uint256 index) external view returns (uint256); function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); }
Storage Patterns: Store minimal data on-chain. Keep images and detailed metadata off-chain. Only store what you absolutely need for the contract logic.

Common Use Cases

Digital art platforms use ERC-721 for unique pieces. Each artwork is one token.
Gaming items work well as NFTs. Weapons, characters, and land parcels can each be a distinct token with unique stats.
Domain name systems like ENS use ERC-721. Each domain is a unique token you own and control.
Ticketing systems can represent each ticket as an NFT. This enables resale tracking and authenticity verification.

Security Patterns

Always check that token IDs exist before operations:
require(owners[tokenId] != address(0), "Token does not exist");
Verify permissions before transfers. Check if the caller is the owner or has approval.
Use reentrancy guards on functions that make external calls, especially safe transfers.
Validate recipient addresses. Prevent transfers to the zero address.

Comparing ERC-721, ERC-20, and ERC-1155

ERC-721 vs ERC-20

The fundamental difference lies in fungibility. ERC-20 tokens are fungible and divisible. ERC-721 tokens are non-fungible and indivisible.
Interface Comparison:
// ERC-20 Interface interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } // ERC-721 Interface interface IERC721 { function balanceOf(address owner) external view returns (uint256); function ownerOf(uint256 tokenId) external view returns (address); function transferFrom(address from, address to, uint256 tokenId) external; function approve(address to, uint256 tokenId) external; function getApproved(uint256 tokenId) external view returns (address); function setApprovalForAll(address operator, bool approved) external; }
Key Differences:
Transfer Mechanism: ERC-20 transfers amounts. ERC-721 transfers specific token IDs.
// ERC-20: Transfer 100 tokens token.transfer(recipient, 100); // ERC-721: Transfer token #5 nft.transferFrom(owner, recipient, 5);
Approval System: ERC-20 approves spending amounts. ERC-721 approves specific tokens or all tokens.
// ERC-20: Approve spending up to 1000 tokens token.approve(spender, 1000); // ERC-721: Approve transfer of token #5 nft.approve(spender, 5); // ERC-721: Approve all tokens nft.setApprovalForAll(operator, true);
Balance Representation: ERC-20 balances are quantities. ERC-721 balances are counts.
// ERC-20: User has 250.5 tokens uint256 balance = token.balanceOf(user); // Returns 250500000000000000000 // ERC-721: User owns 3 NFTs uint256 count = nft.balanceOf(user); // Returns 3
Divisibility: ERC-20 tokens can be split. You can send 0.001 tokens. ERC-721 tokens cannot be divided. You transfer the entire token or nothing.
Value: All ERC-20 tokens of the same contract have equal value. Each ERC-721 token has its own unique value based on its properties.
Supply Model: ERC-20 tracks total supply as a single number. ERC-721 tracks individual tokens and may have a max supply or unlimited minting.
Storage Structure:
// ERC-20 Storage mapping(address => uint256) private balances; mapping(address => mapping(address => uint256)) private allowances; // ERC-721 Storage mapping(uint256 => address) private owners; mapping(address => uint256) private balances; mapping(uint256 => address) private tokenApprovals; mapping(address => mapping(address => bool)) private operatorApprovals;
Gas Costs: ERC-20 transfers are generally cheaper because they only update two balance mappings. ERC-721 transfers update ownership mappings and may clear approvals, making them more expensive.
Use Case Matrix:
Fungible + Divisible = ERC-20
├─ Currencies (USDC, DAI)
├─ Governance tokens (UNI, AAVE)
└─ Reward points

Non-Fungible + Indivisible = ERC-721
├─ Digital art
├─ Collectibles
├─ Real estate titles
└─ Identity tokens

ERC-721 vs ERC-1155

ERC-1155 is a multi-token standard that handles both fungible and non-fungible tokens in one contract.
Batch Operations: ERC-1155 supports batch transfers. You can transfer multiple token types in one transaction. ERC-721 requires separate transactions for each token.
// ERC-1155: Transfer multiple tokens at once contract.safeBatchTransferFrom(from, to, [id1, id2, id3], [amount1, amount2, amount3], data); // ERC-721: Requires multiple transactions nft.transferFrom(from, to, tokenId1); nft.transferFrom(from, to, tokenId2); nft.transferFrom(from, to, tokenId3);
Supply Model: ERC-1155 can have multiple copies of the same token ID. ERC-721 has only one token per ID.
Complexity vs Simplicity: ERC-721 is simpler for projects that only need unique tokens. ERC-1155 adds flexibility but requires more complex logic.
Ecosystem Support: ERC-721 has wider adoption in marketplaces and wallets. ERC-1155 support is growing but not universal.
When to Choose Which:
Pick ERC-20 when building currencies, governance tokens, or anything where each unit is identical and divisible.
Pick ERC-721 when each item is unique and indivisible. Good for art, collectibles, domain names, and certificates.
Pick ERC-1155 when you need both fungible and non-fungible tokens in the same system, or when you need efficient batch operations for game items.
ERC-721 provides a straightforward way to create unique digital assets on Ethereum with clear ownership rules and a mature ecosystem of tools and marketplaces that support the standard out of the box.

Copyright © 2025

Akash Vaghela