NFTs (ERC-721 and ERC-1155): Minting and Marketplaces
In this tutorial, you'll learn NFT development including ERC-721 and ERC-1155 standards, minting, metadata, IPFS storage, and building a marketplace for your NFT collection. Why it matters: NFTs have created a multi-billion dollar digital asset economy for art, gaming, music, and real estate with over $40 billion in trading volume on Ethereum. By the end, you'll deploy an NFT collection with a marketplace.
NFTs (Non-Fungible Tokens) are unique digital assets verified on the blockchain using ERC-721 and ERC-1155 standards, each token representing distinct ownership of digital or physical items.
Understanding ERC-721
ERC-721 is the standard for non-fungible tokens, where each token has a unique ID and is owned by a single address.
| Function | Description |
|---|---|
balanceOf(address) |
Number of NFTs owned |
ownerOf(uint256) |
Owner of a specific token ID |
safeTransferFrom(address,address,uint256) |
Transfer with safety check |
transferFrom(address,address,uint256) |
Direct transfer |
approve(address,uint256) |
Approve one token |
setApprovalForAll(address,bool) |
Approve all tokens |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract MyNFT is ERC721, Ownable {
using Strings for uint256;
uint256 private _tokenIdCounter;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant MINT_PRICE = 0.01 ether;
string private _baseTokenURI;
constructor(string memory baseURI) ERC721("MyNFT Collection", "MNFT") Ownable(msg.sender) {
_baseTokenURI = baseURI;
}
function mint() external payable {
require(_tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.value == MINT_PRICE, "Incorrect payment");
_tokenIdCounter++;
_safeMint(msg.sender, _tokenIdCounter);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseURI(string memory baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
Expected behavior: Users mint NFTs by sending 0.01 ETH. Each token gets a unique ID starting from 1. The metadata URI is constructed from the base URI plus token ID.
flowchart LR A[User] --> B[Call mint()
Send 0.01 ETH] B --> C[_safeMint
msg.sender, tokenId] C --> D[ERC721 Contract] D --> E[Token ID: 1
Owner: User] D --> F[Token ID: 2
Owner: User] D --> G[metadata URI
ipfs://baseURI/1.json] E --> H[Frontend displays
Name, Image, Attributes]
ERC-1155: Multi-Token Standard
ERC-1155 enables a single contract to manage multiple token types, including both fungible and non-fungible tokens, significantly reducing gas costs.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GameItems is ERC1155, Ownable {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant SWORD = 2;
uint256 public constant SHIELD = 3;
mapping(uint256 => uint256) public maxSupply;
mapping(uint256 => uint256) public minted;
constructor() ERC1155("ipfs://metadata/{id}.json") Ownable(msg.sender) {
// Gold is fungible (10,000 supply)
maxSupply[GOLD] = 10000;
// Sword is a unique NFT (only 1)
maxSupply[SWORD] = 1;
maxSupply[SHIELD] = 100;
}
function mint(uint256 id, uint256 amount) external payable {
require(minted[id] + amount <= maxSupply[id], "Exceeds max supply");
_mint(msg.sender, id, amount, "");
minted[id] += amount;
}
function uri(uint256 id) public view override returns (string memory) {
return string(abi.encodePacked(super.uri(id), id.toString()));
}
}
Expected behavior: A single contract handles gold (fungible, 10k supply), swords (NFT, 1 supply), and shields (semi-fungible, 100 supply). Batch minting multiple token types costs less than separate ERC-721 deployments.
NFT Metadata and IPFS
Each NFT has a metadata JSON file (usually on IPFS) containing the token details.
{
"name": "MyNFT #1",
"description": "A unique digital artwork from MyNFT Collection",
"image": "ipfs://QmZ4tDuvesWe9iwSqGQF7NwmGmejVmZB8vFPpKp9xPx8F1",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Eyes", "value": "Green" },
{ "trait_type": "Fur", "value": "Gold" },
{ "trait_type": "Rarity", "value": "Legendary" }
],
"properties": {
"files": [
{ "uri": "ipfs://QmZ4t...x8F1", "type": "image/png" }
],
"category": "image"
}
}
Upload your metadata and images to IPFS using Pinata or the IPFS CLI:
const pinataSDK = require("@pinata/sdk");
const pinata = new pinataSDK("YOUR_API_KEY", "YOUR_API_SECRET");
const fs = require("fs");
async function uploadToIPFS() {
// Upload image
const imageStream = fs.createReadStream("./nft-image.png");
const imageResult = await pinata.pinFileToIPFS(imageStream, {
pinataMetadata: { name: "nft-image.png" }
});
console.log("Image IPFS hash:", imageResult.IpfsHash);
// Upload metadata
const metadata = {
name: "MyNFT #1",
description: "A unique digital artwork",
image: `ipfs://${imageResult.IpfsHash}`,
attributes: [
{ trait_type: "Background", value: "Blue" }
]
};
const metadataResult = await pinata.pinJSONToIPFS(metadata, {
pinataMetadata: { name: "nft-1.json" }
});
console.log("Metadata IPFS hash:", metadataResult.IpfsHash);
}
uploadToIPFS();
// Expected output:
// Image IPFS hash: QmZ4tDuvesWe9iwSqGQF7NwmGmejVmZB8vFPpKp9xPx8F1
// Metadata IPFS hash: QmXfTmdYTiBjxRKFfK5kMPF5xQLRPCv3GSFg4K8KjF9XqZ
Building an NFT Marketplace
Create a simple marketplace where users can list and trade NFTs.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract NFTMarketplace is ReentrancyGuard, Ownable {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
uint256 public listingCount;
mapping(uint256 => Listing) public listings;
mapping(address => mapping(uint256 => bool)) public isListed;
event Listed(uint256 indexed listingId, address indexed seller, address nftContract, uint256 tokenId, uint256 price);
event Sold(uint256 indexed listingId, address indexed buyer, address nftContract, uint256 tokenId, uint256 price);
event Cancelled(uint256 indexed listingId);
function listNFT(address _nftContract, uint256 _tokenId, uint256 _price) external {
require(_price > 0, "Price must be positive");
IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(!isListed[_nftContract][_tokenId], "Already listed");
listingCount++;
listings[listingCount] = Listing(msg.sender, _nftContract, _tokenId, _price, true);
isListed[_nftContract][_tokenId] = true;
emit Listed(listingCount, msg.sender, _nftContract, _tokenId, _price);
}
function buyNFT(uint256 _listingId) external payable nonReentrant {
Listing storage listing = listings[_listingId];
require(listing.active, "Listing not active");
require(msg.value >= listing.price, "Insufficient payment");
listing.active = false;
isListed[listing.nftContract][listing.tokenId] = false;
IERC721(listing.nftContract).safeTransferFrom(listing.seller, msg.sender, listing.tokenId);
payable(listing.seller).transfer(listing.price);
emit Sold(_listingId, msg.sender, listing.nftContract, listing.tokenId, listing.price);
}
function cancelListing(uint256 _listingId) external {
Listing storage listing = listings[_listingId];
require(listing.seller == msg.sender, "Not the seller");
require(listing.active, "Not active");
listing.active = false;
isListed[listing.nftContract][listing.tokenId] = false;
emit Cancelled(_listingId);
}
}
Expected behavior: NFT owners list tokens with a price. Buyers purchase by sending ETH. The marketplace transfers the NFT and forwards payment to the seller. Sellers can cancel active listings.
Common Errors and Misunderstandings
1. Forgetting to Approve
Marketplaces must be approved (via setApprovalForAll) to transfer NFTs on behalf of sellers. Without approval, transfers fail.
2. URI Not Updating
After deploying, changing the base URI does not retroactively update metadata for already-minted tokens. Some marketplaces cache metadata permanently.
3. Reentrancy in Marketplaces
Transferring ETH or NFTs can trigger callbacks. Always use ReentrancyGuard and follow checks-effects-interactions.
4. On-Chain vs Off-Chain Metadata
Storing metadata fully on-chain is expensive. Most NFTs store a URI pointing to off-chain JSON. This creates a dependency on external storage.
5. Royalties Not Enforced
ERC-721 does not enforce royalties on-chain. EIP-2981 provides a standard but marketplaces must respect it. Always verify marketplace royalty support.
Practice Questions
What is the main difference between ERC-721 and ERC-1155? ERC-721 is for unique NFTs (one token ID per asset). ERC-1155 is a multi-token standard handling both fungible and non-fungible tokens in one contract, reducing gas costs.
How does IPFS store NFT metadata? IPFS uses content-addressed storage. Metadata JSON files are uploaded and return a hash (CID). The hash is used in the token URI. Pinning ensures files remain available.
What is the purpose of
safeTransferFromovertransferFrom?safeTransferFromchecks if the recipient implementsonERC721Received, preventing tokens from being locked in contracts that don't handle them.How do NFT marketplaces handle royalties? Through EIP-2981, which defines a
royaltyInfofunction returning the royalty amount and recipient. Marketplaces implement this to pay creators on secondary sales.What happens if IPFS goes down for your NFT metadata? Your NFT becomes a "dead" token with no visible metadata. Using Filecoin or Arweave for permanent storage, or pinning across multiple services, prevents this.
Challenge
Build a complete NFT collection project with ERC-721A (gas-optimized consecutive minting), a Dutch auction minting mechanism (price decreases over time), on-chain SVG generation for images (no IPFS dependency), and a marketplace with royalty enforcement via EIP-2981.
Real-World Task
Create a generative art NFT collection using ERC-721 and Hardhat. Deploy to Sepolia, upload 10,000 unique generative images to IPFS using Pinata, build a React minting dApp with MetaMask, and verify the contract on Etherscan with full source code.
Frequently Asked Questions
Next Steps
After mastering NFTs, explore DeFi protocols and Layer 2 scaling to build next-generation NFT experiences with lower fees and higher throughput.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro