Difficulty: Intermediate
NFT marketplaces are platforms that facilitate the buying, selling, and trading of non-fungible tokens. While the concept sounds simple, the smart contract architecture behind a marketplace is surprisingly complex, involving order matching, escrow mechanics, fee distribution, and security considerations. Understanding how marketplaces work at the protocol level is essential for developers building NFT infrastructure, integrating with existing marketplaces, or working on NFT-related smart contracts. The marketplace landscape has evolved from simple escrow-based designs to sophisticated protocol-level solutions like OpenSea's Seaport.
The simplest marketplace pattern uses an escrow model: the seller transfers their NFT to the marketplace contract, which holds it until a buyer pays the asking price. When a purchase occurs, the contract transfers the NFT to the buyer and the payment to the seller (minus marketplace fees). This approach is straightforward but has drawbacks: the seller loses custody of their NFT while it's listed, gas is required for both listing (transfer to escrow) and delisting (transfer back), and the seller cannot list the same NFT on multiple marketplaces simultaneously. Despite these limitations, the escrow pattern is still used for auctions where holding the asset in escrow is necessary for trust.
Modern marketplaces like OpenSea use an off-chain order book with on-chain settlement model. Instead of transferring the NFT to escrow, the seller signs an off-chain order (using EIP-712) that authorizes the marketplace contract to transfer the NFT when matched with a valid buy order. The NFT stays in the seller's wallet until the moment of sale. Orders are stored on the marketplace's backend servers (off-chain), reducing gas costs. When a buyer wants to purchase, they submit the seller's signed order along with payment, and the marketplace contract atomically transfers the NFT and the payment in a single transaction. This enables gasless listings, cross-marketplace listing, and instant delisting.
OpenSea's Seaport protocol represents the state of the art in marketplace design. Seaport is a generalized exchange protocol where any combination of ERC-20, ERC-721, ERC-1155, and native ETH can be offered and received. An order in Seaport consists of an 'offer' (what the offerer will give) and a 'consideration' (what the offerer expects to receive). This flexible structure supports listings, offers, bartering, bundle sales, partial fills, and Dutch auctions. Seaport uses a zone system for order validation, criteria-based orders for collection-wide offers, and conduit channels for token approvals.
Fee mechanisms in NFT marketplaces typically involve three components: the marketplace fee (e.g., OpenSea's 2.5%), creator royalties (e.g., 5-10% defined by the collection), and potential protocol fees. These fees are deducted from the sale price and distributed atomically during the transaction. The enforcement of creator royalties has been contentious - marketplaces like Blur initially bypassed royalties to attract volume, leading to the development of on-chain enforcement mechanisms. The debate between optional and mandatory royalties reflects the broader tension between open market dynamics and creator compensation in the NFT ecosystem.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract NFTMarketplace is ERC721Holder, ReentrancyGuard {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
uint256 public listingCounter;
uint256 public constant MARKETPLACE_FEE_BPS = 250;
address public feeRecipient;
mapping(uint256 => Listing) public listings;
mapping(address => mapping(uint256 => uint256)) public activeListing;
event Listed(uint256 indexed listingId, address indexed seller, address nftContract, uint256 tokenId, uint256 price);
event Sale(uint256 indexed listingId, address indexed buyer, uint256 price);
event ListingCanceled(uint256 indexed listingId);
constructor(address _feeRecipient) {
feeRecipient = _feeRecipient;
}
function listNFT(
address nftContract,
uint256 tokenId,
uint256 price
) external returns (uint256) {
require(price > 0, "Price must be > 0");
require(
activeListing[nftContract][tokenId] == 0,
"Already listed"
);
IERC721(nftContract).safeTransferFrom(
msg.sender, address(this), tokenId
);
uint256 listingId = ++listingCounter;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});
activeListing[nftContract][tokenId] = listingId;
emit Listed(listingId, msg.sender, nftContract, tokenId, price);
return listingId;
}
function buyNFT(uint256 listingId) external payable nonReentrant {
Listing storage listing = listings[listingId];
require(listing.active, "Not active");
require(msg.value >= listing.price, "Insufficient payment");
listing.active = false;
activeListing[listing.nftContract][listing.tokenId] = 0;
uint256 fee = (listing.price * MARKETPLACE_FEE_BPS) / 10000;
uint256 sellerProceeds = listing.price - fee;
IERC721(listing.nftContract).safeTransferFrom(
address(this), msg.sender, listing.tokenId
);
(bool sellerPaid, ) = listing.seller.call{value: sellerProceeds}("");
require(sellerPaid, "Seller payment failed");
(bool feePaid, ) = feeRecipient.call{value: fee}("");
require(feePaid, "Fee payment failed");
if (msg.value > listing.price) {
(bool refunded, ) = msg.sender.call{value: msg.value - listing.price}("");
require(refunded, "Refund failed");
}
emit Sale(listingId, msg.sender, listing.price);
}
function cancelListing(uint256 listingId) external nonReentrant {
Listing storage listing = listings[listingId];
require(listing.active, "Not active");
require(listing.seller == msg.sender, "Not seller");
listing.active = false;
activeListing[listing.nftContract][listing.tokenId] = 0;
IERC721(listing.nftContract).safeTransferFrom(
address(this), msg.sender, listing.tokenId
);
emit ListingCanceled(listingId);
}
}
This marketplace uses the escrow pattern: sellers transfer NFTs to the contract, buyers pay ETH to receive them. The ReentrancyGuard protects against reentrancy during ETH transfers. A 2.5% marketplace fee is deducted from each sale.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SignatureMarketplace is EIP712, ReentrancyGuard {
using ECDSA for bytes32;
struct Order {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
uint256 nonce;
uint256 expiry;
}
bytes32 private constant ORDER_TYPE_HASH = keccak256(
"Order(address seller,address nftContract,uint256 tokenId,uint256 price,uint256 nonce,uint256 expiry)"
);
uint256 public constant FEE_BPS = 250;
address public feeRecipient;
mapping(address => uint256) public nonces;
mapping(bytes32 => bool) public filledOrders;
event OrderFilled(bytes32 indexed orderHash, address indexed buyer, uint256 price);
event OrderCanceled(address indexed seller, uint256 newNonce);
constructor(address _feeRecipient)
EIP712("SignatureMarketplace", "1")
{
feeRecipient = _feeRecipient;
}
function fillOrder(
Order calldata order,
bytes calldata signature
) external payable nonReentrant {
require(block.timestamp < order.expiry, "Order expired");
require(msg.value >= order.price, "Insufficient payment");
require(order.nonce >= nonces[order.seller], "Order cancelled");
bytes32 orderHash = _hashOrder(order);
require(!filledOrders[orderHash], "Already filled");
bytes32 digest = _hashTypedDataV4(orderHash);
address signer = digest.recover(signature);
require(signer == order.seller, "Invalid signature");
filledOrders[orderHash] = true;
IERC721(order.nftContract).safeTransferFrom(
order.seller, msg.sender, order.tokenId
);
uint256 fee = (order.price * FEE_BPS) / 10000;
uint256 sellerAmount = order.price - fee;
(bool s1, ) = order.seller.call{value: sellerAmount}("");
require(s1, "Seller payment failed");
(bool s2, ) = feeRecipient.call{value: fee}("");
require(s2, "Fee payment failed");
if (msg.value > order.price) {
(bool s3, ) = msg.sender.call{value: msg.value - order.price}("");
require(s3, "Refund failed");
}
emit OrderFilled(orderHash, msg.sender, order.price);
}
function cancelAllOrders() external {
nonces[msg.sender]++;
emit OrderCanceled(msg.sender, nonces[msg.sender]);
}
function _hashOrder(Order calldata order) internal pure returns (bytes32) {
return keccak256(abi.encode(
ORDER_TYPE_HASH,
order.seller,
order.nftContract,
order.tokenId,
order.price,
order.nonce,
order.expiry
));
}
function verifyOrder(
Order calldata order,
bytes calldata signature
) external view returns (bool) {
bytes32 digest = _hashTypedDataV4(_hashOrder(order));
address signer = digest.recover(signature);
return signer == order.seller
&& !filledOrders[_hashOrder(order)]
&& order.nonce >= nonces[order.seller]
&& block.timestamp < order.expiry;
}
}
This signature-based marketplace keeps NFTs in sellers' wallets until sale. Sellers sign EIP-712 orders off-chain (gasless listing), and buyers submit the signed order with payment to execute the sale. The nonce system allows batch cancellation of all outstanding orders.
NFT Marketplace, Escrow Pattern, Order Book, Seaport Protocol, Listing, Offer