Created
October 27, 2020 15:05
-
-
Save 0age/561a12f71e5aa2e0c783a04bcc332daf to your computer and use it in GitHub Desktop.
Node.js web3 script for getting UNI proposal information.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Web3 = require('web3'); | |
const fs = require('fs'); | |
process.chdir(__dirname); | |
const PROPOSAL_ID = 2; | |
const QUORUM = 40_000_000; | |
// Be sure to use a provider that can retrieve all required events. | |
const web3Provider = `https://eth-mainnet.alchemyapi.io/jsonrpc/${process.env.WEB3_API_KEY}`; | |
const web3 = new Web3(web3Provider); | |
// Getting all the delegates who can vote on the proposal takes a while, so | |
// write the results to a file and reuse it next time. | |
const cachePath = `./proposal-${PROPOSAL_ID}-cache.json`; | |
let cache = null; | |
try { | |
if (fs.existsSync(cachePath)) { | |
cache = require(cachePath); | |
} else { | |
console.log("no cache detected for proposal", PROPOSAL_ID, "— generating!"); | |
} | |
} catch(err) { | |
console.error("Could not find cache for proposal", PROPOSAL_ID, "— generating!"); | |
} | |
// Use DelegateChanged event and getPriorVotes view function on UNI. | |
const UNI_ADDRESS = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"; | |
const UNI_PARTIAL_ABI = [ | |
{ | |
"anonymous": false, | |
"inputs": [ | |
{ | |
"indexed": true, | |
"internalType": "address", | |
"name": "delegator", | |
"type": "address" | |
}, | |
{ | |
"indexed": true, | |
"internalType": "address", | |
"name": "fromDelegate", | |
"type": "address" | |
}, | |
{ | |
"indexed": true, | |
"internalType": "address", | |
"name": "toDelegate", | |
"type": "address" | |
} | |
], | |
"name": "DelegateChanged", | |
"type": "event" | |
}, | |
{ | |
"constant": true, | |
"inputs": [ | |
{ | |
"internalType": "address", | |
"name": "account", | |
"type": "address" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "blockNumber", | |
"type": "uint256" | |
} | |
], | |
"name": "getPriorVotes", | |
"outputs": [ | |
{ | |
"internalType": "uint96", | |
"name": "", | |
"type": "uint96" | |
} | |
], | |
"payable": false, | |
"stateMutability": "view", | |
"type": "function" | |
} | |
]; | |
const UNI = new web3.eth.Contract(UNI_PARTIAL_ABI, UNI_ADDRESS); | |
const UNI_DEPLOYMENT = 10861674; | |
// Use VoteCast event and proposals view function on GovernorAlpha. | |
const GOVERNOR_ADDRESS = "0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F"; | |
const GOVERNOR_PARTIAL_ABI = [ | |
{ | |
"anonymous": false, | |
"inputs": [ | |
{ | |
"indexed": false, | |
"internalType": "address", | |
"name": "voter", | |
"type": "address" | |
}, | |
{ | |
"indexed": false, | |
"internalType": "uint256", | |
"name": "proposalId", | |
"type": "uint256" | |
}, | |
{ | |
"indexed": false, | |
"internalType": "bool", | |
"name": "support", | |
"type": "bool" | |
}, | |
{ | |
"indexed": false, | |
"internalType": "uint256", | |
"name": "votes", | |
"type": "uint256" | |
} | |
], | |
"name": "VoteCast", | |
"type": "event" | |
}, | |
{ | |
"constant": true, | |
"inputs": [ | |
{ | |
"internalType": "uint256", | |
"name": "", | |
"type": "uint256" | |
} | |
], | |
"name": "proposals", | |
"outputs": [ | |
{ | |
"internalType": "uint256", | |
"name": "id", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "address", | |
"name": "proposer", | |
"type": "address" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "eta", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "startBlock", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "endBlock", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "forVotes", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "uint256", | |
"name": "againstVotes", | |
"type": "uint256" | |
}, | |
{ | |
"internalType": "bool", | |
"name": "canceled", | |
"type": "bool" | |
}, | |
{ | |
"internalType": "bool", | |
"name": "executed", | |
"type": "bool" | |
} | |
], | |
"payable": false, | |
"stateMutability": "view", | |
"type": "function" | |
} | |
]; | |
const GOVERNOR = new web3.eth.Contract(GOVERNOR_PARTIAL_ABI, GOVERNOR_ADDRESS); | |
const toPercent = (value) => (100 * value).toFixed(2); | |
const parseVotes = (votes) => parseInt(web3.utils.fromWei(votes, 'ether')); | |
const getAllDelegates = async () => { | |
const delegatedEvents = await UNI.getPastEvents( | |
'DelegateChanged', | |
{fromBlock: UNI_DEPLOYMENT} | |
); | |
return Array.from( | |
new Set(delegatedEvents.map(x => x.returnValues.toDelegate)) | |
); | |
} | |
const getAllVotes = async (proposal) => { | |
const voteEvents = await GOVERNOR.getPastEvents( | |
'VoteCast', | |
{ | |
fromBlock: proposal.startBlock, | |
toBlock: proposal.endBlock, | |
filter: {proposalID: [1]} | |
} | |
); | |
const voters = new Set(voteEvents.map(x => x.returnValues.voter.toLowerCase())); | |
const forVotes = Object.fromEntries( | |
voteEvents | |
.filter(x => x.returnValues.support) | |
.map(x => [ | |
x.returnValues.voter.toLowerCase(), | |
parseVotes(x.returnValues.votes) | |
]) | |
); | |
const againstVotes = Object.fromEntries( | |
voteEvents | |
.filter(x => !x.returnValues.support) | |
.map(x => [ | |
x.returnValues.voter.toLowerCase(), | |
parseVotes(x.returnValues.votes) | |
]) | |
); | |
return [voters, forVotes, againstVotes]; | |
} | |
const getProposalSummary = async (proposalID) => { | |
// Get proposal details, including current vote totals. | |
const proposal = await GOVERNOR.methods.proposals(proposalID).call(); | |
const totalForVotes = parseVotes(proposal.forVotes); | |
const percentToQuorum = toPercent(totalForVotes / QUORUM); | |
const totalAgainstVotes = parseVotes(proposal.againstVotes); | |
const totalVotes = totalForVotes + totalAgainstVotes; | |
// Get details on each voter for the given proposal. | |
const [voters, forVotes, againstVotes] = await getAllVotes(proposal); | |
// Get details on the "voter base" for the proposal. | |
let priorVotesByDelegate; | |
if (cache === null) { | |
// Retrieve all potential delegates. | |
const potentialDelegates = await getAllDelegates(); | |
// Retrieve the voting weight of each potential delegate as of the snapshot. | |
// To speed up, try querying delegate addresses in batches or in parallel. | |
priorVotesByDelegate = {}; | |
for (const potentialDelegate of potentialDelegates) { | |
const delegateVotes = await UNI.methods.getPriorVotes( | |
potentialDelegate, | |
proposal.startBlock | |
).call() | |
if (delegateVotes !== '0') { | |
priorVotesByDelegate[potentialDelegate] = parseVotes(delegateVotes); | |
} | |
} | |
// Write the results to the cache. | |
fs.writeFileSync(cachePath, JSON.stringify(priorVotesByDelegate, null, 2)); | |
} else { | |
// Use the cache if available. | |
priorVotesByDelegate = cache; | |
} | |
const votingBase = Object.keys(priorVotesByDelegate).length; | |
const votingUNI = parseInt( | |
Object.values(priorVotesByDelegate).reduce((a, b) => a + b, 0) | |
); | |
const abstainingVotes = Object.fromEntries( | |
Object.entries(priorVotesByDelegate).filter( | |
([voter, _]) => !voters.has(voter.toLowerCase()) | |
) | |
); | |
const summary = ` | |
########## Proposal ${proposal.id} ########## | |
* ${(totalForVotes / 1e6).toFixed(3)}M votes FOR (${percentToQuorum}% quorum) | |
* ${(totalAgainstVotes / 1e6).toFixed(3)}M votes AGAINST | |
* ${((votingUNI - totalVotes) / 1e6).toFixed(3)}M not yet voted | |
${voters.size} unique voters | |
* ${Object.entries(forVotes).length} unique FOR voters | |
* ${Object.entries(againstVotes).length} unique AGAINST voters | |
* ${votingBase - voters.size} delegates haven't yet voted | |
* Voter participation rate: ${toPercent(voters.size / votingBase)}% | |
Delegated UNI at snapshot: ${(votingUNI / 1e6).toFixed(2)}M | |
* UNI participation rate: ${toPercent(totalVotes/votingUNI)}% | |
`; | |
console.log(summary); | |
process.exit(0); | |
} | |
const main = async () => getProposalSummary(PROPOSAL_ID); | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment