How to implement EIP-2981 Royalties in your NFT smart contract
Plus marketplaces that already implement the standard
Intro to EIP-2981. The Royalties Standard.
The EIP-2981 aims to standardize the way that marketplaces and other intermediaries pay royalties to the original creator of a given non-fungible token.
Soon after the NFT explosion in popularity (Thank you, Beeple?) it became evident that blockchain technology could enable creators and artists to receive a share of the sale price of their creative production every time it changed hands. This kind of royalty payment required ownership and exchange tracking that was basically impossible in a market as opaque as the art market or as free as the Internet.
The problem was, that each marketplace implemented this royalty payment differently and they were not compatible across them. This made it impossible again to track the royalty amount to be paid and the receiver of those funds. The implementation of this standard solves this problem and allows for the "ongoing funding of artists and other NFT creators"(EIP-2981).
Key characteristics
Royalties are just a piece of information, that live the smart contract and say "Hey, this is the creator of this collection or token, and he would like to be paid x% of the price paid by his artwork)
- It is non-enforceable by design. It is recognized that not all NFT transfers are part of a sale, so it doesn't make sense to create a standard that would pay creators for every time an NFT changes wallet. Marketplaces as an industry are expected to implement it.
- It is agnostic to the unit of exchange. Royalties are calculated as a percentage of the sale price, independently of the token used for payment. Therefore royalties can be paid in ETH or any ERC-20.
I was about to call this section the "NFT Royalties Standard" but then I realized that it would be inaccurate. Although originally designed with non-fungible tokens in mind, the standard is intentionally designed to be as minimal as possible. It is compatible with ERC1155 and ERC721 but it isn't exclusive to them and it could be used to set royalties payments for any asset class.
Implementing EIP-2981
Before diving into the full implementation, we need to be aware of some limitations of the standard and different ways of assigning the royalties.
Currently, the standard only allows for ONE address as the payee of royalties. If any given NFT, collection, etc... has a number of creators or royalties that want to be shared among different team members, then the best course of action is to set a split payment contract as the recipient of the royalties and then periodically send each participant's share of the balance to their wallets. For quick implementation, check out OpenZeppelin´s Payment Splitter contract.
In this article, we will set a single royalties recipient for all the tokens in an NFT contract (contract-wide royalties). However, the standard allows for more granularity so you could set a different royalty recipient and amount for each Token ID in a given NFT collection. I will show you how in the bonus section.
Which marketplaces currently support EIP-2981?
So far, I have not easily found a list of marketplaces that implement this standard. So, I went to the docs of the major ones and checked for myself so you don't have to. I am only including the ones where I could find explicitly in their docs that they support EIP-2981.
- Mintable. One of the original actors involved in EIP-2981. See here
- Rarible. They use 3 different royalties system, including EIP-2981. See here
- LooksRare. Also uses a double system, with EIP-2981 taking precedence. See here
I am sure I am missing some others. If so, please let me know!
Opensea does NOT support EIP-2981. You can set up contract wide royalties through their UI or a parameter in contractURI
see here.
Lets code!
The EIP-2981 implements a single function:
royaltyInfo(uint256 _tokenID, uint256 _salePrice) external view returns(address receiver, uint256 royaltyAmount)
Before implementing the body of the function, we will need some auxiliary structures to store this information and will add a utility function setRoyalties
so we are able to modify the recipient of our royalties proceeds and the amount to be paid.
struct RoyaltyInfo {
address recipient;
uint256 amount;
}
RoyaltyInfo private _royalties;
Using the information in the Royalties struct, now we can implement the body of royaltyInfo()
. Note that we are calculating the percentage to be paid as a fraction of 10,000 in order to increase the accuracy of the division.
function royaltyInfo(uint256 tokenId, uint256 value) external view returns (address receiver, uint256 royaltyAmount)
{
RoyaltyInfo memory royalties = _royalties;
receiver = royalties.recipient;
royaltyAmount = (value * royalties.amount) / 10000;
}
How do you set those royalties? You could set the info in _royalties
inside the constructor, but it's a common pattern to just create a function that updates that information.
function setRoyalties(address recipient, uint256 value) public onlyOwner {
require(value <= 10000, 'ERC2981Royalties: Too high');
_royalties = RoyaltyInfo(recipient, value);
}
Also, if you want to set up the royalties upon contract creation, then you can still call this function inside the constructor()
function.
Lastly, we need to inform other contracts (marketplaces) about the fact that ours is implementing EIP-2981:
//Interface for royalties
bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool){
return interfaceId == _INTERFACE_ID_ERC2981 || super.supportsInterface(interfaceId);
}
That was a bit messy and not so well structured. See the whole NFT contract below with proper formatting.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract CoolPinapple is ERC721, Ownable {
using Counters for Counters.Counter;
struct RoyaltyInfo {
address recipient;
uint256 amount;
}
RoyaltyInfo private _royalties;
//Interface for royalties
bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 10001;
uint256 public constant PRICE = 1 ether;
string public baseTokenURI;
constructor(string memory baseURI) ERC721("NFTContract", "NFT") {
baseTokenURI = baseURI;
setRoyalties(owner(), 500); //5%
}
function _baseURI() internal view override returns (string memory) {
return baseTokenURI;
}
function mintNFTs(uint256 _number) public payable {
uint256 totalMinted = _tokenIds.current();
require(totalMinted + _number < MAX_SUPPLY, "Not enough NFTs!");
require(msg.value == PRICE * _number , "Not enough/too much ether sent");
mintsPerAddress[msg.sender] += _number;
for (uint i = 0; i < _number; ++i) {
_mintSingleNFT();
}
}
function _mintSingleNFT() internal {
uint newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
_tokenIds.increment();
}
function getCurrentId() public view returns (uint256) {
return _tokenIds.current();
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool){
return interfaceId == _INTERFACE_ID_ERC2981 || super.supportsInterface(interfaceId);
}
function setRoyalties(address recipient, uint256 percentagePoints) public onlyOwner {
require(value < 10001, 'ERC2981Royalties: Too high');
_royalties = RoyaltyInfo(recipient, percentagePoints);
}
function royaltyInfo(uint256 tokenId, uint256 value) external view returns (address receiver, uint256 royaltyAmount)
{
RoyaltyInfo memory royalties = _royalties;
receiver = royalties.recipient;
royaltyAmount = (value * royalties.amount) / 10000;
}
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Transfer failed.");
}
receive() external payable {
revert();
}
}
Bonus: Implementing token-level royalties.
To implement token-level royalties (minter becomes the receiver of the royalties, for example) you just need to add a mapping, modify royaltyInfo()
as well as setRoyalties()
and then call setRoyalties
in your mint()
function.
mapping(uint256=>RoyaltyInfo) royaltyPerTokenId; //Change here
function royaltyInfo(uint256 tokenId, uint256 value) external view returns (address receiver, uint256 royaltyAmount)
{
RoyaltyInfo memory royalties = royaltyPerTokenId[tokenId]; //Change here
receiver = royalties.recipient;
royaltyAmount = (value * royalties.amount) / 10000;
}
function setRoyalties(address recipient, uint256 value, uint256 tokenId) public onlyOwner {
require(value <= 10000, 'ERC2981Royalties: Too high');
royaltyPerTokenId[tokenId] = RoyaltyInfo(recipient, value); //Change here
}
function _mintSingleNFT() internal {
uint newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
setRoyalties(msg.sender, 500, newTokenID); //Change here
_tokenIds.increment();
}
You can see the full contract as CoolPinapleTokenIdRoyalties.sol
in the Github Repo.
Bonus 2. Let's make it cleaner
If separation of concerns and making your project more orderly is important for you, you could implement the EIP-2981 interface and a base contract with the core Royalty logic in separate contracts, and then make your core NFT contract inherit from them.
If you are interested in that kind of file structure, check out this other Github repo.
Reference
- Zach Burks, James Morgan, Blaine Malone, James Seibel, "EIP-2981: NFT Royalty Standard," Ethereum Improvement Proposals, no. 2981, September 2020. [Online serial]. Available: eips.ethereum.org/EIPS/eip-2981.
- medium.com/quick-programming/erc2981-nft-ro..
- github.com/dievardump/EIP2981-implementation
- ethereum-blockchain-developer.com/121-erc72..
- mintable.medium.com/grant-from-ethereum-fou..
- medium.com/knownorigin/eip-2981-simple-and-..