governance_vote.gno
4.58 Kb ยท 163 lines
1package v1
2
3import (
4 "chain"
5 "chain/runtime"
6 "time"
7
8 "gno.land/r/gnoswap/emission"
9 "gno.land/r/gnoswap/halt"
10
11 "gno.land/r/gnoswap/gov/governance"
12)
13
14// Vote casts a vote on a proposal.
15//
16// Records on-chain vote with weight based on delegated xGNS.
17// Uses 24-hour average voting power to prevent manipulation.
18// Votes are final and cannot be changed.
19//
20// Parameters:
21// - proposalID: ID of the proposal to vote on
22// - yes: true for yes vote, false for no vote
23//
24// Vote Weight Calculation:
25// - Based on delegated xGNS amount
26// - 24-hour average before proposal creation
27// - Prevents flash loan attacks
28// - Includes both self-stake and delegations received
29//
30// Requirements:
31// - Proposal must be in voting period
32// - Voter must have xGNS delegated
33// - Cannot vote twice on same proposal
34// - Voting period typically 7 days
35//
36// Returns voting weight used as string.
37func (gv *governanceV1) Vote(proposalID int64, yes bool) string {
38 halt.AssertIsNotHaltedGovernance()
39
40 // Get current blockchain state and caller information
41 currentHeight := runtime.ChainHeight()
42 currentAt := time.Now()
43
44 // Mint and distribute GNS tokens as part of the voting process
45 emission.MintAndDistributeGns(cross)
46
47 // Extract voter address from realm context
48 voterRealm := runtime.PreviousRealm()
49 voter := voterRealm.Address()
50
51 // Process the vote and get updated vote tallies
52 userVote, totalYesVoteWeight, totalNoVoteWeight, err := gv.vote(
53 proposalID,
54 voter,
55 yes,
56 currentHeight,
57 currentAt.Unix(),
58 )
59 if err != nil {
60 panic(err)
61 }
62
63 // Emit voting event for tracking and transparency
64 // previousRealm := runtime.PreviousRealm()
65 userVoteWeight := formatInt(userVote.VotedWeight())
66 voterStr := voter.String()
67
68 chain.Emit(
69 "Vote",
70 "prevAddr", voterStr,
71 "prevPkgPath", voterRealm.PkgPath(),
72 "proposalId", formatInt(proposalID),
73 "voter", voterStr,
74 "yes", userVote.VotingType(),
75 "voteWeight", userVoteWeight,
76 "voteYes", formatInt(totalYesVoteWeight),
77 "voteNo", formatInt(totalNoVoteWeight),
78 )
79
80 return userVoteWeight
81}
82
83// vote handles core voting logic.
84func (gv *governanceV1) vote(
85 proposalID int64,
86 voterAddress address,
87 votedYes bool,
88 votedHeight,
89 votedAt int64,
90) (*governance.VotingInfo, int64, int64, error) {
91 // Retrieve the proposal from storage
92 proposal, ok := gv.getProposal(proposalID)
93 if !ok {
94 return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found proposal")
95 }
96
97 proposalResolver := NewProposalResolver(proposal)
98
99 // Check if current time is within voting period
100 if !proposalResolver.IsVotingPeriod(votedAt) {
101 return nil, 0, 0, makeErrorWithDetails(errUnableToVoteOutOfPeriod, "cannot vote out of voting period")
102 }
103
104 // Check if user has already voted on this proposal
105 userVote, hasVoted := gv.getProposalUserVotingInfo(proposalID, voterAddress)
106 if hasVoted && userVote.IsVoted() {
107 return nil, 0, 0, makeErrorWithDetails(errAlreadyVoted, "user has already voted")
108 }
109
110 // Get user's voting weight using average between proposal time and snapshot time.
111 snapshotTime := proposal.SnapshotTime()
112 createdAt := proposal.CreatedAt()
113
114 weightAtSnapshot, ok := gv.stakerAccessor.GetUserDelegationAmountAtSnapshot(voterAddress, snapshotTime)
115 if !ok {
116 weightAtSnapshot = 0
117 }
118
119 weightAtCreated, ok := gv.stakerAccessor.GetUserDelegationAmountAtSnapshot(voterAddress, createdAt)
120 if !ok {
121 weightAtCreated = 0
122 }
123
124 votingWeight := safeAddInt64(weightAtSnapshot, weightAtCreated) / 2
125 if votingWeight <= 0 {
126 return nil, 0, 0, makeErrorWithDetails(
127 errNotEnoughVotingWeight, "no voting weight at snapshot time")
128 }
129
130 // Create or update voting info for this user
131 if userVote == nil {
132 userVote = governance.NewVotingInfo(votingWeight)
133 }
134
135 userVoteResolver := NewVotingInfoResolver(userVote)
136 // Record the vote in user's voting info (this also prevents double voting)
137 err := userVoteResolver.vote(votedYes, votingWeight, votedHeight, votedAt)
138 if err != nil {
139 return nil, 0, 0, err
140 }
141
142 // Store the user's vote in the proposal voting infos
143 votingInfosTree, _ := gv.getProposalUserVotingInfos(proposalID)
144 if votingInfosTree == nil {
145 return nil, 0, 0, makeErrorWithDetails(
146 errDataNotFound, "voting infos tree not found for proposal")
147 }
148
149 votingInfosTree.Set(voterAddress.String(), userVote)
150 err = gv.store.SetProposalVotingInfos(proposalID, votingInfosTree)
151 if err != nil {
152 return nil, 0, 0, err
153 }
154
155 // Update proposal vote tallies
156 err = proposalResolver.Vote(votedYes, votingWeight)
157 if err != nil {
158 return nil, 0, 0, err
159 }
160
161 // Return updated vote information and current tallies
162 return userVote, proposal.VotingYesWeight(), proposal.VotingNoWeight(), nil
163}