πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž

Today, we’re building a fully functional NFT Marketplace in Solidity β€” a platform where users can buy, sell, and trade NFTs, while automatically paying royalties to creators and fees to the platform.

It’s like creating your own digital store for NFTs…


This content originally appeared on DEV Community and was authored by Saurav Kumar

Today, we’re building a fully functional NFT Marketplace in Solidity β€” a platform where users can buy, sell, and trade NFTs, while automatically paying royalties to creators and fees to the platform.

It’s like creating your own digital store for NFTs, powered entirely by smart contracts! ⚑

🧠 What You’ll Learn

  • How NFT marketplaces work on-chain
  • Listing and buying NFTs with ETH
  • Applying ERC-2981 royalties for creators
  • Adding marketplace fees
  • Preventing reentrancy & ensuring safe transfers
  • Testing & deploying with Foundry

🧱 Project Structure

day-26-nft-marketplace/
β”œβ”€ foundry.toml
β”œβ”€ src/
β”‚  β”œβ”€ NFTCollection.sol
β”‚  └─ NFTMarketplace.sol
β”œβ”€ test/
β”‚  └─ Marketplace.t.sol
β”œβ”€ script/
β”‚  └─ Deploy.s.sol
└─ README.md

βš™οΈ Setup

forge init day-26-nft-marketplace
cd day-26-nft-marketplace
forge install OpenZeppelin/openzeppelin-contracts

In foundry.toml:

[default]
src = "src"
out = "out"
libs = ["lib"]
tests = "test"
remappings = ["openzeppelin/=lib/openzeppelin-contracts/"]

🧩 Step 1 β€” Create the NFT Contract (ERC-721 + Royalties)

We’ll create an NFT collection with ERC-2981 (royalty) support.

πŸ“„ src/NFTCollection.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "openzeppelin/token/ERC721/ERC721.sol";
import "openzeppelin/access/Ownable.sol";
import "openzeppelin/token/common/ERC2981.sol";
import "openzeppelin/utils/Counters.sol";

contract NFTCollection is ERC721, ERC2981, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;

    string private _baseTokenURI;

    event Minted(address indexed to, uint256 indexed tokenId);

    constructor(string memory name_, string memory symbol_, string memory baseURI_)
        ERC721(name_, symbol_)
    {
        _baseTokenURI = baseURI_;
    }

    function mint(address to, uint96 royaltyBps) external onlyOwner returns (uint256) {
        _tokenIdCounter.increment();
        uint256 tokenId = _tokenIdCounter.current();
        _safeMint(to, tokenId);
        if (royaltyBps > 0) {
            _setTokenRoyalty(tokenId, owner(), royaltyBps);
        }
        emit Minted(to, tokenId);
        return tokenId;
    }

    function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
        _setDefaultRoyalty(receiver, feeNumerator);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
}

βœ… Features:

  • Mint NFTs with optional per-token royalty.
  • Default royalty for all tokens (via ERC2981).
  • Each token has metadata base URI.

πŸ›’ Step 2 β€” Build the Marketplace Smart Contract

Now, let’s create a marketplace to list, buy, and cancel NFT sales.
We’ll add marketplace fees and royalty distribution automatically.

πŸ“„ src/NFTMarketplace.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "openzeppelin/security/ReentrancyGuard.sol";
import "openzeppelin/token/ERC721/IERC721.sol";
import "openzeppelin/access/Ownable.sol";
import "openzeppelin/utils/Address.sol";
import "openzeppelin/token/common/ERC2981.sol";

contract NFTMarketplace is ReentrancyGuard, Ownable {
    using Address for address payable;

    struct Listing {
        address seller;
        uint256 price;
    }

    mapping(address => mapping(uint256 => Listing)) public listings;

    uint96 public marketplaceFeeBps;
    uint96 public constant FEE_DENOMINATOR = 10000;

    event Listed(address indexed nft, uint256 indexed tokenId, address indexed seller, uint256 price);
    event Cancelled(address indexed nft, uint256 indexed tokenId);
    event Bought(address indexed nft, uint256 indexed tokenId, address indexed buyer, uint256 price);

    constructor(uint96 _feeBps) {
        marketplaceFeeBps = _feeBps; // 250 = 2.5%
    }

    function list(address nft, uint256 tokenId, uint256 price) external nonReentrant {
        require(price > 0, "Price must be > 0");
        IERC721 token = IERC721(nft);
        require(token.ownerOf(tokenId) == msg.sender, "Not owner");
        require(token.getApproved(tokenId) == address(this) ||
                token.isApprovedForAll(msg.sender, address(this)), "Not approved");

        listings[nft][tokenId] = Listing(msg.sender, price);
        emit Listed(nft, tokenId, msg.sender, price);
    }

    function cancel(address nft, uint256 tokenId) external nonReentrant {
        Listing memory l = listings[nft][tokenId];
        require(l.seller == msg.sender, "Not seller");
        delete listings[nft][tokenId];
        emit Cancelled(nft, tokenId);
    }

    function buy(address nft, uint256 tokenId) external payable nonReentrant {
        Listing memory l = listings[nft][tokenId];
        require(l.price > 0, "Not listed");
        require(msg.value == l.price, "Wrong value");

        delete listings[nft][tokenId];

        uint256 fee = (msg.value * marketplaceFeeBps) / FEE_DENOMINATOR;
        uint256 remaining = msg.value - fee;

        (address royaltyReceiver, uint256 royaltyAmount) =
            _getRoyaltyInfo(nft, tokenId, msg.value);

        if (royaltyReceiver != address(0) && royaltyAmount > 0) {
            if (royaltyAmount > remaining) royaltyAmount = remaining;
            remaining -= royaltyAmount;
            payable(royaltyReceiver).sendValue(royaltyAmount);
        }

        payable(l.seller).sendValue(remaining);
        payable(owner()).sendValue(fee);

        IERC721(nft).safeTransferFrom(l.seller, msg.sender, tokenId);

        emit Bought(nft, tokenId, msg.sender, msg.value);
    }

    function _getRoyaltyInfo(address nft, uint256 tokenId, uint256 price)
        internal
        view
        returns (address, uint256)
    {
        try ERC2981(nft).royaltyInfo(tokenId, price)
            returns (address receiver, uint256 amount)
        {
            return (receiver, amount);
        } catch {
            return (address(0), 0);
        }
    }

    receive() external payable {}
}

βœ… Key Logic:

  • Sellers list NFTs after approval.
  • Buyers send ETH to buy NFTs.
  • Marketplace automatically splits:

    • Creator royalty (ERC2981)
    • Marketplace fee
    • Seller payout

πŸ§ͺ Step 3 β€” Test the Marketplace (Foundry)

πŸ“„ test/Marketplace.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";
import "../src/NFTCollection.sol";
import "../src/NFTMarketplace.sol";

contract MarketplaceTest is Test {
    NFTCollection nft;
    NFTMarketplace market;

    address owner = address(0xABCD);
    address seller = address(0xBEEF);
    address buyer = address(0xCAFE);

    function setUp() public {
        vm.startPrank(owner);
        nft = new NFTCollection("MyNFT", "MNFT", "ipfs://base/");
        market = new NFTMarketplace(250); // 2.5%
        nft.mint(seller, 500); // 5% royalty
        vm.stopPrank();
    }

    function testBuyNFT() public {
        vm.prank(seller);
        nft.approve(address(market), 1);
        vm.prank(seller);
        market.list(address(nft), 1, 1 ether);

        vm.deal(buyer, 2 ether);
        vm.prank(buyer);
        market.buy{value: 1 ether}(address(nft), 1);

        assertEq(nft.ownerOf(1), buyer);
    }
}

βœ… Run tests:

forge test -vv

πŸš€ Step 4 β€” Deploy the Contracts

πŸ“„ script/Deploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Script.sol";
import "../src/NFTCollection.sol";
import "../src/NFTMarketplace.sol";

contract Deploy is Script {
    function run() external {
        vm.startBroadcast();

        NFTCollection nft = new NFTCollection("MyNFT", "MNFT", "ipfs://base/");
        NFTMarketplace market = new NFTMarketplace(250);

        nft.setDefaultRoyalty(msg.sender, 200);

        vm.stopBroadcast();
    }
}

Deploy using Foundry:

forge script script/Deploy.s.sol --broadcast --private-key <YOUR_PRIVATE_KEY>

πŸ’° Example Flow

  1. Owner deploys NFTCollection & NFTMarketplace.
  2. Creator mints NFTs with 5% royalty.
  3. Seller lists NFT for 1 ETH.
  4. Buyer buys NFT:
  • 2.5% β†’ Marketplace owner
  • 5% β†’ Creator (royalty)
  • 92.5% β†’ Seller

Everything is on-chain, transparent, and automatic πŸ’«

πŸ”’ Security Features

  • Reentrancy protection (nonReentrant)
  • Checks-Effects-Interactions pattern
  • Royalties capped (no overpayment)
  • Only seller can cancel listings
  • Funds transferred securely via Address.sendValue

πŸ’‘ Future Enhancements

  • Add ERC-20 token payments (e.g., USDC)
  • Add auction and bidding system
  • Build React frontend with Ethers.js
  • Integrate The Graph for indexing listings

🧠 Concepts Covered

  • ERC-721 & ERC-2981 standards
  • Royalty mechanism
  • Marketplace fee handling
  • Secure ETH transfers
  • Foundry-based testing

🏁 Conclusion

You just built your own decentralized NFT Marketplace β€” the foundation of OpenSea-like platforms.
Now you understand how to:

  • Manage NFT listings and trades
  • Handle royalties automatically
  • Keep trades secure and transparent

Your smart contracts handle trading, royalties, and fees β€” all without intermediaries! πŸš€

πŸ”— Connect With Me

If you found this useful β€” drop a πŸ’¬ comment or ❀️ like!
Let’s connect πŸ‘‡
πŸ§‘β€πŸ’» Saurav Kumar
πŸ’Ό LinkedIn
🧡 Twitter/X
πŸ“š More #30DaysOfSolidity Posts


This content originally appeared on DEV Community and was authored by Saurav Kumar


Print Share Comment Cite Upload Translate Updates
APA

Saurav Kumar | Sciencx (2025-10-29T20:24:19+00:00) πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž. Retrieved from https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/

MLA
" » πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž." Saurav Kumar | Sciencx - Wednesday October 29, 2025, https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/
HARVARD
Saurav Kumar | Sciencx Wednesday October 29, 2025 » πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž., viewed ,<https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/>
VANCOUVER
Saurav Kumar | Sciencx - » πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/
CHICAGO
" » πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž." Saurav Kumar | Sciencx - Accessed . https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/
IEEE
" » πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž." Saurav Kumar | Sciencx [Online]. Available: https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/. [Accessed: ]
rf:citation
» πŸͺ Day 26 of #30DaysOfSolidity β€” Build a Decentralized NFT Marketplace with Royalties πŸ’Ž | Saurav Kumar | Sciencx | https://www.scien.cx/2025/10/29/%f0%9f%8f%aa-day-26-of-30daysofsolidity-build-a-decentralized-nft-marketplace-with-royalties-%f0%9f%92%8e/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.