Whitelist

Luiz Soares
4 min readMay 12, 2021

How do I implement a whitelist in Solidity?

Your PM ask you to implement a whitelist in your Smart Contract. At first, it seems very simple. You create a mapping called “whitelisted”:

mapping(address => bool) public whitelisted;

Than you create a function that whitelists an address:

function whitelist(address _user) external {
require(msg.sender == owner, “Only owner can whitelist”);
whitelisted[_user] = true;
}

Great. It is ready. It can’t be easier. You can go and drink your coffee.

But are you going to whitelist 20.000 address?? A transaction would cost around 45.000 gas. If you pay 20 Gwei, it is going to cost 0.0009 Eth. Today 1 eth in usd values around $ 4.000,00. So, it is going to cost only $ 3,00. Ok, not bad (remember, we are paying only 20 Gwei and today gas price is around 300 Gwei). Your project is going to spend $ 60.000,00. “My project is rich and we can afford”. Good. But how long would it take to whitelist 20.000 addresses? If it takes 10s for each one, 55hs!!!

But I am smart and I will whitelist using a batch approach. I send an array full of addresses that I want to whitelist.

function batchWhitelist(address[] memory _users) external {
require(msg.sender == owner, “Only owner can whitelist”);

uint size = _users.length;

for(uint256 i=0; i< size; i++){
address user = _users[i];
whitelisted[user] = true;
}
}

If blocksize is 8M, you can fit a bunch of address here. I can say around 200 addresses. Very good, now you are going to spend 16 minutes. But remember, you have other factors here: Infura (or another node) network, confirmations, you have to create a script and a list of addresses. For a large project, it is still not a good solution. And the price would be the same if you whitelist one by one or in a batch. And to make things worse, you may need another mapping to blacklist some addresses. You have just multiplied your problem by 2.

Before showing another solution, let’s improve the solution above. OpenZeppelin has a Smart Contract that can help with that. It is AccessControl. Here are some functions that helps you organizing your code.

  function hasRole(bytes32 role, address account) 
function getRoleAdmin(bytes32 role)
function grantRole(bytes32 role, address account)
function revokeRole(bytes32 role, address account)
function renounceRole(bytes32 role, address account)

Your code would be easier to read and maintain and is reusing a well used code.

But using a mapping can cost some money and take some time. How do we solve this problem?

To make it cheapier you can sign user address. Only signed addresses could execute a function.

Is it a complex solution? Well, it is going to need some effort, but not huge. After it is setup, it will not look hard to implement.

First, you create a lambda service where your admin address will sign a data for your customer. Here we need a backend and a database so you know which addresses can be signed. Maybe you already have this information because your Dapp needs a KYC.

For this example, let’s check a code used in a successful token sale. You can find here, https://etherscan.io/address/0x3ce3b6d9372a4d761172a89cf0139129309fa0ae#code .

Code above was used during Foam token sale (It was developed by ConsenSys team -Thanks https://github.com/vdrg, https://github.com/hensha256 and https://github.com/thekscar ). To contribute to the token sale user had already passed a KYC. Then, to be able to contribute, user had a contribution limit.

Lambda service would receive contributor address, contribution limit and a expiration date and it would sign it using Admin address.

bytes _rawHash = keccak256( abi.encodePacked(
_contributor,
_contributionLimitUnits,
_payloadExpiration )

Now, when calling contribute function from the Sale, it would receive:

function contribute(
address _contributor,
uint256 _contributionLimitUnits,
uint256 _payloadExpiration,
bytes _sig )

Notice that sig is a byte returned by the Lambda service. When contribute() is called, it will get _contributor, _contributionLimitUnits, _payloadExpiration and hash it and check the signature. If sig was signed by the Admin address, then it is a valid signature and user can call contribute() function. _rawHash is the hash of _contributor, _contributionLimitUnits and
_payloadExpiration.

  bytes32 hash = _rawHash.toEthSignedMessageHash();
return whitelistAdmin == hash.recover(_sig);

Foam Sale code has more functions, like invalidateHash(). It is needed because if your service signed message to an address and then you later found that address sent fake documents during the KYC, you can invalidate the hash to prevent being used.

If you want to learn more how to hash data and recover address that signed it, please check OpenZeppelin signature test example here.

--

--

Luiz Soares

I enjoy life. I read. I eat. I sleep. I dream. I work. I wake up in the morning.