NFTs (Non-Fungible Tokens) — Explained with Examples
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[<b>NFT Standards</b>]
ERC721[ERC-721<br/>Each token is unique<br/>One contract per collection]
ERC1155[ERC-1155<br/>Multiple token types<br/>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[<b>Key Features</b>]
Meta[M Metadata<br/>URI pointing to JSON]
Royalty[Royalties<br/>Earning on secondary sales]
Burn[Burning<br/>Destroy tokens]
Mint[Minting<br/>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 EtherscanStep 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://QmXp4aBqy8z9K3x5C7v2dN6rL9mW1jR4tV8sG3hK5fA2pERoyalties — 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