LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Lập trình Blockchain, Lập trình Công cụ, Lập trình Smart Contract

Hướng dẫn viết Smart Contract sử dụng Merkle Tree để airdrop cho hàng trăm ngàn người dùng

Hướng dẫn viết Smart Contract sử dụng Merkle Tree để airdrop cho hàng chục ngàn người dùng

Hướng dẫn viết Smart Contract sử dụng Merkle Tree để airdrop cho hàng chục ngàn người dùng

Chia sẻ bài viết
5
(6)

Thường hầu hết các dự án sẽ có các chương trình airdrop token, NFT hoặc USDT cho member khi họ giúp ích cho dự án bằng nhiều cách khác nhau, thường thấy nhất là làm các nhiệm vụ tham gia group, channel telegram, follow/retweet twitter…Mỗi event sẽ có tầm vài ngàn đến vài chục ngàn, thậm chí vài trăm ngàn ví được nhận airdrop, vậy thì không thể nào bắn token cho từng ví được, vì như vậy rất tốn phí gas, ngoài ra còn ko tối ưu về tokenomic nữa.

Vậy nên cần phải làm 1 smart contract để người dùng tự lên claim phần thưởng về, nhưng mà mấy chục ngàn address kia lưu ở đâu? Không lẽ lưu vào 1 mảng trong contract? Việc này thứ nhất là làm lộ danh sách ví được claim, thứ 2 là contract size sẽ vượt ngưỡng không thể deploy được. Vậy làm 1 mapping rồi admin gọi hàm để update từng ví? Vậy thì lại quay lại bài toán tốn gas ban đầu, với 1 đến 2000 ví thì ok cắn răng trả được, chứ khoảng 200k ví thì bạn không muốn là người trả phí gas đâu .

Vậy giải pháp là gì? Đó là sử dụng Markle Tree, đây là giải pháp học được từ uniswap năm 2020 khi claim token mà họ thưởng cho mình, đợt đó mình được đâu 6k$.

MERKLE TREE là gì?

Merkle Tree là 1 kỹ thuật xây dựng cấu trúc dữ liệu để mã hoá thông tin, phục vụ việc xác thực xem mẩu dữ liệu cần xét có thuộc danh sách dữ liệu ban đầu hay không. Xưa kia nó hay được dùng trong mạng ngang hàng (torent chẳng hạn), xác định xem mẩu file mình tải về có đúng là thuộc file gốc hay không, và nhiều ứng dụng khác. Trong trường hợp của chúng ta, merkle tree được dùng để contract xác định xem ví đang gọi claim có thuộc danh sách claim hay không, trong khi contract không hề biết danh sách full, đồng thời dữ liệu lưu trên contract là cực kỳ nhỏ (chỉ lưu rootHash với khoảng 32 byte)

Cụ thể về merkle tree và cách cài đặt nó mời bạn tham khảo bài viết Cài đặt merkle tree bằng nodejs để phục vụ xác thực dữ liệu của mình.

Dùng Merkle Tree để giải bài toán Airdrop

Các bước thực hiện như sau:

  • B1. Từ danh sách ví airdrop, ta generate ra được rootHash, đem rootHash này lưu trên contract
  • B2. Khi người dùng truy cập web airdrop, web sẽ tiến hành kiểm tra ví này có thuộc list hay không và generate ra path (bước kiểm tra và gen path này có thể làm ở web hay server, tuỳ nhu cầu từng dự án)
  • B3. Call contract với path đã gen được, contract từ user address (msg.sender) và path truyền lên sẽ lội ngược lên đến rootHash và tiến hành so khớp với rootHash đã lưu, khớp thì hợp lệ.

Cài đặt Airdrop cho các địa chỉ ví với số lượng giống nhau

Ở đây mình cài đặt để thực hiện Airdrop trên mạng BSC-TESTNET để thực hiện Airdrop cho hàng trăm ngàn địa chỉ VÍ, các ví này được Airdrop số lượng như nhau. Chúng ta cần thực hiện như sau:

  • B1 – Tạo Danh sách Whitelist:
    Thông thường danh sách này có sẵn, nhưng do chúng ta test nên chúng ta cần viết một tool nhỏ bằng NodeJs để tạo danh sách ví đủ lớn ~ 200K ví.
  • B2 – Sinh ra rootHash:
    Viết một tool nhỏ khác để sinh ra rootHash từ tệp danh sách Whitelist ở trên. Giá trị rootHash này sẽ được sử dụng trong Smart Contract.
  • B3 – Viết service nhỏ để lấy Path
    Việc này có thể làm trực tiếp trên Frontend, hoặc tạo service nhỏ phía backend. Mục đích là khi người dùng kích vào nút Claim trên Frontend thì Frontend sẽ lấy được thông tin Path.
  • B4 – Viết contract để người dùng thực hiện Airdrop
    Viết contract để người dùng claim 1 BUSD.
  • B5 – Chạy thử
    Chúng ta test thử Smart Contract xem có chạy đúng như những gì chúng ta mong muốn không?

Toàn bộ source code trên Github tại địa chỉ: https://github.com/laptrinhbockchain/airdrop-distribution

B1 – Tạo danh sách Whitelist để test

Ta viết tool nhỏ bằng NodeJs sử dụng thư viện Web3 để sinh ra 200,000 địa chỉ ví, các địa chỉ ví này sẽ ghi ra tệp “whitelist.txt“.

Chi tiết code trong tệp generate-wallets.js:

const fs = require('fs');
const Web3 = require("web3");

const WALLET_NUM = 200000;
const BATCH_NUM = 5000;
const WHITELIST_FILE = "whitelist.txt";

function getWeb3() {
    var web3 = new Web3('https://data-seed-prebsc-2-s1.binance.org:8545');
    return web3;
}

function appendFile(filepath, lines) {
    let fd = fs.openSync(filepath, "a");
    lines.forEach(line => {
        fs.writeSync(fd, (line + "\n"));
    });
    fs.closeSync(fd);
}

function generateWallets() {
    let web3 = getWeb3();
    let batch = BATCH_NUM;
    let startTime = Date.now();
    let addresses = [];
    for (let idx=0; idx<WALLET_NUM; idx++) {
        let account = web3.eth.accounts.create();
        addresses.push(account.address);
        if (addresses.length>=batch) {
            appendFile(WHITELIST_FILE, addresses);
            addresses = [];
            console.log(`Generated ${idx+1}/${WALLET_NUM} addresses in ${((Date.now() - startTime)/(60*1000)).toFixed(2)} mins...`);
        }   
    }
    if (addresses.length>0) {
        console.log(`Generated ${WALLET_NUM}/${WALLET_NUM} addresses in ${((Date.now() - startTime)/(60*1000)).toFixed(2)} mins...`);
    }
    console.log("DONE!!!");
}

generateWallets();

Bạn đánh lệnh sau để sinh:

node .\generate-wallets.js

Trên máy mình hiện tại chạy mất tổng cộng khoảng 3.31 phút:

Thời gian sinh 200000 ví
Thời gian sinh 200000 ví

Hiện tại tệp này tôi đã tạo sẵn rùi, bạn xem tại: whitelist.txt. Khi sử dụng bạn nên thêm một vài địa chỉ ví test của bạn vào cuối tệp, để tiện sau này bạn còn chạy thử hàm claim().

B2 – Sinh ra rootHash

Chúng ta sử dụng thư viện utils\MerkleTree.js:

const Web3 = require('web3');

const web3Provider = new Web3.providers.HttpProvider('https://bsc-dataseed1.binance.org:443');
const web3 = new Web3(web3Provider);

const merkleTree = {
    leaves: null,
    root: null
};

function genRootHash(whiteList) {
    const leaves = genLeaveHashes(whiteList);
    merkleTree.leaves = leaves;
    merkleTree.root = buildMerkleTree(leaves);
    return merkleTree.root.hash;
}
exports.genRootHash = genRootHash;

function genLeaveHashes(chunks) {
    const leaves = []
    chunks.forEach((data) => {
        const hash = buildHash(data)
        const node = {
            hash,
            parent: null,
        }
        leaves.push(node)
    })
    return leaves
}

function buildMerkleTree(leaves) {
    const numLeaves = leaves.length
    if (numLeaves === 1) {
        return leaves[0]
    }
    const parents = []
    let i = 0
    while (i < numLeaves) {
        const leftChild = leaves[i]
        const rightChild = i + 1 < numLeaves ? leaves[i + 1] : leftChild
        parents.push(createParent(leftChild, rightChild))
        i += 2
    }
    return buildMerkleTree(parents)
}

function createParent(leftChild, rightChild) {
    const hash = leftChild.hash < rightChild.hash ? buildHash(leftChild.hash, rightChild.hash) : buildHash(rightChild.hash, leftChild.hash)
    const parent = {
        hash,
        parent: null,
        leftChild,
        rightChild
    }
    leftChild.parent = parent
    rightChild.parent = parent
    return parent
}

function buildHash(...data) {
    return web3.utils.soliditySha3(...data)
}

function getMerklePath(data) {
    const hash = buildHash(data)
    for (let i = 0; i < merkleTree.leaves.length; i += 1) {
        const leaf = merkleTree.leaves[i]
        if (leaf.hash === hash) {
            return generateMerklePath(leaf)
        }
    }
}
exports.getMerklePath = getMerklePath;

function generateMerklePath(node, path = []) {
    if (node.hash === merkleTree.root.hash) {
        return path
    }
    const isLeft = (node.parent.leftChild === node)
    if (isLeft) {
        path.push(node.parent.rightChild.hash)
    } else {
        path.push(node.parent.leftChild.hash)
    }
    return generateMerklePath(node.parent, path)
}

function verifyPath(data, path) {
    let hash = buildHash(data)
    for (let i = 0; i < path.length; i += 1) {
        hash = hash < path[i] ? buildHash(hash, path[i]) : buildHash(path[i], hash)
    }
    return hash === merkleTree.root.hash
}
exports.verifyPath = verifyPath;

Chúng ta sẽ viết tool để đọc toàn bộ danh sách whitelist và tạo rootHash. Code trong tệp generate-root-hash.js:

const fs = require('fs');
const readline = require('readline');
const MerkleTree = require('./utils/MerkleTree');

const WHITELIST_FILE = "whitelist.txt";

async function loadWhiteList() {
    let wallets = [];
    const fileStream = fs.createReadStream(WHITELIST_FILE);

    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
    });

    for await (const line of rl) {
        if (line) wallets.push(line);
    }
    return wallets;
}

async function generateRootHash() {
    let wallets = await loadWhiteList();
    console.log(`Number of wallets: ${wallets.length}`);
    let startTime = Date.now();
    let rootHash = MerkleTree.genRootHash(wallets);
    console.log(`Root Hash: ${rootHash}`);
    let diffTime = Date.now() - startTime;
    console.log(`Done in ${(diffTime/(60*1000)).toFixed(2)} mins!`);
}

generateRootHash();

Lệnh chạy như sau:

node generate-root-hash.js

Sau khi chạy ta được rootHash= 0xee2d6fd9478c1dbcd5ccc3d8ee275b0e75718392db1cdb13b19e561f61cb53bc và thời gian khoảng 0.22 phút.

Kết quả chạy sinh rootHash cho hơn 200K ví
Kết quả chạy sinh rootHash cho hơn 200K ví

B3 – Viết service nhỏ để lấy Path

Source code trong tệp get-airdrop-path.js:

const fs = require('fs');
const readline = require('readline');
const MerkleTree = require('./utils/MerkleTree');

const WHITELIST_FILE = "whitelist.txt";

async function loadWhiteList() {
    let wallets = [];
    const fileStream = fs.createReadStream(WHITELIST_FILE);

    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
    });

    for await (const line of rl) {
        if (line) wallets.push(line);
    }
    return wallets;
}

async function getAirdropPath(address) {
    // Generate Merkle Tree
    let wallets = await loadWhiteList();
    MerkleTree.genRootHash(wallets);

    // Get path
    let path = MerkleTree.getMerklePath(address);
    console.log(`Airdrop path for address ${address}: ${path}`);
}

getAirdropPath("0xE42C439D836708f43F77a65C198A2d7f55b3f3f4");

Lệnh chạy như sau:

node get-airdrop-info.js

Lệnh này hơi lâu do phải build lại cây Merkle Tree, trong thực tế việc tạo cây thực hiện lần đầu tiên khi chạy service, còn mỗi khi Frontend gọi lấy path thì nó không cần phải tạo lại nên sẽ rất nhanh. Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4:

[0x2f41f070959496430be8d042f46fdb59722ac0d2d2a2a2f6c57065f0238a3629, 0x90987bde2b69112eb4d71801f3b57dd9a0a552623efec75cc7d4a7c83f8ed58e, 0x45ce8fca3f0d055c8bfbaf5998bab44b28ee751c5d37a939bf1419aae980e42d, 0x497992f74ba1f271357bda3e781fd957f01c9f965ae43cbd9b3b960bc5f31ef1, 0xe82f37b843716e26a863ead91fa74b83d147a9fa50b4445dca1c2ad67599a7b8, 0xa0ed5400511455ab1ceb8cf5efe7dda517bedbc060f8d177eb561d4bc0dee386, 0x9cc31a5b4b70f7ed424fbccb62fe34518b721a4ff4277056539b6a1b512b0c86, 0x9dc7054bec1e23aa1b982d5c6b50618c459b3e9f37d9cf31155d2e2dab1734c0, 0x5718f3b6ef4000f4e76ff63c12d99ed39d34b257f7cde6566319dbc06943e438, 0x360a586473952dbc67a7e13a92126587589f03e2de8cfa1d1cefb070049f8558, 0x94ec4973b5c72034a348ae9d944c08826e455bca191fa205b1b2648d2df32298, 0xafe7b989357344a3e87f49398003326d33a66d4103d8501b17a4ca7601e114af, 0x909bd10f9611be622028d20be2876f8edf760a47509835deb5bfc10d4f95d2c5, 0x5b14ef07ce152c318b5e058339e361e7e28e3889017080cdd939f37fca0c55ec, 0x16528a79536ca3b7cacbf718411474788806841b607c54342f254e539c5597a7, 0x589e50d7721919474d2de9a8a23b1947d9aa7a0de4997f0f94252e11108bd218, 0xe60d4b6e447bf5a197d133a9aa390ac17b2b20eadfc864ea3fa22ce24c9c749b, 0x60d72eb5456e0b03de423dda767fccc13a91bb860aa5253cc76acdd5b874ac91]

Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4
Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4

B4 – Viết contract để người dùng thực hiện Airdrop

Bây giờ ta viết Smart Contract thực hiện Airdrop, khi người dùng Claim họ sẽ nhận được 1 BUSD. Mã nguồn trong tệp contracts/AirdropDistributor.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.6.11;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

/**
 * @dev These functions deal with verification of Merkle trees (hash trees),
 */
library MerkleProof {
    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     */
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        bytes32 computedHash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];

            if (computedHash <= proofElement) {
                // Hash(current computed hash + current element of the proof)
                computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
            } else {
                // Hash(current element of the proof + current computed hash)
                computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
            }
        }

        // Check if the computed hash (root) is equal to the provided root
        return computedHash == root;
    }
}

contract AirdropDistributor {
    address public token = 0x78867BbEeF44f2326bF8DDd1941a4439382EF2A7;                                    // BUSD token on BSC Testnet
    bytes32 public merkleRoot = 0xee2d6fd9478c1dbcd5ccc3d8ee275b0e75718392db1cdb13b19e561f61cb53bc;       // Merkle Root
    uint256 public airdropAmount = 1000000000000000000;
    mapping(address => bool) claimMarker;

    // This event is triggered whenever a call to #claim succeeds.
    event Claimed(address account, uint256 amount);

    constructor() public {
    }

    function claim(bytes32[] calldata path) external {
        require(!claimMarker[msg.sender], 'MerkleDistributor: Drop already claimed.');

        // Verify the merkle proof.
        bytes32 hash = keccak256(abi.encodePacked(msg.sender));
        require(MerkleProof.verify(path, merkleRoot, hash), 'MerkleDistributor: Invalid proof.');

        // Mark it claimed and send the token.
        claimMarker[msg.sender] = true;
        require(IERC20(token).transfer(msg.sender, airdropAmount), 'MerkleDistributor: Transfer failed.');

        emit Claimed(msg.sender, airdropAmount);
    }
}

Sau khi triển khai bằng Remix trên BSC Testnet ta có địa chỉ contract: 0xB48449541C9f7BA8fA7dd363D9b31AC6935C032c

B5 – Chạy thử

Bây giờ ta chuyển 10 BUSD vào contract trên để Contract có BUSD để thực hiện Airdrop. Bây giờ từ ví trong danh sách Airdrop ta gọi hàm Claim():

Gọi hàm Claim
Gọi hàm Claim

Giao dịch claim: 0xfc25ea539cb207b1e291aa4a1e4e8eef628985fc8c59afe3b2b1999af910d5e8

Bài viết này chúng ta giải quyết bài toán Airdrop cho các địa chỉ với cùng số lượng giống nhau, nếu mỗi địa chỉ 1 số lượng khác nhau thì phải làm thế nào?

Cài đặt Airdrop cho các địa chỉ ví với số lượng khác nhau

Nếu mỗi ví có số lượng token airdrop khác nhau thì ngoài tham số là địa chỉ ví thì còn cần tham số số lượng token airdrop.

Danh sách Whitelist vẫn như bên trên trong tệp whitelist.txt, nhưng bây giờ ta quy ước amount được tính như sau:
amount = 1 + (index % 10)
Trong đó index là thứ tự trong danh sách (Tương đương vị trí dòng trừ đi 1). Tất nhiên trong thực tế cài đặt ta phải nhân thêm với 10**decimal nữa.

Bây giờ ta sẽ tiến hành cài đặt trên mạng BSC-TESTNET. Các bước chúng ta cần thực hiện như sau:

  • B1 – Sinh ra rootHash:
    Viết một tool nhỏ khác để sinh ra rootHash từ tệp danh sách Whitelist ở trên. Giá trị rootHash này sẽ được sử dụng trong Smart Contract.
  • B2 – Viết service nhỏ để lấy Path
    Việc này có thể làm trực tiếp trên Frontend, hoặc tạo service nhỏ phía backend. Mục đích là khi người dùng kích vào nút Claim trên Frontend thì Frontend sẽ lấy được thông tin Path Amount.
  • B3 – Viết contract để người dùng thực hiện Airdrop
    Viết contract để người dùng có thể thực hiện claim, hoặc có thể thực hiện claim cho một địa chỉ ví bất kỳ.
  • B4 – Chạy thử
    Chúng ta test thử Smart Contract xem có chạy đúng như những gì chúng ta mong muốn không?

Toàn bộ source code vẫn trên Github tại địa chỉ: https://github.com/laptrinhbockchain/airdrop-distribution

B1 – Sinh ra rootHash

Chúng ta sử dụng thư viện utils\MerkleTree1.js. So với thư viện cũ thì thư viện này sửa một chút ở đoạn sinh hash, cái cũ sinh hash từ địa chỉ ví thì cái mới sinh hash từ địa chỉ ví và số lượng token.

const Web3 = require('web3');

const web3Provider = new Web3.providers.HttpProvider('https://bsc-dataseed1.binance.org:443');
const web3 = new Web3(web3Provider);

const merkleTree = {
    leaves: null,
    root: null
};

// whiteList is array of object: { address, amount }
function genRootHash(whiteList) {
    const leaves = genLeaveHashes(whiteList);
    merkleTree.leaves = leaves;
    merkleTree.root = buildMerkleTree(leaves);
    return merkleTree.root.hash;
}
exports.genRootHash = genRootHash;

function genLeaveHashes(chunks) {
    const leaves = []
    chunks.forEach((data) => {
        const hash = buildHash(data.address, data.amount);
        const node = {
            hash,
            parent: null,
        }
        leaves.push(node)
    })
    return leaves
}

function buildMerkleTree(leaves) {
    const numLeaves = leaves.length
    if (numLeaves === 1) {
        return leaves[0]
    }
    const parents = []
    let i = 0
    while (i < numLeaves) {
        const leftChild = leaves[i]
        const rightChild = i + 1 < numLeaves ? leaves[i + 1] : leftChild
        parents.push(createParent(leftChild, rightChild))
        i += 2
    }
    return buildMerkleTree(parents)
}

function createParent(leftChild, rightChild) {
    const hash = leftChild.hash < rightChild.hash ? buildHash(leftChild.hash, rightChild.hash) : buildHash(rightChild.hash, leftChild.hash)
    const parent = {
        hash,
        parent: null,
        leftChild,
        rightChild
    }
    leftChild.parent = parent
    rightChild.parent = parent
    return parent
}

function buildHash(...data) {
    return web3.utils.soliditySha3(...data)
}

function getMerklePath(data) {
    const hash = buildHash(data.address, data.amount);
    for (let i = 0; i < merkleTree.leaves.length; i += 1) {
        const leaf = merkleTree.leaves[i]
        if (leaf.hash === hash) {
            return generateMerklePath(leaf)
        }
    }
}
exports.getMerklePath = getMerklePath;

function generateMerklePath(node, path = []) {
    if (node.hash === merkleTree.root.hash) {
        return path
    }
    const isLeft = (node.parent.leftChild === node)
    if (isLeft) {
        path.push(node.parent.rightChild.hash)
    } else {
        path.push(node.parent.leftChild.hash)
    }
    return generateMerklePath(node.parent, path)
}

function verifyPath(data, path) {
    let hash = buildHash(data)
    for (let i = 0; i < path.length; i += 1) {
        hash = hash < path[i] ? buildHash(hash, path[i]) : buildHash(path[i], hash)
    }
    return hash === merkleTree.root.hash
}
exports.verifyPath = verifyPath;

Chúng ta sẽ viết tool để đọc toàn bộ danh sách whitelist và tạo rootHash. Code trong tệp generate-root-hash1.js:

const fs = require('fs');
const readline = require('readline');
const MerkleTree1 = require('./utils/MerkleTree1');

const WHITELIST_FILE = "whitelist.txt";

async function loadWhiteList() {
    let wallets = [];
    const fileStream = fs.createReadStream(WHITELIST_FILE);

    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
    });

    for await (const line of rl) {
        if (line) {
            let idx = wallets.length;
            let amount = BigInt((1 + idx%10)*10**18).toString();
            wallets.push({ address: line, amount: amount });
        }
    }
    return wallets;
}

async function generateRootHash() {
    let wallets = await loadWhiteList();
    console.log(`Number of wallets: ${wallets.length}`);
    let startTime = Date.now();
    let rootHash = MerkleTree1.genRootHash(wallets);
    console.log(`Root Hash: ${rootHash}`);
    let diffTime = Date.now() - startTime;
    console.log(`Done in ${(diffTime/(60*1000)).toFixed(2)} mins!`);
}

generateRootHash();

Lệnh chạy như sau:

node generate-root-hash1.js

Sau khi chạy ta được rootHash= 0x36d8b974d3ee6f19502f2c8c114dc0c1577b94cb94aaccf72bc44dc582362eca thời gian khoảng 0.26 phút.

Kết quả chạy sinh rootHash cho hơn 200K ví
Kết quả chạy sinh rootHash cho hơn 200K ví

B2 – Viết service nhỏ để lấy Path và Amount

Source code trong tệp get-airdrop-path1.js:

const fs = require('fs');
const readline = require('readline');
const MerkleTree1 = require('./utils/MerkleTree1');

const WHITELIST_FILE = "whitelist.txt";

async function loadWhiteList() {
    let wallets = [];
    const fileStream = fs.createReadStream(WHITELIST_FILE);

    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
    });

    for await (const line of rl) {
        let idx = wallets.length;
        let amount = BigInt((1 + idx%10)*10**18).toString();
        wallets.push({ address: line, amount: amount });
    }
    return wallets;
}

async function getAirdropPath(address) {
    // Generate Merkle Tree
    let wallets = await loadWhiteList();
    MerkleTree1.genRootHash(wallets);

    // Get path
    let wallet = wallets.find(item => item.address==address);
    let path = MerkleTree1.getMerklePath(wallet);
    console.log(`Airdrop path for address ${address} (${wallet.amount}): ${path}`);
}

getAirdropPath("0xE42C439D836708f43F77a65C198A2d7f55b3f3f4");

Lệnh chạy như sau:

node get-airdrop-info1.js

Lệnh này hơi lâu do phải build lại cây Merkle Tree, trong thực tế việc tạo cây thực hiện lần đầu tiên khi chạy service, còn mỗi khi Frontend gọi lấy path thì nó không cần phải tạo lại nên sẽ rất nhanh. Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4 với amount=1000000000000000000:

[0x08ed75e790cba66f8936e7cb2e66eb2de6b911bc2472c14a458dcda29ee8202c,0xc5c3cfedab71019d157cdb2506dced3f8c30ae5802c0f8d9cd95e85dd6d2c052,0xc11039c1fce63879169587080a6d4df737d8858741f9d9c8bf319a6cac8c061e,0x99da5cc9ed50fd0828ffa29b3fa14889520d42b11eebdd6b36c0a9c92cd10f65,0x067e290d25227e6d5b423095d3595d4f222a8f840238979824d43b3bc14ada33,0xdabc7df3129ffa4141f8e4f39c7b42cd9a1963c0bef677a28bd73ff04280cdf5,0x56dd8395cfbc7a0e4fe76c62ec3e9d409f61800e2bb3936369b95b50197a29d3,0xe9e771eeb7e12e33d5c203372ba190ee47109c9a67c5abeceaf9eef4089b03e6,0x04ab1f5dbcbfc16293fb0119291dca315fd0b04b63f92a2ba7ce2cba1bb4d0fb,0x78b9decb52a02a7b76d632b48ecf817dd801da58ad10b212963d974b1cfc770c,0xe8adc42bb5f459864bf6ebb94afb1e922f89344c9e6478496537b0e3d4e837e8,0xbb24a7eb6bc5eedc58f0f45aa751da48191fcaacdc5704792482c43ab1b86607,0x8fa439b3acaf85418c0969946cb3af1f574d2765af6a4e9b26d877af270b3bc8,0x2ced1d3760c9617073e6cacbb0e3eeaafc576e8e61100ab03efd5101b3e3cb7e,0x74e2df364e51c8ee63feda483bb9a90864ab76be5e4fe977e252679fa6652a01,0xfd2c7dac8ca8b53ea0641628b7d96de3c1310a5d3d4e45573f1b1d6e0f9928cd,0x3058afd6289de912b84c11a17ce2eaf789ff7e569f3ff73ac90a64568ebdc533,0xa726e6c76ed52d427b785ee8520945963115ef11da9c1d1c3a0dcbf0adca503e]

Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4
Kết quả path cho địa chỉ 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4

B3 – Viết contract để người dùng thực hiện Airdrop

Bây giờ ta viết Smart Contract thực hiện Airdrop, khi người dùng Claim đúng số lượng, họ sẽ nhận được số BUSD tương ứng. Trong Smart Contract này, tôi sửa để ví bất kỳ có thể thực hiện Claim cho ví khác. Mã nguồn trong tệp contracts/AirdropDistributor1.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.6.11;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

/**
 * @dev These functions deal with verification of Merkle trees (hash trees),
 */
library MerkleProof {
    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     */
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        bytes32 computedHash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];

            if (computedHash <= proofElement) {
                // Hash(current computed hash + current element of the proof)
                computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
            } else {
                // Hash(current element of the proof + current computed hash)
                computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
            }
        }

        // Check if the computed hash (root) is equal to the provided root
        return computedHash == root;
    }
}

contract AirdropDistributor1 {
    address public token = 0x78867BbEeF44f2326bF8DDd1941a4439382EF2A7;                                    // BUSD token on BSC Testnet
    bytes32 public merkleRoot = 0x36d8b974d3ee6f19502f2c8c114dc0c1577b94cb94aaccf72bc44dc582362eca;       // Merkle Root
    mapping(address => bool) claimMarker;

    // This event is triggered whenever a call to #claim succeeds.
    event Claimed(address account, uint256 amount);

    constructor() public {
    }

    function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external {
        require(!claimMarker[account], 'MerkleDistributor: Drop already claimed.');

        // Verify the merkle proof.
        bytes32 hash = keccak256(abi.encodePacked(account, amount));
        require(MerkleProof.verify(merkleProof, merkleRoot, hash), 'MerkleDistributor: Invalid proof.');

        // Mark it claimed and send the token.
        claimMarker[account] = true;
        require(IERC20(token).transfer(account, amount), 'MerkleDistributor: Transfer failed.');

        emit Claimed(account, amount);
    }
}

Sau khi triển khai bằng Remix trên BSC Testnet ta có địa chỉ contract: 0xB697edA6047cA15A8043A9731b180a3b40B3aB0D

B4 – Chạy thử

Bây giờ ta chuyển 100 BUSD vào contract trên để Contract có BUSD để thực hiện Airdrop. Bây giờ từ ví trong danh sách Airdrop ta gọi hàm Claim():

Gọi hàm Claim
Gọi hàm Claim

Giao dịch claim 1: 0x6cfe4f68f9aa33659d375629b4efa2ca09b04f4ff7b51d9590aaf9beb71521e5

Giao dịch claim 2: 0xc4ea94ba3c5e8610a169542e8439cbb0848fb3a38775e88a0afdf62b302aca14

Nguồn:

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 6

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

5 Bình luận

  1. BlockChaien

    Bài viết quá hay, tuy nhiên có đoạn này mình thấy vẫn chưa có giải pháp:
    *Không lẽ lưu vào 1 mảng trong contract? … thứ 2 là contract size sẽ vượt ngưỡng không thể deploy được. *
    Trong contract claim token thì mỗi khi 1 ví claim xong sẽ push vào mảng *claimMarker*, như vậy thì theo lý thuyết thì cuối cùng contract vẫn lưu toàn bộ mảng địa chỉ nhận Airdrop. Có cách nào tối ưu hơn để kiểm tra 1 địa chỉ đã nhận Airdrop hay chưa mà không cần lưu hết vào 1 mảng như vậy không admin?

    • Phần này đã giải quyết rồi bạn nhé. Contract chỉ lưu đúng 1 cái là rootHash thôi, kích thước rất nhỏ. Khi một ví muốn claim token cần truyền path vào là verify được nhé!

  2. fomoguy

    Bài này áp dụng cho repo Merkle Distributor của Uniswap được không vậy Ad.Nghĩa là mình deploy contract này, rồi thay cho contract Airdrop của Uniswap trong Frontend đó, nó có nhận không?

    • Không rõ ý bạn. Quan trong sau khi verify phải thực hiện chuyển token airdrop cho người dùng. Bạn lấy đâu ra token UNI mà gửi cho người dùng.

      • fomoguy

        Mình đang vọc bản uniswap interface v2 ấy ad, trong đó có chức năng claim Uni airrop trên đó.Định vọc testnet, mình sẽ fork Token Uni test, mà mình chưa biết các bước để chạy cái merkle disrributor trên github của uniswap. Ví dụ: bước 1 là tạo cái địa chỉ example.json, bước 2 chạy lệnh generate merkle distributor, bước 3 verify, …. rồi bước nào mới ra cái địa chỉ contract Merkle_distributor thay cho địa chỉ của uniswap ấy ad??

Trả lời

Giao diện bởi Anders Norén