Skip to content

Instantly share code, notes, and snippets.

@Reinis-FRP
Last active November 19, 2021 19:08
Show Gist options
  • Save Reinis-FRP/a6d77e8e432924d33ef1fb93548d4ea6 to your computer and use it in GitHub Desktop.
Save Reinis-FRP/a6d77e8e432924d33ef1fb93548d4ea6 to your computer and use it in GitHub Desktop.
require('dotenv').config();
const Web3 = require('web3');
const web3 = new Web3(process.env.NODE_URL_CHAIN_1);
const web3Polygon = new Web3(process.env.NODE_URL_CHAIN_137);
const {hexToUtf8, toBN, toWei, fromWei} = web3.utils;
const fetch = require('node-fetch');
const abiDecoder = require('abi-decoder');
const governorAddress = '0x592349F7DeDB2b75f9d4F194d4b7C16D82E507Dc';
const governorAbi = [
{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"indexed":false,"internalType":"struct Governor.Transaction[]","name":"transactions","type":"tuple[]"}],"name":"NewProposal","type":"event"},
];
const governorRootTunnelAbi = [
{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"relayGovernance","outputs":[],"stateMutability":"nonpayable","type":"function"},
];
abiDecoder.addABI(governorRootTunnelAbi);
const tokenAbi = [
{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},
];
// Some tokens (e.g. MKR) return symbol as bytes:
const altTokenAbi = [
{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},
];
const lookback = 3600 * 24; // Check availability of CoinGecko prices during the last 24h
const waitRateLimit = 60000; // If CoinGecko rate limits the response wait 60s before making the next request
let rateLimitedTill = 0; // Do not make CoinGecko request before this timestamp (need to store this globally due to multiple async requests)
// Wait timeout in miliseconds
const delay = ms => new Promise(res => setTimeout(res, ms));
// Get token symbol from passed contract and web3 objects
async function getTokenSymbol(contract, web3) {
let symbol;
// First try string type, but if it fails fallback to alternative ABI encoded as bytes
try {
symbol = await contract.methods.symbol().call();
} catch(e) {
const altContract = new web3.eth.Contract(altTokenAbi, contract.options.address);
symbol = hexToUtf8(await altContract.methods.symbol().call());
}
return symbol;
}
// Convert to human readable token amount given raw hex value and token decimals
function scaleDownDecimals(hexAmount, decimals) {
return fromWei(toBN(hexAmount).mul(toBN(toWei('1'))).div(toBN('10').pow(toBN(decimals))));
}
// Fetch CoinGecko price from token address
async function getCoingeckoPrice(platform, address, ccy, from, to) {
const url = 'https://api.coingecko.com/api/v3/coins/' + platform + '/contract/' + address + '/market_chart/range?vs_currency=' + ccy + '&from=' + from + '&to=' + to;
const response = await fetch(url);
if (response.status != 200) {
return [null, response.status];
}
const json = await response.json();
const prices = json.prices;
const lastPrice = prices.length ? prices.slice(-1)[0][1] : null;
return [lastPrice, response.status];
}
// Wrap getCoingeckoPrice function to try repeatedly if blocked due to rate limiting
async function getRateLimitedPrice(platform, address, ccy, from, to) {
let lastPrice
let status;
while (true) {
const currentTime = Date.now();
if (currentTime < rateLimitedTill) {
await delay(rateLimitedTill - currentTime);
}
[lastPrice, status] = await getCoingeckoPrice(platform, address, ccy, from, to);
if (status == 429) {
rateLimitedTill = Date.now() + waitRateLimit;
} else {
break;
}
}
return lastPrice;
}
// Decode proposed fee from event data section and add to the passed proposedFees object
function decodeFeeProposal(proposedFees, data) {
if (data.substr(0, 10) == web3.utils.sha3('setFinalFee(address,(uint256))').substr(0, 10)) {
const collateral = '0x' + data.substr(34, 40);
const rawFee = '0x' + data.substr(74, 64);
// If the collateral fee is changed it will be updated by the newest proposal since events are processed in order
proposedFees[collateral] = {rawFee: rawFee};
return true;
}
return false;
}
// Add token symbol and scale down fee decimals
async function formatFees(proposedFees, web3) {
await Promise.all(Object.keys(proposedFees).map(async (collateral) => {
const contract = new web3.eth.Contract(tokenAbi, collateral);
const decimals = await contract.methods.decimals().call();
const symbol = await getTokenSymbol(contract, web3);
const fee = scaleDownDecimals(proposedFees[collateral].rawFee, decimals);
proposedFees[collateral].fee = fee;
proposedFees[collateral].decimals = decimals;
proposedFees[collateral].symbol = symbol;
}));
}
// Wrap everything in an async function to allow the use of async/await.
(async () => {
const governor = new web3.eth.Contract(governorAbi, governorAddress);
const newProposals = await governor.getPastEvents('NewProposal', {fromBlock: 0});
const proposedFeesEthereum = {};
const proposedFeesPolygon = {};
newProposals.forEach(proposal => {
const transactions = proposal.returnValues.transactions;
transactions.forEach(transaction => {
const data = transaction.data;
// First try to decode proposal as setFinalFee on Ethereum mainnet
const proposedOnEthereum = decodeFeeProposal(proposedFeesEthereum, data);
// If not an Ethereum proposal try to decode as relayGovernance for Polygon
if (!proposedOnEthereum && data.substr(0, 10) == web3.utils.sha3('relayGovernance(address,bytes)').substr(0, 10)) {
// Decode nested relayGovernance transaction
const decodedData = abiDecoder.decodeMethod(data);
if (decodedData && decodedData.params && decodedData.params[1] && decodedData.params[1].name == 'data' && decodedData.params[1].type == 'bytes') {
const nestedData = decodedData.params[1].value;
const proposedOnPolygon = decodeFeeProposal(proposedFeesPolygon, nestedData);
}
}
});
});
// Add token symbols and scale down fees
await formatFees(proposedFeesEthereum, web3);
await formatFees(proposedFeesPolygon, web3Polygon);
// Set time period for fetching CoinGecko prices
const toTime = parseInt(Date.now()/1000);
const fromTime = toTime - lookback;
// Try adding CoinGecko prices for Ethereum collateral tokens
await Promise.all(Object.keys(proposedFeesEthereum).map(async (collateral) => {
const price = await getRateLimitedPrice('ethereum', collateral, 'usd', fromTime, toTime);
if (price) proposedFeesEthereum[collateral].price = price;
}));
console.log('Ethereum mainnet (address, symbol, decimals, fee, price):');
Object.keys(proposedFeesEthereum).forEach(collateral => {
if (proposedFeesEthereum[collateral].price) {
console.log(collateral, proposedFeesEthereum[collateral].symbol, proposedFeesEthereum[collateral].decimals, proposedFeesEthereum[collateral].fee, proposedFeesEthereum[collateral].price);
} else {
console.log(collateral, proposedFeesEthereum[collateral].symbol, proposedFeesEthereum[collateral].decimals, proposedFeesEthereum[collateral].fee);
}
});
console.log('Polygon mainnet (address, symbol, decimals, fee):');
Object.keys(proposedFeesPolygon).forEach(collateral => {
console.log(collateral, proposedFeesPolygon[collateral].symbol, proposedFeesPolygon[collateral].decimals, proposedFeesPolygon[collateral].fee);
});
process.exit(0);
})().catch((e) => {
console.error(e);
process.exit(1); // Exit with a nonzero exit code to signal failure.
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment