Multi-tier NFTs
Have you ever wondered if it’s possible to create tiers within the same NFT project, with different prices and token supplies? A few weeks ago me and some friends were brainstorming ideas for a utility based NFT project that we are working on. The NFTs would act as a sort of subscription to our service so we quick realized that it would be benficial to have different tiers, just like you have different subscription levels. Essentially there would be a larger pool of cheaper, more accessible tokens and also a smaller supply of rarer and more expensive ones.
Each tier would have different values for properties like price and supply effectively ‘enforcing’ their rarity from the very beginning. As an additional requirement it should be possible to mint tokens from any of the tiers at any point, by simply specifying it in the contract call.
The idea went against what is traditionally done in NFT projects where there’s usually 1 mint price for all tokens or a sequential pricing structure where the prices changes after a number X of tokens is minted. This last example is somewhat similar but still different from what we are trying to achieve, due to the fact that each tier can only be minted after the previous one is finished.
Being a developer I immediately started wondering how such functionality would look like in code. My intuition said that building such a smartcontract should be easy peasy and that probably someone had already done it. To my surprise I couldn’t find any good solutions online so I decided to write my own, which is what I want to share with you today.
Smartcontract
I wrote the smartcontract in Solidity, since it’s the most accessible and widely used blockchain programming language.
We start by specifying the solidity compiler version and importing the two basic interfaces that we need for an NFT project, ERC721 and Ownable. The first state variable saleIsActive is a boolean that just indicates if a sale is active for this smartcontract, or in other words, are people allowed to mint NFTs, nothing new.
The interesting part starts in the definition of the Tier struct, where we will store the tier specific properties. Each Tier will have:
- price: the price of this tier
- totalSupply: how many tokens of this tier have been minted
- maxSupply: how many tokens of this tier can be minted
- startingIndex: at what tokenID does this tier start
- mintsPerAddress: how many mints is each address allowed to make for this tier
Next we create a nested mapping where we will keep the information of how many mints an has address made for a specific tier. The first index will point to the corresponding tier, and the second index to the number of mints of an address, at that tier.
This is followed by another mapping, tiers, that will simply hold all the data we just described, indexed on the tier.
And finally we have the base and token URI variables which will allows us to have specific URIs associated with each tokenID. These will be pointing to a distributed file system like IPFS which will hold the metadata for that token.
So how will our constructor look like?
We specify the name and symbol as arguments in the constructor and then we hardcode each tier and add it to the tiers mapping. For this example project we go with a 3 tier system where
- Tier 0 has a maxSupply of 300 and costs 0.42 ether
- Tier 1 has a maxSupply of 100 and costs 0.6 ether
- Tier 2 has a maxSupply of 20 and costs 0.9 ether
We are now ready to go over the mint function.
We will follow the usual Checks Effects Interaction pattern so we start by asserting that all the requirements for the mint function are fulfilled. In this case we check the requirements for
- Sale being active
- Tier max supply not having been reached yet
- The transaction value being sufficient to mint this tier
- The address making the transaction not having exceed the maximum amount of mints per address for this tier.
Notice that these requirements all depend on the tier that is specified as an argument to the mint function.
With our checks done, we can move on to effects. We update the number of mints by the sender’s address for the specified tier and we also update the tier total supply. Before this last step, we save the current total supply value in a variable in order to be able to specify the tokenId in the _safeMint method, which will be our only interaction.
Tests
To be sure that our smartcontract fulfills our initial requirements we can write some tests in python.
The most basic test consists of minting one token and asserting a number of values that we expect to have afterwards. We start by flipping the sale state to make sure the sale is active. Then we mint a token from the lowest tier by providing the 0.42 ether, as specified for tier 0 in our constructor. Having done this, theres a few things we can expect regarding the state of our smartcontract
- Naturally, the balance of the account that minted 1 token should be 1
- The total supply of tier 0 should be 1 since only 1 token of this tier has been minted
- The total supply of tier 1 and 2 should be 0 since no tokens were minted from those tiers
- Finally, the owner of token with ID 0 should be the same account that just minted a token from tier 0, since we specified that this tier should start at index 0
New to trading? Try crypto trading bots or copy trading
That was simple enough! Now a slightly more complex test where multiple accounts mint multiple tokens
Here we have one account mint a token from tier 0 and 1, while another account mints tier 1 and 2. Again, we can expect:
- Both accounts should have a balance of 2, since they both minted 2 tokens
- The total supply of tier 0 and 2 should be 1, while the total supply of tier 1 should be 2
- The owner of token id 0 and 300 should be account 0, while the owner of id 301 and 400 should be account 1. Again, this is based on the starting indeces defined in the constructor
Obviously these two tests alone are not conclusive but give a good indication that our smartcontract behaves like we designed to.
Issues
It seems like indeed having tiers in an ERC721 smartcontract is easy but it does come with two major drawbacks:
- The tier structure in the contract makes it impossible to have the usual lottery style raffle that NFT projects usually have. The user minting a token can always check what is the id of the token that they will be minting, and so that element of surprise is gone. The randomness in most project is introduced by starting the mint at an arbitrary index which is not possible in the way we designed this contract.
- Another drawback is that the tier structure requires us to specify what is the tokenId being minted at any time. This makes it incompatible with optimized NFT interfaces like ERC721A and so doing gas optimization for minting multiple tokens would have to be done manually.
Thanks for taking the time to read my post, I hope it was useful to you in some way! If you want to checkout the full source code here is a link to the repository: https://github.com/filipkny/TieredNFT
And if you would like to follow me on twitter: https://twitter.com/indiefilipk