governance_vote.gno
5.04 Kb · 164 lines
1package governance
2
3import (
4 "chain"
5 "chain/runtime"
6 "time"
7
8 "gno.land/r/gnoswap/access/v1"
9
10 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
11 "gno.land/p/gnoswap/utils/v1"
12
13 "gno.land/r/gnoswap/emission"
14
15 "gno.land/r/gnoswap/gov/governance"
16)
17
18// Vote casts a vote on a proposal.
19//
20// Records an on-chain vote with weight based on delegated xGNS.
21// Uses the proposal's timestamp anchors and configured smoothing duration to
22// prevent manipulation. Votes are final and cannot be changed.
23//
24// Parameters:
25// - _: leading realm-call discriminator; callers pass 0 while threading the realm context
26// - rlm: current realm context; implementation validates it before reading or writing governance state
27// - proposalID: ID of the proposal to vote on
28// - yes: true for yes vote, false for no vote
29//
30// Vote Weight Calculation:
31// - Based on the voter's delegated xGNS amount
32// - Averages delegation at the stored smoothing timestamp and proposal creation time
33// - Uses Unix timestamp history, not a block-height snapshot
34// - Includes self-delegation and delegations received by the voter
35//
36// Requirements:
37// - Proposal must be in voting period
38// - Voter must have xGNS delegated at one of the lookup timestamps
39// - Cannot vote twice on same proposal
40// - Voting period is 7 days by default and is configurable
41//
42// Returns:
43// - voteWeight: voting weight applied to the caller's vote, formatted as a string
44func (gv *governanceV1) Vote(_ int, rlm realm, proposalID int64, yes bool) string {
45 access.AssertIsRlmCurrent(0, rlm)
46
47 assertIsNotHaltedGovernanceForProposal(gv, proposalID)
48
49 // Get current blockchain state and caller information
50 currentHeight := runtime.ChainHeight()
51 currentAt := time.Now()
52
53 // Mint and distribute GNS tokens as part of the voting process
54 emission.MintAndDistributeGns(cross(rlm))
55
56 // Extract voter address from realm context
57 voterRealm := rlm.Previous()
58 voter := voterRealm.Address()
59
60 // Process the vote and get updated vote tallies
61 userVote, totalYesVoteWeight, totalNoVoteWeight, err := gv.vote(
62 0, rlm,
63 proposalID,
64 voter,
65 yes,
66 currentHeight,
67 currentAt.Unix(),
68 )
69 if err != nil {
70 panic(err)
71 }
72
73 // Emit voting event for tracking and transparency
74 voteWeight := utils.FormatInt(userVote.VotedWeight())
75 voterStr := voter.String()
76
77 chain.Emit(
78 "Vote",
79 "prevAddr", voterStr,
80 "prevPkgPath", voterRealm.PkgPath(),
81 "proposalId", utils.FormatInt(proposalID),
82 "voter", voterStr,
83 "yes", userVote.VotingType(),
84 "voteWeight", voteWeight,
85 "voteYes", utils.FormatInt(totalYesVoteWeight),
86 "voteNo", utils.FormatInt(totalNoVoteWeight),
87 )
88
89 return voteWeight
90}
91
92// vote handles core voting logic.
93func (gv *governanceV1) vote(
94 _ int, rlm realm,
95 proposalID int64,
96 voterAddress address,
97 votedYes bool,
98 votedHeight,
99 votedAt int64,
100) (governance.VotingInfo, int64, int64, error) {
101 // Retrieve the proposal from storage
102 proposal, ok := gv.getProposal(proposalID)
103 if !ok {
104 return governance.DefaultVotingInfo(), 0, 0, makeErrorWithDetails(errDataNotFound, "not found proposal")
105 }
106
107 proposalResolver := NewProposalResolver(proposal)
108
109 // Check if current time is within voting period
110 if !proposalResolver.IsVotingPeriod(votedAt) {
111 return governance.DefaultVotingInfo(), 0, 0, makeErrorWithDetails(errUnableToVoteOutOfPeriod, "cannot vote out of voting period")
112 }
113
114 // Get user's voting weight by averaging timestamp-history values at the
115 // proposal's stored smoothing anchor and creation 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 := gnsmath.SafeAddInt64(weightAtSnapshot, weightAtCreated) / 2
130 if votingWeight <= 0 {
131 return governance.DefaultVotingInfo(), 0, 0, makeErrorWithDetails(
132 errNotEnoughVotingWeight, "no voting weight at snapshot time")
133 }
134
135 userVote, exists := gv.getProposalUserVotingInfo(proposalID, voterAddress)
136 if !exists {
137 userVote = governance.NewVotingInfo(votingWeight)
138 }
139
140 if userVote.IsVoted() {
141 return governance.DefaultVotingInfo(), 0, 0, makeErrorWithDetails(errAlreadyVoted, "user has already voted")
142 }
143 userVote = governance.NewVotedVotingInfo(votingWeight, votedYes, votingWeight, votedHeight, votedAt)
144
145 votingInfosTree, _ := gv.getProposalUserVotingInfos(proposalID)
146 if votingInfosTree == nil {
147 return governance.DefaultVotingInfo(), 0, 0, makeErrorWithDetails(
148 errDataNotFound, "voting infos tree not found for proposal")
149 }
150
151 votingInfosTree.Set(voterAddress.String(), userVote)
152 err := gv.store.SetProposalVotingInfos(0, rlm, proposalID, votingInfosTree)
153 if err != nil {
154 return governance.DefaultVotingInfo(), 0, 0, err
155 }
156
157 // Update proposal vote tallies
158 err = proposalResolver.Vote(votedYes, votingWeight)
159 if err != nil {
160 return governance.DefaultVotingInfo(), 0, 0, err
161 }
162
163 return userVote, proposal.VotingYesWeight(), proposal.VotingNoWeight(), nil
164}