In this tutorial we will create a Solidity ERC721 contract that supports secondary sales royalties. This contract will allow you to create NFT tokens, post them for sale on different exchanges and allow the contract owner to claim a creator royalty on each secondary sale. This contract supports secondary sales royalties on Open Sea, Rarible, and Mintable trading platforms.
Secondary sales or resales of digital collectibles “Non Fungible Tokens” are considered as part of the secondary market. The secondary market of NFT’s is much more active than the primary market in terms of the number of traded volume in USD.
This is an advanced ERC721 contract that supports secondary sales royalties and if it is your first time creating an NFT I would recommend reading “How to create an ERC721 Solidity smart contract“. Then come back to this tutorial to learn how to implement advanced features in a ERC721 contract.
Getting Started with a Solidity contract
At this time make sure you setup your development environment. You can use any Solidity IDE that you are familiar with. For this tutorial we will use Remix as it is simple, web based, and there is a lot of training material on the web to learn how to get started.
ERC721 contract dependencies
To create an ERC721 contract that supports secondary sales royalties it should be noted that the smart contract we will create requires several dependency files. These dependency files are critical for the NFT contract to support secondary sales royalties for the contract owner. The contract dependencies are as follows:
- Ownable methods needed for Open Sea
- Royalty methods for Rarible
- ERC2981 NFT Royalty methods for Mintable
We will add each of the dependency files to Remix and import them into our ERC721 contract. As an alternative you can save them in your Github repository and import them into your contract.
Configure ERC721 contract sales royalty dependencies in Remix
First create the following directories in Remix in the file explorer. It is important that you follow this folder and file structure in Remix with the same naming conventions. You can always change the structure and names at a later date.
Create the following directories:
- raribles/royalties/contracts
- raribles/royalties/contracts/impl
In these two folders we will put 5 of the dependency contracts that we need for this NFT contract.
Directory – raribles/royalties/contracts
Second, put the following dependency files in the raribles/royalties/contracts directory
- LibPart
- LibRoyaltiesV2
- RoyaltiesV2
LibPart – Needed for Rarible secondary sales royalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library LibPart {
bytes32 public constant TYPE_HASH = keccak256("Part(address account,uint96 value)");
struct Part {
address payable account;
uint96 value;
}
function hash(Part memory part) internal pure returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, part.account, part.value));
}
}
Try it in Remix
LibRoyaltiesV2 – Needed for Rarible secondary sales royalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library LibRoyaltiesV2 {
/*
* bytes4(keccak256('getRaribleV2Royalties(uint256)')) == 0xcad96cca
*/
bytes4 constant _INTERFACE_ID_ROYALTIES = 0xcad96cca;
}
Try it in Remix
RoyaltiesV2 – Needed for Rarible secondary sales royalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./LibPart.sol";
interface RoyaltiesV2 {
event RoyaltiesSet(uint256 tokenId, LibPart.Part[] royalties);
function getRaribleV2Royalties(uint256 id) external view returns (LibPart.Part[] memory);
}
Try it in Remix
Directory – raribles/royalties/contracts/impl
Third, put the following dependency files in the raribles/royalties/contracts/impl directory
- AbstractRoyalties
- RoyaltiesV2Impl
AbstractRoyalties – Needed for Rarible secondary sales royalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../LibPart.sol";
abstract contract AbstractRoyalties {
mapping (uint256 => LibPart.Part[]) internal royalties;
function _saveRoyalties(uint256 id, LibPart.Part[] memory _royalties) internal {
uint256 totalValue;
for (uint i = 0; i < _royalties.length; i++) {
require(_royalties[i].account != address(0x0), "Recipient should be present");
require(_royalties[i].value != 0, "Royalty value should be positive");
totalValue += _royalties[i].value;
royalties[id].push(_royalties[i]);
}
require(totalValue < 10000, "Royalty total value should be < 10000");
_onRoyaltiesSet(id, _royalties);
}
function _updateAccount(uint256 _id, address _from, address _to) internal {
uint length = royalties[_id].length;
for(uint i = 0; i < length; i++) {
if (royalties[_id][i].account == _from) {
royalties[_id][i].account = payable(address(uint160(_to)));
}
}
}
function _onRoyaltiesSet(uint256 id, LibPart.Part[] memory _royalties) virtual internal;
}
Try it in Remix
RoyaltiesV2Impl – Needed for Rarible secondary sales royalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./AbstractRoyalties.sol";
import "../RoyaltiesV2.sol";
contract RoyaltiesV2Impl is AbstractRoyalties, RoyaltiesV2 {
function getRaribleV2Royalties(uint256 id) override external view returns (LibPart.Part[] memory) {
return royalties[id];
}
function _onRoyaltiesSet(uint256 id, LibPart.Part[] memory _royalties) override internal {
emit RoyaltiesSet(id, _royalties);
}
}
Try it in Remix
Create an ERC721 smart contract
Now that all of our dependent files have been set up and created lets create the ERC721 Smart contract. In the root of the Remix file explorer create a new file for our ERC721 contract. It should be noted that if the folder and file structure illustrated above and below is not followed the dependent contracts with not import into your smart contract properly.
Now copy the contract below and paste into Remix. This contract contains the necessary import statements for Open Zeppelin and the dependent files listed above. The contract supports:
- minting new NFT’s
- revealing the NFT project
- pausing the ability to mint NFT’s
- secondary sales royalties on multiple platforms
- etc.
Furthermore, read the comments in the Solidity smart contract code to understand how the different functions work. You will get an understanding of the entire NFT process including how secondary trading royalties work.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
//Ownable is needed to setup sales royalties on Open Sea
//if you are the owner of the contract you can configure sales Royalties in the Open Sea website
import "@openzeppelin/contracts/access/Ownable.sol";
//the rarible dependency files are needed to setup sales royalties on Rarible
import "rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol";
import "rarible/royalties/contracts/LibPart.sol";
import "rarible/royalties/contracts/LibRoyaltiesV2.sol";
//give your contract a name
contract NAMEYOURCONTRACT is ERC721Enumerable, Ownable, RoyaltiesV2Impl {
using Strings for uint256;
//configuration
string baseURI;
string public baseExtension = ".json";
//set the cost to mint each NFT
uint256 public cost = 0.00 ether;
//set the max supply of NFT's
uint256 public maxSupply = 100;
//set the maximum number an address can mint at a time
uint256 public maxMintAmount = 1;
//is the contract paused from minting an NFT
bool public paused = false;
//are the NFT's revealed (viewable)? If true users can see the NFTs.
//if false everyone sees a reveal picture
bool public revealed = true;
//the uri of the not revealed picture
string public notRevealedUri;
bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
constructor(
string memory _name,
string memory _symbol,
string memory _initBaseURI,
string memory _initNotRevealedUri
) ERC721(_name, _symbol) {
setBaseURI(_initBaseURI);
setNotRevealedURI(_initNotRevealedUri);
}
//internal function for base uri
function _baseURI() internal view virtual override returns (string memory) {
return baseURI;
}
//function allows you to mint an NFT token
function mint(uint256 _mintAmount) public payable {
uint256 supply = totalSupply();
require(!paused);
require(_mintAmount > 0);
require(_mintAmount <= maxMintAmount);
require(supply + _mintAmount <= maxSupply);
if (msg.sender != owner()) {
require(msg.value >= cost * _mintAmount);
}
for (uint256 i = 1; i <= _mintAmount; i++) {
_safeMint(msg.sender, supply + i);
}
}
//function returns the owner
function walletOfOwner(address _owner)
public
view
returns (uint256[] memory)
{
uint256 ownerTokenCount = balanceOf(_owner);
uint256[] memory tokenIds = new uint256[](ownerTokenCount);
for (uint256 i; i < ownerTokenCount; i++) {
tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokenIds;
}
//input a NFT token ID and get the IPFS URI
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
if(revealed == false) {
return notRevealedUri;
}
string memory currentBaseURI = _baseURI();
return bytes(currentBaseURI).length > 0
? string(abi.encodePacked(currentBaseURI, tokenId.toString(), baseExtension))
: "";
}
//only owner
function reveal() public onlyOwner {
revealed = true;
}
//set the cost of an NFT
function setCost(uint256 _newCost) public onlyOwner {
cost = _newCost;
}
//set the max amount an address can mint
function setmaxMintAmount(uint256 _newmaxMintAmount) public onlyOwner {
maxMintAmount = _newmaxMintAmount;
}
//set the not revealed URI on IPFS
function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
notRevealedUri = _notRevealedURI;
}
//set the base URI on IPFS
function setBaseURI(string memory _newBaseURI) public onlyOwner {
baseURI = _newBaseURI;
}
function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
baseExtension = _newBaseExtension;
}
//pause the contract and do not allow any more minting
function pause(bool _state) public onlyOwner {
paused = _state;
}
function withdraw() public payable onlyOwner {
(bool success, ) = payable(msg.sender).call{value: address(this).balance}("");
require(success);
}
//configure royalties for Rariable
function setRoyalties(uint _tokenId, address payable _royaltiesRecipientAddress, uint96 _percentageBasisPoints) public onlyOwner {
LibPart.Part[] memory _royalties = new LibPart.Part[](1);
_royalties[0].value = _percentageBasisPoints;
_royalties[0].account = _royaltiesRecipientAddress;
_saveRoyalties(_tokenId, _royalties);
}
//configure royalties for Mintable using the ERC2981 standard
function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view returns (address receiver, uint256 royaltyAmount) {
//use the same royalties that were saved for Rariable
LibPart.Part[] memory _royalties = royalties[_tokenId];
if(_royalties.length > 0) {
return (_royalties[0].account, (_salePrice * _royalties[0].value) / 10000);
}
return (address(0), 0);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Enumerable) returns (bool) {
if(interfaceId == LibRoyaltiesV2._INTERFACE_ID_ROYALTIES) {
return true;
}
if(interfaceId == _INTERFACE_ID_ERC2981) {
return true;
}
return super.supportsInterface(interfaceId);
}
}
Try it in Remix
Finally modify the contract above to meet your NFT needs. You can use the tutorial How to create and deploy an NFT collection + the contract above to construct a great NFT project.
Set the contracts constructor variables with your projects IPFS information. Make sure you put a “/” at the end of the base URI. At the end of the not revealed URI include the file name.
After you create an ERC721 contract make sure you deploy it to the Goerli environment and test with Open Sea, Rarible and Mintable. Make sure you test minting tokens, change parameters in the contract and set sales royalties.
Setup secondary sales royalties for your NFT collection
Open Sea sales royalties
The Open Sea test environment at https://testnets.opensea.io/ allows contract owners to connect their wallets and set sales royalty percentages.
First visit the Open Sea website and navigate to collections and select create a collection.
Then scroll down to the royalties section and input a percentage you would like to configure for sales royalties.
Rarible and Mintable sales royalties in your ERC721 contract
Finally, to set secondary sales royalties for Rarible and Mintable use the set royalties function in your ERC721 contract. Royalty percentages are in basis points so a 10% sales royalty should be entered as 1000.
Last but not least don’t forget to review your code, audit your contract and write unit test cases. In addition it is important to do your own research, auditing, and testing before deploying a smart contract to the blockchain. This ERC721 contract that supports secondary sales royalties and other contracts on this site are for educational purposes only.
Resources
Blockchain Networks
Below is a list of EVM compatible Mainnet and Testnet blockchain networks. Each link contains network configuration, links to multiple faucets for test ETH and tokens, bridge details, and technical resources for each blockchain. Basically everything you need to test and deploy smart contracts or decentralized applications on each chain. For a list of popular Ethereum forums and chat applications click here.
Ethereum test network configuration and test ETH faucet information | |
Optimistic Ethereum Mainnet and Testnet configuration, bridge details, etc. | |
Polygon network Mainnet and Testnet configuration, faucets for test MATIC tokens, bridge details, etc. | |
Binance Smart Chain Mainnet and Testnet configuration, faucets for test BNB tokens, bridge details, etc. | |
Fanton networt Mainnet and Testnet configuration, faucets for test FTM tokens, bridge details, etc. | |
Kucoin Chain Mainnet and Testnet configuration, faucets for test KCS tokens, bridge details, etc. |
Web3 Software Libraries
You can use the following libraries to interact with an EVM compatible blockchain.
- Python: Web3.py Python library for interacting with Ethereum. Web3.py examples
- Js: web3.js Ethereum JavaScript API
- Java: web3j Web3 Java Ethereum Ðapp API
- PHP: web3.php A php interface for interacting with the Ethereum blockchain and ecosystem.
Nodes
Learn how to run a Geth node. Read getting started with Geth to run an Ethereum node.
Fix a transaction
How to fix a pending transaction stuck on Ethereum or EVM compatible chain