Skip to content
NFT Ecosystem Guide — ERC-721, ERC-1155, Minting, and Marketplaces

NFT Ecosystem Guide — ERC-721, ERC-1155, Minting, and Marketplaces

DodaTech Updated Jun 20, 2026 11 min read

The NFT ecosystem spans token standards, minting workflows, decentralized storage, marketplace dynamics, and a growing list of use cases from digital art to real estate tokenization.

What You’ll Learn

By the end of this tutorial, you’ll understand ERC-721 and ERC-1155 token standards, the minting process, how IPFS stores metadata, how marketplaces work, and how rarity and floor price are calculated.

Why NFTs Matter

NFTs (Non-Fungible Tokens) prove digital ownership on a blockchain. Unlike cryptocurrencies where one Bitcoin equals another, each NFT is unique. This enables digital art ownership, in-game assets, music rights, identity verification, and real estate tokenization. Doda Browser integrates NFT previews in its wallet feature, letting users view their collection without leaving the browser.

NFT Learning Path


flowchart LR
  A[Blockchain Basics] --> B[Ethereum & Smart Contracts]
  B --> C[NFT Ecosystem]
  C --> D{You Are Here}
  D --> E[ERC-721]
  D --> F[ERC-1155]
  D --> G[Minting & IPFS]
  D --> H[Marketplaces]
  E --> I[Unique Tokens]
  F --> J[Batch Transfers]

Prerequisites: blockchain basics, Ethereum fundamentals, smart contract basics.

ERC-721: The Original NFT Standard

ERC-721 defines a minimum interface for non-fungible tokens. Each token has a unique tokenId and belongs to an owner. Key functions:

// IERC721.sol — Core interface
interface IERC721 {
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

Complete ERC-721 Contract

// DodaNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract DodaNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    
    uint256 public mintPrice = 0.01 ether;
    uint256 public maxSupply = 100;
    
    constructor() ERC721("DodaNFT", "DNFT") Ownable(msg.sender) {}
    
    function mintNFT(address recipient, string memory tokenURI) 
        public payable returns (uint256) 
    {
        require(_tokenIds.current() < maxSupply, "Max supply reached");
        require(msg.value >= mintPrice, "Insufficient payment");
        
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();
        
        _safeMint(recipient, newTokenId);
        _setTokenURI(newTokenId, tokenURI);
        
        return newTokenId;
    }
    
    function totalSupply() public view returns (uint256) {
        return _tokenIds.current();
    }
    
    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No balance to withdraw");
        payable(owner()).transfer(balance);
    }
}

ERC-1155: Multi-Token Standard

ERC-1155 handles fungible, semi-fungible, and non-fungible tokens in a single contract. It’s more gas-efficient for batch operations.

// DodaGameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract DodaGameItems is ERC1155, Ownable {
    uint256 public constant SWORD = 0;
    uint256 public constant SHIELD = 1;
    uint256 public constant LEGENDARY_ARMOR = 2;  // NFT (supply = 1)
    
    mapping(uint256 => uint256) public maxSupply;
    
    constructor() ERC1155("https://api.dodatech.io/items/{id}.json") Ownable(msg.sender) {
        maxSupply[SWORD] = 1000;     // 1000 swords (fungible)
        maxSupply[SHIELD] = 500;      // 500 shields (fungible)
        maxSupply[LEGENDARY_ARMOR] = 1;  // 1 unique armor (NFT)
    }
    
    function mint(address account, uint256 id, uint256 amount) external onlyOwner {
        require(totalSupply(id) + amount <= maxSupply[id], "Max supply");
        _mint(account, id, amount, "");
    }
    
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) 
        external onlyOwner 
    {
        _mintBatch(to, ids, amounts, "");
    }
}

ERC-721 vs ERC-1155

FeatureERC-721ERC-1155
Token typeStrictly NFTsFungible + semi-fungible + NFTs
Batch transfersSeparate tx per tokenSingle tx for multiple tokens
Gas efficiencyHigher per tokenLower (amortized)
MetadataPer token via tokenURIPer token type via URI
Use caseDigital art, collectiblesGaming, metaverse items

Minting Process

// mint_nft.js
// Full minting flow with ethers.js
import { ethers } from "ethers";

const CONTRACT_ADDRESS = "0xYourContractAddress";
const ABI = [
    "function mintNFT(address recipient, string memory tokenURI) public payable returns (uint256)",
    "function totalSupply() public view returns (uint256)",
    "function mintPrice() public view returns (uint256)",
];

async function mintNFT() {
    // Connect to Ethereum (use MetaMask or private key)
    const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_KEY");
    const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
    const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet);
    
    // 1. Prepare metadata (already uploaded to IPFS)
    const tokenURI = "ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/1.json";
    
    // 2. Get mint price
    const price = await contract.mintPrice();
    console.log(`Mint price: ${ethers.formatEther(price)} ETH`);
    
    // 3. Execute mint
    const tx = await contract.mintNFT(wallet.address, tokenURI, {
        value: price,
    });
    console.log(`Transaction sent: ${tx.hash}`);
    
    // 4. Wait for confirmation
    const receipt = await tx.wait();
    console.log(`Minted! Block: ${receipt.blockNumber}`);
    
    // 5. Get token ID from event
    const mintEvent = receipt.logs.find(log => 
        contract.interface.parseLog({
            topics: log.topics,
            data: log.data,
        })?.name === "Transfer"
    );
    console.log("NFT minted successfully!");
}

mintNFT().catch(console.error);

Expected output:

Mint price: 0.01 ETH
Transaction sent: 0xabc123def456...
Minted! Block: 12345678
NFT minted successfully!

IPFS Metadata Storage

NFTs store metadata off-chain (on IPFS) to save gas. The on-chain tokenURI points to a JSON file:

// ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/1.json
{
    "name": "DodaPixel Art #1",
    "description": "A unique pixel art piece from the DodaTech collection.",
    "image": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/art_1.webp",
    "attributes": [
        {"trait_type": "Background", "value": "Cosmic"},
        {"trait_type": "Rarity", "value": "Legendary"},
        {"trait_type": "Color", "value": "Neon Blue"},
        {"display_type": "boost_number", "trait_type": "Power", "value": 95}
    ],
    "external_url": "https://dodatech.io/nft/1"
}

Uploading to IPFS

// upload_ipfs.js
// Upload metadata to IPFS via Pinata
import axios from "axios";
import FormData from "form-data";
import fs from "fs";

const PINATA_API_KEY = "YOUR_KEY";
const PINATA_SECRET_KEY = "YOUR_SECRET";

async function uploadToIPFS(metadata, imagePath) {
    // Upload image first
    const imageForm = new FormData();
    imageForm.append("file", fs.createReadStream(imagePath));
    
    const imageRes = await axios.post(
        "https://api.pinata.cloud/pinning/pinFileToIPFS",
        imageForm,
        {
            headers: {
                "Content-Type": `multipart/form-data; boundary=${imageForm._boundary}`,
                pinata_api_key: PINATA_API_KEY,
                pinata_secret_api_key: PINATA_SECRET_KEY,
            },
        }
    );
    
    const imageHash = imageRes.data.IpfsHash;
    console.log(`Image uploaded: ipfs://${imageHash}`);
    
    // Update metadata with image hash
    metadata.image = `ipfs://${imageHash}`;
    
    // Upload metadata
    const metadataRes = await axios.post(
        "https://api.pinata.cloud/pinning/pinJSONToIPFS",
        metadata,
        {
            headers: {
                pinata_api_key: PINATA_API_KEY,
                pinata_secret_api_key: PINATA_SECRET_KEY,
            },
        }
    );
    
    console.log(`Metadata uploaded: ipfs://${metadataRes.data.IpfsHash}`);
    return `ipfs://${metadataRes.data.IpfsHash}`;
}

const nftMetadata = {
    name: "DodaPixel Art #1",
    description: "A unique pixel art piece.",
    attributes: [
        { trait_type: "Rarity", value: "Legendary" },
    ],
};

uploadToIPFS(nftMetadata, "./art_1.webp").catch(console.error);

Marketplace Dynamics

Floor Price and Rarity

Floor price is the lowest listed price for an NFT collection. Rarity is calculated using trait frequency:

// rarity.js
// Calculate NFT rarity from trait frequencies
const collection = [
    { id: 1, traits: { Background: "Cosmic", Rarity: "Legendary", Color: "Neon Blue" }},
    { id: 2, traits: { Background: "Space", Rarity: "Common", Color: "Red" }},
    { id: 3, traits: { Background: "Cosmic", Rarity: "Uncommon", Color: "Neon Blue" }},
    { id: 4, traits: { Background: "Galaxy", Rarity: "Common", Color: "Green" }},
    { id: 5, traits: { Background: "Space", Rarity: "Common", Color: "Blue" }},
];

function calculateRarity(nfts) {
    // Count trait frequencies
    const traitCounts = {};
    for (const nft of nfts) {
        for (const [trait, value] of Object.entries(nft.traits)) {
            if (!traitCounts[trait]) traitCounts[trait] = {};
            if (!traitCounts[trait][value]) traitCounts[trait][value] = 0;
            traitCounts[trait][value]++;
        }
    }
    
    // Calculate rarity score (1 / frequency for each trait)
    const total = nfts.length;
    for (const nft of nfts) {
        let score = 0;
        for (const [trait, value] of Object.entries(nft.traits)) {
            const frequency = traitCounts[trait][value] / total;
            score += 1 / frequency;  // Lower frequency = higher score
        }
        nft.rarityScore = score;
    }
    
    // Sort by rarity (highest first)
    nfts.sort((a, b) => b.rarityScore - a.rarityScore);
    return nfts;
}

const ranked = calculateRarity(collection);
console.log("Rarity Rankings:");
for (const nft of ranked) {
    console.log(`  #${nft.id}: score ${nft.rarityScore.toFixed(2)} | ${nft.traits.Rarity}`);
}

Expected output:

Rarity Rankings:
  #1: score 8.33 | Legendary
  #3: score 4.17 | Uncommon
  #5: score 3.33 | Common
  #2: score 3.33 | Common
  #4: score 3.33 | Common

Royalties

ERC-2981 standardizes royalty payments for NFT creators:

// ERC-2981 Royalty implementation
import "@openzeppelin/contracts/token/common/ERC2981.sol";

contract DodaNFTWithRoyalty is ERC721URIStorage, ERC2981, Ownable {
    constructor() ERC721("DodaNFT", "DNFT") Ownable(msg.sender) {
        // 5% royalty
        _setDefaultRoyalty(msg.sender, 500);  // 500 = 5% (basis points)
    }
    
    function setRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
        _setDefaultRoyalty(receiver, feeNumerator);
    }
    
    // Override required by Solidity
    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721URIStorage, ERC2981) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

NFT Ecosystem Architecture


flowchart TB
  subgraph "Creation"
    ART[Digital Art]
    MUS[Music Files]
    GAME[Game Assets]
    META[Metadata JSON]
  end
  subgraph "Storage"
    IPFS[(IPFS / Filecoin)]
    ARWV[Arweave]
    CENT[Centralized CDN]
  end
  subgraph "Blockchain"
    ETH[Ethereum
ERC-721 / ERC-1155] SOL[Solana] POLY[Polygon] end subgraph "Marketplaces" OS[OpenSea] BLUR[Blur] LR[LooksRare] RR[Rarible] end subgraph "Wallets & Tools" MM[MetaMask] WC[WalletConnect] ENS[ENS Domains] end ART --> IPFS MUS --> IPFS GAME --> ARWV META --> IPFS IPFS --> ETH IPFS --> SOL ARWV --> POLY ETH --> OS ETH --> BLUR ETH --> LR SOL --> OS POLY --> RR OS --> MM BLUR --> WC MM --> ENS

Common NFT Mistakes

1. Storing Metadata On-Chain

Storing full metadata (including images) in the contract costs thousands in gas. Always store images on IPFS or Arweave and put only the URI on-chain. A 100KB image in an Ethereum transaction would cost over $10,000.

2. Not Checking for Reentrancy in Minting

The _safeMint function calls the recipient’s onERC721Received callback, which can re-enter your contract. Always follow the checks-effects-interactions pattern: update state before calling external contracts.

3. Centralized Metadata Storage

Using a regular web server for tokenURI means if the server goes down, all NFT images disappear. Use IPFS with Pinata or NFT.Storage for decentralized, permanent storage.

4. Ignoring Royalty Standards

Without ERC-2981, marketplaces have no standard way to pay royalties. Creators relying on custom royalty logic find that OpenSea and Blur don’t enforce non-standard royalties.

5. Minting Without Supply Cap

A contract without maxSupply that has a non-zero mintPrice allows unlimited minting, diluting the collection’s value and potentially draining user funds.

6. Not Testing on Testnet First

Deploying directly to mainnet without Sepolia or Goerli testing wastes thousands on failed transactions and bugs. Always mint a test collection first.

7. Assuming Floor Price Equals Value

Floor price is the cheapest listed NFT, not necessarily the market value. A collection with 90% of holders not listing has an artificial floor. Also, some NFTs are not listed at all, making floor price misleading.

Practice Questions

1. What is the difference between ERC-721 and ERC-1155?

ERC-721 represents unique, non-fungible tokens (one tokenId = one unique item). ERC-1155 supports both fungible and non-fungible tokens in a single contract, with batch transfers that save gas.

2. Why is IPFS used for NFT metadata instead of storing everything on-chain?

Storing images and metadata on-chain is prohibitively expensive — a single high-res image could cost thousands in gas. IPFS provides decentralized, content-addressed storage where the hash (not the content) goes on-chain.

3. How is NFT rarity calculated?

Rarity is calculated by trait frequency. The rarest NFTs have the least common combination of traits. Common methods include statistical rarity (1 / trait frequency summed) and trait normalization.

4. What are NFT royalties and how does ERC-2981 enable them?

Royalties pay creators a percentage of secondary sales. ERC-2981 standardizes a royaltyInfo(tokenId, salePrice) function that returns the royalty amount and receiver. Marketplaces call this on every sale.

5. Challenge: Design a music NFT contract that allows artists to mint songs as NFTs, pays 10% royalties on secondary sales, and lets fans buy fractional ownership of expensive songs.

Use ERC-1155 for the song (ID = song ID, supply = number of ownership fractions). Implement ERC-2981 for 10% royalties. For fractional ownership, mint multiple copies: a song with 1000 supply at 0.01 ETH each = 10 ETH total valuation. The artist burns their supply when selling.

Mini Project: NFT Gallery Viewer

// nft_gallery.js
// Fetch and display owned NFTs using Alchemy API
import axios from "axios";

const ALCHEMY_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY";
const WALLET_ADDRESS = "0xYourWallet...";

async function fetchNFTs(owner) {
    const response = await axios.post(ALCHEMY_URL, {
        jsonrpc: "2.0",
        method: "alchemy_getNFTs",
        params: [owner, { contractAddresses: [], pageSize: 10 }],
        id: 1,
    });
    
    const nfts = response.data.result.ownedNfts;
    console.log(`Found ${nfts.length} NFTs for ${owner}\n`);
    
    for (const nft of nfts) {
        const metadata = nft.metadata || {};
        const attributes = (metadata.attributes || [])
            .map(a => `${a.trait_type}: ${a.value}`)
            .join(", ");
        
        console.log(`Name: ${metadata.name || nft.title}`);
        console.log(`Contract: ${nft.contract.address}`);
        console.log(`Token ID: ${nft.id.tokenId}`);
        console.log(`Type: ${nft.id.tokenMetadata?.tokenType || "Unknown"}`);
        console.log(`Attributes: ${attributes || "None"}`);
        console.log(`Image: ${metadata.image || "N/A"}`);
        console.log("---");
    }
}

fetchNFTs(WALLET_ADDRESS).catch(console.error);

Expected output:

Found 3 NFTs for 0xYourWallet...

Name: DodaPixel Art #1
Contract: 0xabc123...
Token ID: 1
Type: ERC-721
Attributes: Background: Cosmic, Rarity: Legendary, Color: Neon Blue
Image: ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/art_1.webp
---

Related Concepts

FAQ

How do NFT marketplaces verify ownership?
Marketplaces call the ERC-721 ownerOf(tokenId) function to verify ownership before listing. For ERC-1155, they call balanceOf(owner, tokenId). The listing is a signed message authorizing the marketplace to transfer on sale.
Can an NFT have multiple editions?
Yes. ERC-1155 supports multiple copies of the same NFT (e.g., 1000 copies of a concert ticket). ERC-721 strictly has one copy per tokenId. Semi-fungible tokens use ERC-1155 with limited supply.
What happens if IPFS nodes go offline?
Your NFT still exists on-chain (the URI reference), but the image may not load. Use pinning services (Pinata, NFT.Storage) to keep your IPFS content available. Arweave offers permanent storage with one-time payment.
Do I pay gas to list an NFT on OpenSea?
Listing an ERC-721 for sale doesn’t cost gas — you sign an off-chain message. The buyer pays gas when purchasing. However, first-time approvals and lazy minting may require a gas transaction.

What’s Next

You now understand the NFT ecosystem end-to-end. Next, build Web3 dApps that interact with NFT contracts and explore DeFi for combining NFTs with decentralized finance.

  • Practice daily — Mint a test NFT on Sepolia using the provided contract
  • Build a project — Create an NFT collection with ERC-721, upload to IPFS, and list on OpenSea testnet
  • Explore further — Study Blur’s bidding mechanics and how they affect floor price dynamics

Remember: every expert was once a beginner. Keep minting!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro