UNPKG

@gooddollar/goodprotocol

Version:
301 lines (261 loc) 11.6 kB
/** * this version is for testing purposes only, to test greputation initial rootstate with the fixed merkle v2 * which uses sorted pairs so no position is required and it uses double hash for preimage attack * future blockchain state hashes should be recalculated and created using similar method done here */ import { get, range, chunk, flatten, mergeWith, sortBy, uniq } from "lodash"; import fs from "fs"; import { MerkleTree } from "merkletreejs"; import PromisePool from "async-promise-pool"; import { ethers as Ethers } from "hardhat"; import { BigNumber } from "ethers"; const quantile = (sorted, q) => { const pos = (sorted.length - 1) * q; const base = Math.floor(pos); let sum = BigNumber.from(0); for (let i = 0; i < base; i++) sum = sum.add(sorted[i]); return sum; }; export const airdrop = (ethers: typeof Ethers) => { const fuseProvider = new ethers.providers.JsonRpcProvider("https://rpc.fuse.io"); const zxfastProvider = new ethers.providers.JsonRpcProvider("https://eth-eu.0xfast.com/rpc/free"); const cfProvider = new ethers.providers.InfuraProvider(1, "143f9cf968fe4c3da0db77ff525e0da4"); const goodActiveVotes = async (network = "fuse") => { let balances = []; const EPOCH = 60 * 60 * 24; const pool = new PromisePool({ concurrency: 30 }); const epochs = range(1646844945, (Date.now() / 1000).toFixed(0), EPOCH); const graphName = network === "fuse" ? "gooddollarfuse2" : "goodsubgraphs"; const graphUrl = `https://api.thegraph.com/subgraphs/name/gooddollar/${graphName}`; const graphQuery = async (start, skip, step = EPOCH) => { const query = `{ goodBalances(first: 1000 skip:${skip} where: { memberSince_gte: ${start} memberSince_lt:${start + step} }) { id coreBalance totalVotes activeVotes blockchainsBalance } }`; try { const { data = {}, errors } = await fetch(graphUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }) }).then(_ => _.json()); errors && console.log({ errors }); if (errors?.[0]?.message === "The `skip` argument must be between 0 and 5000, but is 6000") { throw new Error("skip"); } if (data?.goodBalances?.length === 1000) { return data.goodBalances.concat(await graphQuery(start, skip + 1000, step)); } return data.goodBalances || []; } catch (error) { if (error.message === "skip") throw error; console.log({ query, error }); return []; } }; const poolAdd = (start, step) => { pool.add(async () => { try { const goodBalances = await graphQuery(start, 0, step); balances = balances.concat(goodBalances); console.log({ curDate: start, step, records: goodBalances.length }); } catch (e) { if (e.message === "skip") { const newstep = Number((step / 4).toFixed(0)); console.log("creating sub epochs", { start, end: start + step, newstep }); const subepochs = range(start, start + step, newstep); subepochs.forEach(e => poolAdd(e, newstep)); } } }); }; epochs.forEach(e => { poolAdd(e, EPOCH); }); await pool.all(); console.log(`total ${network} GOOD Holders:`, balances.length, { uniques: uniq(balances.map(_ => _.id)).length, delegatees: uniq(balances.filter(_ => Number(_.coreBalance) < Number(_.totalVotes)).map(_ => _.id)).length, sample: balances.slice(0, 5) }); const treeData = {}; console.log("calculating leaves hashes..."); balances.forEach(record => { treeData[record.id] = { address: record.id, rep: record.activeVotes, hash: ethers.utils.keccak256( ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [record.id, record.activeVotes]) ) ) }; }); fs.writeFileSync( `goodCheckpoints/goodCheckpoint_${network}.json`, JSON.stringify({ treeData, balances, until: Date.now() }) ); console.log(balances.filter(_ => Number(_.coreBalance) < Number(_.totalVotes))); }; const collectAirdropData = async () => { const calcGoodMints = async () => { const step = 10000; const START_BLOCK = 14344739; const LAST_BLOCK = await good.provider.getBlockNumber(); console.log({ LAST_BLOCK }); const blocks = range(START_BLOCK, LAST_BLOCK, step); const allLogs = []; for (let blockChunk of chunk(blocks, 10)) { // Get the filter (the second null could be omitted) const ps = blockChunk.map(async (bc: number) => { const logs = await good.queryFilter(good.filters.Mint(), bc, Math.min(bc + step - 1, LAST_BLOCK)).catch(e => { console.log("block transfer logs failed retrying...", bc); return good.queryFilter(good.filters.Mint(), bc, Math.min(bc + step - 1, LAST_BLOCK)); }); const claimedLogs = await good .queryFilter(good.filters.StateHashProof(), bc, Math.min(bc + step - 1, LAST_BLOCK)) .catch(e => { console.log("block transfer logs failed retrying...", bc); return good.queryFilter(good.filters.StateHashProof(), bc, Math.min(bc + step - 1, LAST_BLOCK)); }); console.log("found logs:", claimedLogs.length, logs.length, bc); return logs.concat(claimedLogs); }); let chunkLogs = flatten(await Promise.all(ps)); console.log("chunk logs: ", chunkLogs.length, { blockChunk }); allLogs.push(chunkLogs); } const logs = flatten(allLogs); const mints = {}; const claims = {}; const result = {}; console.log("logs found:", logs.length); let totalMints = ethers.constants.Zero; logs.forEach(l => { if (l.event === "Mint") { mints[l.args._to.toLowerCase()] = (mints[l.args._to.toLowerCase()] || BigNumber.from("0")).add( l.args._amount ); totalMints = totalMints.add(l.args._amount); } else claims[l.args.user.toLowerCase()] = l.args.repBalance.toString(); }); Object.entries(mints).forEach(([k, v]) => (result[k] = { claim: "0", ...result[k], mint: v.toString() })); Object.entries(claims).forEach(([k, v]) => (result[k] = { mint: "0", ...result[k], claim: v.toString() })); console.log({ result, totalMints: totalMints.toString() }); fs.writeFileSync("goodCheckpointMainnet.json", JSON.stringify(result)); }; // await goodActiveVotes(); await goodActiveVotes("mainnet"); // await calcGoodMints(); }; const buildMerkleTree = async () => { const buildTree = async (network = "fuse") => { const good = await ethers .getContractAt("GReputation", "0x603B8C0F110E037b51A381CBCacAbb8d6c6E4543") .then(_ => _.connect(network === "fuse" ? fuseProvider : cfProvider)); const checkpoint = JSON.parse(fs.readFileSync(`goodCheckpoints/goodCheckpoint_${network}.json`).toString()); const treeData = checkpoint.treeData; let toTree: Array<[string, BigNumber]> = Object.entries(treeData).map(([k, v]) => { return [k, BigNumber.from((v as any).rep)]; }); toTree = toTree.sort((a, b) => (a[1].gte(b[1]) ? 1 : -1)).reverse(); // console.log({ toTree }); // const topContracts = toTree.filter(_ => _[2] === true); const totalReputation = toTree.reduce((c, a) => c.add(a[1]), BigNumber.from(0)); const totalSupply = (await good.totalSupply()).toString(); console.log({ totalSupply, totalReputation: totalReputation.toString(), numberOfAccounts: toTree.length }); const sorted = toTree.map(_ => _[1]); console.log(toTree.slice(0, 10).map(_ => [_[0], _[1]])); // fs.writeFileSync(`${folder}/reptree.json`, JSON.stringify(toTree)); console.log("Reputation Distribution"); [0.001, 0.01, 0.1, 0.5].forEach(q => { const quantileRep = quantile(sorted, q); console.log({ precentile: q * 100 + "%", addresses: (sorted.length * q).toFixed(0), quantileRep, rep: Number(quantileRep.div(1e10).toString()) / Number(totalReputation.div(1e10).toString()) }); }); const items = Object.values(treeData); const elements = items.map((e: any) => e.hash); console.log("creating merkletree sorted...", elements.length); //NOTICE: we use a non sorted merkletree to save generation time, this requires also a different proof verification algorithm which //is not in the default openzeppelin library const merkleTree = new MerkleTree(elements, ethers.utils.keccak256, { sortPairs: true }); // get the merkle root // returns 32 byte buffer const merkleRoot = merkleTree.getHexRoot(); console.log("Merkle Root:", merkleRoot); // generate merkle proof // returns array of 32 byte buffers const proof = merkleTree.getHexProof(elements[0]); const validProof = merkleTree.verify(proof, elements[0], merkleRoot); const lastProof = merkleTree.getHexProof(elements[elements.length - 1]); const lastValidProof = merkleTree.verify(lastProof, elements[elements.length - 1], merkleRoot); //check for possible address preimage const danger = (merkleTree.getHexLayers() as any).map(_ => _.find(_ => _.startsWith("0x000000"))); console.log({ danger, merkleRoot, proof, validProof, lastProof, lastValidProof, proofFor: items[0], lastProofFor: items[items.length - 1], index: elements.length, leaf: elements[elements.length - 1].toString("hex") }); checkpoint.merkleRoot = merkleRoot; checkpoint.totalSupply = totalSupply; fs.writeFileSync("goodCheckpoints/goodCheckpoint_fuse.json", JSON.stringify(checkpoint)); }; await buildTree(); await buildTree("mainnet"); }; const getProof = addr => { const { treeData, merkleRoot } = JSON.parse(fs.readFileSync("airdrop/airdrop.json").toString()); let entries = Object.entries(treeData as Tree); let elements = entries.map(e => Buffer.from(e[1].hash.slice(2), "hex")); console.log("creating merkletree sorted pairs...", elements.length); const merkleTree = new MerkleTree(elements, ethers.utils.keccak256, { sortPairs: true }); const calcMerkleRoot = merkleTree.getHexRoot(); console.log("merkleroots:", { fromFile: merkleRoot, calculated: calcMerkleRoot }); const addrData = treeData[addr] || treeData[addr.toLowerCase()]; const proofFor = Buffer.from(addrData.hash.slice(2), "hex"); const proof = merkleTree.getHexProof(proofFor); const proofIndex = elements.findIndex(_ => "0x" + _.toString("hex") === addrData.hash) + 1; console.log({ proofIndex, proof, [addr]: addrData }); console.log("checkProof:", merkleTree.verify(proof, proofFor, calcMerkleRoot)); }; return { buildMerkleTree, collectAirdropData, getProof }; }; const _timer = async (name, promise) => { const start = Date.now(); const res = await promise; const milis = Date.now() - start; console.log(`done task ${name} in ${milis / 1000} seconds`); return res; };