DEV Community

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

Posted on

πŸͺ 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, 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
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Setup

forge init day-26-nft-marketplace
cd day-26-nft-marketplace
forge install OpenZeppelin/openzeppelin-contracts
Enter fullscreen mode Exit fullscreen mode

In foundry.toml:

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

🧩 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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 {}
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Run tests:

forge test -vv
Enter fullscreen mode Exit fullscreen mode

πŸš€ 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Deploy using Foundry:

forge script script/Deploy.s.sol --broadcast --private-key <YOUR_PRIVATE_KEY>
Enter fullscreen mode Exit fullscreen mode

πŸ’° 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

Top comments (0)