Skip to content

Instantly share code, notes, and snippets.

@0age
Created October 27, 2020 15:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save 0age/561a12f71e5aa2e0c783a04bcc332daf to your computer and use it in GitHub Desktop.
Save 0age/561a12f71e5aa2e0c783a04bcc332daf to your computer and use it in GitHub Desktop.
Node.js web3 script for getting UNI proposal information.
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