Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}