NFT: Royalty

NFT: Royalty

Wednesday Feb 21, 2024

ERC721 with Royalty Fee

In this course, we will be building on our ERC721 token by adding a royalty fee to it. This will allow us (the creator of the token) to receive a percentage of the sale every time the token is sold.

What is a royalty fee?

A royalty fee is a fee that is paid to the original owner of a token every time it is sold. This is a common feature in the NFT space, and it allows the creator to receive a percentage of the sale every time the token is sold.

What is the motivation behind a royalty fee?

There are a few reasons why the royalty fee is appealing:

  1. It provides a continuous revenue stream to creators
  2. It's an incentive for creators to create high-quality content that will have trading value
  3. Royalty fees also constantly accredit the creator of the token, which is so often overlooked in the digital space

Does this extension alter the ERC721 standard?

The royalty fee proposal is actually separate from the ERC721 standard. It is a separate standard called ERC2981. This standard only specifies a way to signal royalty information and does not enforce its payment. Therefore it does not alter the ERC721 standard, but rather tell us how to implement it if we wanted to.

Setting up our project

Before we start, we need to create a few files.

In the contracts folder:

  1. Create a new file called ERC721Royalty.sol.

In the test folder:

  1. Create a new file called ERC721Royalty.ts.

Writing our smart contract

In the ERC721Royalty.sol file, add the following code and read the comments:

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.18;
3
4import "@openzeppelin/contracts/access/Ownable.sol";
5import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";
6
7contract ERC721RoyaltyToken is ERC721Royalty, Ownable {
8    using Strings for uint256;
9
10    string private _baseTokenURI;
11
12    /**
13     * The token contstructor
14     * @param name Our token name
15     * @param symbol Our token symbol
16     * @param baseURI The base token URI
17     * @param royaltyReceiver_ The address of the royalty receiver
18     * @param royaltyFee_ The royalty fee
19     * @dev The token URI is appended with the token ID to form the full token URI
20     * For example:
21     * - tokenURI = https://mytoken.com/api/
22     * - token ID = 1
23     * - token's URI = https://mytoken.com/api/1
24     */
25    constructor(
26        address owner,
27        string memory name,
28        string memory symbol,
29        string memory baseURI,
30        address royaltyReceiver_,
31        uint96 royaltyFee_
32    ) ERC721(name, symbol) {
33        _transferOwnership(owner);
34        _baseTokenURI = baseURI;
35        _setDefaultRoyalty(royaltyReceiver_, royaltyFee_); // 5%
36        mintNFT(owner, 0);
37    }
38
39    /**
40     * Mints a new NFT
41     * @param recipient The address of the recipient
42     * @return The new token ID
43     */
44    function mintNFT(
45        address recipient,
46        uint256 tokenId
47    ) public onlyOwner returns (uint256) {
48        _safeMint(recipient, tokenId);
49        return tokenId;
50    }
51
52    /**
53     * Returns the base token URI
54     * In the base ERC721 contract, this function returns an empty string.
55     * We override it to return our own base token URI.
56     */
57    function _baseURI() internal view override returns (string memory) {
58        return _baseTokenURI;
59    }
60
61    /**
62     * We are overriding this function from the base ERC721 contract.
63     * We want to append .json to the token URI since we'll be storing our token metadata as JSON files.
64     * @dev See {IERC721Metadata-tokenURI}.
65     */
66    function tokenURI(
67        uint256 tokenId
68    ) public view virtual override returns (string memory) {
69        _requireMinted(tokenId);
70
71        string memory baseURI = _baseURI();
72        return
73            bytes(baseURI).length > 0
74                ? string(abi.encodePacked(baseURI, tokenId.toString(), ".json"))
75                : "";
76    }
77
78    function setTokenRoyalty (
79        uint256 tokenId,
80        address receiver,
81        uint96 feeNumerator
82    ) public onlyOwner {
83        _setTokenRoyalty(tokenId, receiver, feeNumerator);
84    }
85}
86

So let's look at what we did here that is new:

  1. We imported the ERC721Royalty contract and inherited from it.

I want to emphasize that this is the absolute best way to build contracts fast and efficiently - learn to use your resources. Learn what OpenZeppelin provides us with. As we already know, all of their contracts are audited and battle-tested.

As we will see in a bit, the ERC721Royalty contract also inherits from the ERC2981 contract, which is the royalty fee standard. So let's look at both of these contracts in more detail:

First the ERC2981 contract:

1abstract contract ERC2981 is IERC2981, ERC165 {
2    struct RoyaltyInfo {
3        address receiver;
4        uint96 royaltyFraction;
5    }
6
7    RoyaltyInfo private _defaultRoyaltyInfo;
8    mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo;
9
10    /**
11     * @dev See {IERC165-supportsInterface}.
12     */
13    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
14        return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
15    }
16
17    /**
18     * @inheritdoc IERC2981
19     */
20    function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) {
21        RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId];
22
23        if (royalty.receiver == address(0)) {
24            royalty = _defaultRoyaltyInfo;
25        }
26
27        uint256 royaltyAmount = (salePrice * royalty.royaltyFraction) / _feeDenominator();
28
29        return (royalty.receiver, royaltyAmount);
30    }
31
32    /**
33     * @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a
34     * fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an
35     * override.
36     */
37    function _feeDenominator() internal pure virtual returns (uint96) {
38        return 10000;
39    }
40
41    /**
42     * @dev Sets the royalty information that all ids in this contract will default to.
43     *
44     * Requirements:
45     *
46     * - `receiver` cannot be the zero address.
47     * - `feeNumerator` cannot be greater than the fee denominator.
48     */
49    function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual {
50        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
51        require(receiver != address(0), "ERC2981: invalid receiver");
52
53        _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator);
54    }
55
56    /**
57     * @dev Removes default royalty information.
58     */
59    function _deleteDefaultRoyalty() internal virtual {
60        delete _defaultRoyaltyInfo;
61    }
62
63    /**
64     * @dev Sets the royalty information for a specific token id, overriding the global default.
65     *
66     * Requirements:
67     *
68     * - `receiver` cannot be the zero address.
69     * - `feeNumerator` cannot be greater than the fee denominator.
70     */
71    function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual {
72        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
73        require(receiver != address(0), "ERC2981: Invalid parameters");
74
75        _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator);
76    }
77
78    /**
79     * @dev Resets royalty information for the token id back to the global default.
80     */
81    function _resetTokenRoyalty(uint256 tokenId) internal virtual {
82        delete _tokenRoyaltyInfo[tokenId];
83    }
84}

So in a nutshell, this contract allows us to set a default royalty fee for all tokens, and also set a royalty fee and the receiver for a specific token.

Now let's look at the ERC721Royalty contract:

1abstract contract ERC721Royalty is ERC2981, ERC721 {
2    /**
3     * @dev See {IERC165-supportsInterface}.
4     */
5    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981) returns (bool) {
6        return super.supportsInterface(interfaceId);
7    }
8
9    /**
10     * @dev See {ERC721-_burn}. This override additionally clears the royalty information for the token.
11     */
12    function _burn(uint256 tokenId) internal virtual override {
13        super._burn(tokenId);
14        _resetTokenRoyalty(tokenId);
15    }
16}

This contract inherits from the ERC2981 contract and the ERC721 contract. It overrides the _burn function from the ERC721 contract to clear the royalty information for the token since it is being burned.

  1. In the contructor, we set the default royalty information for all tokens. We can set the specific royalty fee and receiver for a specific token by calling the setTokenRoyalty function as we will see in a bit.

  2. We also changed the way token ids are handled. Instead of monitoring the total supply and automatically updating the token ids, we are instead allowing the caller to pass in the token id.

  3. Lastly, we added a setTokenRoyalty function that allows us to set the royalty fee and receiver for a specific token id. This function is only accessible by the owner of the contract.

Writing our tests

In the ERC721Royalty.ts file, add the following code:

1import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
2import { expect } from "chai";
3import { ethers } from "hardhat";
4
5describe("ERC721", function () {
6  async function deployERC721Fixture() {
7    const [owner, account_1] = await ethers.getSigners();
8    const ERC721RoyaltyToken = await ethers.getContractFactory(
9      "ERC721RoyaltyToken"
10    );
11    const erc721RoyaltyToken = await ERC721RoyaltyToken.deploy(
12      owner.address,
13      "My NFT",
14      "MNFT",
15      "https://example.com/",
16      owner.address,
17      "500"
18    );
19    return { erc721RoyaltyToken, owner, account_1 };
20  }
21
22  describe("Deployment Tests", function () {
23    it("Should set the right owner", async function () {
24      const { erc721RoyaltyToken, owner } = await loadFixture(
25        deployERC721Fixture
26      );
27      expect(await erc721RoyaltyToken.owner()).to.equal(owner.address);
28    });
29
30    it("Should mint a new token with the correct URI", async function () {
31      const { erc721RoyaltyToken, owner } = await loadFixture(
32        deployERC721Fixture
33      );
34      expect(await erc721RoyaltyToken.balanceOf(owner.address)).to.equal(1);
35      expect(await erc721RoyaltyToken.tokenURI(0)).to.equal(
36        "https://example.com/0.json"
37      );
38    });
39  });
40
41  describe("Testing default royalty", function () {
42    it("Should return the correct royalty fee info for token 0 if it was listed for 1 ETH", async function () {
43      const { erc721RoyaltyToken, owner, account_1 } = await loadFixture(
44        deployERC721Fixture
45      );
46      expect(
47        await erc721RoyaltyToken.royaltyInfo(0, ethers.parseEther("1"))
48      ).to.deep.equal([owner.address, ethers.parseEther("0.05")]);
49    });
50  });
51
52  describe("Testing custom royalty per token", function () {
53    it("Should return the correct royalty fee info for token 0 if it was listed for 1 ETH", async function () {
54      const { erc721RoyaltyToken, owner, account_1 } = await loadFixture(
55        deployERC721Fixture
56      );
57      await erc721RoyaltyToken.setTokenRoyalty(0, account_1.address, "1000"); // 10%
58      expect(
59        await erc721RoyaltyToken.royaltyInfo(0, ethers.parseEther("1"))
60      ).to.deep.equal([account_1.address, ethers.parseEther("0.1")]);
61    });
62  });
63});

Since we extended our contract, we have a few more things to test:

  1. We are making sure that the default royalty is being set correctly on deployment. To do this, we are calling the royaltyInfo function and passing in the token id and the sale price. We are expecting the royalty fee to be 5% of the sale price and the receiver to be the owner of the contract. Since the sale price is 1 ETH, we are expecting the royalty fee to be 0.05 ETH.

We set the default royalty fee to be 5% when we call the deployment function in the deployERC721Fixture function. We pass in the receiver and fee numerator as the last two arguments.

  1. Next we are changing the royalty fee for token 0 to be 10% and also the receiver to be account_1. We are then calling the royaltyInfo function again and expecting the royalty fee to be 10% of the sale price and the receiver to be account_1.

Compiling and running our smart contracts

To run our tests and make sure everything works, run the following command in your terminal:

1npx hardhat test

We have successfully implemented the royalty fee extension to our ERC721 token! Let's deploy it to the test network.

Deploying our smart contract

Create a new deploy file in the scripts folder called deploy-erc721royalty.ts and add the following code:

1// We require the Hardhat Runtime Environment explicitly here. This is optional
2// but useful for running the script in a standalone fashion through `node <script>`.
3//
4// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
5// will compile your contracts, add the Hardhat Runtime Environment's members to the
6// global scope, and execute the script.
7import hre from "hardhat";
8
9const config = {
10  owner_address: "your address here",
11  token_name: "My Awesome NFT",
12  token_symbol: "MANFT",
13  base_uri: "ipfs://QmfAJoYp518dvdRg87jMeTMtzzs7AzXS4XjasfhTTLcWyB/",
14  royalty_receiver: "your address here",
15  royalty_fee: "500",
16};
17
18async function main() {
19  const {
20    owner_address,
21    token_name,
22    token_symbol,
23    base_uri,
24    royalty_receiver,
25    royalty_fee,
26  } = config;
27
28  console.log(`Deploying token ${token_name}. Owner: ${owner_address}...`);
29
30  const Token = await hre.ethers.getContractFactory("ERC721RoyaltyToken");
31  const token = await Token.deploy(
32    owner_address,
33    token_name,
34    token_symbol,
35    base_uri,
36    royalty_receiver,
37    royalty_fee
38  );
39
40  console.log(`Deployed token. Owner: ${owner_address}`);
41
42  console.log("Waiting 1 minute for Etherscan to index the contract...");
43  await new Promise((r) => setTimeout(r, 60000));
44
45  console.log("Verifying contract...");
46  await hre.run("verify:verify", {
47    address: token.getAddress(),
48    constructorArguments: [
49      owner_address,
50      token_name,
51      token_symbol,
52      base_uri,
53      royalty_receiver,
54      royalty_fee,
55    ],
56  });
57
58  console.log("Contract verified! 🎉");
59  console.log(
60    `Please find the verified contract on Etherscan: https://sepolia.etherscan.io/address/${token.getAddress()}`
61  );
62}
63
64// We recommend this pattern to be able to use async/await everywhere
65// and properly handle errors.
66main().catch((error) => {
67  console.error(error);
68  process.exitCode = 1;
69});

YES

And that's it! Now to deploy our contract, run the following command in your terminal:

1npx hardhat run scripts/deploy-erc721royalty.ts --network sepolia

Conclusion

In this course, we learned how to add a royalty fee to our ERC721 token. This contract is now marketplace ready and can be displayed on OpenSea!