Back to glossary

Merkle Airdrop (Solidity Code Example)

Table of Contents

Example of an airdrop contract using a Merkle tree:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {MerkleProof} from "./MerkleProof.sol";

interface IToken {
    function mint(address to, uint256 amount) external;
}

contract Airdrop {
    event Claim(address to, uint256 amount);

    IToken public immutable token;
    bytes32 public immutable root;
    mapping(bytes32 => bool) public claimed;

    constructor(address _token, bytes32 _root) {
        token = IToken(_token);
        root = _root;
    }

    function getLeafHash(address to, uint256 amount)
        public
        pure
        returns (bytes32)
    {
        return keccak256(abi.encode(to, amount));
    }

    function claim(bytes32[] memory proof, address to, uint256 amount)
        external
    {
        // NOTE: (to, amount) cannot have duplicates
        bytes32 leaf = getLeafHash(to, amount);

        require(!claimed[leaf], "airdrop already claimed");
        require(MerkleProof.verify(proof, root, leaf), "invalid merkle proof");
        claimed[leaf] = true;

        token.mint(to, amount);

        emit Claim(to, amount);
    }
}

Token:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// ERC20 + mint + authorization
contract Token {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner, address indexed spender, uint256 value
    );

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    string public name;
    string public symbol;
    uint8 public decimals;
    mapping(address => bool) public authorized;

    constructor(string memory _name, string memory _symbol, uint8 _decimals) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        authorized[msg.sender] = true;
    }

    function setAuthorized(address addr, bool auth) external {
        require(authorized[msg.sender], "not authorized");
        authorized[addr] = auth;
    }

    function transfer(address recipient, uint256 amount)
        external
        returns (bool)
    {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool)
    {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function _mint(address to, uint256 amount) internal {
        balanceOf[to] += amount;
        totalSupply += amount;
        emit Transfer(address(0), to, amount);
    }

    function mint(address to, uint256 amount) external {
        require(authorized[msg.sender], "not authorized");
        _mint(to, amount);
    }
}

Libraries copied from OpenZeppelin:

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts

pragma solidity ^0.8.20;

import {Hashes} from "./Hashes.sol";

library MerkleProof {
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
        internal
        pure
        returns (bool)
    {
        return processProof(proof, leaf) == root;
    }

    function processProof(bytes32[] memory proof, bytes32 leaf)
        internal
        pure
        returns (bytes32)
    {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
        }
        return computedHash;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts

pragma solidity ^0.8.0;

library Hashes {
    function commutativeKeccak256(bytes32 a, bytes32 b)
        internal
        pure
        returns (bytes32)
    {
        return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a);
    }

    function _efficientKeccak256(bytes32 a, bytes32 b)
        private
        pure
        returns (bytes32 value)
    {
        /// @solidity memory-safe-assembly
        assembly {
            mstore(0x00, a)
            mstore(0x20, b)
            value := keccak256(0x00, 0x40)
        }
    }
}

Test:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

library MerkleHelper {
    // Bubble sort
    function sort(bytes32[] memory arr)
        internal
        pure
        returns (bytes32[] memory)
    {
        uint256 n = arr.length;
        for (uint256 i = 0; i < n; i++) {
            for (uint256 j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    (arr[j], arr[j + 1]) = (arr[j + 1], arr[j]);
                }
            }
        }

        return arr;
    }

    function yulKeccak256(bytes32 a, bytes32 b)
        internal
        pure
        returns (bytes32 v)
    {
        assembly {
            mstore(0x00, a)
            mstore(0x20, b)
            v := keccak256(0x00, 0x40)
        }
    }

    function calcRoot(bytes32[] memory hashes)
        internal
        pure
        returns (bytes32)
    {
        uint256 n = hashes.length;

        while (n > 1) {
            for (uint256 i = 0; i < n; i += 2) {
                bytes32 left = hashes[i];
                bytes32 right = hashes[i + 1 < n ? i + 1 : i];
                (left, right) = left <= right ? (left, right) : (right, left);
                hashes[i >> 1] = yulKeccak256(left, right);
            }
            n = (n + (n & 1)) >> 1;
        }

        return hashes[0];
    }

    function getProof(bytes32[] memory hashes, uint256 index)
        internal
        pure
        returns (bytes32[] memory)
    {
        bytes32[] memory proof = new bytes32[](0);
        uint256 len = 0;

        uint256 n = hashes.length;
        uint256 k = index;

        while (n > 1) {
            // Get proof for this level
            uint256 j = k & 1 == 1 ? k - 1 : (k + 1 < n ? k + 1 : k);
            bytes32 h = hashes[j];

            // proof.push(h)
            assembly {
                len := add(len, 1)
                let pos := add(proof, shl(5, len))
                mstore(pos, h)
                mstore(proof, len)
                mstore(0x40, add(pos, 0x20))
            }

            k >>= 1;

            // Calculate next level of hashes
            for (uint256 i = 0; i < n; i += 2) {
                bytes32 left = hashes[i];
                bytes32 right = hashes[i + 1 < n ? i + 1 : i];
                (left, right) = left <= right ? (left, right) : (right, left);
                hashes[i >> 1] = yulKeccak256(left, right);
            }
            n = (n + (n & 1)) >> 1;
        }

        return proof;
    }

    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
        internal
        pure
        returns (bool)
    {
        bytes32 h = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            (bytes32 left, bytes32 right) =
                h <= proof[i] ? (h, proof[i]) : (proof[i], h);
            h = yulKeccak256(left, right);
        }

        return h == root;
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test, console2} from "forge-std/Test.sol";
import {MerkleHelper} from "./MerkleHelper.sol";
import {Airdrop} from "../../../src/app/airdrop/Airdrop.sol";
import {Token} from "../../../src/app/airdrop/Token.sol";

contract AirdropTest is Test {
    Token private token;
    Airdrop private airdrop;

    struct Reward {
        address to;
        uint256 amount;
    }

    Reward[] private rewards;
    bytes32[] private hashes;
    mapping(bytes32 => Reward) private hashToReward;

    uint256 constant N = 100;

    function setUp() public {
        token = new Token("test", "TEST", 18);

        // Initialize users and airdrop amounts
        for (uint256 i = 0; i < N; i++) {
            rewards.push(
                Reward({to: address(uint160(i)), amount: (i + 1) * 100})
            );
            hashes.push(keccak256(abi.encode(rewards[i].to, rewards[i].amount)));
            hashToReward[hashes[i]] = rewards[i];
        }

        hashes = MerkleHelper.sort(hashes);

        bytes32 root = MerkleHelper.calcRoot(hashes);

        airdrop = new Airdrop(address(token), root);

        token.setAuthorized(address(airdrop), true);
    }

    function test_valid_proof() public {
        for (uint256 i = 0; i < N; i++) {
            bytes32 h = hashes[i];
            Reward memory reward = hashToReward[h];
            bytes32[] memory proof = MerkleHelper.getProof(hashes, i);

            airdrop.claim(proof, reward.to, reward.amount);
            assertEq(token.balanceOf(reward.to), reward.amount);
        }
    }
}

Related Terms

No items found.