Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}