NFTs (Non-Fungible Tokens) — Explained with Examples
In this tutorial, you'll learn about NFTs (Non. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
NFTs (Non-Fungible Tokens) are unique digital assets verified on a Blockchain that represent ownership of specific items — art, music, collectibles, or in-game assets — using smart contract standards like ERC-721 to guarantee scarcity and provenance.
Why NFTs Matter
NFTs represent a fundamental shift in digital ownership. For the first time, you can truly own a digital item — it can't be copied, forged, or taken away by a platform. The NFT market exceeded $40 billion in trading volume. Beyond profile pictures, NFTs enable digital art royalties, gaming assets, ticketing, and identity verification. DodaTech explores NFTs for software license verification and digital asset management.
ERC-721 vs ERC-1155
Think of ERC-721 like a limited edition print — each one is unique and numbered. ERC-1155 is like a trading card pack — it can contain both unique cards (1-of-1) and multiple copies of common cards.
graph TD
subgraph Standards[NFT Standards]
ERC721[ERC-721
Each token is unique
One contract per collection]
ERC1155[ERC-1155
Multiple token types
One contract, many collections]
end
ERC721 --> CryptoPunks
ERC721 --> BAYC[Bored Ape Yacht Club]
ERC721 --> ArtBlocks
ERC1155 --> OpenSea[Shared storefront]
ERC1155 --> Enjin[Gaming items]
ERC1155 --> MintSq[Mint Square]
subgraph Features[Key Features]
Meta[M Metadata
URI pointing to JSON]
Royalty[Royalties
Earning on secondary sales]
Burn[Burning
Destroy tokens]
Mint[Minting
Creating new tokens]
end
ERC721 --> Features
ERC1155 --> Features
style Standards fill:#3b82f6,color:#fff
| Feature | ERC-721 | ERC-1155 |
|---|---|---|
| Token uniqueness | Each token is unique | Supports fungible + non-fungible |
| Batch transfers | No (one at a time) | Yes (multiple tokens) |
| Gas efficiency | Higher per token | Lower (batch operations) |
| Use case | Art, collectibles | Gaming, metaverse items |
| Popular examples | CryptoPunks, BAYC | Enjin, OpenSea storefront |
Minting an NFT — Step by Step
Let's build a complete NFT contract, deploy it with Hardhat, and mint tokens.
Step 1: The Smart Contract
// MyNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
uint256 public mintPrice = 0.01 ether;
uint256 public maxSupply = 100;
uint256 public totalMinted;
event NFTMinted(address indexed owner, uint256 indexed tokenId, string tokenURI);
constructor() ERC721("DodaTech Collection", "DTECH") {
_nextTokenId = 1;
}
function mintNFT(string memory tokenURI) public payable returns (uint256) {
require(totalMinted < maxSupply, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");
uint256 tokenId = _nextTokenId;
_nextTokenId++;
totalMinted++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
emit NFTMinted(msg.sender, tokenId, tokenURI);
return tokenId;
}
function withdrawFunds() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
function updateMintPrice(uint256 newPrice) external onlyOwner {
mintPrice = newPrice;
}
}
Line-by-line explanation:
ERC721URIStorage— OpenZeppelin's ERC-721 implementation with per-token URI storageOwnable— restricts certain functions to the contract owner_safeMint— mints and safely transfers to recipient (checks if recipient can handle NFTs)_setTokenURI— sets the metadata URI for this specific token
Step 2: NFT Metadata (JSON)
{
"name": "DodaTech Genesis #1",
"description": "The first NFT from DodaTech's collection. Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.",
"image": "ipfs://QmXp4aBqy8z9K3x5C7v2dN6rL9mW1jR4tV8sG3hK5fA2pE",
"attributes": [
{"trait_type": "Background", "value": "Cyber Blue"},
{"trait_type": "Rarity", "value": "Legendary"},
{"trait_type": "Year", "value": "2026"},
{"trait_type": "Artist", "value": "DodaTech Team"}
]
}
The metadata must be stored on IPFS (InterPlanetary File System) so it's permanent and decentralized.
Step 3: Deployment Script (Hardhat)
// hardhat.config.js
require("@nomiclabs/hardhat-ethers");
module.exports = {
solidity: "0.8.19",
networks: {
sepolia: {
url: `https://sepolia.infura.io/v3/YOUR_PROJECT_ID`,
accounts: [process.env.PRIVATE_KEY]
}
}
};
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
console.log("Deploying MyNFT...");
const MyNFT = await hre.ethers.getContractFactory("MyNFT");
const contract = await MyNFT.deploy();
await contract.deployed();
console.log("MyNFT deployed to:", contract.address);
// Verify on Etherscan
if (hre.network.name !== "hardhat") {
console.log("Waiting for block confirmations...");
await contract.deployTransaction.wait(5);
await hre.run("verify:verify", {
address: contract.address,
constructorArguments: [],
});
console.log("Contract verified on Etherscan");
}
}
main().catch(console.error);
# Deploy to Sepolia testnet
npx hardhat run scripts/deploy.js --network sepolia
# Expected output:
# Deploying MyNFT...
# MyNFT deployed to: 0x1234...5678
# Contract verified on Etherscan
Step 4: Minting
// scripts/mint.js
const hre = require("hardhat");
async function main() {
const contractAddress = "0x1234...5678"; // Your deployed contract
const MyNFT = await hre.ethers.getContractFactory("MyNFT");
const contract = MyNFT.attach(contractAddress);
const tokenURI = "ipfs://QmXp4aBqy8z9K3x5C7v2dN6rL9mW1jR4tV8sG3hK5fA2pE";
const tx = await contract.mintNFT(tokenURI, {
value: hre.ethers.utils.parseEther("0.01")
});
await tx.wait();
console.log(`NFT minted! Transaction: ${tx.hash}`);
// Check token owner
const owner = await contract.ownerOf(1);
console.log(`Token #1 owned by: ${owner}`);
// Check token URI
const uri = await contract.tokenURI(1);
console.log(`Token #1 URI: ${uri}`);
}
main().catch(console.error);
Expected output:
NFT minted! Transaction: 0xa1b2c3d4e5f6...
Token #1 owned by: 0xYourAddress
Token #1 URI: ipfs://QmXp4aBqy8z9K3x5C7v2dN6rL9mW1jR4tV8sG3hK5fA2pE
Royalties — Earning on Secondary Sales
EIP-2981 standardizes royalty payments for NFTs. Creators earn a percentage every time their NFT is resold.
// Adding royalties to your NFT contract
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
contract RoyaltyNFT is ERC721URIStorage, Ownable, IERC2981 {
uint256 private _royaltyPercent = 500; // 5% (500 basis points)
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
override
returns (address receiver, uint256 royaltyAmount)
{
return (owner(), salePrice * _royaltyPercent / 10000);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721URIStorage, IERC165)
returns (bool)
{
return interfaceId == type(IERC2981).interfaceId
|| super.supportsInterface(interfaceId);
}
}
NFT Marketplaces
| Marketplace | Chain | Standard | Royalties | Fees |
|---|---|---|---|---|
| OpenSea | Ethereum, Polygon, Solana | ERC-721, ERC-1155 | Up to 10% | 2.5% |
| Blur | Ethereum | ERC-721 | Optional | 0.5% |
| Rarible | Ethereum, Polygon, Tezos | ERC-721, ERC-1155 | Up to 10% | 1% |
| LooksRare | Ethereum | ERC-721 | 5% default | 2% |
| Magic Eden | Solana, Polygon, Ethereum | ERC-721 | Variable | 2% |
Gas Optimization for NFT Minting
// Gas-optimized batch minting
contract BatchMintNFT is ERC721 {
uint256 private _nextTokenId;
string private _baseTokenURI;
// Batch mint — much cheaper than individual mints
function batchMint(address to, uint256 quantity) external {
for (uint256 i = 0; i < quantity; i++) {
_safeMint(to, _nextTokenId + i);
}
_nextTokenId += quantity;
}
// Use base URI instead of per-token URI to save gas
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
// Lazy minting — only update state when the token transfers
mapping(uint256 => address) private _owners;
function ownerOf(uint256 tokenId) public view override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "Nonexistent token");
return owner;
}
}
Gas comparison:
| Operation | Standard | Optimized | Savings |
|---|---|---|---|
| Single mint | ~150K gas | ~90K gas | 40% |
| Batch mint (10) | ~1.5M gas | ~600K gas | 60% |
| Token URI storage | ~50K gas | 0 (base URI) | 100% |
Common Mistakes
- Storing metadata on centralized servers: If the server goes down, your NFT has no image or description. Always use IPFS or Arweave for permanent storage.
- Forgetting royalties: Without EIP-2981, you earn nothing on secondary sales. Always include royalty support in your contract.
- Not testing on testnet: Gas costs real money. Test every interaction on Sepolia or Goerli before mainnet deployment.
- Using uncompressed images: Large images cost more to store and load. Use WebP format and compress to under 200KB for metadata images.
- Minting without verifying the metadata URL: A typo in the IPFS hash means your NFT points to nothing. Always verify the URI resolves correctly.
Practice Questions
- What is the difference between ERC-721 and ERC-1155?
- Why should NFT metadata be stored on IPFS?
- What does EIP-2981 provide for NFT creators?
- What is "lazy minting"?
- How do batch mints save gas?
Answers:
- ERC-721 is for unique 1-of-1 tokens. ERC-1155 supports both unique and multiple-copy tokens in one contract with batch transfers.
- IPFS is decentralized and content-addressed. Metadata on centralized servers can be changed or deleted, breaking the NFT's immutability promise.
- EIP-2981 standardizes royalty payments. Creators earn a percentage (e.g., 5%) on all secondary sales across all compliant marketplaces.
- Lazy minting defers the cost of minting to the buyer. The creator signs a voucher off-chain. The buyer pays gas when they claim the NFT.
- Batch minting uses a single loop in one Transaction, spreading the fixed Transaction overhead across multiple tokens instead of paying it per token.
Mini Project: NFT Collection Minter
Build a complete NFT minting dApp:
- Write an ERC-721 smart contract with minting, royalties, and max supply
- Create metadata JSON files and upload to IPFS (use Pinata or web3.storage)
- Write a Hardhat deployment script
- Deploy to Sepolia testnet
- Write a mint script that accepts an address and mints an NFT
- Verify the contract on Etherscan
- View your NFT on OpenSea testnet (testnets.opensea.com)
- Extend the contract with batch mint functionality
This Process is what DodaTech uses for experimental digital collectibles and software license NFTs.
Related topics: Ethereum, Smart Contracts, Web3, DeFi, Layer 2
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro