In cryptography and computer science, a hash tree or Merkle tree is a tree in which every “leaf” (node) is labelled with the cryptographic hash of a data block. Merkle Trees are desirable for very long arrays of whitelist addresses because no matter how long the list is, by adding all of these addresses to the Merkle Tree, it will return a hash (called the “root”) which will contain every address and, thanks to cryptography, will be the same length no matter what. In order to prove that a whitelist address belongs to the root, we can write a function that checks for its existence inside the hash.
We will first unpack how this works. Then, we will generate our own Merkle Tree. Lastly, we will incorporate that code into a React App, so a whitelisted user can mint from our frontend.
The whitelist is composed of addresses and each address represents a leaf.
The Merkle Tree Proof is basically the path necessary to link the target “leaf” to the Root.
We will need to install the following:
npm install merkletreejs
npm install keccak256
This won’t be a Solidity tutorial. But as an example, I am building a simple ERC-721 Contract. You will need the MerkleProof.sol file imported to your main Solidity file.
https://github.com/protofire/zeppelin-solidity/blob/master/contracts/MerkleProof.sol
import './MerkleProof.sol'import '/ERC721.sol'
MerkleProof contains two functions: verify and processProof
In the main solidity file, all you will need is a mint function. We will also make a helper function that will allow you to check if a user is whitelisted.
function whitelistMint(uint256 quantity, bytes32[] calldata merkleproof) public payable
{
require(
isValid(merkleproof, keccak256(abi.encodePacked(msg.sender))),
"Not whitelisted"
);
_safeMint(msg.sender, quantity);
}function isValid(bytes32[] memory merkleproof, bytes32 leaf)public view returns (bool)
{
return MerkleProof.verify(merkleproof, merkleRoot, leaf);
}
Note that the isValid function we’ve written accepts a proof and a leaf. The whitelistMint function accepts a proof and requires that the sender isValid. The following line of code:
keccak256(abi.encodePacked(msg.sender))
This is the sender’s leaf.
Create a new js file. You will need the following:
const { MerkleTree } = require('merkletreejs')
const KECCAK256 = require('keccak256')
Store your whitelisted addresses in an array, like so:
const addresses = [
'0xEE0b356B07c57491b1F5B22Ab773F50D0761b100',
'0x1Ef783A063bbF0c62887E256bfEcC6dF93e14164',
'0xE72785921778Ec0004161BBDd5A160B1Ad5947F4',
'0x59806fddA0300CB31D5620C9cD49D4757C14f221',
'0x3243aD4099af28b77a3aB24c66d98D68AdeB6984',
'0xA9bd3F876E2153603bbc871dD566034774c370b5',
'0x7bBc228012689c5E1CC0D1175458B10fCA6cE063',
'0xc2eAdacB575BBdD4A434b8DB44477A678D60EE69',
'0x49af09BEB384739719Ab9a89F8C92A9c85dc84e3',
'0x3Bb63438736e7930B8148f73E402f1807f93e53d'
]
Our MerkleProof verify function requires three parameters: proof, leaf, and root.
First, let’s generate the leaves of the tree.
const leaves = addresses.map(x => KECCAK256(x));const tree = new MerkleTree(leaves, KECCAK256, { sortPairs: true })
In order for this to work, you need to add a ‘0x’ to the beginning of the generated hashes. Use this function:
const buf2hex = x => '0x' + x.toString('hex')
Now generate the root.
const root = buf2hex(tree.getRoot())
Lastly, let’s get the proof.
const proof = tree.getProof(leaf).map(x => buf2hex(x.data));
If we were to pass any of the addresses in the array into the leaf, convert it to a hash and get its proof.
const leaf = buf2hex(KECCAK256(addresses[2]));const proof = tree.getProof(leaf).map(x => buf2hex(x.data));console.log(tree.verify(proof, leaf, root))
This function uses the getProof function to pass in the leaf, and then create an array with 0x attached to each hash.
Add a console.log(root)
With node installed, you can run the file from terminal. Take that root and add it as a variable to your main Sol file.
bytes32 public merkleRoot =0x2d7545217ad74ec5c75f3427630b05946abd64f74da1c0949852e9d0d827d65e;
After generating the root and adding to the Solidity file, you can now compile and deploy the contract.
In your App.js file (or whatever your main js file is called in your React project), again add merkletreejs and keccak256.
const { MerkleTree } = require('merkletreejs')
const KECCAK256 = require('keccak256')
For the next part, I’ll be using the web3js library, but feel free to use ethers:
const abi = [
...
];const contractAddress = "YOUR_CONTRACT_ADDRESS";//Get the contract instanceconst web3 = await getWeb3();const accounts = await web3.eth.getAccounts();const networkId = await web3.eth.net.getId();const instance = new web3.eth.Contract(abi, contractAddress);
//Generate the treeconst leaves = addresses.map(x => KECCAK256(x));const tree = new MerkleTree(leaves, KECCAK256, { sortPairs: true })
You will need to add an array containing the ABI of the deployed contract and also the contract address saved to a string.
Add a function to buy tokens:
const BuyTokens = () => {let leaf = buf2hex(KECCAK256(accounts[0]));let proof = tree.getProof(leaf).map(x => buf2hex(x.data));let price = web3.utils.toWei('1', 'ether');await instance.methods.whitelistMint(1, proof).send({ from: accounts[0], value: price, gas: 300000 })
Lastly, in your HTML, add:
<button class="button" onClick={BuyTokens}> Mint!</button>
The function will check if the sender is whitelisted and then allows them to mint. Try it out and let me know how it goes!