Skip to content
NFTs (Non-Fungible Tokens) — Explained with Examples

NFTs (Non-Fungible Tokens) — Explained with Examples

DodaTech Updated Jun 15, 2026 7 min read

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
  
FeatureERC-721ERC-1155
Token uniquenessEach token is uniqueSupports fungible + non-fungible
Batch transfersNo (one at a time)Yes (multiple tokens)
Gas efficiencyHigher per tokenLower (batch operations)
Use caseArt, collectiblesGaming, metaverse items
Popular examplesCryptoPunks, BAYCEnjin, 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 storage
  • Ownable — 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

MarketplaceChainStandardRoyaltiesFees
OpenSeaEthereum, Polygon, SolanaERC-721, ERC-1155Up to 10%2.5%
BlurEthereumERC-721Optional0.5%
RaribleEthereum, Polygon, TezosERC-721, ERC-1155Up to 10%1%
LooksRareEthereumERC-7215% default2%
Magic EdenSolana, Polygon, EthereumERC-721Variable2%

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:

OperationStandardOptimizedSavings
Single mint~150K gas~90K gas40%
Batch mint (10)~1.5M gas~600K gas60%
Token URI storage~50K gas0 (base URI)100%

Common Mistakes

  1. 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.
  2. Forgetting royalties: Without EIP-2981, you earn nothing on secondary sales. Always include royalty support in your contract.
  3. Not testing on testnet: Gas costs real money. Test every interaction on Sepolia or Goerli before mainnet deployment.
  4. Using uncompressed images: Large images cost more to store and load. Use WebP format and compress to under 200KB for metadata images.
  5. 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

  1. What is the difference between ERC-721 and ERC-1155?
  2. Why should NFT metadata be stored on IPFS?
  3. What does EIP-2981 provide for NFT creators?
  4. What is “lazy minting”?
  5. How do batch mints save gas?

Answers:

  1. ERC-721 is for unique 1-of-1 tokens. ERC-1155 supports both unique and multiple-copy tokens in one contract with batch transfers.
  2. IPFS is decentralized and content-addressed. Metadata on centralized servers can be changed or deleted, breaking the NFT’s immutability promise.
  3. EIP-2981 standardizes royalty payments. Creators earn a percentage (e.g., 5%) on all secondary sales across all compliant marketplaces.
  4. 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.
  5. 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:

  1. Write an ERC-721 smart contract with minting, royalties, and max supply
  2. Create metadata JSON files and upload to IPFS (use Pinata or web3.storage)
  3. Write a Hardhat deployment script
  4. Deploy to Sepolia testnet
  5. Write a mint script that accepts an address and mints an NFT
  6. Verify the contract on Etherscan
  7. View your NFT on OpenSea testnet (testnets.opensea.com)
  8. 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