ERC721 contract that supports sales royalties

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:

  1. Ownable methods needed for Open Sea
  2. Royalty methods for Rarible
  3. 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:

  1. raribles/royalties/contracts
  2. raribles/royalties/contracts/impl

In these two folders we will put 5 of the dependency contracts that we need for this NFT contract.

Remix directory structure for NFT ERC721 contract that supports secondary sales royalties

Directory – raribles/royalties/contracts

Second, put the following dependency files in the raribles/royalties/contracts directory

  1. LibPart
  2. LibRoyaltiesV2
  3. RoyaltiesV2
Remix directory structure for NFT ERC721 contract that supports secondary sales royalties file explorer in remix

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

  1. AbstractRoyalties
  2. RoyaltiesV2Impl
Remix directory structure for NFT ERC721 contract that supports secondary sales royalties file explorer in remix. Dependencies

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.

Remix directory structure for NFT ERC721 contract that supports secondary sales royalties file explorer in remix

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:

  1. minting new NFT’s
  2. revealing the NFT project
  3. pausing the ability to mint NFT’s
  4. secondary sales royalties on multiple platforms
  5. 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.

remix constructor data for NFT contract

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.

Open Sea collection royalties for secondary NFT sales

Then scroll down to the royalties section and input a percentage you would like to configure for sales royalties.

Open Sea collection royalties for secondary sales

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.

set secondary sales royalties for rarible and mintable for NFTs

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.

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

Next Review – Mint a Scary Ghost NFT on Polygon