With the finalization of the ERC721 standard, non-fungible tokens (NFTs) started receiving a large amount of attention. These provably unique assets are stored on the blockchain and introduce a new way to collect and trade art, music, profile pictures (PFPs), and more. In the summer of 2021, creating and selling NFTs became a quick way to accumulate wealth because of the boom in popularity.
However, when reading the underlying specification, you'll notice no functionality for acquiring and splitting royalties in either the ERC721 or ERC1155 standards. These standards only deal with ownership state tracking, approvals, and direct transfers.
If the interface doesn’t contain any native royalty features, as a creator, how can you make NFTs that enable wealth accumulation long after the initial sale? And how do the trading platforms, such as OpenSea, take their cut? And what if you have a more complicated royalty situation, such as splitting royalties?
In this article, we will explore several aspects of royalties with NFTs. We’ll look at ways to implement royalties, including proprietary solutions, registries, and EIP-2981. We’ll also look at one way to split payments. Finally, we will run through a project and see royalty splitting in action.
Let’s get started!
What Are NFT Royalties?
Just months after the first NFTs saw the light of day, marketplace contracts were built that allowed holders to put price tags on their items, bid on and ask for them, and trade them safely with others. Many of these marketplaces don’t even store their users’ bidding transactions on-chain; their match-making contracts collect off-chain signatures for trades and store them on a centralized server infrastructure. The idea of owning something unique on a blockchain has made OpenSea one of the most successful marketplaces in the world, constantly being number one on the gas guzzler leaderboard.
The true success story of those progressively decentralizing marketplaces is written from the fees they take per trade. For example, for each Bored Ape traded at 100 Eth, OpenSea steadily earns a whopping 2.5 Eth for fulfilling the deal on-chain. An incentive to retain creators on their marketplace platforms was to provide them with an option to profit long-term from secondary market sales. That is commonly referred to as “royalties” in the NFT space.
Earning Money for NFT Creators: Minting Fees and Royalties
When someone launches a basic NFT collection by deploying an ERC721 contract, they’ll first have to think about minting, or how do new tokens come into life?
It might surprise you that ERC721 does not define any default minting rules itself. Even its official specification mentions the term “mint” only once. However, it became common sense that nearly all collectible NFT contracts contain a “mint” method that’s invoked by sending fees. Newly minted tokens are instantly tradeable on marketplace platforms, and depending on their social market mechanics, freshly minted assets might sell for a multitude of their minting fee within hours.
Minting fees can be profitable to the beneficiary account of an NFT contract. However, the major revenue driver of popular collections is royalties, also known as fees that marketplaces split off the sales price when items are traded on their platform. Since ERC721 doesn’t pertain to economic concepts and even less about NFT trading, how does an NFT collection enforce a royalty cut for secondary sales? The simple answer? It cannot.
It’s important to understand that royalties are not a concept that’s enforceable by a collection itself. There have been attempts to build collection contracts that come with their own marketplace logic and prohibit transfers outside of their controlled environment, but they never gained much attention since markets are made on the dominating platforms.
For those, royalty payments are a voluntary concept that each marketplace implements individually. So when NFT contracts are just dealing as registries and marketplaces manage royalty payments individually, how can a collection owner define their royalty scheme so that every marketplace supports it?
Proprietary Solutions
The simplest way for a marketplace to figure out how many royalty fees to collect and where to transfer them is by relying on its own proprietary interface that’s implemented by the collection. A good example is the Rarible exchange contract that tries to support all kinds of external royalty interfaces, among them two that Rarible defined themselves, being an early player in the NFT space:
interface RoyaltiesV1 {
event SecondarySaleFees(uint256 tokenId, address[] recipients, uint[] bps);
function getFeeRecipients(uint256 id) external view returns (address payable[] memory);
function getFeeBps(uint256 id) external view returns (uint[] memory);
}
interface RoyaltiesV2 {
event RoyaltiesSet(uint256 tokenId, LibPart.Part[] royalties);
function getRaribleV2Royalties(uint256 id) external view returns (LibPart.Part[] memory);
}
NFT collections could implement those interfaces to return the amount of royalties and an array of receivers. Then, when a trade on the marketplace contract takes place, the trading contract checks whether the involved NFT collection implemented one of those interfaces, calls it, and uses its return values to split fees accordingly.
Note that the actual sales price is not part of the methods’ interfaces. Instead, they’re yielding the royalty share as basis points (bps), a term commonly used in royalty distribution schemes and usually translates to 1/10000—a share of 500 means that 5% of the trade value should be sent to the collection owner as royalties.
Royalty Registries
However, proprietary interfaces can cause issues. The NFT contract authors cannot know which interfaces might become mandatory to implement since they cannot predict which marketplaces their tokens will be traded on. Even worse, if they launch a collection contract before publishing the relevant marketplace contracts, there’s usually no easy way for them to later add the respective royalty distribution scheme.
To solve this issue, a consortium of major NFT marketplaces around manifold.xyz agreed to deploy an industry-wide registry contract that collection builders can use to signal royalty splits independently from their token contracts. The Royalty Registry’s open source code base reveals that it supports many of the most important marketplace interfaces.
For example, if an NFT collection owner only implemented one of Rarible’s royalty distribution schemes mentioned above, another marketplace that’s not aware of that interface can simply call the common registry’s getRoyaltyView
function. It tries to query all known royalty interfaces on the token contract and translates any response to a commonly useable result.
The registry even goes a step further. Collection owners who haven’t put any royalty signaling scheme into their contract can deploy an extended “override” contract and register it with the common registry. This registration method will ensure that only collection owners (identified by the owner
public member) can call it.
EIP-2981: A Standard for Signaling NFT Royalties Across Marketplaces
In 2020, some ambitious individuals started to define a common interface that’s flexible enough to cover most royalty-related use cases and that’s simple to understand and implement: EIP-2981. It defines only one method that NFT contracts can implement:
function royaltyInfo(uint256 _tokenId, uint256 _salePrice)
external view
returns (address receiver, uint256 royaltyAmount);
Note its intentional lack of features: It neither cares about a split between several parties, nor does it impose any notion of percentages or base points. It’s crystal clear to callers what they’ll receive as a return value and straightforward for implementers how to achieve that.
The interface also completely works off-chain, so marketplaces that trade assets on alternative infrastructure can still query the creator fee without knowing anything else besides the interface signature of the EIP-2981 method.
The interface works for sale amounts denoted in Eth, as well as any other currency. An implementer only has to divide _salePrice
by their calculation base and multiply it with the royalty percentage on the same base. While implementers could run complex logic that computes a dynamic royalty depending on external factors, it’s advisable to keep this method’s execution as small as possible, since it will be executed during the sales transfer transactions between trading parties, and their gas fees are supposed to be rather low.
To give you an idea of what a non-trivial EIP-2981 implementation could look like, here’s a snippet you could find on 1/1 NFT collections that signals the original creator’s address and their royalty claim to any marketplace compatible with the standard:
https://gist.github.com/elmariachi111/4df402dcefa4a86c78545e5e0a44bc6b
(unpacked below)
// contracts/Splice.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
contract OneOnOneNFTMarketPlace {
using SafeMath for uint256;
struct RoyaltyReceiver {
address creator;
uint8 royaltyPercent;
}
mapping(uint256 => RoyaltyReceiver) royalties;
function mint(
/*...mint args...*/
uint8 _royaltyPercent
) public {
//... minting logic ...
uint256 token_id = 1;
royalties[token_id] = RoyaltyReceiver({
creator: msg.sender,
royaltyPercent: _royaltyPercent
});
}
function royaltyInfo(uint256 tokenId, uint256 salePrice)
public
view
returns (address receiver, uint256 royaltyAmount)
{
receiver = royalties[tokenId].creator;
royaltyAmount = (royalties[tokenId].royaltyPercent * salePrice).div(100);
}
}
If you’re using OpenZeppelin’s ERC721 base contracts to build NFT contracts, you might already have noticed that they recently added an ERC721Royalty
base contract that contains management methods and private members to simplify handling dedicated token royalties.
Royalties for ERC1155 Prints
Marketplaces aren’t the only applications that let their users profit from royalty schemes. For example, Treum’s EulerBeats uses the multi-token standard ERC1155 in their collection of contracts, which represent NFTs that combine computer-generated tunes and generative artworks. After minting a seed token, users can derive a limited amount of prints from it, and the price for each print increases along a bonding curve defined by the token contract.
Every time a new print of an Enigma seed is minted, the contract transfers a 50% royalty cut of the minting fee to the current seed’s owner. If the receiving side implements the platform-specific IEulerBeatsRoyaltyReceiver
interface, it can even react to royalty payouts and execute code once a print of their seed has been minted.
PaymentSplitters: Sending NFT Royalties To More Than One Receiver.
EIP-2981 falls short for a use case that other approaches solve out of the box. It can only signal one recipient address for royalties to the requesting side. As a result, situations that require royalties to be split among several recipients must be implemented individually.
This can impose several new issues: First, the caller/marketplace doesn’t necessarily have to send funds along with the same transaction that triggered the trade but could decide to do so later, such as in a gas efficient multi-call from another account. Second, payout calls to addresses might be highly restricted in gas usage. Any default receiver function in Solidity is highly encouraged to use as little gas as possible since senders might not be aware that they’re transferring funds to a contract.
The most important consideration is that sending money directly from contract interactions imposes the risk of running into reentrancy holes; that’s why it’s highly advisable to favor pull mechanics that allow beneficiaries to withdraw their earnings from time to time instead of pushing funds directly to addresses unknown to the calling contract.
Luckily, OpenZeppelin’s base contracts cover us again. Their PaymentSplitter primitive allows setting up individual split contracts that keep funds safe until their payees claim them, and their receive function requires the bare minimum of gas to run. NFT collection builders can create an inline PaymentSplitter containing the wanted list of beneficiaries and their respective share amounts and let their EIP-2981 implementation yield the address of that split contract.
The tradeoffs of that approach might be neglectable for many use cases: PaymentSplitter deployments are comparatively gas-intensive and it’s impossible to replace payees or shares once a splitter has been initialized. A sample implementation of how to effectively replace splitter participants and instantiate gas-efficient subcontracts can be found in the generative art project Splice.
Testing NFT Royalty Payouts With a Local Mainnet Fork
Engineering marketplaces that interact with an arbitrary NFT contract is not a simple task since it’s unpredictable whether contracts on live networks behave according to the ERC interfaces. However, it can be helpful to test our code against these contracts using Ganache. This powerful tool lets us create an instant fork of the Ethereum network on our local machine without setting up our own blockchain node. Instead, it relies on Infura nodes to read the current state of contracts and accounts we’re interacting with.
Before we start our blockchain instance, let’s clone the repository of our proof-of-concept, change into the new directory, and install any dependencies:
git clone https://github.com/elmariachi111/royalty-marketplace.git
cd royalty-marketplace
npm i
To see what’s going on in this NFT marketplace example, let’s take a look at the ClosedDesert.sol code in the contracts folder.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@manifoldxyz/royalty-registry-solidity/contracts/IRoyaltyEngineV1.sol";
struct Offer {
IERC721 collection;
uint256 token_id;
uint256 priceInWei;
}
/**
* DO NOT USE IN PRODUCTION!
* a fixed reserve price marketplace
*/
contract ClosedDesert is ReentrancyGuard {
mapping(bytes32 => Offer) public offers;
// https://royaltyregistry.xyz/lookup
IRoyaltyEngineV1 royaltyEngineMainnet = IRoyaltyEngineV1(0x0385603ab55642cb4Dd5De3aE9e306809991804f);
event OnSale(bytes32 offerHash, address indexed collection, uint256 token_id, address indexed owner);
event Bought(address indexed collection, uint256 token_id, address buyer, uint256 price);
function sellNFT(IERC721 collection, uint256 token_id, uint256 priceInWei) public {
require(collection.ownerOf(token_id) == msg.sender, "must own the NFT");
require(collection.getApproved(token_id) == address(this), "must approve the marketplace to sell");
bytes32 offerHash = keccak256(abi.encodePacked(collection, token_id));
offers[offerHash] = Offer({
collection: collection,
token_id: token_id,
priceInWei: priceInWei
});
emit OnSale(offerHash, address(collection), token_id, msg.sender);
}
function buyNft(bytes32 offerHash) public payable nonReentrant {
Offer memory offer = offers[offerHash];
require(address(offer.collection) != address(0x0), "no such offer");
require(msg.value >= offer.priceInWei, "reserve price not met");
address payable owner = payable(offer.collection.ownerOf(offer.token_id));
emit Bought(address(offer.collection), offer.token_id, msg.sender, offer.priceInWei);
// effect: clear offer
delete offers[offerHash];
(address payable[] memory recipients, uint256[] memory amounts) =
royaltyEngineMainnet.getRoyalty(address(offer.collection), offer.token_id, msg.value);
uint256 payoutToSeller = offer.priceInWei;
//transfer royalties
for(uint i = 0; i < recipients.length; i++) {
payoutToSeller = payoutToSeller - amounts[i];
Address.sendValue(recipients[i], amounts[i]);
}
//transfer remaining sales revenue to seller
Address.sendValue(owner, payoutToSeller);
//finally transfer asset
offer.collection.safeTransferFrom(owner, msg.sender, offer.token_id);
}
}
}
In our example, sellers can list their assets for a fixed sales price after being approved for transfers. Buyers can watch for OnSale
events and respond by issuing buyNft
transactions and sending along the wanted Eth value. The marketplace contract checks the open mainnet NFT royalties registry during a sale transaction to see whether the collection owners are requesting royalties and then pays them out accordingly. As stated above, the public royalty registry already takes EIP-2981 compatible contracts into account. Still, it supports many other proprietary distribution schemes as well.
Next we will deploy our local blockchain instance and test our contract using the accounts and NFTs of real users.
To test the contract behavior under mainnet conditions, we first need access to an Infura mainnet node by requesting a project id and installing Ganache v7 locally on our machine. We can then use our favorite NFT marketplace to look up a collection and find an NFT holder account that will play the seller role in our test. The seller must actually own the NFT we will sell.
Finally, find an account with sufficient mainnet funds (at least 1 Eth) to pay for the seller’s requested sales price. With these accounts and tools at hand, we can spin up a local Ganache mainnet instance using the following command in a new terminal window:
npx ganache --fork https://mainnet.infura.io/v3/<infuraid> --unlock <0xseller-account> --unlock <0xbuyer-account>
Be sure to use your own Infura mainnet endpoint for the URL in the command above.
If you are having trouble finding accounts to unlock, here are a couple to try:
Seller Address:0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2
Buyer Address:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Note: Because we are simulating the Ethereum mainnet in our Ganache instance, by the time you read this, the seller may no longer own the NFT we will be selling or the buyer may no longer have enough Eth to actually make the purchase. So if these addresses don’t work, you will have to find ones that meet the above criteria.
Using the example addresses above, our command looks like this:
npx ganache --fork https://mainnet.infura.io/v3/<infuraid> --unlock 0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2 --unlock 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Next, in our original terminal window, we’ll compile and deploy the marketplace contract from the repository and choose our local mainnet fork provider, which can be found in the truffle-config.js:
npx truffle compile
npx truffle migrate --network mainfork
Now we can test our royalty-aware marketplace contract under mainnet conditions without paying a penny for gas costs. All upcoming transactions will be executed by the local Ganache chain on behalf of the accounts of real users.
Let’s take a look at the testMarketplace.js script (found in the scripts folder) we will use to interact with our deployed marketplace smart contract:
const ClosedDesert = artifacts.require("ClosedDesert");
const IErc721 = require("../build/contracts/IERC721.json");
//Change these constants:
const collectionAddress = "0xed5af388653567af2f388e6224dc7c4b3241c544"; // Azuki
const tokenId = 9183;
let sellerAddress = "0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2";
const buyerAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
module.exports = async function(callback) {
try {
const marketplace = await ClosedDesert.deployed();
const erc721 = new web3.eth.Contract(IErc721.abi, collectionAddress);
const salesPrice = web3.utils.toWei("1", "ether");
//buyerAddress = await web3.utils.toChecksumAddress(buyerAddress);
// marketplace needs the seller's approval to transfer their tokens
const approval = await erc721.methods.approve(marketplace.address, tokenId).send({from: sellerAddress});
const sellReceipt = await marketplace.sellNFT(collectionAddress, tokenId, salesPrice, {
from: sellerAddress
});
const { offerHash } = sellReceipt.logs[0].args;
const oldOwner = await erc721.methods.ownerOf(tokenId).call();
console.log(`owner of ${collectionAddress} #${tokenId}`, oldOwner);
const oldSellerBalance = web3.utils.toBN(await web3.eth.getBalance(sellerAddress));
console.log("Seller Balance (Eth):", web3.utils.fromWei(oldSellerBalance));
// buyer buys the item for a sales price of 1 Eth
const buyReceipt = await marketplace.buyNft(offerHash, {from: buyerAddress, value: salesPrice});
const newOwner = await erc721.methods.ownerOf(tokenId).call();
console.log(`owner of ${collectionAddress} #${tokenId}`, newOwner);
const newSellerBalance = web3.utils.toBN(await web3.eth.getBalance(sellerAddress));
console.log("Seller Balance (Eth):", web3.utils.fromWei(newSellerBalance));
console.log("Seller Balance Diff (Eth):", web3.utils.fromWei(newSellerBalance.sub(oldSellerBalance)));
} catch(e) {
console.error(e)
} finally {
callback();
}
}
Note: ThecollectionAddress
, sellerAddress
, and buyerAddress
constants must all be legitimate mainnet addresses that meet the before-mentioned criteria, while the sellerAddress
and buyerAddress
must both be unlocked in your Ganache instance. The tokenId
constant must also be the actual tokenId
of the NFT the seller owns.
In this helper script, we’re setting up references to the contracts we will interact with. We decided to get the EIP-2981 compatible Azuki collection in the sample code, but it could be any NFT collection. We run the script using the following command:
npx truffle exec scripts/testMarketplace.js --network mainfork
If everything ran correctly, you should receive an output in your console like the following:
owner of Azuki 0xed5af388653567af2f388e6224dc7c4b3241c544 #9183 0x27b4582D577d024175ed7FFB7008cC2B1ba7e1C2
Seller Balance (Eth): 0.111864414925655418
owner of Azuki 0xed5af388653567af2f388e6224dc7c4b3241c544 #9183 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Seller Balance (Eth): 1.061864414925655418
Seller Balance Diff (Eth): 0.95
Let’s run through the steps that just happened so we can understand how it works. First, the script calls for the seller’s approval to transfer their NFT once it’s sold, a step usually handled by the respective marketplace contracts. Then, we create a sales offer by calling sellNft
on behalf of the current owner. Finally, we simply reuse the offer hash contained in the sale event and let our buyer call the buyNft
method and send the requested sales price of 1 Eth.
When you compare the seller’s balance before and after the trade, you’ll notice that they didn’t receive the requested amount of 1 Eth, but only 0.95. The remaining funds have been transferred to Azuki’s royalty recipients as they were signaled by the mainnet royalty registry contract.
Conclusion
Royalties are the primary driver of success in the NFT space. Previously being an add-on feature of proprietary marketplaces, they have evolved into a mandatory property of the non-fungible token economy. They extend the promise to any NFT collection builder to take profits when their creations start to attract a broad audience. They are a great economic concept to distribute sales revenue in a way that provides incentives to the original code authors or NFT artists.
ERC721 doesn’t contain any notion of economic features; hence NFT royalties cannot be directly enforced by the token contracts. Instead, marketplace builders had to provide interfaces for token contracts to signal their claim on trading fees and where to send them. The EIP-2981 royalty signaling interface is a concise and powerful industry standard to achieve that without adding more complexity to the implementer’s side. Every new ERC721 contract should consider implementing at least a basic royalty signal so proprietary marketplace tools can pick it up and refer to it.
Top comments (1)
This looks like a great article! 🙌
In fact, the topic of your post would also work really well in the Meta Punk Community too!
Metapunk Web3 Community 🦙
We’re a community where blockchain builders and makers, web3 devs, and nft creators can connect, learn and share 🦙
Meta Punk is a really cool international NFT and web3 community where artists, web3 developers, and traders can connect, learn, and share exciting discoveries and ideas. 🦙
Would you consider posting this article there too? Because Meta Punk is built on the same platform as DEV (Forem) you can fairly easily copy the Markdown and post it there as well.
Really hope that you'll share this awesome post with the community there and consider browsing the other Forem communities out there!