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
- 
Owner deploys NFTCollection&NFTMarketplace.
- Creator mints NFTs with 5% royalty.
- Seller lists NFT for 1 ETH.
- 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
 
	
			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/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.
