ERC-2981 NFT Royalty Standards
NFT creators face a critical challenge: ensuring they receive compensation when their digital assets are resold. Before ERC-2981, each marketplace implemented proprietary royalty systems, creating a fragmented ecosystem where artists lost revenue when collectors moved between platforms. ERC-2981 solves this by providing a standardized interface that any marketplace can query to retrieve royalty payment information, enabling creators to monetize their work regardless of where it’s sold. This guide explores the technical implementation, design decisions, and real-world adoption of this foundational NFT standard.
What is ERC-2981?
ERC-2981 is a standardized Ethereum interface for retrieving NFT royalty payment information. Created in September 2020 by Zach Burks, James Morgan, Blaine Malone, and James Seibel, the standard defines a simple method that smart contracts can implement to communicate royalty details to marketplaces and other platforms.
The core of ERC-2981 is a single function: royaltyInfo(uint256 tokenId, uint256 salePrice) which returns two values: the recipient address who should receive the royalty, and the royalty amount calculated as a percentage of the sale price. The standard extends ERC-165 for interface detection, allowing marketplaces to automatically check if a contract supports royalties before executing a sale.
ERC-2981 works with both ERC-721 (unique tokens) and ERC-1155 (semi-fungible tokens) NFT standards, making it universally applicable across the NFT ecosystem. The official EIP specification provides the exact Solidity interface and implementation guidelines.
The Problem ERC-2981 Solves
Before ERC-2981, the NFT ecosystem suffered from severe fragmentation in royalty implementation. Each marketplace built incompatible, proprietary systems for tracking and paying creator royalties. An NFT minted on one platform with royalty expectations couldn’t enforce those royalties when sold on a different marketplace.
This fragmentation had real consequences for creators. Artists who sold their work on Marketplace A would lose all royalty revenue when collectors moved to Marketplace B, which used a completely different system. The common response from platforms was: “If your NFT is sold on another marketplace, we cannot enforce payment.”
The lack of a shared standard also created technical barriers. Developers building NFT projects had to integrate with each marketplace’s custom royalty API, multiplying implementation complexity. Worse, some marketplaces stored royalty information in off-chain databases rather than on the blockchain itself, creating single points of failure and trust dependencies. This fragmentation hindered the growth of the NFT ecosystem and threatened the long-term sustainability of creator compensation.
How ERC-2981 Works Technically
ERC-2981’s design is deliberately minimal. The standard defines a single interface method that accepts two parameters: tokenId (which NFT is being sold) and salePrice (how much it’s selling for). The function returns receiver (the address that should receive payment) and royaltyAmount (how much to pay).
interface IERC2981 {
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external view
returns (address receiver, uint256 royaltyAmount);
}
The royalty amount must be calculated as a percentage of the sale price, not a fixed amount. This ensures the standard works regardless of the currency used—whether ETH, WETH, USDC, or any other token. If an NFT sells for 100 USDC and the royalty is 5%, the contract returns 5 USDC as the royalty amount.
ERC-2981 leverages ERC-165’s supportsInterface() pattern for capability detection. The interface ID is 0x2a55205a, derived from the bytes4 keccak hash of the function signature. Marketplaces query this ID to determine if a contract implements royalties before processing a sale.
Importantly, the standard is query-based—marketplaces retrieve royalty information by calling the contract directly, rather than relying on a central registry. This decentralized approach eliminates single points of failure and ensures royalty information remains accessible as long as the blockchain exists.
Implementation Approaches
The most widely used implementation comes from OpenZeppelin’s ERC2981 contract, which provides production-ready code that can be inherited by any NFT contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract MyNFT is ERC721, ERC2981 {
constructor() ERC721("MyNFT", "MNFT") {
// Set 5% royalty (500 basis points) to contract creator
_setDefaultRoyalty(msg.sender, 500);
}
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
OpenZeppelin’s implementation uses basis points (denominator of 10,000) for royalty calculations. A 5% royalty equals 500 basis points, a 2.5% royalty equals 250 basis points. This approach provides fine-grained control while avoiding floating-point arithmetic.
The contract supports two configuration patterns. _setDefaultRoyalty(receiver, feeNumerator) applies the same royalty to all tokens in a collection, minimizing storage costs. For collections with multiple artists, _setTokenRoyalty(tokenId, receiver, feeNumerator) allows per-token customization:
function mintWithCustomRoyalty(
address to,
uint256 tokenId,
address royaltyRecipient,
uint96 royaltyBps
) public onlyOwner {
_safeMint(to, tokenId);
_setTokenRoyalty(tokenId, royaltyRecipient, royaltyBps);
}
When inheriting from multiple contracts that implement supportsInterface(), you must override the function and call super.supportsInterface() to ensure all parent interfaces are properly detected.
Key Design Decisions and Rationale
ERC-2981’s most controversial design decision is its voluntary nature. The standard cannot force marketplaces to pay royalties because NFT transfers occur through the standard transferFrom() method, which has no built-in payment mechanism. This differs from some creators’ expectations of “automatic” enforcement.
The decision to use percentage-based calculations rather than fixed amounts ensures the standard scales with market conditions. A 100 ETH sale and a 1 ETH sale both pay the correct proportional royalty. The standard deliberately avoids making unit assumptions—it works with any currency, as long as the royalty is paid in the same token as the sale price.
ERC-2981 specifies a simple single recipient rather than supporting payment splits on-chain. This keeps gas costs minimal and implementation simple. Collections that need to split royalties among multiple creators can use a separate royalty splitter contract as the receiver address, which then distributes funds according to its own logic.
The standard intentionally omits a notification mechanism when royalties are paid. While some proposed a callback that would notify the NFT contract of successful payment, this was left to future EIPs to avoid increasing gas costs for basic implementations.
Implementers can make royalty percentages dynamic based on predictable on-chain variables. For example, royalties could decrease after a certain number of transfers, or vary based on how long a collector has held the token. The standard doesn’t prescribe these patterns but allows for creative smart contract development approaches.
Marketplace Integration
Integrating ERC-2981 into a marketplace follows a straightforward workflow. First, check if the NFT contract supports the standard by calling supportsInterface(0x2a55205a) through the ERC-165 interface. If the contract doesn’t support ERC-2981, the marketplace can fall back to alternative methods like the Royalty Registry.
When a sale is initiated, the marketplace calls royaltyInfo(tokenId, salePrice) before executing the transfer:
function getRoyaltyForSale(
address nftContract,
uint256 tokenId,
uint256 salePrice
) public view returns (address receiver, uint256 royaltyAmount) {
IERC2981 royaltyContract = IERC2981(nftContract);
return royaltyContract.royaltyInfo(tokenId, salePrice);
}
After receiving the royalty information, the marketplace executes the sale transaction in this order:
- Transfer the NFT from seller to buyer
- Transfer the royalty amount to the specified receiver address
- Transfer the remaining funds (salePrice - royaltyAmount) to the seller
The royalty must be paid in the same currency as the _salePrice parameter that was queried. If a sale executes in USDC, the royalty payment must also be in USDC. This ensures consistency across different payment methods and prevents currency mismatch issues.
If royaltyAmount returns 0, the marketplace should skip the royalty payment step to avoid wasting gas on a zero-value transfer. The standard applies to all sale types: on-chain DEX sales, over-the-counter transfers with payment, and traditional auction house models.
Common Pitfalls and Best Practices
The most critical implementation error is returning a fixed royaltyAmount that ignores the salePrice parameter. This violates the standard’s core principle that royalties must scale with sale price. Always calculate the amount as a percentage:
// CORRECT: Percentage-based calculation
royaltyAmount = (salePrice * basisPoints) / 10000;
// INCORRECT: Fixed amount ignoring salePrice
royaltyAmount = 1 ether; // This breaks the standard
Never compare salePrice to hardcoded constants like 1 ether. This makes currency assumptions and breaks when used with stablecoins or other tokens. The standard must work regardless of the currency denomination.
When implementing royalty setter functions, validate that the receiver is not the zero address and that feeNumerator <= feeDenominator. OpenZeppelin’s implementation includes these checks, but custom implementations sometimes overlook them.
For percentage calculations that result in remainders, you can round up or down—the standard doesn’t prescribe a specific rounding method. However, be consistent across your implementation to avoid confusion.
From a security perspective, use OpenZeppelin’s battle-tested implementation rather than writing custom royalty logic. Their contracts have been audited and used in production by thousands of projects. Custom implementations often contain subtle bugs that lead to incorrect royalty calculations or security vulnerabilities.
For gas optimization, store royalty information efficiently. Using the default royalty pattern avoids per-token storage costs. Only use _setTokenRoyalty() when you genuinely need different royalty settings for individual tokens.
Royalty Registry and Discoverability
While ERC-2981 provides the standard interface, not all NFT contracts implement it—particularly older collections minted before the standard gained adoption. The Royalty Registry (royaltyregistry.xyz) addresses this gap by providing a lookup service where creators can register royalty information for contracts that don’t natively support ERC-2981.
OpenSea and other major marketplaces use the Royalty Registry as a fallback. When encountering an NFT without ERC-2981 support, they query the registry for royalty information. Contract ownership is typically determined via the ERC-173 Ownership standard.
For collection-level metadata that changes infrequently, ERC-7572 (ContractURI) provides a standard way to expose information like royalty policies. When royalty information updates, contracts should emit the ContractURIUpdated() event to notify indexers and marketplaces:
event ContractURIUpdated();
function updateRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
_setDefaultRoyalty(receiver, feeNumerator);
emit ContractURIUpdated();
}
This event-based notification pattern helps marketplaces keep cached royalty information in sync with on-chain state.
ERC-2981 vs Alternative Approaches
Several competing approaches to royalty enforcement have emerged. The Operator Filter Registry, pioneered by OpenSea, takes a more aggressive stance by maintaining a blacklist of marketplaces that don’t honor royalties. NFT contracts using this registry can block transfers to non-compliant platforms, though this approach has generated controversy around collector property rights.
Some protocols implement on-chain enforcement by wrapping NFTs or modifying transfer logic to require royalty payments before allowing transfers. While technically effective, these approaches often reduce composability and create friction in the user experience.
Off-chain enforcement relies on social pressure and marketplace reputation. Platforms that ignore creator royalties face backlash from artists and collectors who value supporting creators. This approach preserves the voluntary nature of payments while leveraging community norms.
ERC-2981’s philosophy prioritizes voluntary cooperation and minimal on-chain overhead. The standard provides the information needed for royalty payments without attempting to force compliance through technical means. This represents a trade-off between creator control and collector freedom.
Despite its voluntary nature, ERC-2981 has achieved significant traction. Major marketplaces including OpenSea, Rarible, LooksRare, Foundation, and Zora all support the standard and default to paying royalties when they’re specified. The market has largely converged on ERC-2981 as the de facto standard, even as debates continue about enforcement mechanisms.
Real-World Adoption and Limitations
ERC-2981 has been widely adopted across the NFT ecosystem. The major marketplaces—OpenSea, Rarible, LooksRare, Foundation, and Zora—all query ERC-2981 royalty information and pay royalties by default when they’re specified. Most modern NFT minting platforms and tools like Manifold Studio, thirdweb, and OpenZeppelin’s contract wizard include ERC-2981 support out of the box.
However, the standard’s voluntary nature creates enforcement challenges. Collectors can still execute royalty-free peer-to-peer transfers or use marketplaces that deliberately ignore royalties. The 2022-2023 period saw intense debates in the NFT community, with some marketplaces making royalties optional to gain competitive advantage.
Creator responses have varied. Some projects implement hybrid models that combine ERC-2981 with on-chain incentives—offering benefits to collectors who pay royalties while not technically blocking transfers. Others have adopted the Operator Filter Registry to restrict which platforms can trade their NFTs.
Despite enforcement challenges, ERC-2981 remains the dominant technical standard for communicating royalty information. Even marketplaces that make royalty payments optional typically still read ERC-2981 data to display royalty expectations to users. The standard has achieved its core goal of creating a common language for royalty information, even if payment compliance varies.
Layer 2 networks like Polygon, Arbitrum, and Base have accelerated adoption by reducing transaction costs, making it economically feasible for creators to mint NFT collections with ERC-2981 support that would be cost-prohibitive on Ethereum mainnet.
Step-by-Step Implementation Tutorial
Installing the necessary dependencies is the first step. For projects using npm:
npm install @openzeppelin/contracts
Create a new Solidity file for your NFT contract and import both the ERC-721 standard and ERC-2981:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract MyNFTCollection is ERC721, ERC2981 {
uint256 private _tokenIdCounter;
constructor() ERC721("My NFT Collection", "MNFT") {
// Set 5% royalty (500 basis points) to the contract deployer
_setDefaultRoyalty(msg.sender, 500);
}
function mint(address to) public {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter++;
_safeMint(to, tokenId);
}
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
The supportsInterface override is critical—it ensures that interface detection works correctly when inheriting from multiple contracts. Without this, marketplace queries for ERC-2981 support will fail.
Deploy your contract to a testnet like Sepolia or Mumbai for testing. Use a tool like Hardhat or Foundry to compile and deploy:
// Hardhat deployment script
const MyNFT = await ethers.getContractFactory("MyNFTCollection");
const nft = await MyNFT.deploy();
await nft.deployed();
console.log("NFT deployed to:", nft.address);
After deployment, verify the contract on Etherscan or Polygonscan to make the source code publicly readable. Then test the royaltyInfo() function with various sale prices to confirm the calculations are correct:
// Using ethers.js
const ERC2981_INTERFACE_ID = "0x2a55205a";
const supportsRoyalty = await nft.supportsInterface(ERC2981_INTERFACE_ID);
console.log(`ERC-2981 supported: ${supportsRoyalty}`);
const salePrice = ethers.utils.parseEther("1.0");
const [receiver, royaltyAmount] = await nft.royaltyInfo(0, salePrice);
console.log(`Receiver: ${receiver}`);
console.log(`Royalty: ${ethers.utils.formatEther(royaltyAmount)} ETH`);
Finally, list your NFT on a testnet marketplace like OpenSea’s test environment and verify that the royalty information displays correctly in the marketplace interface.
Advanced Patterns
Dynamic royalty percentages can adapt to collection behavior over time. For example, decreasing royalties after multiple resales to reward long-term holders:
mapping(uint256 => uint256) private _transferCount;
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
internal override
{
super._beforeTokenTransfer(from, to, tokenId);
if (from != address(0)) {
_transferCount[tokenId]++;
}
}
function royaltyInfo(uint256 tokenId, uint256 salePrice)
public view override
returns (address, uint256)
{
uint256 transfers = _transferCount[tokenId];
uint96 bps = transfers < 5 ? 500 : 250; // 5% initially, 2.5% after 5 transfers
(address receiver, ) = _defaultRoyaltyInfo;
return (receiver, (salePrice * bps) / 10000);
}
Time-based royalties could lower percentages as NFTs age, reflecting depreciation:
mapping(uint256 => uint256) private _mintTimestamp;
function mint(address to) public {
uint256 tokenId = _tokenIdCounter++;
_mintTimestamp[tokenId] = block.timestamp;
_safeMint(to, tokenId);
}
function royaltyInfo(uint256 tokenId, uint256 salePrice)
public view override
returns (address, uint256)
{
uint256 age = block.timestamp - _mintTimestamp[tokenId];
uint256 bps = age < 365 days ? 500 : 250; // Higher royalty in first year
// ... calculate and return
}
For multi-signature receivers, send royalties to a treasury contract that implements payment splitting logic. This keeps the ERC-2981 implementation simple while enabling complex distribution:
constructor() ERC721("MyNFT", "MNFT") {
address paymentSplitter = 0x...; // PaymentSplitter contract address
_setDefaultRoyalty(paymentSplitter, 500);
}
Royalty buyouts allow collectors to pay a lump sum to eliminate future royalty obligations. This requires careful design to prevent gaming:
mapping(uint256 => bool) private _royaltyBuyout;
function buyoutRoyalty(uint256 tokenId) external payable {
require(msg.value >= 10 ether, "Insufficient buyout payment");
require(ownerOf(tokenId) == msg.sender, "Not token owner");
_royaltyBuyout[tokenId] = true;
// Transfer buyout payment to creator
}
function royaltyInfo(uint256 tokenId, uint256 salePrice)
public view override
returns (address, uint256)
{
if (_royaltyBuyout[tokenId]) {
return (address(0), 0);
}
// Normal royalty calculation
}
Token-gated royalties could offer different rates based on whether the buyer holds a community membership NFT, incentivizing ecosystem participation.
Testing and Verification
Comprehensive testing ensures your ERC-2981 implementation behaves correctly across edge cases. Using Hardhat or Foundry, write unit tests that verify:
describe("ERC2981 Royalty", function() {
it("Should return correct royalty for 1 ETH sale", async function() {
const salePrice = ethers.utils.parseEther("1.0");
const [receiver, amount] = await nft.royaltyInfo(0, salePrice);
expect(amount).to.equal(ethers.utils.parseEther("0.05")); // 5%
});
it("Should support ERC2981 interface", async function() {
const supported = await nft.supportsInterface("0x2a55205a");
expect(supported).to.be.true;
});
it("Should handle zero sale price", async function() {
const [receiver, amount] = await nft.royaltyInfo(0, 0);
expect(amount).to.equal(0);
});
it("Should handle maximum uint256 sale price", async function() {
const maxPrice = ethers.constants.MaxUint256;
const [receiver, amount] = await nft.royaltyInfo(0, maxPrice);
// Should not revert, amount should be proportional
});
});
Test per-token royalty overrides if your contract supports them:
it("Should use token-specific royalty when set", async function() {
await nft.setTokenRoyalty(0, creator.address, 1000); // 10%
const [receiver, amount] = await nft.royaltyInfo(0, parseEther("1.0"));
expect(amount).to.equal(parseEther("0.10"));
});
Integration tests should simulate the complete marketplace workflow: querying royalty info, executing the sale, and verifying payment distribution. This ensures your contract works correctly within the broader ecosystem context.
Gas Optimization Considerations
Using default royalties rather than per-token storage significantly reduces gas costs. The _setDefaultRoyalty() pattern stores a single royalty configuration that applies to all tokens, while _setTokenRoyalty() consumes storage slots for each token.
OpenZeppelin stores the fee numerator as uint96 to enable struct packing with addresses, reducing storage costs. If you’re implementing custom royalty logic, consider similar optimizations:
struct RoyaltyInfo {
address receiver;
uint96 feeNumerator; // Packed in same slot as receiver
}
The royaltyInfo() function is a view function, so its gas cost only matters for off-chain queries and when called from other contracts. Avoid complex calculations—pre-calculate values where possible and store them during minting rather than computing them on every query.
For high-volume collections, consider deploying on Layer 2 networks like Polygon, Arbitrum, or Base. These networks offer dramatically lower transaction costs while maintaining ERC-2981 compatibility. Most major marketplaces support multiple chains, so L2 deployment doesn’t limit discoverability.
Common Misconceptions
Misconception: ERC-2981 automatically enforces royalty payments.
Reality: The standard provides information only. Marketplaces voluntarily choose whether to honor royalties. There’s no on-chain enforcement mechanism within the standard itself.
Misconception: Royalties are legally enforceable contracts.
Reality: ERC-2981 royalties are smart contract features, not legal guarantees. The voluntary nature means no built-in legal enforcement. Jurisdiction-specific creator rights laws (which vary significantly between the EU and US) may provide some legal recourse, but this operates entirely separately from the technical standard.
Misconception: All NFT sales will automatically pay royalties.
Reality: Peer-to-peer transfers and marketplaces that don’t integrate ERC-2981 won’t pay royalties. The standard requires marketplace cooperation to function.
Misconception: You can update royalties retroactively on existing tokens.
Reality: While OpenZeppelin’s implementation allows updating royalty settings via _setDefaultRoyalty(), whether to allow this is a design decision. Some collections make royalty info immutable to provide certainty to collectors. The standard itself doesn’t prescribe either approach.
Related Articles
Understanding ERC-2981 requires knowledge of broader NFT implementation fundamentals. For creators just starting out, our beginner’s guide to NFT minting covers the basics of creating and deploying NFT contracts.
When implementing ERC-2981, follow smart contract security best practices to avoid common vulnerabilities. Consider gas optimization techniques to minimize costs for your users. All blockchain development should incorporate comprehensive blockchain security considerations from the design phase forward.