Recommended resources
The freely available resources this lesson will direct you to.
- Use #merkle tree algorithm for handling #presale / #whitelist in your #SmartContractswww.youtube.com
- Create a gasless whitelist to NFT Mint Smart Contract | NFT Presale smart contractwww.youtube.com
- Checking Whitelisted Addresses on a Solidity Smart ...dev.to
- How to Implement a Whitelist in Smart Contracts (ERC-721 ...www.freecodecamp.org
Hello! Let's get started on the next feature for our token sale contract.
In our previous lessons, we've established robust controls over our crowdsale: we set a funding cap to limit the total investment and defined a specific time window to control when the sale is active. Now, we'll turn our attention to controlling who can participate.
Today, your goal is to implement a whitelist to restrict participation to specific addresses. This is a common requirement for token sales, used to grant exclusive access to early supporters, community members, or individuals who have completed a Know Your Customer (KYC) process.
1. Why Use a Whitelist?
Restricting access to a token sale might seem counterintuitive, but it serves several strategic purposes:
- Exclusivity and Reward: It allows you to reward early backers or specific community members with guaranteed or early access to the sale.
- Regulatory Compliance: In many jurisdictions, token sales must be limited to accredited investors or users who have passed identity verification (KYC/AML) checks. A whitelist is the technical mechanism to enforce this.
- Preventing "Gas Wars": For highly anticipated sales, a whitelist can prevent a chaotic public rush where users bid up gas prices to extreme levels to get their transactions included first.
2. Strategies for Implementing a Whitelist
There are several ways to implement a whitelist in a smart contract, each with different trade-offs in terms of gas cost, flexibility, and complexity. As a backend developer, you can think of this as choosing the right database or caching strategy based on performance and cost requirements.
Let's explore the three most common methods. The following article provides an excellent overview and comparison of them all.
How to Implement a Whitelist in Smart Contracts
This article from freeCodeCamp provides a comprehensive comparison of the three main whitelisting strategies: on-chain, digital signature, and Merkle tree. It includes code examples, pros and cons, and gas cost analysis for each.
Please read the entire article. Pay close attention to the three methods presented and the final summary table comparing their gas costs. This will give you a solid framework for understanding the trade-offs.
Let's break down these three approaches.
Method 1: On-Chain Whitelist (The Simple Approach)
This is the most straightforward method. You store the list of approved addresses directly on the blockchain within your smart contract, typically using a mapping.
- How it works: A
mapping(address => bool)keeps track of who is whitelisted. A function restricted to the contract owner is used to add or remove addresses. Each purchase transaction then simply checks ifwhitelisted[msg.sender]istrue. - Pros: Simple to implement and understand. The whitelist's state is transparent and fully managed on-chain.
- Cons: Extremely expensive. Every address you add to the mapping is a storage write on the blockchain, which costs a significant amount of gas. For a whitelist of thousands of users, the cost to the project owner can become enormous.
The video below highlights why this approach is often impractical for real-world projects with large whitelists.
Use #merkle tree algorithm for handling #presale / #whitelist in your #SmartContracts
This video by Uncle Aaroh explains the significant gas cost problem associated with storing whitelist addresses directly on-chain, referencing a real-world project that spent 1 ETH just to add addresses.
Watch the first two minutes of the video (0:00 - 2:02). The key takeaway is understanding the financial downside of the on-chain mapping/array method.
Method 2: Digital Signatures (The Flexible Approach)
This method moves the storage of the whitelist off-chain (e.g., to a server database) and uses cryptography to verify a user's eligibility.
- How it works: When a whitelisted user wants to buy tokens, your backend server generates a unique cryptographic signature for their address. The user sends this signature along with their transaction. The smart contract uses a function like
ECDSA.recoverto verify the signature was created by a trusted signer address (whose private key is on your server) for that specific user. - Pros: Managing the whitelist off-chain is free (no gas costs). The list can be updated at any time without interacting with the blockchain.
- Cons: The verification process adds gas cost to the user's transaction. It also introduces a centralized dependency on your server to issue signatures.
Method 3: Merkle Trees (The Efficient & Scalable Approach)
This is the industry-standard solution for managing large whitelists efficiently. It combines off-chain data management with on-chain cryptographic proof.
- How it works:
- Off-chain, you take your list of whitelisted addresses and build a Merkle tree. This is a data structure where addresses are hashed, then paired and hashed again, repeating until you have a single final hash, the Merkle root.
- You store only this single
bytes32Merkle root in your smart contract. This is a one-time, cheap storage write. - When a user wants to buy, your frontend/backend provides them with a Merkle proof—the "branch" of hashes connecting their address to the root.
- The user sends this proof with their transaction. The smart contract uses a verification function (
MerkleProof.verify) to confirm that the user's address, combined with the proof, correctly reconstructs the Merkle root.
- Pros: Extremely gas-efficient. Storing the root is cheap, and the verification cost for the user is low and scales well with the size of the whitelist.
- Cons: More complex to implement, as it requires both off-chain logic to generate the tree/proofs and on-chain logic to verify them. If the whitelist changes, a new Merkle root must be calculated and updated in the contract.
The video and article below provide a deeper dive into the theory and practice of implementing a Merkle tree whitelist.
Use #merkle tree algorithm for handling #presale / #whitelist in your #SmartContracts
Having seen the problem with on-chain lists, let's see the solution. This part of the video introduces Merkle trees as the gas-efficient alternative and explains the core concept.
Watch the rest of the video (2:02 - 6:05). Focus on how only a single root hash is stored on-chain and how a user provides a proof to verify their inclusion. Notice the mention of OpenZeppelin's MerkleProof.sol library, which we'll use.
Checking Whitelisted Addresses on a Solidity Smart Contract Using Merkle Tree Proofs
For a hands-on walkthrough, this article from dev.to provides a step-by-step guide on creating a Merkle tree with a JavaScript script and then verifying proofs in a Solidity smart contract.
Read the sections "Understanding Merkle Trees", "Constructing the Merkle Root", and "Verifying the proof in the smart contract". This will show you the full workflow from a list of addresses to a verifiable on-chain implementation.
3. Implementing a Whitelist in MyTokenSale
We have a choice to make. The Merkle tree method is the most scalable, but it's not compatible with the standard OpenZeppelin Crowdsale contract "out of the box." The buyTokens function in the base contract does not accept a Merkle proof as an argument. Integrating it would require us to override buyTokens and replicate much of the underlying purchase logic, which moves us away from the modular approach we've been using.
The on-chain mapping method, while expensive for large lists, integrates seamlessly with our existing contract structure by using the _preValidatePurchase hook.
For this lesson, to maintain our modular pattern and focus on the integration itself, we will implement the on-chain mapping method. This provides a clear and functional whitelist, and you now have the knowledge of how and why you would upgrade to a Merkle tree for a large-scale, production-ready project.
We will add the whitelisting logic directly into our MyTokenSale contract. This will involve:
- Adding an
Ownableimport and inheriting from it to get theonlyOwnermodifier. - Defining a
mapping(address => bool)for the whitelist. - Creating an
onlyOwnerfunction to add addresses to the whitelist. - Overriding the
_preValidatePurchasefunction to add the whitelist check.
Here is the updated MyTokenSale.sol contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Inherit from Ownable to get owner-only controls
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "@openzeppelin/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Add Ownable to the inheritance list
contract MyTokenSale is CappedCrowdsale, TimedCrowdsale, Ownable {
// 1. Mapping to store whitelisted addresses
mapping(address => bool) private _whitelist;
event AddedToWhitelist(address[] accounts);
constructor(
uint256 rate,
address payable wallet,
IERC20 token,
uint256 cap,
uint256 openingTime,
uint256 closingTime
)
CappedCrowdsale(cap)
TimedCrowdsale(openingTime, closingTime)
Crowdsale(rate, wallet, token)
Ownable(msg.sender) // Initialize Ownable, setting deployer as owner
{}
// 2. Function for the owner to add addresses to the whitelist
function add_to_whitelist(address[] calldata accounts) external onlyOwner {
for (uint i = 0; i < accounts.length; i++) {
_whitelist[accounts[i]] = true;
}
emit AddedToWhitelist(accounts);
}
// Public view function to check if an address is whitelisted
function isWhitelisted(address account) public view returns (bool) {
return _whitelist[account];
}
// 3. Override the internal hook to check for whitelist status
function _preValidatePurchase(address beneficiary, uint256 weiAmount) internal view override {
// Call the parent contracts' validation first
super._preValidatePurchase(beneficiary, weiAmount);
// Add our new validation
require(isWhitelisted(beneficiary), "MyTokenSale: address is not whitelisted");
}
}
The super._preValidatePurchase(...) call is crucial. It ensures that the checks from CappedCrowdsale and TimedCrowdsale are still executed. We are simply adding our new whitelist check on top of them. This is the power of Solidity's inheritance and virtual functions.
Test your understanding!
A user reports they are whitelisted but their transaction to buyTokens is failing with the "address is not whitelisted" error. You've confirmed their address is in the _whitelist mapping. What is a likely reason for this, given the buyTokens function signature is buyTokens(address beneficiary)?
Show answer
The most likely reason is that they are calling buyTokens but specifying a different address as the beneficiary. Our check validates isWhitelisted(beneficiary). If a whitelisted user (msg.sender) tries to buy tokens for a non-whitelisted friend (beneficiary), the transaction will correctly fail. The beneficiary is the one who needs to be on the whitelist.
Conclusion
Congratulations! You have successfully added an essential access control feature to your crowdsale contract. You now have fine-grained control over who can participate in your token sale.
Key Takeaways:
- A whitelist is used to grant exclusive sale access for reasons like community rewards or regulatory compliance.
- There are three main implementation strategies: on-chain mapping, digital signatures, and Merkle trees, each with significant trade-offs in gas cost and complexity.
- The on-chain mapping method is simple to implement but costly to manage for large lists.
- The Merkle tree method is the industry standard for scalable, gas-efficient whitelists.
- You can integrate an on-chain whitelist into an OpenZeppelin
Crowdsalecontract by adding a mapping and overriding the_preValidatePurchasehook to check the beneficiary's status.
Preview of the Next Lesson:
Our crowdsale contract is now quite robust. However, as with any software, bugs can happen, and real-world conditions can be unpredictable. To ensure our contract is production-ready, we need to test it thoroughly. In the next lesson, you will learn how to write tests to ensure the crowdsale contract handles funds and token distribution correctly.