diff --git a/schema.graphql b/schema.graphql index 047ff9c..9febcfc 100755 --- a/schema.graphql +++ b/schema.graphql @@ -388,6 +388,9 @@ type BroadcasterDay @entity { broadcaster: Broadcaster! } +""" +Stake weighted treasury proposal +""" type TreasuryProposal @entity { "Governor proposal ID formatted as a decimal number" id: ID! @@ -405,6 +408,40 @@ type TreasuryProposal @entity { voteEnd: BigInt! "Description of the proposal" description: String! + "Votes cast for this proposal" + votes: [TreasuryVote!]! @derivedFrom(field: "proposal") + "Total weight of votes in favor" + forVotes: BigDecimal! + "Total weight of votes against" + againstVotes: BigDecimal! + "Total weight of abstaining votes" + abstainVotes: BigDecimal! + "Sum of all vote weights" + totalVotes: BigDecimal! +} + +enum TreasuryVoteSupport{ + Against + For + Abstain +} + +""" +Stake weighted vote on a treasury proposal +""" +type TreasuryVote @entity { + "Proposal ID + voter address" + id: ID! + "The proposal that was voted on" + proposal: TreasuryProposal! + "Account that cast the vote" + voter: LivepeerAccount! + "The voter's position" + support: TreasuryVoteSupport! + "Stake-weighted voting power" + weight: BigDecimal! + "Optional reason string provided by the voter" + reason: String } ############################################################################### @@ -905,6 +942,30 @@ type ParameterUpdateEvent implements Event @entity { param: String! } +""" +TreasuryVoteEvent entities are created for every emitted VoteCast/VoteCastWithParams event. +""" +type TreasuryVoteEvent implements Event @entity { + "Ethereum transaction hash + event log index" + id: ID! + "Reference to the transaction the event was included in" + transaction: Transaction! + "Timestamp of the transaction the event was included in" + timestamp: Int! + "Reference to the round the event occured in" + round: Round! + "Account that cast the vote" + voter: LivepeerAccount! + "Proposal that the vote was cast for" + proposal: TreasuryProposal! + "The voter's position" + support: TreasuryVoteSupport! + "Stake-weighted voting power" + weight: BigDecimal! + "Optional reason string provided by the voter" + reason: String +} + """ VoteEvent entities are created for every emitted Vote event. """ diff --git a/src/mappings/treasury.ts b/src/mappings/treasury.ts index 1e787c7..4a539e6 100644 --- a/src/mappings/treasury.ts +++ b/src/mappings/treasury.ts @@ -1,16 +1,154 @@ -import { TreasuryProposal } from "../types/schema"; -import { ProposalCreated } from "../types/Treasury/LivepeerGovernor"; +import { BigDecimal, BigInt, ethereum, log } from "@graphprotocol/graph-ts"; + +import { + convertToDecimal, + createOrLoadRound, + createOrLoadTransactionFromEvent, + createOrUpdateLivepeerAccount, + getBlockNum, + makeEventId, + ZERO_BD, +} from "../../utils/helpers"; +import { + TreasuryProposal, + TreasuryVote, + TreasuryVoteEvent, +} from "../types/schema"; +import { + ProposalCreated, + VoteCast, + VoteCastWithParams, +} from "../types/Treasury/LivepeerGovernor"; + +// Workaround: Graph entities store enums as strings (no string enums in AS/codegen). +namespace TreasurySupport { + export const Against = "Against"; + export const For = "For"; + export const Abstain = "Abstain"; +} export function proposalCreated(event: ProposalCreated): void { const p = event.params; + const proposer = createOrUpdateLivepeerAccount( + p.proposer.toHex(), + event.block.timestamp.toI32() + ); const proposal = new TreasuryProposal(p.proposalId.toString()); - proposal.proposer = p.proposer.toHex(); + proposal.proposer = proposer.id; proposal.targets = p.targets.map((t) => t.toHex()); proposal.values = p.values; proposal.calldatas = p.calldatas; proposal.voteStart = p.voteStart; proposal.voteEnd = p.voteEnd; proposal.description = p.description; + proposal.forVotes = ZERO_BD; + proposal.againstVotes = ZERO_BD; + proposal.abstainVotes = ZERO_BD; + proposal.totalVotes = ZERO_BD; + proposal.save(); +} + +export function voteCast(event: VoteCast): void { + handleVote( + event, + event.params.proposalId, + event.params.voter.toHex(), + event.params.support, + event.params.weight, + event.params.reason + ); +} + +export function voteCastWithParams(event: VoteCastWithParams): void { + handleVote( + event, + event.params.proposalId, + event.params.voter.toHex(), + event.params.support, + event.params.weight, + event.params.reason + ); +} + +function handleVote( + event: ethereum.Event, + proposalId: BigInt, + voter: string, + support: i32, + weightRaw: BigInt, + reason: string +): void { + const proposal = TreasuryProposal.load(proposalId.toString()); + + if (!proposal) { + log.error("Treasury vote for unknown proposal {}", [proposalId.toString()]); + return; + } + + const supportLabelValue = supportFromValue(support); + if (supportLabelValue == null) { + return; + } + const supportLabel = supportLabelValue as string; + + const account = createOrUpdateLivepeerAccount( + voter, + event.block.timestamp.toI32() + ); + const round = createOrLoadRound(getBlockNum()); + const transaction = createOrLoadTransactionFromEvent(event); + const voteId = proposal.id.concat("-").concat(voter); + let vote = TreasuryVote.load(voteId); + const weight = convertToDecimal(weightRaw); + + if (!vote) { + vote = new TreasuryVote(voteId); + vote.proposal = proposal.id; + vote.voter = account.id; + } + + vote.support = supportLabel; + vote.weight = weight; + vote.reason = reason.length > 0 ? reason : null; + vote.save(); + + increaseProposalTotals(proposal, supportLabel, weight); + proposal.save(); + + const voteEvent = new TreasuryVoteEvent( + makeEventId(event.transaction.hash, event.logIndex) + ); + voteEvent.transaction = transaction.id; + voteEvent.timestamp = event.block.timestamp.toI32(); + voteEvent.round = round.id; + voteEvent.voter = account.id; + voteEvent.proposal = proposal.id; + voteEvent.support = supportLabel; + voteEvent.weight = weight; + voteEvent.reason = reason.length > 0 ? reason : null; + voteEvent.save(); +} + +function supportFromValue(value: i32): string | null { + if (value == 0) return TreasurySupport.Against; + if (value == 1) return TreasurySupport.For; + if (value == 2) return TreasurySupport.Abstain; + return null; +} + +function increaseProposalTotals( + proposal: TreasuryProposal, + support: string, + weight: BigDecimal +): void { + if (support == TreasurySupport.For) { + proposal.forVotes = proposal.forVotes.plus(weight); + } else if (support == TreasurySupport.Against) { + proposal.againstVotes = proposal.againstVotes.plus(weight); + } else { + proposal.abstainVotes = proposal.abstainVotes.plus(weight); + } + proposal.totalVotes = proposal.totalVotes.plus(weight); } diff --git a/subgraph.template.yaml b/subgraph.template.yaml index c41d2d3..4f9163e 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -290,12 +290,23 @@ dataSources: abis: - name: LivepeerGovernor file: ./abis/LivepeerGovernor.json + - name: RoundsManager + file: ./abis/RoundsManager.json entities: - TreasuryProposal + - TreasuryVote + - TreasuryVoteEvent - LivepeerAccount + - Round + - Protocol + - Transaction eventHandlers: - event: ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string) handler: proposalCreated + - event: VoteCast(indexed address,uint256,uint8,uint256,string) + handler: voteCast + - event: VoteCastWithParams(indexed address,uint256,uint8,uint256,string,bytes) + handler: voteCastWithParams - kind: ethereum/contract name: ServiceRegistry network: {{networkName}}