# ERC-721 NFTs — Blockchain & Smart Contracts (Solidity)

Source: https://www.geekswithgeeks.com/en/solidity/t-erc721

> Create non-fungible tokens with metadata and safe transfers.

## Unique tokens

**ERC-721** defines **non-fungible tokens (NFTs)**: each token has a unique `tokenId` and one owner. The core interface includes `ownerOf(tokenId)`, `balanceOf(owner)`, `transferFrom`, **`safeTransferFrom`** (which checks that a receiving contract implements `onERC721Received`, preventing tokens from being locked in contracts that cannot handle them), `approve` for a single token and `setApprovalForAll` for an operator such as a marketplace. The optional metadata extension provides `name`, `symbol` and **`tokenURI(tokenId)`**, which points to a JSON document with a name, description, image and attributes, usually stored on **IPFS** or **Arweave**, or generated on chain. Storing images on chain is expensive, so most NFTs store only a URI; if that URI points to a centralised server, the NFT's content can change or disappear. NFTs are used for tickets, certificates and credentials, memberships, in-game items, domain names (ENS) and art. **ERC-2981** standardises royalty information, but marketplaces choose whether to enforce it.

## A certificate NFT

Minting with an incrementing id and per-token metadata.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract CourseCertificate is ERC721URIStorage, Ownable {
    uint256 private _nextId = 1;
    mapping(address => mapping(bytes32 => bool)) public hasCertificate;   // learner => course => issued

    error AlreadyIssued(address learner, bytes32 courseId);

    constructor(address issuer) ERC721("Course Certificate", "CERT") Ownable(issuer) {}

    function issue(address learner, bytes32 courseId, string calldata metadataUri)
        external
        onlyOwner
        returns (uint256 tokenId)
    {
        if (hasCertificate[learner][courseId]) revert AlreadyIssued(learner, courseId);
        hasCertificate[learner][courseId] = true;
        tokenId = _nextId++;
        _safeMint(learner, tokenId);           // checks contract receivers
        _setTokenURI(tokenId, metadataUri);    // e.g. "ipfs://bafy.../42.json"
    }
}

// Example metadata JSON at the URI:
// { "name": "Solidity Basics", "description": "Completed 2026-10-03",
//   "image": "ipfs://bafy.../cert.png",
//   "attributes": [{ "trait_type": "Grade", "value": "A" }] }
```

## A numbered concert ticket

An ERC-20 token is like cash: any ten-rupee note is as good as another. An ERC-721 token is a numbered concert ticket: seat 42 is different from seat 43, and the ticket's details are printed on a card stored elsewhere (the token URI).

**Quiz:** What does safeTransferFrom check that transferFrom does not?

- [ ] The gas price
- [x] That a receiving contract implements onERC721Received, so tokens are not locked
- [ ] The token's price
- [ ] The sender's balance in ether

*Answer:* That a receiving contract implements onERC721Received, so tokens are not locked. Safe transfers prevent sending NFTs to contracts that cannot handle them.
