DEV Community

Cover image for Create a whitelist for your NFT project
Abdul Rauf
Abdul Rauf

Posted on

Create a whitelist for your NFT project

Many NFT projects have been using whitelists/allowlists to reward their most active community members. The members in this list are allowed to mint their NFTs before the rest of the public. This saves them from competing in a gas war with others.

wen whitelist??

Well, I don't know anything about that but I can show how you can implement it in your smart contracts.

Prerequisites

We're going to examine the smart contract of the Doodles NFT project, and see how they stored a list of their members on the blockchain. We're also going to learn about different function types, modifiers, and data locations in Solidity.

To understand this tutorial, you need to know about NFTs, crypto wallets, and the Solidity programming language. We do go through some Solidity concepts as mentioned above, but you need to know how smart contracts work in general.

But why Doodles?

Doodles is one of the best 10k NFT collectible projects out there. At the time of writing (5th Jan 2022), they've already traded around 46.3k ETH on Opensea and have a floor price of 9.35 ETH. Their minting process went relatively smooth, so they are a good project to learn from.

Doodles OpenSea link: https://opensea.io/collection/doodles-official

The process

The process is simple. We just need to store all the whitelisted addresses in a list. Doodles have gone one step ahead and stored the amount of NFTs the members can mint as well. They've used a data structure called mapping to do that.

What is mapping?

Mapping in Solidity acts like a hash table or dictionary in any other language. It is used to store the data in the form of key-value pairs. Maps are created with the syntax mapping(keyType => valueType).

  • keyType could be a type such as uint, address, or bytes
  • valueType could be all types including another mapping or an array.

Maps are not iterable, which means you cannot loop through them. You can only access a value through its key.

This is where Doodles are storing all the members and the number of NFTs they can mint:

mapping(address => uint8) private _allowList;
Enter fullscreen mode Exit fullscreen mode

You can access data from a mapping similar to how you'd do it from an array. Instead of an index, you'll just give it a key.

_allowList[someAddress] = someNumber
Enter fullscreen mode Exit fullscreen mode

Adding members to the whitelist

Let's look at the setAllowList function where everything happens.

function setAllowList(address[] calldata addresses, uint8 numAllowedToMint) external onlyOwner {
    for (uint256 i = 0; i < addresses.length; i++) {
        _allowList[addresses[i]] = numAllowedToMint;
    }
}
Enter fullscreen mode Exit fullscreen mode

We're passing in an array of addresses and the number of tokens to the function. Inside the function, we loop through the addresses and store them in the _allowList. Pretty straightforward, eh?

The most interesting keywords here are calldata, external, and onlyOwner.

1. The "OnlyOwner" modifier

This keyword is imported by the OpenZeppelin library. OpenZeppelin provides Ownable for implementing ownership in your contracts. By adding this modifier to your function, you're only allowing it to be called by a specific address. By default, onlyOwner refers to the account that deployed the contract.

import "@openzeppelin/contracts/access/Ownable.sol";

// ...

function setAllowList() external {
    // anyone can call this setAllowList()
}

function setAllowList() external onlyOwner {
    // only the owner can call setAllowList()!
}
Enter fullscreen mode Exit fullscreen mode

2. Different types of functions

There are four types of Solidity functions: external, internal, public, and private.

  • private functions can be only called from inside the contract.
  • internal functions can be called from inside the contract as well other contracts inheriting from it.
  • external functions can only be invoked from the outside.
  • public functions can be called from anywhere.

Why are we using an external function here instead of, maybe, public? Well, because external functions are sometimes more efficient when they receive large arrays of data.

The difference is because in public functions, Solidity immediately copies array arguments to memory, while external functions can read directly from calldata. Memory allocation is expensive, whereas reading from calldata is cheap.

This was taken from a StackoverExchange answer on this topic.

3. Data locations

Variables in Solidity can be stored in three different locations: storage, memory, and calldata.

  • storage variables are stored directly on the blockchain.
  • memory variables are stored in memory and only exist while a function is being called.
  • calldata variables are special (more efficient) data locations that contain function arguments. They are only available for external functions.

Since our list of whitelisted members can be large, we're using calldata to store our array of addresses.

How to mint

Let's look at the mint function they've used for the members in the whitelist:

function mintAllowList(uint8 numberOfTokens) external payable {
    uint256 ts = totalSupply();

    require(isAllowListActive, "Allow list is not active");
    require(numberOfTokens <= _allowList[msg.sender], "Exceeded max available to purchase");
    require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens");
    require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct");

    _allowList[msg.sender] -= numberOfTokens;
    for (uint256 i = 0; i < numberOfTokens; i++) {
        _safeMint(msg.sender, ts + i);
    }
 }
Enter fullscreen mode Exit fullscreen mode

The following line is relevant for this tutorial:

require(numberOfTokens <= _allowList[msg.sender], "Exceeded max available to purchase");
Enter fullscreen mode Exit fullscreen mode

if the wallet minting the token i.e. msg.sender is not available in the _allowList, this line will throw an exception.

You can take a look at the complete source code here.


Don't leave me, take me with you

Like what you read? Follow me on social media to know more about NFTs, Web development, and shit-posting.

Twitter: @lilcoderman

Instagram: @lilcoderman

Top comments (21)

Collapse
 
efe_acikgoz_f372e7abe3ba6 profile image
Efe Acikgoz

While this is a great solution, it comes with great cost haha. The gas cost per address is 20k, so it costs around 0.0035 eth per address added, and if you have A LOT of addresses, this will start to become a problem

Collapse
 
lilcoderman profile image
Abdul Rauf

Thanks for sharing! Do you know a better way to do the whitelist?

Collapse
 
efe_acikgoz_f372e7abe3ba6 profile image
Efe Acikgoz

Depending on your use case using a patricia tree or using signatures can save a lot of gas.

For example you can create an empty wallet and use it’s private key to sign a message on your backend that your contract can recreate and use ecrecover to check if the address is correct.

Thread Thread
 
lilcoderman profile image
Abdul Rauf

Do you know any good resource explaining this? Or maybe a smart contract using this technique?

Thread Thread
 
efe_acikgoz_f372e7abe3ba6 profile image
Efe Acikgoz

Patricia tree or signatures?

Thread Thread
 
lilcoderman profile image
Abdul Rauf

You can share whatever you can 🙂

Thread Thread
 
efe_acikgoz_f372e7abe3ba6 profile image
Efe Acikgoz

bytes32 hash = keccak256(abi.encodePacked(this, msg.sender, quantity, tier));
bytes memory prefix = "\x19Ethereum Signed Message:\n32";
bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix, hash));
address signer = ecrecover(prefixedHashMessage, _v, _r, _s);
require(signer == serverAddress, "Invalid signature");

The contract will expect a message signed with contract address, sender address, how many you need to buy and the price tier. Even if the message is intercepted and changed, the sign will be invalid because it will be different than what the server's signed message. For example if the server signed a message with 1 quantity at tier4 pricing, you cannot change the variables to 5 mints at tier1 because the contract side generated hash will be different and transaction will fail.

I'm sure in time we will be able to handle all of these with web3 solutions but for now I think we still need a mixed solution due to gas prices

Thread Thread
 
bronteraspital profile image
bronteraspital

this sounds pretty interesting. I'm in the process of finishing up my own NFT project and can't afford the gas fee to set up a whitelist. How much cheaper do you think this would come out to relatively?

Thread Thread
 
efe_acikgoz_f372e7abe3ba6 profile image
Efe Acikgoz

Ecrecover costs 3000 gas, so it will save 17k gas per adress

Thread Thread
 
corentindallenogare profile image
corentinDallenogare

Hello, could you also do a little tutorial? Thank you

Thread Thread
 
bronteraspital profile image
bronteraspital

Sounds great! By any chance would you be able to help me implement this method in my whitelist? I can send you a percentage of sales!

Thread Thread
 
redcomethk profile image
Ralph Chan

I found another NFT Project, MetaAngels, which should be doing the whitelist in this approach. The contract location is: etherscan.io/address/0xad265ab9b99...

Collapse
 
cvega21 profile image
Christian Vega-Munguia

this is great, thank you!! looked this up after the fishy fam fiasco, their contract only checked for the current NFT balance of the WL minter in the WL mint function. so they could just transfer out the tokens and keep minting more. unfortunate, but very easy fix.

thanks to this tutorial, i am confident we will not be facing the same issue in our mint 😸

Collapse
 
lilcoderman profile image
Abdul Rauf

Wow! I'm glad you found this helpful. Also, I'd love to know more about your project 😁

Collapse
 
catsmeow profile image
cat mie

This is a great article. Thank you! I am wondering if this approach is practical if setAllowList() is used within an contract running on Polygon with an allowList array size of ~9000? What interface could I use to input such an array to the instantiated contract? Etherscan seems to have a limit on the size of the array that can be inputted.

Collapse
 
loobj126 profile image
loobj126 • Edited

Hi sir , I try load this contract in remix . This is step i perform however it doesnt work

1) setIsAllowListActive = true
2) setAllowList - put an address and numallowedtomint =1

The error execption has trriggered

transact to Doodles.setAllowList errored: Error encoding arguments: Error: expected array value (argument=null, value="0x623CD18A2344476063Ee2f806EEdDdbcE9cd5499", code=INVALID_ARGUMENT, version=abi/5.5.0)

Can you pls advise ?

dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
loobj126 profile image
loobj126

Hi Rauf , can you pls advise

Collapse
 
nakedrunnersnft profile image
Naked Runners NFT

Hey! Thank you for this write-up. I'm building a NFT project around mindfulness-based running community, and learning more about smart contracts to add whitelist and tier functions to it (after learning from Hashlips on YouTube).

This might be a stupid question, but I don't see any Whitelists on Doodle's current smart contract. My understanding is that you need to have the addresses with WL access need to be in the smart contract so they can be verified by the owner's contract. Is that because they had it on only for the pre-sale, and removed the addresses from the contract after that? Or is the contract able to retrieve the list of WL addresses out side of the contract code? Much thanks! 🙏🏽

Collapse
 
mohmmadanas profile image
Mohmmadanas

This is amazing 🤩 Thank You
But I have question
I want ask about the NFT price
How we can make another price for whitelist user’s?

Collapse
 
lilcoderman profile image
Abdul Rauf

Sorry for the late reply!

You can create two separate payable functions. One for public mint and one for the whitelisted members. You can use require to check if someone's paying the right amount.

Public mint function

function mint() public payable {
    ...
    require(msg.value == PRICE, "Insufficient payment, 0.02 ETH per item");
    ...
  }
Enter fullscreen mode Exit fullscreen mode

Whitelist mint function

function mintPresale() public payable {
    ...
    require(msg.value == WHITELIST_PRICE, "Insufficient payment, 0.01 ETH per item");
    ...
  }
Enter fullscreen mode Exit fullscreen mode

Let me know if this answers your question.

Collapse
 
jacob_cai_83b53cda1c3959a profile image
Jacob Cai

How and where do people pay for their guaranteed nft ?