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

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}