Merkle Airdrop
Contents
How to play?
- Airdrop a bunch of ERC20 tokens to users
- Deploy a smart contract
- Send an email with wallet address, token amount and proof to each user
- The user claims their tokens from the smart contract
Build a merkle tree
-
import { MerkleTree } from 'merkletreejs'; import keccak256 from 'keccak256'; import { solidityPacked } from 'ethers'; function main() { const whitelist = [ { address: '0x2173dEd80D3c17D2f349234a31ab8789D28Dd333', amount: 100 }, { address: '0x2AFAF65965e08DBd0e0aD7a5594E6b5BE398CBd6', amount: 200 }, { address: '0xfbc89Db3927bCCda61F550d3d9f3998750b7b93f', amount: 300 }, ]; const leaves = whitelist.map((x) => keccak256(solidityPacked(['address', 'uint256'], [x.address, x.amount]))); const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); const merkleRoot = tree.getRoot().toString('hex'); console.log('Merkle Root:', '0x' + merkleRoot); console.log('Merkle Tree:\n', tree.toString()); for (let i = 0; i < leaves.length; i++) { const proof = tree.getHexProof(leaves[i], i); console.log(`Proof for ${whitelist[i].address}: [${proof}]`); } } main();
Deploy a contract
-
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; contract MerkleAirdrop { bytes32 public merkleRoot; mapping(address => bool) public claimed; constructor(bytes32 _merkleRoot) { merkleRoot = _merkleRoot; } function claim(address user, uint256 amount, bytes32[] calldata proof) external { require(!claimed[user], "Already claimed"); bytes32 leaf = keccak256(abi.encodePacked(user, amount)); bool isValid = MerkleProof.verify(proof, merkleRoot, leaf); require(isValid, "Invalid proof"); claimed[user] = true; // the logic of minting NFT // ... } } -
See the depolyed MerkleAirdrop on Holesky Testnet