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