Difficulty: Intermediate
Creator royalties are a percentage of the sale price that is paid to the original creator (or a designated recipient) every time an NFT is resold on a secondary market. Unlike physical art sales where artists typically receive nothing from resales, NFT royalties promised a new economic model where creators earn ongoing revenue from their work's increasing value. The standard royalty rate in the NFT space has been around 5-10%, though rates vary widely. However, the enforcement of royalties has been one of the most contentious issues in the NFT ecosystem, as the fundamental open and permissionless nature of blockchain makes mandatory enforcement technically challenging.
ERC-2981, the NFT Royalty Standard, provides a standardized way for smart contracts to signal royalty information. The standard defines a single function, royaltyInfo(uint256 tokenId, uint256 salePrice), which returns the royalty recipient address and the royalty amount for a given sale price. This is a read-only function - it does not enforce royalty payment, it merely provides the information. Marketplaces that want to honor royalties query this function and include the royalty payment in their settlement logic. ERC-2981 supports both collection-wide royalties (same percentage for all tokens) and per-token royalties (different rates for different tokens), making it flexible for various business models.
The royalty enforcement debate intensified in late 2022 when marketplaces like Blur and X2Y2 began offering zero-royalty or optional-royalty trading to attract volume. In response, OpenSea introduced the Operator Filter Registry - a system where NFT contracts can restrict transfers to only approved operators (marketplaces that honor royalties). Collections that implemented the operator filter would block transfers via non-compliant marketplaces. This approach was controversial because it contradicted the open, permissionless nature of blockchain assets. Eventually, OpenSea itself moved away from mandatory royalty enforcement, and the industry has largely settled on a model where royalties are optional but honored by most major marketplaces.
Implementing royalties effectively requires understanding both the on-chain standard and the off-chain marketplace ecosystem. On-chain, you implement ERC-2981's royaltyInfo function and optionally the operator filter. Off-chain, you register your collection's royalty information with major marketplaces and monitor compliance. Some projects have explored alternative approaches to creator compensation: mint revenue sharing, token-gated benefits, licensing fees for commercial use, and on-chain royalty enforcement through custom transfer hooks. The ideal approach depends on the project's goals and the creator's relationship with their community.
From a technical perspective, the royalty amount should be reasonable and clearly communicated. Excessively high royalties (>10%) reduce the secondary market value of NFTs because buyers factor in the cost of future resale. The royalty recipient can be a single address or a payment splitter contract that distributes royalties among multiple creators. For collections with evolving royalty needs, the royalty configuration should be updatable by an authorized party (typically the collection owner) but with reasonable limits to protect holder interests.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract RoyaltyNFT is ERC721, ERC2981, Ownable {
uint256 public totalSupply;
uint256 public constant MAX_SUPPLY = 10000;
uint96 public constant MAX_ROYALTY_BPS = 1000;
constructor(
address royaltyRecipient,
uint96 royaltyBps
) ERC721("RoyaltyNFT", "RNFT") Ownable(msg.sender) {
require(royaltyBps <= MAX_ROYALTY_BPS, "Royalty too high");
_setDefaultRoyalty(royaltyRecipient, royaltyBps);
}
function mint(address to) external onlyOwner {
require(totalSupply < MAX_SUPPLY, "Max supply reached");
_safeMint(to, totalSupply++);
}
function setTokenRoyalty(
uint256 tokenId,
address recipient,
uint96 feeBps
) external onlyOwner {
require(feeBps <= MAX_ROYALTY_BPS, "Royalty too high");
_setTokenRoyalty(tokenId, recipient, feeBps);
}
function setDefaultRoyalty(
address recipient,
uint96 feeBps
) external onlyOwner {
require(feeBps <= MAX_ROYALTY_BPS, "Royalty too high");
_setDefaultRoyalty(recipient, feeBps);
}
function resetTokenRoyalty(uint256 tokenId) external onlyOwner {
_resetTokenRoyalty(tokenId);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC2981) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
This contract inherits from both ERC721 and OpenZeppelin's ERC2981 implementation. It sets a default royalty for all tokens in the constructor and allows per-token overrides. The MAX_ROYALTY_BPS cap prevents setting unreasonable royalties.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/finance/PaymentSplitter.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MultiCreatorNFT is ERC721, ERC2981, Ownable {
PaymentSplitter public immutable royaltySplitter;
uint256 public totalSupply;
constructor(
address[] memory creators,
uint256[] memory shares
) ERC721("MultiCreatorNFT", "MCNFT") Ownable(msg.sender) {
royaltySplitter = new PaymentSplitter(creators, shares);
_setDefaultRoyalty(address(royaltySplitter), 500);
}
function mint() external {
_safeMint(msg.sender, totalSupply++);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC2981) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
// Usage:
// After royalties are paid to the splitter contract,
// each creator calls royaltySplitter.release(payable(creatorAddress))
// to withdraw their share.
//
// Example with 3 creators:
// creators[0] = 0xArtist; // 60% of royalties
// creators[1] = 0xDeveloper; // 30% of royalties
// creators[2] = 0xAdvisor; // 10% of royalties
This pattern combines ERC-2981 with OpenZeppelin's PaymentSplitter to distribute royalties among multiple creators. The splitter contract receives all royalty payments, and each creator can withdraw their proportional share at any time.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract RoyaltyAwareMarketplace is ReentrancyGuard {
uint256 public constant MARKETPLACE_FEE_BPS = 250;
address public feeRecipient;
struct Listing {
address seller;
uint256 price;
}
mapping(address => mapping(uint256 => Listing)) public listings;
constructor(address _feeRecipient) {
feeRecipient = _feeRecipient;
}
function list(
address nftContract,
uint256 tokenId,
uint256 price
) external {
require(price > 0, "Invalid price");
require(
IERC721(nftContract).ownerOf(tokenId) == msg.sender,
"Not owner"
);
listings[nftContract][tokenId] = Listing(msg.sender, price);
}
function buy(
address nftContract,
uint256 tokenId
) external payable nonReentrant {
Listing memory listing = listings[nftContract][tokenId];
require(listing.price > 0, "Not listed");
require(msg.value >= listing.price, "Insufficient payment");
delete listings[nftContract][tokenId];
uint256 marketplaceFee = (listing.price * MARKETPLACE_FEE_BPS) / 10000;
uint256 royaltyAmount = 0;
address royaltyRecipient;
if (IERC165(nftContract).supportsInterface(type(IERC2981).interfaceId)) {
(royaltyRecipient, royaltyAmount) = IERC2981(nftContract)
.royaltyInfo(tokenId, listing.price);
if (royaltyAmount > listing.price / 2) {
royaltyAmount = 0;
}
}
uint256 sellerProceeds = listing.price - marketplaceFee - royaltyAmount;
IERC721(nftContract).safeTransferFrom(
listing.seller, msg.sender, tokenId
);
_pay(listing.seller, sellerProceeds);
_pay(feeRecipient, marketplaceFee);
if (royaltyAmount > 0 && royaltyRecipient != address(0)) {
_pay(royaltyRecipient, royaltyAmount);
}
if (msg.value > listing.price) {
_pay(msg.sender, msg.value - listing.price);
}
}
function _pay(address to, uint256 amount) internal {
(bool success, ) = to.call{value: amount}("");
require(success, "Payment failed");
}
}
This marketplace checks for ERC-2981 support using ERC-165 interface detection, queries royaltyInfo for the recipient and amount, then distributes the sale price three ways: marketplace fee, creator royalty, and seller proceeds. A safety check prevents unreasonable royalty amounts.
ERC-2981, Royalty, Creator Economics, Operator Filter, Secondary Sales