Search Apps Documentation Source Content File Folder Download Copy Actions Download

governance_vote.gno

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