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
Top comments (0)