Skip to content

Instantly share code, notes, and snippets.

@mediavrog
Created February 17, 2023 15:41
Show Gist options
  • Save mediavrog/75bc67ca7564d2d2fb790fe087a8c9ba to your computer and use it in GitHub Desktop.
Save mediavrog/75bc67ca7564d2d2fb790fe087a8c9ba to your computer and use it in GitHub Desktop.

Revisions

  1. mediavrog created this gist Feb 17, 2023.
    82 changes: 82 additions & 0 deletions merkle.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,82 @@
    import { task, types } from "hardhat/config";
    import fs from "fs";
    import { MerkleTree } from "merkletreejs";
    import keccak256 from "keccak256";

    // ethers library
    import "@nomiclabs/hardhat-ethers";

    interface PresaleEntry {
    address: string,
    amount?: number
    }

    interface PresaleEntryWithProof extends PresaleEntry {
    proof: string[]
    }

    interface MerkleEntries {
    [key: string]: PresaleEntryWithProof
    }

    export default task("merkle", "Create the merkle root for a list of addresses")
    .addParam("file", "The CSV file containing Ethereum addresses separated by a newline (\\n)")
    .addOptionalParam("includeAmounts", "Whether to encode amounts in the merkle tree", true, types.boolean)
    .addOptionalParam("safeEncode", "Whether to encode a `,` in between address and amount", false, types.boolean)
    .addOptionalParam("title", "Title for this merkle configuration", "", types.string)
    .setAction(async (taskArgs, { ethers }) => {
    const includeAmounts = !!taskArgs.includeAmounts;
    const safeEncode = !!taskArgs.safeEncode;
    const title = taskArgs.title;

    const data = fs.readFileSync(taskArgs.file, 'utf8');
    console.log(`\nContent of reserved list:\n`);
    console.log(data);

    const entries = data
    .split("\n")
    .filter((entry) => !!entry)
    .map((entryLine) => entryLine.split(","))
    .map((entry) => ({
    address: ethers.utils.getAddress(entry[0]), // normalize to checksum address
    ...(includeAmounts && entry[1] ? { amount: parseInt(entry[1], 10) } : {})
    } as PresaleEntry))

    // create merkle root
    const leaves = entries
    .map((entry) => {
    const types = entry.amount
    ? (safeEncode ? ['address', 'string', 'uint256'] : ['address', 'uint256'])
    : ['address']
    const values = entry.amount
    ? (safeEncode ? [entry.address, ',', entry.amount] : [entry.address, entry.amount])
    : [entry.address]

    return Buffer.from(ethers.utils.solidityKeccak256(types, values).slice(2), 'hex')
    })
    // console.log(leaves)
    const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true });
    // console.log(merkleTree)
    console.log(`\n${leaves.length} addresses in reserved list. Merkle root is:`);
    console.log(merkleTree.getHexRoot());

    const entriesWithProof = entries.reduce((agg, entry, index) => {
    agg[entry.address] = {
    ...entry,
    proof: merkleTree.getHexProof(leaves[index]),
    }
    return agg
    }, {} as MerkleEntries)

    // create file to use in frontend
    const websiteListContent = `{
    "root": "${merkleTree.getHexRoot()}",
    "lastUpdatedAt": ${new Date().getTime()},
    "title": "${title}",
    "encodingSafe": ${safeEncode},
    "entries": ${JSON.stringify(entriesWithProof, null, 2)}
    }`;
    const newFile = taskArgs.file.substr(0, taskArgs.file.lastIndexOf(".")) + ".merkle.json";
    fs.writeFileSync(newFile, websiteListContent);
    console.log(`Reserved list file for website saved to ${newFile}`);
    });