Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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
}

###############################################################################
Expand Down Expand Up @@ -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.
"""
Expand Down
144 changes: 141 additions & 3 deletions src/mappings/treasury.ts
Original file line number Diff line number Diff line change
@@ -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<string>((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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we should be using the string here, and instead should use the generated enum type (TreasuryVoteSupport.For or something I believe).

This would also mean we can tweak supportFromValue and increaseProposalTotals. Relying on the enum type instead of string values feels like a more appropriate way to handle this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmulq I looked into this while working on the PR. Per the Graph docs on enums https://thegraph.com/docs/en/subgraphs/guides/enums/, enum fields in mappings are set using their string values, and the generated AssemblyScript entities expose them as string, not typed enums. AssemblyScript/codegen also don't currently support string enums (see AssemblyScript/assemblyscript#560).

I might be missing something, but if not, would d39ea1a be an acceptable workaround for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rickstaa - apologies for the confusion. I've been developing in subtreams world recently and wrongly said we can use the generated types. These aren't available in the handlers as you correctly mentioned.

There are similar workaround to yours in this example repo - https://github.com/chidubemokeke/Subgraph-Tutorial-Enums/tree/main.

However, I am happy for it to be the original block of code as well. Whatever you guys find easier to reason about.

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);
}
11 changes: 11 additions & 0 deletions subgraph.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down