mirror of
https://github.com/luxfi/oracle.git
synced 2026-11-03 09:43:36 +00:00
696 lines
28 KiB
JavaScript
696 lines
28 KiB
JavaScript
/**
|
|
*
|
|
* This script calculates transaction fees spent by voters who (1) revealed votes and (2) claimed voting rewards
|
|
* during a specified time period. The output is a JSON that displays the amount of UMA or ETH that can be used to "rebate"
|
|
* these voters for their transaction costs incurred while voting.
|
|
*
|
|
* This script therefore relies on accurate (1) average gas price data over the specified period (i.e. in gwei), (2) average
|
|
* ETH price data over the period, and (3) average UMA price data over the period. It also needs the current UMA-ETH price.
|
|
*
|
|
* Run: (from repo root) node ./packages/affiliates/gas-rebate/VoterGasRebate.js --rebateNumber 4 --start-block 11112533 --end-block 11413784 --network mainnet_mnemonic
|
|
*
|
|
* Config options (can be specified as CLI flags):
|
|
* - start: {Number, optional} start timestamp to query Reveal and Claim events from, described in Unix time in seconds.
|
|
* Defaults to default end time - 5 days.
|
|
* - end: {Number, optional} end timestamp to query Reveal and Claim events up to, described in Unix time in seconds.
|
|
* Defaults to current time minus 1 day.
|
|
* - start-block: {Number, optional} start block to query Reveal and Claim events from.
|
|
* Defaults to default end time - 5 days.
|
|
* - end-block: {Number, optional} end block to query Reveal and Claim events up to.
|
|
* Defaults to current time minus 1 day.
|
|
* - reveal-only: {Boolean, optional} Only query and parse Reveal events. Skip Claim events
|
|
* Defaults to false.
|
|
* - claim-only: {Boolean, optional} Only query and parse Claim events. Skip Reveal events
|
|
* Defaults to false.
|
|
*/
|
|
|
|
require("dotenv").config();
|
|
const moment = require("moment");
|
|
const fetch = require("node-fetch");
|
|
const cliProgress = require("cli-progress");
|
|
const argv = require("minimist")(process.argv.slice(), {
|
|
string: ["start", "end", "rebateNumber", "start-block", "end-block"],
|
|
boolean: ["reveal-only", "claim-only"],
|
|
});
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
// Locally stored list of DesignatedVoting wallets to substitute with their hot wallets:
|
|
let addressesToReplace = [];
|
|
try {
|
|
addressesToReplace = require("./2KEY_ADDRESS_OVERRIDE.json").map((a) => a.toLowerCase());
|
|
} catch (err) {
|
|
// Do nothing, file doesn't exist.
|
|
}
|
|
|
|
const FindBlockAtTimestamp = require("./FindBlockAtTimeStamp");
|
|
const { getAbi, getAddress } = require("@uma/contracts-node");
|
|
const { getWeb3 } = require("@uma/common");
|
|
/** *****************************************
|
|
*
|
|
* SETUP
|
|
*
|
|
*******************************************/
|
|
const web3 = getWeb3();
|
|
const { toBN, toWei, fromWei, toChecksumAddress } = web3.utils;
|
|
const SCALING_FACTOR = toBN(toWei("1"));
|
|
const multibar = new cliProgress.MultiBar(
|
|
{
|
|
format: "{label} [{bar}] {percentage}% | ⏳ ETA: {eta}s | events parsed: {value}/{total}",
|
|
hideCursor: true,
|
|
clearOnComplete: false,
|
|
stopOnComplete: true,
|
|
},
|
|
cliProgress.Presets.shades_classic
|
|
);
|
|
// Keep track of useful metrics for this current script run, such as unique identifiers seen across all claim and reveal events:
|
|
const metrics = {};
|
|
metrics["identifiers"] = [];
|
|
|
|
/** *****************************************
|
|
*
|
|
* HELPER MODULES
|
|
*
|
|
*******************************************/
|
|
// Return the day that the timestamp falls into. Used to pull the daily average
|
|
// gas/ETH price for a specific timestamp from an array of historical data from Etherscan Pro API.
|
|
function getDataForTimestamp(dayData, timestamp) {
|
|
const sortedDayData = dayData.sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
// If timestamp is before any of the days, return the earliest day.
|
|
if (timestamp < sortedDayData[0].timestamp) return sortedDayData[0];
|
|
|
|
for (let i = 0; i < sortedDayData.length - 1; i++) {
|
|
if (timestamp >= sortedDayData[i].timestamp && timestamp < sortedDayData[i + 1].timestamp) {
|
|
return sortedDayData[i];
|
|
}
|
|
}
|
|
|
|
// If we get here, then we will just use last day.
|
|
return sortedDayData[sortedDayData.length - 1];
|
|
}
|
|
async function parseRevealEvents({ committedVotes, revealedVotes, priceData, rebateOutput, debug = false }) {
|
|
// Any entries in `revealVotersToRebate` will result in an UMA rebate.
|
|
const revealVotersToRebate = {};
|
|
// Store transaction hashes so that we can prevent duplicate rebates for different Reveals within the same batch transaction.
|
|
const batchTxns = {};
|
|
|
|
let progressBarReveal;
|
|
if (!debug) {
|
|
progressBarReveal = multibar.create(revealedVotes.length, 0, { label: "Reveal Events" });
|
|
}
|
|
|
|
for (let i = 0; i < revealedVotes.length; i++) {
|
|
try {
|
|
const reveal = revealedVotes[i];
|
|
|
|
const voter = reveal.returnValues.voter;
|
|
const roundId = reveal.returnValues.roundId;
|
|
const identifier = web3.utils.hexToUtf8(reveal.returnValues.identifier);
|
|
const requestTime = reveal.returnValues.time;
|
|
const [transactionBlock, transactionReceipt] = await Promise.all([
|
|
web3.eth.getBlock(reveal.blockNumber),
|
|
web3.eth.getTransactionReceipt(reveal.transactionHash),
|
|
]);
|
|
|
|
// Metrics:
|
|
const uniqueVoteKey = `${identifier}-${requestTime}`;
|
|
if (metrics.identifiers.indexOf(uniqueVoteKey) === -1) metrics.identifiers.push(uniqueVoteKey);
|
|
|
|
const key = `${voter}-${roundId}-${identifier}-${requestTime}`;
|
|
const val = {
|
|
voter,
|
|
roundId,
|
|
identifier,
|
|
requestTime,
|
|
reveal: {
|
|
transactionBlock: transactionBlock.number,
|
|
hash: transactionReceipt.transactionHash,
|
|
gasUsed: parseInt(transactionReceipt.gasUsed),
|
|
txnTimestamp: transactionBlock.timestamp,
|
|
},
|
|
};
|
|
|
|
// Try to find associated commit with this reveal
|
|
const latestCommitEvent = committedVotes.find((e) => {
|
|
return (
|
|
e.returnValues.voter === voter &&
|
|
e.returnValues.roundId === roundId &&
|
|
e.returnValues.identifier === reveal.returnValues.identifier &&
|
|
e.returnValues.time === requestTime
|
|
);
|
|
});
|
|
if (latestCommitEvent) {
|
|
const [commitBlock, commitReceipt] = await Promise.all([
|
|
web3.eth.getBlock(latestCommitEvent.blockNumber),
|
|
web3.eth.getTransactionReceipt(latestCommitEvent.transactionHash),
|
|
]);
|
|
val.commit = {
|
|
transactionBlock: commitBlock.number,
|
|
hash: commitReceipt.transactionHash,
|
|
gasUsed: parseInt(commitReceipt.gasUsed),
|
|
txnTimestamp: commitBlock.timestamp,
|
|
};
|
|
} else {
|
|
console.error(
|
|
`Could not find VoteCommitted event matching the reveal event: ${JSON.stringify(reveal.returnValues)}`
|
|
);
|
|
}
|
|
|
|
// Save and continue to lookup txn data for next event. Skip this reveal if it was already included as
|
|
// part of a batch transaction.
|
|
if (!batchTxns[transactionReceipt.transactionHash]) {
|
|
batchTxns[transactionReceipt.transactionHash] = true;
|
|
revealVotersToRebate[key] = val;
|
|
}
|
|
|
|
if (progressBarReveal) {
|
|
progressBarReveal.update(i + 1);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to query data for reveal", revealedVotes[i], err);
|
|
}
|
|
}
|
|
if (progressBarReveal) {
|
|
progressBarReveal.stop();
|
|
}
|
|
|
|
// Rebate voters
|
|
const rebateReceipts = {};
|
|
let totalGasUsed = 0;
|
|
let totalEthSpent = 0;
|
|
let totalUmaRepaid = 0;
|
|
let umaToPay;
|
|
for (let voterKey of Object.keys(revealVotersToRebate)) {
|
|
// Reveal
|
|
const revealData = revealVotersToRebate[voterKey].reveal;
|
|
let revealGasUsed = revealData.gasUsed;
|
|
totalGasUsed += revealGasUsed;
|
|
const revealGasData = getDataForTimestamp(priceData.dailyAvgGasPrices, revealData.txnTimestamp);
|
|
let revealEthToPay = toBN(toWei(revealGasData.avgGwei, "gwei")).mul(toBN(revealGasUsed));
|
|
let ethToPay = revealEthToPay;
|
|
const revealUmaData = getDataForTimestamp(priceData.dailyAvgUmaEthPrices, revealData.txnTimestamp);
|
|
let revealUmaToPay = revealEthToPay.mul(SCALING_FACTOR).div(toBN(toWei(revealUmaData.avgPx)));
|
|
umaToPay = revealUmaToPay;
|
|
|
|
// Commit
|
|
const commitData = revealVotersToRebate[voterKey].commit;
|
|
let commitGasData, commitUmaData, commitGasUsed, commitEthToPay, commitUmaToPay;
|
|
if (commitData) {
|
|
commitGasUsed = commitData.gasUsed;
|
|
totalGasUsed += commitGasUsed;
|
|
commitGasData = getDataForTimestamp(priceData.dailyAvgGasPrices, commitData.txnTimestamp);
|
|
commitEthToPay = toBN(toWei(commitGasData.avgGwei, "gwei")).mul(toBN(commitGasUsed));
|
|
ethToPay = ethToPay.add(commitEthToPay);
|
|
commitUmaData = getDataForTimestamp(priceData.dailyAvgUmaEthPrices, commitData.txnTimestamp);
|
|
commitUmaToPay = commitEthToPay.mul(SCALING_FACTOR).div(toBN(toWei(commitUmaData.avgPx)));
|
|
umaToPay = umaToPay.add(commitUmaToPay);
|
|
}
|
|
|
|
const revealTxn = revealData.hash;
|
|
const commitTxn = commitData ? commitData.hash : "N/A";
|
|
|
|
totalEthSpent += Number(fromWei(ethToPay.toString()));
|
|
totalUmaRepaid += Number(fromWei(umaToPay.toString()));
|
|
|
|
rebateReceipts[voterKey] = {
|
|
revealTimestamp: revealData.txnTimestamp,
|
|
revealGasUsed,
|
|
revealGasPrice: revealGasData.avgGwei,
|
|
revealEthSpent: Number(fromWei(revealEthToPay)),
|
|
revealUmaSpent: Number(fromWei(revealUmaToPay)),
|
|
revealUmaEthPrice: revealUmaData.avgPx,
|
|
revealTxn,
|
|
commitTimestamp: commitData ? commitData.txnTimestamp : "N/A",
|
|
commitGasUsed,
|
|
commitGasPrice: commitGasData ? commitGasData.avgGwei : "N/A",
|
|
commitEthSpent: commitEthToPay ? Number(fromWei(commitEthToPay)) : "N/A",
|
|
commitUmaSpent: commitUmaToPay ? Number(fromWei(commitUmaToPay)) : "N/A",
|
|
commitUmaEthPrice: commitUmaData ? commitUmaData.avgPx : "N/A",
|
|
commitTxn,
|
|
ethToPay: Number(fromWei(ethToPay)),
|
|
umaToPay: Number(fromWei(umaToPay)),
|
|
};
|
|
|
|
const voter = revealVotersToRebate[voterKey].voter;
|
|
if (rebateOutput.shareHolderPayout[voter.toLowerCase()]) {
|
|
rebateOutput.shareHolderPayout[voter.toLowerCase()] += Number(fromWei(umaToPay.toString()));
|
|
} else {
|
|
rebateOutput.shareHolderPayout[voter.toLowerCase()] = Number(fromWei(umaToPay.toString()));
|
|
}
|
|
}
|
|
|
|
return {
|
|
rebateReceipts,
|
|
totals: { totalGasUsed: totalGasUsed, totalEthSpent: totalEthSpent, totalUmaRepaid: totalUmaRepaid },
|
|
};
|
|
}
|
|
|
|
// The UMA dev account sometimes claims rewards on behalf of other voters, to save them gas
|
|
// speed up the reward retrieval, and ensure that no rewards expire. We want to ignore RewardsRetrieved
|
|
// events that arise from such a batch retrieval.
|
|
// Example of such a transaction: https://etherscan.io/tx/0xed907cc499fb6bdccb6fb350dd8dd9cf90e7b24c855a5e857b24156f18e0e4bb#eventlog
|
|
const UMA_DEV_ACCOUNT = "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d";
|
|
|
|
async function parseClaimEvents({ claimedRewards, priceData, rebateOutput, debug = false }) {
|
|
// Any entries in `rewardedVotersToRebate` will result in an UMA rebate.
|
|
const rewardedVotersToRebate = {};
|
|
// Store transaction hashes so that we can prevent duplicate rebates for different Reveals within the same batch transaction.
|
|
const batchTxns = {};
|
|
|
|
let progressBarClaim;
|
|
if (!debug) {
|
|
progressBarClaim = multibar.create(claimedRewards.length, 0, { label: "Claim Events" });
|
|
}
|
|
|
|
for (let i = 0; i < claimedRewards.length; i++) {
|
|
try {
|
|
const claim = claimedRewards[i];
|
|
const [transactionBlock, transactionReceipt] = await Promise.all([
|
|
web3.eth.getBlock(claim.blockNumber),
|
|
web3.eth.getTransactionReceipt(claim.transactionHash),
|
|
]);
|
|
// Check if claim txn was sent by an UMA dev batch retrieval.
|
|
if (toChecksumAddress(transactionReceipt.from) !== toChecksumAddress(UMA_DEV_ACCOUNT)) {
|
|
// To prevent rebating voters who experienced someone else claiming their rewards (and thus
|
|
// paying gas on the voter's behalf), filter out claim calls that were not instantiated
|
|
// by the voter. Note that this might have some false negatives for edge case voters
|
|
// who claim from different accounts.
|
|
const voter = claim.returnValues.voter;
|
|
if (toChecksumAddress(voter) === toChecksumAddress(transactionReceipt.from)) {
|
|
const roundId = claim.returnValues.roundId;
|
|
const identifier = web3.utils.hexToUtf8(claim.returnValues.identifier);
|
|
const requestTime = claim.returnValues.time;
|
|
|
|
// Metrics.
|
|
const uniqueVoteKey = `${identifier}-${requestTime}`;
|
|
if (metrics.identifiers.indexOf(uniqueVoteKey) === -1) metrics.identifiers.push(uniqueVoteKey);
|
|
|
|
const gasUsed = parseInt(transactionReceipt.gasUsed);
|
|
const txnTimestamp = transactionBlock.timestamp;
|
|
|
|
const key = `${voter}-${roundId}-${identifier}-${requestTime}`;
|
|
const val = {
|
|
voter,
|
|
roundId,
|
|
requestTime,
|
|
identifier,
|
|
claim: {
|
|
transactionBlock: transactionBlock.number,
|
|
hash: transactionReceipt.transactionHash,
|
|
gasUsed,
|
|
txnTimestamp,
|
|
},
|
|
};
|
|
|
|
// Save and continue to lookup txn data for next event. Skip this claim if it was already included as
|
|
// part of a batch transaction.
|
|
if (!batchTxns[transactionReceipt.transactionHash]) {
|
|
batchTxns[transactionReceipt.transactionHash] = true;
|
|
rewardedVotersToRebate[key] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (progressBarClaim) {
|
|
progressBarClaim.update(i + 1);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to query data for claim", claimedRewards[i], err);
|
|
}
|
|
}
|
|
if (progressBarClaim) {
|
|
progressBarClaim.stop();
|
|
}
|
|
|
|
// Rebate voters
|
|
const rebateReceipts = {};
|
|
let totalGasUsed = 0;
|
|
let totalEthSpent = 0;
|
|
let totalUmaRepaid = 0;
|
|
for (let voterKey of Object.keys(rewardedVotersToRebate)) {
|
|
const claimData = rewardedVotersToRebate[voterKey].claim;
|
|
const gasUsed = claimData.gasUsed;
|
|
const transactionDayGasData = getDataForTimestamp(priceData.dailyAvgGasPrices, claimData.txnTimestamp);
|
|
const ethToPay = toBN(toWei(transactionDayGasData.avgGwei, "gwei")).mul(toBN(gasUsed));
|
|
const transactionDayUmaData = getDataForTimestamp(priceData.dailyAvgUmaEthPrices, claimData.txnTimestamp);
|
|
const umaToPay = ethToPay.mul(SCALING_FACTOR).div(toBN(toWei(transactionDayUmaData.avgPx)));
|
|
const claimTxn = claimData.hash;
|
|
|
|
totalGasUsed += gasUsed;
|
|
totalEthSpent += Number(fromWei(ethToPay.toString()));
|
|
totalUmaRepaid += Number(fromWei(umaToPay.toString()));
|
|
|
|
rebateReceipts[voterKey] = {
|
|
timestamp: claimData.txnTimestamp,
|
|
gasUsed,
|
|
gasPrice: transactionDayGasData.avgGwei,
|
|
ethToPay: Number(fromWei(ethToPay)),
|
|
umaEthPrice: transactionDayUmaData.avgPx,
|
|
umaToPay: Number(fromWei(umaToPay)),
|
|
claimTxn,
|
|
};
|
|
|
|
const voter = rewardedVotersToRebate[voterKey].voter;
|
|
if (rebateOutput.shareHolderPayout[voter.toLowerCase()]) {
|
|
rebateOutput.shareHolderPayout[voter.toLowerCase()] += Number(fromWei(umaToPay.toString()));
|
|
} else {
|
|
rebateOutput.shareHolderPayout[voter.toLowerCase()] = Number(fromWei(umaToPay.toString()));
|
|
}
|
|
}
|
|
|
|
return {
|
|
rebateReceipts,
|
|
totals: { totalGasUsed: totalGasUsed, totalEthSpent: totalEthSpent, totalUmaRepaid: totalUmaRepaid },
|
|
};
|
|
}
|
|
|
|
async function calculateRebate({
|
|
rebateNumber,
|
|
startBlock,
|
|
endBlock,
|
|
revealOnly,
|
|
claimOnly,
|
|
dailyAvgGasPrices,
|
|
dailyAvgUmaEthPrices,
|
|
debug = false,
|
|
}) {
|
|
try {
|
|
const voting = new web3.eth.Contract(getAbi("Voting"), await getAddress("Voting", 1));
|
|
console.log(`Using DVM @ ${voting.options.address}`);
|
|
|
|
if (!debug) {
|
|
console.log("\n\n*=======================================*");
|
|
console.log("* *");
|
|
console.log("* 🐲⛽️ UMA Gas Rebater 🐲 ⛽️ *");
|
|
console.log("* *");
|
|
console.log("*=======================================*");
|
|
console.log(`- Calculating gas rebates from block ${startBlock} until ${endBlock}`);
|
|
}
|
|
|
|
// Query past contract events.
|
|
const [committedVotes, revealedVotes, claimedRewards] = await Promise.all([
|
|
voting.getPastEvents("VoteCommitted", { fromBlock: startBlock, toBlock: endBlock }),
|
|
voting.getPastEvents("VoteRevealed", { fromBlock: startBlock, toBlock: endBlock }),
|
|
voting.getPastEvents("RewardsRetrieved", { fromBlock: startBlock, toBlock: endBlock }),
|
|
]);
|
|
|
|
const priceData = { dailyAvgGasPrices, dailyAvgUmaEthPrices };
|
|
const readablePriceData = { dailyAvgGasPrices, dailyAvgUmaEthPrices };
|
|
if (!debug) {
|
|
Object.keys(readablePriceData).forEach((k) => {
|
|
if (typeof readablePriceData[k] !== "object") {
|
|
console.log(`- ${k}: ${readablePriceData[k]}`);
|
|
} else {
|
|
console.log(`- ${k}: ${JSON.stringify(readablePriceData[k], null, 4)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Final UMA rebates to send
|
|
const rebateOutput = {
|
|
votingContractAddress: voting.options.address,
|
|
rebate: rebateNumber,
|
|
fromBlock: startBlock,
|
|
toBlock: endBlock,
|
|
shareHolderPayout: {}, // {[voter:string]: amountUmaToRebate:number}
|
|
priceData: readablePriceData,
|
|
};
|
|
|
|
// Parallelize fetching of event data:
|
|
const parsePromises = [];
|
|
|
|
// Parse data for vote reveals to rebate.
|
|
if (!claimOnly) {
|
|
parsePromises.push(parseRevealEvents({ committedVotes, revealedVotes, priceData, rebateOutput, debug }));
|
|
} else {
|
|
parsePromises.push(null);
|
|
}
|
|
|
|
// Parse data for claimed rewards to rebate
|
|
if (!revealOnly) {
|
|
parsePromises.push(parseClaimEvents({ claimedRewards, priceData, rebateOutput, debug }));
|
|
} else {
|
|
parsePromises.push(null);
|
|
}
|
|
|
|
if (!debug) {
|
|
console.log("\n\n*=======================================*");
|
|
console.log("* *");
|
|
console.log("* 🌏 Fetching Blockchain Data 🌎 *");
|
|
console.log("* *");
|
|
console.log("*=======================================*");
|
|
}
|
|
const [revealRebates, claimRebates] = await Promise.all(parsePromises);
|
|
|
|
if (!debug) {
|
|
console.log("\n\n*=======================================*");
|
|
console.log("* *");
|
|
console.log("* ✅ Results *");
|
|
console.log("* *");
|
|
console.log("*=======================================*");
|
|
}
|
|
if (revealRebates && !debug) {
|
|
const savePath = `${path.resolve(__dirname)}/debug/Reveals_${rebateNumber}.json`;
|
|
fs.writeFileSync(savePath, JSON.stringify(revealRebates.rebateReceipts, null, 4));
|
|
console.log(
|
|
`🗄 ${Object.keys(revealRebates.rebateReceipts).length} Reveal Transactions successfully written to`,
|
|
savePath
|
|
);
|
|
console.log("㊗️ Reveal Totals:", revealRebates.totals);
|
|
}
|
|
if (claimRebates && !debug) {
|
|
const savePath = `${path.resolve(__dirname)}/debug/Claims_${rebateNumber}.json`;
|
|
fs.writeFileSync(savePath, JSON.stringify(claimRebates.rebateReceipts, null, 4));
|
|
console.log(
|
|
`🗄 ${Object.keys(claimRebates.rebateReceipts).length} Claim Transactions successfully written to`,
|
|
savePath
|
|
);
|
|
console.log("㊗️ Claim Totals:", claimRebates.totals);
|
|
}
|
|
// Output JSON parseable via disperse.app
|
|
let totalUMAToRebate = 0;
|
|
for (let voter of Object.keys(rebateOutput.shareHolderPayout)) {
|
|
totalUMAToRebate += rebateOutput.shareHolderPayout[voter];
|
|
}
|
|
|
|
if (!debug) {
|
|
console.log("\n\n*=======================================*");
|
|
console.log("* *");
|
|
console.log("* 🧮 Final UMA Rebate *");
|
|
console.log("* *");
|
|
console.log("*=======================================*");
|
|
console.log(
|
|
`🎟 UMA to rebate: ${totalUMAToRebate} across ${Object.keys(rebateOutput.shareHolderPayout).length} voters`
|
|
);
|
|
// Add useful data to output file:
|
|
rebateOutput.countVoters = Object.keys(rebateOutput.shareHolderPayout).length;
|
|
rebateOutput.uniqueIdentifiers = metrics.identifiers;
|
|
rebateOutput.totalRebateAmount = Object.keys(rebateOutput.shareHolderPayout).reduce(
|
|
(agg, curr) => agg + rebateOutput.shareHolderPayout[curr],
|
|
0
|
|
);
|
|
|
|
// Replace designated voting wallets output file with hot wallets:
|
|
for (let voterAddress of Object.keys(rebateOutput.shareHolderPayout)) {
|
|
if (addressesToReplace.includes(voterAddress)) {
|
|
try {
|
|
// Get hot wallet
|
|
const designatedVoting = new web3.eth.Contract(getAbi("DesignatedVoting"), voterAddress);
|
|
const hotWallet = (await designatedVoting.methods.getMember(1).call()).toLowerCase();
|
|
// Add hot wallet to output
|
|
rebateOutput.shareHolderPayout[hotWallet] = rebateOutput.shareHolderPayout[voterAddress];
|
|
// Remove cold wallet from output
|
|
delete rebateOutput.shareHolderPayout[voterAddress];
|
|
console.log(`Replaced [COLD-WALLET] ${voterAddress} with [HOT-WALLET]: ${hotWallet}`);
|
|
} catch (err) {
|
|
// Address is not a DesignatedVoting wallet, skip it.
|
|
console.error(
|
|
`${voterAddress} found in "2KEY_ADDRESS_OVERRIDE.json" but is not a DesignatedVoting wallet.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Format output and save to file.
|
|
const savePath = `${path.resolve(__dirname)}/rebates/Rebate_${rebateNumber}.json`;
|
|
fs.writeFileSync(savePath, JSON.stringify(rebateOutput, null, 4));
|
|
console.log("🗄 File successfully written to", savePath);
|
|
}
|
|
|
|
// Return debug and prod outputs for testing
|
|
return { revealRebates, claimRebates, rebateOutput };
|
|
} catch (err) {
|
|
console.error("calculateRebate ERROR:", err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function getUmaPriceAtTimestamp(timestamp) {
|
|
try {
|
|
const dateFormatted = moment.unix(timestamp).format("DD-MM-YYYY");
|
|
const query = `https://api.coingecko.com/api/v3/coins/uma/history?date=${dateFormatted}`;
|
|
|
|
const response = await fetch(query, {
|
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
});
|
|
|
|
let pricesResponse = await response.json();
|
|
let ethExchangeRate = pricesResponse.market_data.current_price.eth;
|
|
return ethExchangeRate;
|
|
} catch (err) {
|
|
console.error("Failed to fetch UMA historical price from Coingecko, falling back to default");
|
|
return 0.0016;
|
|
}
|
|
}
|
|
|
|
async function getHistoricalGasPrice(startBlock, endBlock) {
|
|
const startTime = (await web3.eth.getBlock(startBlock)).timestamp;
|
|
const startTimeString = moment.unix(startTime).format("YYYY-MM-DD");
|
|
const endTime = (await web3.eth.getBlock(endBlock)).timestamp;
|
|
const endTimeString = moment.unix(endTime).format("YYYY-MM-DD");
|
|
|
|
// Construct list of days:
|
|
const days = [];
|
|
let counter = moment.unix(startTime);
|
|
while (counter.isBefore(moment.unix(endTime))) {
|
|
days.push(moment(counter));
|
|
counter = counter.add(1, "days");
|
|
}
|
|
|
|
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
|
|
if (!etherscanApiKey) {
|
|
console.error("Missing ETHERSCAN_API_KEY in your environment, falling back to default gas price");
|
|
return days.map((day) => {
|
|
return { timestamp: day.unix().toString(), avgGwei: "20" };
|
|
});
|
|
} else {
|
|
const query = `https://api.etherscan.io/api?module=stats&action=dailyavggasprice&startdate=${startTimeString}&enddate=${endTimeString}&sort=asc&apikey=${etherscanApiKey}`;
|
|
const response = await fetch(query, {
|
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
});
|
|
|
|
let data = (await response.json()).result;
|
|
|
|
if (!Array.isArray(data)) throw new Error("Unexpected ETH gas price historical data format", data);
|
|
|
|
// Return daily gas price (in Gwei) mapped to Unix timestamps so we can best estimate
|
|
// the gas price for each transaction.
|
|
const dailyPrices = data.map((_data) => {
|
|
return { timestamp: Number(_data.unixTimeStamp), avgGwei: fromWei(_data.avgGasPrice_Wei, "gwei") };
|
|
});
|
|
return dailyPrices;
|
|
}
|
|
}
|
|
|
|
async function getHistoricalUmaEthPrice(dailyPrices) {
|
|
let dailyUmaPrices = [];
|
|
|
|
// Fetch UMA-ETH exchange rates for each day
|
|
let umaEthPricePromises = [];
|
|
for (let day of dailyPrices) {
|
|
umaEthPricePromises.push(getUmaPriceAtTimestamp(day.timestamp));
|
|
}
|
|
const umaEthPrices = await Promise.all(umaEthPricePromises);
|
|
|
|
// Return UMA-ETH array that has same shape as daily price array
|
|
for (let i = 0; i < dailyPrices.length; i++) {
|
|
dailyUmaPrices.push({
|
|
timestamp: dailyPrices[i].timestamp,
|
|
avgPx: umaEthPrices[i].toFixed(18), // Cut off any digits beyond 18th decimal so that we we can parse
|
|
// this `avgPx` using toWei.
|
|
});
|
|
}
|
|
|
|
return dailyUmaPrices;
|
|
}
|
|
|
|
/** *****************************************
|
|
*
|
|
* MAIN MODULES
|
|
*
|
|
*******************************************/
|
|
// Implement async callback to enable the script to be run by truffle or node.
|
|
async function Main(callback) {
|
|
try {
|
|
console.log("\n\n*=======================================*");
|
|
console.log("* *");
|
|
console.log("* 🐣 Setup 🐣 *");
|
|
console.log("* - Fetching block number for timestamp *");
|
|
console.log("* - Fetching historical gas px data *");
|
|
console.log("* - Fetching UMA-ETH exchange rates *");
|
|
console.log("* *");
|
|
console.log("*=======================================*");
|
|
|
|
// Make sure user is on Mainnet.
|
|
const networkId = await web3.eth.net.getId();
|
|
if (networkId !== 1)
|
|
throw new Error("You are not using a Mainnet web3 provider, script will produce devastating results 🧟♂️");
|
|
|
|
const rebateNumber = argv.rebateNumber ? argv.rebateNumber : "1";
|
|
// First check if block numbers are passed in, then default to timetamps.
|
|
let startBlock;
|
|
let endBlock;
|
|
if (argv["start-block"] || argv["end-block"]) {
|
|
endBlock = argv["end-block"] ? argv["end-block"] : (await web3.eth.getBlockNumber()) - 5000; // Default: Current block minus ~1 day of blocks.
|
|
startBlock = argv["start-block"] ? argv["start-block"] : endBlock - 25000; // Default: End time - ~5 days of blocks.
|
|
} else {
|
|
const endDate = argv.end ? argv.end : Math.round(Date.now() / 1000 - 24 * 60 * 60); // Default: Current time minus 1 day.
|
|
const startDate = argv.start ? argv.start : endDate - 60 * 60 * 24 * 5; // Default: End time - 5 days
|
|
console.log(`- Using start date: ${moment.unix(startDate).toString()}`);
|
|
console.log(`- Using end date: ${moment.unix(endDate).toString()}`);
|
|
|
|
endBlock = (await FindBlockAtTimestamp._findBlockNumberAtTimestamp(web3, Number(endDate), 1, 1, 1, 14))
|
|
.blockNumber;
|
|
startBlock = (await FindBlockAtTimestamp._findBlockNumberAtTimestamp(web3, Number(startDate), 1, 1, 1, 14))
|
|
.blockNumber;
|
|
}
|
|
|
|
// Fetch gas price data in parallel
|
|
const pricePromises = [];
|
|
pricePromises.push(getHistoricalGasPrice(startBlock, endBlock));
|
|
|
|
const [dailyAvgGasPrices] = await Promise.all(pricePromises);
|
|
if (!dailyAvgGasPrices) {
|
|
throw new Error("Missing price data");
|
|
}
|
|
const dailyAvgUmaEthPrices = await getHistoricalUmaEthPrice(dailyAvgGasPrices);
|
|
console.log("- ✅ Success, running main script now");
|
|
|
|
// Pull the parameters from process arguments. Specifying them like this lets tests add its own.
|
|
await calculateRebate({
|
|
rebateNumber,
|
|
startBlock,
|
|
endBlock,
|
|
revealOnly: argv["reveal-only"],
|
|
claimOnly: argv["claim-only"],
|
|
dailyAvgGasPrices,
|
|
dailyAvgUmaEthPrices,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
callback();
|
|
}
|
|
|
|
// If called directly by node:
|
|
function nodeCallback(err) {
|
|
if (err) {
|
|
console.error(err);
|
|
process.exit(1);
|
|
} else process.exit(0);
|
|
}
|
|
|
|
// If called directly by node, execute the core function. This lets the script be run as a node process.
|
|
if (require.main === module) {
|
|
Main(nodeCallback)
|
|
.then(() => {})
|
|
.catch(nodeCallback);
|
|
}
|
|
|
|
Main.getHistoricalUmaEthPrice = getHistoricalUmaEthPrice;
|
|
Main.getHistoricalGasPrice = getHistoricalGasPrice;
|
|
Main.calculateRebate = calculateRebate;
|
|
Main.getDataForTimestamp = getDataForTimestamp;
|
|
Main.SCALING_FACTOR = SCALING_FACTOR;
|
|
module.exports = Main;
|