Non-Fungible Tokens Explained
ERC-721 is the standard for non-fungible tokens (NFTs), where each token is unique and identified by a distinct uint256 tokenId within a contract. Unlike ERC-20, where balances are simple quantities, an ERC-721 contract maps each individual token to exactly one owner. This makes NFTs suitable for representing one-of-a-kind assets: digital art, collectibles, in-game items, event tickets, or tokenized real-world property. Two ERC-721 tokens from the same contract are never interchangeable, because each carries its own identity, ownership record, and metadata.
Cricket analogy: A signed bat from a specific match-winning century is one-of-a-kind, unlike interchangeable runs, which is exactly what a unique ERC-721 tokenId represents.
Ownership and Metadata
The core ERC-721 functions include ownerOf(tokenId), which returns the current owner of a specific token, and balanceOf(owner), which returns how many tokens an address holds. The optional but ubiquitous ERC721Metadata extension adds name(), symbol(), and tokenURI(tokenId), which returns a URI, often pointing to JSON on IPFS or a server, describing the token's attributes and image. The on-chain contract stores ownership and the URI pointer, while the rich media and traits typically live off-chain. Marketplaces read tokenURI to render the artwork and properties buyers see.
Cricket analogy: A trophy cabinet register lists who holds each specific trophy (ownerOf) with a plaque describing it (tokenURI), the NFT's ownership and metadata lookups.
Safe Transfers and Approvals
ERC-721 offers two transfer functions: transferFrom, which blindly moves a token, and safeTransferFrom, which additionally checks that a recipient contract implements onERC721Received. This callback prevents tokens from being permanently trapped in contracts that cannot handle them. For delegated transfers, approve(spender, tokenId) authorizes one token, while setApprovalForAll(operator, true) grants an operator control over an owner's entire collection, the mechanism marketplaces like OpenSea use. Because safeTransferFrom hands control to the receiver's callback, it is also a reentrancy surface that must be guarded when combined with state changes.
Cricket analogy: A safe single where the non-striker confirms the run is on before committing mirrors safeTransferFrom checking the recipient can receive before the token crosses.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, Ownable {
uint256 private _nextTokenId; // auto-incrementing id counter
constructor(address owner) ERC721("MyNFT", "MNFT") Ownable(owner) {}
// Mint a new unique token to `to`, returning its id
function mint(address to) external onlyOwner returns (uint256) {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId); // invokes onERC721Received if `to` is a contract
return tokenId;
}
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmBaseHash/"; // tokenURI = baseURI + tokenId
}
}ERC-721 supports optional extensions: ERC721Enumerable adds totalSupply and on-chain enumeration of all token IDs (at a gas cost), while ERC721URIStorage lets each token store an individual metadata URI instead of deriving it from a shared base URI. Choose extensions based on your query and storage needs.
Minting and Token IDs
Minting creates a new token by assigning a fresh tokenId to an owner, usually through OpenZeppelin's internal _mint or _safeMint. A common pattern is a monotonically increasing counter, so each mint gets the next sequential id, though ids can also be derived from hashes or provided by the caller. Because minting writes new storage (ownership mappings and possibly URIs), it is one of the more gas-intensive operations, which is why large drops sometimes use lazy minting or ERC721A-style optimizations for cheaper batch mints. Every mint emits a Transfer event from the zero address, signaling creation to indexers.
Cricket analogy: A squad-numbering system assigns each debutant a unique cap number, just as minting assigns a fresh tokenId and enumerable extensions let you list the whole squad.
Use safeTransferFrom and _safeMint when the recipient may be a contract, otherwise a token can be sent to a contract that lacks onERC721Received and be locked forever. But remember the receiver callback is a reentrancy vector: follow Checks-Effects-Interactions and guard state-changing mint or transfer functions.
- ERC-721 defines non-fungible tokens where each unique tokenId maps to a single owner.
- ownerOf and balanceOf track ownership; the Metadata extension adds name, symbol, and tokenURI.
- Rich media and traits usually live off-chain (e.g. IPFS), with the contract storing only the URI pointer.
- safeTransferFrom checks onERC721Received so tokens are not trapped in incompatible contracts.
- approve authorizes a single token; setApprovalForAll grants an operator control over the whole collection.
- Minting assigns a new tokenId (often via an incrementing counter) and emits Transfer from the zero address.
- Receiver callbacks in safeTransferFrom and _safeMint are reentrancy surfaces that must be guarded.
Practice what you learned
1. How does ERC-721 differ fundamentally from ERC-20?
2. What does safeTransferFrom check that transferFrom does not?
3. What does setApprovalForAll grant?
4. What event is emitted when a new ERC-721 token is minted?
5. Where does an ERC-721's rich metadata (image, traits) typically live?
Was this page helpful?
You May Also Like
ERC-20 Token Standard
The interface and mechanics behind fungible tokens on Ethereum, covering core functions, the approve/allowance pattern, events, and safe implementation.
Reentrancy and Common Vulnerabilities
How reentrancy attacks drain contracts by re-entering functions before state updates, plus the defensive patterns and other common Solidity vulnerability classes.
Access Control Patterns
Techniques for restricting who can call privileged contract functions, from simple Ownable to role-based access control, safe ownership transfer, and timelocks.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics