@gooddollar/goodprotocol
Version:
GoodDollar Protocol
301 lines (261 loc) • 11.6 kB
text/typescript
/**
* 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;
};