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)