governance_vote.gno
4.63 Kb · 168 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(_ int, rlm realm, proposalID int64, yes bool) string {
38 if !rlm.IsCurrent() {
39 panic(errSpoofedRealm)
40 }
41
42 halt.AssertIsNotHaltedGovernance()
43
44 // Get current blockchain state and caller information
45 currentHeight := runtime.ChainHeight()
46 currentAt := time.Now()
47
48 // Mint and distribute GNS tokens as part of the voting process
49 emission.MintAndDistributeGns(cross(rlm))
50
51 // Extract voter address from realm context
52 voterRealm := rlm.Previous()
53 voter := voterRealm.Address()
54
55 // Process the vote and get updated vote tallies
56 userVote, totalYesVoteWeight, totalNoVoteWeight, err := gv.vote(
57 0, rlm,
58 proposalID,
59 voter,
60 yes,
61 currentHeight,
62 currentAt.Unix(),
63 )
64 if err != nil {
65 panic(err)
66 }
67
68 // Emit voting event for tracking and transparency
69 userVoteWeight := formatInt(userVote.VotedWeight())
70 voterStr := voter.String()
71
72 chain.Emit(
73 "Vote",
74 "prevAddr", voterStr,
75 "prevPkgPath", voterRealm.PkgPath(),
76 "proposalId", formatInt(proposalID),
77 "voter", voterStr,
78 "yes", userVote.VotingType(),
79 "voteWeight", userVoteWeight,
80 "voteYes", formatInt(totalYesVoteWeight),
81 "voteNo", formatInt(totalNoVoteWeight),
82 )
83
84 return userVoteWeight
85}
86
87// vote handles core voting logic.
88func (gv *governanceV1) vote(
89 _ int, rlm realm,
90 proposalID int64,
91 voterAddress address,
92 votedYes bool,
93 votedHeight,
94 votedAt int64,
95) (*governance.VotingInfo, int64, int64, error) {
96 // Retrieve the proposal from storage
97 proposal, ok := gv.getProposal(proposalID)
98 if !ok {
99 return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found proposal")
100 }
101
102 proposalResolver := NewProposalResolver(proposal)
103
104 // Check if current time is within voting period
105 if !proposalResolver.IsVotingPeriod(votedAt) {
106 return nil, 0, 0, makeErrorWithDetails(errUnableToVoteOutOfPeriod, "cannot vote out of voting period")
107 }
108
109 // Check if user has already voted on this proposal
110 userVote, hasVoted := gv.getProposalUserVotingInfo(proposalID, voterAddress)
111 if hasVoted && userVote.IsVoted() {
112 return nil, 0, 0, makeErrorWithDetails(errAlreadyVoted, "user has already voted")
113 }
114
115 // Get user's voting weight using average between proposal time and snapshot time.
116 snapshotTime := proposal.SnapshotTime()
117 createdAt := proposal.CreatedAt()
118
119 weightAtSnapshot, ok := gv.stakerAccessor.GetUserDelegationAmountAtSnapshot(voterAddress, snapshotTime)
120 if !ok {
121 weightAtSnapshot = 0
122 }
123
124 weightAtCreated, ok := gv.stakerAccessor.GetUserDelegationAmountAtSnapshot(voterAddress, createdAt)
125 if !ok {
126 weightAtCreated = 0
127 }
128
129 votingWeight := safeAddInt64(weightAtSnapshot, weightAtCreated) / 2
130 if votingWeight <= 0 {
131 return nil, 0, 0, makeErrorWithDetails(
132 errNotEnoughVotingWeight, "no voting weight at snapshot time")
133 }
134
135 // Create or update voting info for this user
136 if userVote == nil {
137 userVote = governance.NewVotingInfo(votingWeight)
138 }
139
140 userVoteResolver := NewVotingInfoResolver(userVote)
141 // Record the vote in user's voting info (this also prevents double voting)
142 err := userVoteResolver.vote(votedYes, votingWeight, votedHeight, votedAt)
143 if err != nil {
144 return nil, 0, 0, err
145 }
146
147 // Store the user's vote in the proposal voting infos
148 votingInfosTree, _ := gv.getProposalUserVotingInfos(proposalID)
149 if votingInfosTree == nil {
150 return nil, 0, 0, makeErrorWithDetails(
151 errDataNotFound, "voting infos tree not found for proposal")
152 }
153
154 votingInfosTree.Set(voterAddress.String(), userVote)
155 err = gv.store.SetProposalVotingInfos(0, rlm, proposalID, votingInfosTree)
156 if err != nil {
157 return nil, 0, 0, err
158 }
159
160 // Update proposal vote tallies
161 err = proposalResolver.Vote(votedYes, votingWeight)
162 if err != nil {
163 return nil, 0, 0, err
164 }
165
166 // Return updated vote information and current tallies
167 return userVote, proposal.VotingYesWeight(), proposal.VotingNoWeight(), nil
168}