Merkle airdrop

   Merkle Tree,也叫默克尔树或哈希树,是区块链的底层加密技术,被和区块链广泛采用。Merkle Tree允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof)。对于有N个叶子结点的Merkle Tree,在已知root根值的情况下,验证某个数据是否有效(属于Merkle Tree叶子结点)只需要ceil(log₂N)个数据(也叫proof),非常高效。如果数据有误,或者给的proof错误,则无法还原出root根植。 忘记的同学可以参考下方。

        所以我们可以利用Merkel的特性。在链下,创建以账户地址和数量为叶子(addr, amount)的Merkel数,并计算出root hash。然后将roothash放到链上,这样就不需要在链上记录大量address和amount,节省gas。当空投开始后,不需要项目方花费gas,去给每一位用户空投,用户可以自行调用合约领取;

        有用户想要领取空投时,可以进行调用合约进行claim,其实就是验证merkel的roothash;由于链上已经保存了一份roothash,只要在链上使用用户提供的信息生成的roothash与之前保存的一致,就可以证明该用户享有领取空投的权利。

Solidity&Foundry Merkle Airdrop_区块链

准备工作

利用openzeppelin/merkle-tree,生成拥有空投资格用户的Merkel树.

import * as fs from 'fs'
import {StandardMerkleTree} from '@openzeppelin/merkle-tree'

// 1. build a tree
const elements = [
    ['0x0000000000000000000000000000000000000001', 1],
    ['0x0000000000000000000000000000000000000002', 2],
    ['0x0000000000000000000000000000000000000003', 3],
    ['0x0000000000000000000000000000000000000004', 4],
    ['0x0000000000000000000000000000000000000005', 5],
    ['0x0000000000000000000000000000000000000006', 6],
    ['0x0000000000000000000000000000000000000007', 7],
    ['0x0000000000000000000000000000000000000008', 8],
]

let merkleTree = StandardMerkleTree.of(elements, ['address', 'uint256'])
const root = merkleTree.root
const tree = merkleTree.dump()
console.log(merkleTree.render());
fs.writeFileSync('tree.json', JSON.stringify(tree))
fs.writeFileSync('root.json', JSON.stringify({root:root}))

// get proof
const proofs = []
const mtree = StandardMerkleTree.load(JSON.parse(fs.readFileSync("tree.json", "utf8")));
for (const [i, v] of mtree.entries()) {
  proofs.push({'account':v[0], 'amount':v[1],'proof':mtree.getProof(i)})
  if (v[0] === '0x0000000000000000000000000000000000000001') {
    const proof = mtree.getProof(i);
    console.log('Value:', v);
    console.log('Proof:', proof);
  }
}
fs.writeFileSync('proofs.json', JSON.stringify(proofs))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

 首先我们使用StandardMerkleTree.of生成了一个八个账户的merkel树并且记录了roothash;然后我们使用,getProof给每个用户都生成他自己的验证proof。用户想要领取空投的时候,需要提供自己proof——其实就是mekle的验证路径,这个一般都是由项目方保存就行了,保存在链下就可以了。这里还输出了三个json文件,这个三个文件,后边测试的时候,需要用到;

  • tree.json:merkle tree的信息;
  • root.json:merkle的roothash
  • proofs.json:所有用户的proof数据

链上合约

merkle 合约,这个我们使用openzeppelin的MerkelProof库,主要是把验证函数实现一下,就可以了。验证的时候,需要提供用户的proof,address,amount,就可以了;

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

import "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";

contract MerkleAirdrop {
    using MerkleProof for bytes32[];
    bytes32 private _root;

    constructor(bytes32 root) {
        _root = root;
    }

    function verify(
        bytes32[] memory proof,
        address account,
        uint amount
    ) public view returns (bool) {
        bytes32 leaf = keccak256(
            bytes.concat(keccak256(abi.encode(account, amount)))
        );
        return proof.verify(_root, leaf);
    }

    function verifyCalldata(
        bytes32[] calldata proof,
        address account,
        uint amount
    ) public view returns (bool) {
        bytes32 leaf = keccak256(
            bytes.concat(keccak256(abi.encode(account, amount)))
        );
        return proof.verifyCalldata(_root, leaf);
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

airdrop合约,主要实现了claim,享有空投资格的用户,调用之后,而就可以领取空投了;

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

    MockIToken public token;

    constructor(address _token, bytes32 _root) MerkleAirdrop(_root){
        token = MockIToken(_token);
    }

    function claim(bytes32[] memory proof, address account, uint256 amount)
        external returns (bool)
    {
        verify(proof, account, amount);

        token.mint(account, amount);

        emit Claim(account, amount);
    }
}

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

contract MockToken is ERC20 {
    constructor(string memory name, string memory symbol)
    ERC20(name, symbol) {}

    function mint(address account, uint amount) external {
        _mint(account, amount);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

 foundry测试

        使用foundry进行,测试部分比较简单,就是测试了,merkelproof的verify函数以及airdrop的cliam函数;这次测试比较有趣的部分是foundry的json解析部分。也不是特别难,大家可以自行搜索foundry的文档进行学习与联系。

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

import "forge-std/Test.sol";
import {stdJson} from "forge-std/StdJson.sol";
import "../src/MerkleAirdrop.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract MerkleAirdropTest is Test {
    using stdJson for string;

    struct Proof {
        address account;
        uint amount;
        bytes32[] proof;
    }

    string private _jsonTree = vm.readFile("test/data/tree.json");
    string private _jsonRoot = vm.readFile("test/data/root.json");
    string private _jsonProofs = vm.readFile("test/data/proofs.json");
    bytes32 private _rootHash = _jsonRoot.readBytes32(".root");
    MerkleAirdrop private _testing;
    Airdrop private airdrop;
    MockToken private token;

    function setUp() public {
        _testing = new MerkleAirdrop(_rootHash);
        token = new MockToken("test", "TEST");
        airdrop = new Airdrop(address(token), _rootHash);
    }

    function test_verify() external {
        Proof[] memory proofs = abi.decode(_jsonProofs.parseRaw(""), (Proof[]));
        for (uint i = 0; i < proofs.length; ++i) {
            assertTrue(
                _testing.verify(
                    proofs[i].proof,
                    proofs[i].account,
                    proofs[i].amount
                )
            );
        }
    }

    function test_claim() external {
        Proof[] memory proofs = abi.decode(vm.parseJson(_jsonProofs), (Proof[]));
        for (uint i = 0; i < proofs.length; ++i) {
            vm.expectEmit();
            emit Airdrop.Claim(proofs[i].account, proofs[i].amount);
            airdrop.claim(
                proofs[i].proof,
                proofs[i].account,
                proofs[i].amount
            );
            assertEq(token.balanceOf(proofs[i].account), proofs[i].amount);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.

 所有代码点这里;