337 lines
13 KiB
Go
337 lines
13 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"cosmossdk.io/collections"
|
|
"cosmossdk.io/math"
|
|
v1 "cosmossdk.io/x/gov/types/v1"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
)
|
|
|
|
// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the voters
|
|
func (k Keeper) Tally(ctx context.Context, proposal v1.Proposal) (passes, burnDeposits bool, tallyResults v1.TallyResult, err error) {
|
|
validators, err := k.getCurrentValidators(ctx)
|
|
if err != nil {
|
|
return false, false, v1.TallyResult{}, err
|
|
}
|
|
|
|
if k.config.CalculateVoteResultsAndVotingPowerFn == nil {
|
|
k.config.CalculateVoteResultsAndVotingPowerFn = defaultCalculateVoteResultsAndVotingPower
|
|
}
|
|
|
|
totalVoterPower, results, err := k.config.CalculateVoteResultsAndVotingPowerFn(ctx, k, proposal.Id, validators)
|
|
if err != nil {
|
|
return false, false, v1.TallyResult{}, err
|
|
}
|
|
|
|
params, err := k.Params.Get(ctx)
|
|
if err != nil {
|
|
return false, false, v1.TallyResult{}, err
|
|
}
|
|
tallyResults = v1.NewTallyResultFromMap(results)
|
|
|
|
// If there is no staked coins, the proposal fails
|
|
totalBonded, err := k.sk.TotalBondedTokens(ctx)
|
|
if err != nil {
|
|
return false, false, v1.TallyResult{}, err
|
|
}
|
|
|
|
if totalBonded.IsZero() {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// If there are more spam votes than the sum of all other options, proposal fails
|
|
// A proposal with no votes should not be considered spam
|
|
if !totalVoterPower.Equal(math.LegacyZeroDec()) &&
|
|
results[v1.OptionSpam].GTE(results[v1.OptionOne].Add(results[v1.OptionTwo].Add(results[v1.OptionThree].Add(results[v1.OptionFour])))) {
|
|
return false, true, tallyResults, nil
|
|
}
|
|
|
|
switch proposal.ProposalType {
|
|
case v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC:
|
|
return k.tallyOptimistic(totalVoterPower, totalBonded, results, params)
|
|
case v1.ProposalType_PROPOSAL_TYPE_EXPEDITED:
|
|
return k.tallyExpedited(totalVoterPower, totalBonded, results, params)
|
|
case v1.ProposalType_PROPOSAL_TYPE_MULTIPLE_CHOICE:
|
|
return k.tallyMultipleChoice(totalVoterPower, totalBonded, results, params)
|
|
default:
|
|
return k.tallyStandard(ctx, proposal, totalVoterPower, totalBonded, results, params)
|
|
}
|
|
}
|
|
|
|
// tallyStandard tallies the votes of a standard proposal
|
|
// If there is not enough quorum of votes, the proposal fails
|
|
// If no one votes (everyone abstains), proposal fails
|
|
// If more than 1/3 of voters veto, proposal fails
|
|
// If more than 1/2 of non-abstaining voters vote Yes, proposal passes
|
|
// If more than 1/2 of non-abstaining voters vote No, proposal fails
|
|
// Checking for spam votes is done before calling this function
|
|
func (k Keeper) tallyStandard(ctx context.Context, proposal v1.Proposal, totalVoterPower math.LegacyDec, totalBonded math.Int, results map[v1.VoteOption]math.LegacyDec, params v1.Params) (passes, burnDeposits bool, tallyResults v1.TallyResult, err error) {
|
|
tallyResults = v1.NewTallyResultFromMap(results)
|
|
|
|
quorumStr := params.Quorum
|
|
yesQuorumStr := params.YesQuorum
|
|
thresholdStr := params.Threshold
|
|
vetoThresholdStr := params.VetoThreshold
|
|
|
|
if len(proposal.Messages) > 0 {
|
|
// check if any of the message has message based params
|
|
customMessageParams, err := k.MessageBasedParams.Get(ctx, sdk.MsgTypeURL(proposal.Messages[0]))
|
|
if err != nil && !errors.Is(err, collections.ErrNotFound) {
|
|
return false, false, tallyResults, err
|
|
} else if err == nil {
|
|
quorumStr = customMessageParams.GetQuorum()
|
|
thresholdStr = customMessageParams.GetThreshold()
|
|
vetoThresholdStr = customMessageParams.GetVetoThreshold()
|
|
yesQuorumStr = customMessageParams.GetYesQuorum()
|
|
}
|
|
}
|
|
|
|
// If there is not enough quorum of votes, the proposal fails
|
|
percentVoting := totalVoterPower.Quo(math.LegacyNewDecFromInt(totalBonded))
|
|
quorum, _ := math.LegacyNewDecFromStr(quorumStr)
|
|
if percentVoting.LT(quorum) {
|
|
return false, params.BurnVoteQuorum, tallyResults, nil
|
|
}
|
|
|
|
// If no one votes (everyone abstains), proposal fails
|
|
if totalVoterPower.Equal(results[v1.OptionAbstain]) {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// If yes quorum enabled and less than yes_quorum of voters vote Yes, proposal fails
|
|
yesQuorum, _ := math.LegacyNewDecFromStr(yesQuorumStr)
|
|
if yesQuorum.GT(math.LegacyZeroDec()) && results[v1.OptionYes].Quo(totalVoterPower).LT(yesQuorum) {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// If more than 1/3 of voters veto, proposal fails
|
|
vetoThreshold, _ := math.LegacyNewDecFromStr(vetoThresholdStr)
|
|
if results[v1.OptionNoWithVeto].Quo(totalVoterPower).GT(vetoThreshold) {
|
|
return false, params.BurnVoteVeto, tallyResults, nil
|
|
}
|
|
|
|
// If more than 1/2 of non-abstaining voters vote Yes, proposal passes
|
|
threshold, _ := math.LegacyNewDecFromStr(thresholdStr)
|
|
if results[v1.OptionYes].Quo(totalVoterPower.Sub(results[v1.OptionAbstain])).GT(threshold) {
|
|
return true, false, tallyResults, nil
|
|
}
|
|
|
|
// If more than 1/2 of non-abstaining voters vote No, proposal fails
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// tallyExpedited tallies the votes of an expedited proposal
|
|
// If there is not enough expedited quorum of votes, the proposal fails
|
|
// If no one votes (everyone abstains), proposal fails
|
|
// If more than 1/3 of voters veto, proposal fails
|
|
// If more than 2/3 of non-abstaining voters vote Yes, proposal passes
|
|
// If more than 1/2 of non-abstaining voters vote No, proposal fails
|
|
// Checking for spam votes is done before calling this function
|
|
func (k Keeper) tallyExpedited(totalVoterPower math.LegacyDec, totalBonded math.Int, results map[v1.VoteOption]math.LegacyDec, params v1.Params) (passes, burnDeposits bool, tallyResults v1.TallyResult, err error) {
|
|
tallyResults = v1.NewTallyResultFromMap(results)
|
|
|
|
// If there is not enough quorum of votes, the proposal fails
|
|
percentVoting := totalVoterPower.Quo(math.LegacyNewDecFromInt(totalBonded))
|
|
expeditedQuorum, _ := math.LegacyNewDecFromStr(params.ExpeditedQuorum)
|
|
if percentVoting.LT(expeditedQuorum) {
|
|
return false, params.BurnVoteQuorum, tallyResults, nil
|
|
}
|
|
|
|
// If no one votes (everyone abstains), proposal fails
|
|
if totalVoterPower.Equal(results[v1.OptionAbstain]) {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// If yes quorum enabled and less than yes_quorum of voters vote Yes, proposal fails
|
|
yesQuorum, _ := math.LegacyNewDecFromStr(params.YesQuorum)
|
|
if yesQuorum.GT(math.LegacyZeroDec()) && results[v1.OptionYes].Quo(totalVoterPower).LT(yesQuorum) {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// If more than 1/3 of voters veto, proposal fails
|
|
vetoThreshold, _ := math.LegacyNewDecFromStr(params.VetoThreshold)
|
|
if results[v1.OptionNoWithVeto].Quo(totalVoterPower).GT(vetoThreshold) {
|
|
return false, params.BurnVoteVeto, tallyResults, nil
|
|
}
|
|
|
|
// If more than 2/3 of non-abstaining voters vote Yes, proposal passes
|
|
threshold, _ := math.LegacyNewDecFromStr(params.GetExpeditedThreshold())
|
|
if results[v1.OptionYes].Quo(totalVoterPower.Sub(results[v1.OptionAbstain])).GT(threshold) {
|
|
return true, false, tallyResults, nil
|
|
}
|
|
|
|
// If more than 1/2 of non-abstaining voters vote No, proposal fails
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
// tallyOptimistic tallies the votes of an optimistic proposal
|
|
// If proposal has no votes, proposal passes
|
|
// If the threshold of no is reached, proposal fails
|
|
// Any other case, proposal passes
|
|
// Checking for spam votes is done before calling this function
|
|
func (k Keeper) tallyOptimistic(totalVoterPower math.LegacyDec, totalBonded math.Int, results map[v1.VoteOption]math.LegacyDec, params v1.Params) (passes, burnDeposits bool, tallyResults v1.TallyResult, err error) {
|
|
tallyResults = v1.NewTallyResultFromMap(results)
|
|
optimisticNoThreshold, _ := math.LegacyNewDecFromStr(params.OptimisticRejectedThreshold)
|
|
|
|
// If proposal has no votes, proposal passes
|
|
if totalVoterPower.Equal(math.LegacyZeroDec()) {
|
|
return true, false, tallyResults, nil
|
|
}
|
|
|
|
// If the threshold of no is reached, proposal fails
|
|
if results[v1.OptionNo].Quo(totalBonded.ToLegacyDec()).GT(optimisticNoThreshold) {
|
|
return false, false, tallyResults, nil
|
|
}
|
|
|
|
return true, false, tallyResults, nil
|
|
}
|
|
|
|
// tallyMultipleChoice tallies the votes of a multiple choice proposal
|
|
// If there is not enough quorum of votes, the proposal fails
|
|
// Any other case, proposal passes
|
|
// Checking for spam votes is done before calling this function
|
|
func (k Keeper) tallyMultipleChoice(totalVoterPower math.LegacyDec, totalBonded math.Int, results map[v1.VoteOption]math.LegacyDec, params v1.Params) (passes, burnDeposits bool, tallyResults v1.TallyResult, err error) {
|
|
tallyResults = v1.NewTallyResultFromMap(results)
|
|
|
|
// If there is not enough quorum of votes, the proposal fails
|
|
percentVoting := totalVoterPower.Quo(math.LegacyNewDecFromInt(totalBonded))
|
|
quorum, _ := math.LegacyNewDecFromStr(params.Quorum)
|
|
if percentVoting.LT(quorum) {
|
|
return false, params.BurnVoteQuorum, tallyResults, nil
|
|
}
|
|
|
|
// a multiple choice proposal always passes unless it was spam or quorum was not reached.
|
|
return true, false, tallyResults, nil
|
|
}
|
|
|
|
// getCurrentValidators fetches all the bonded validators, insert them into currValidators
|
|
func (k Keeper) getCurrentValidators(ctx context.Context) (map[string]v1.ValidatorGovInfo, error) {
|
|
currValidators := make(map[string]v1.ValidatorGovInfo)
|
|
if err := k.sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator sdk.ValidatorI) (stop bool) {
|
|
valBz, err := k.sk.ValidatorAddressCodec().StringToBytes(validator.GetOperator())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
currValidators[validator.GetOperator()] = v1.NewValidatorGovInfo(
|
|
valBz,
|
|
validator.GetBondedTokens(),
|
|
validator.GetDelegatorShares(),
|
|
math.LegacyZeroDec(),
|
|
v1.WeightedVoteOptions{},
|
|
)
|
|
|
|
return false
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return currValidators, nil
|
|
}
|
|
|
|
// calculateVoteResultsAndVotingPower iterate over all votes, tally up the voting power of each validator
|
|
// and returns the votes results from voters
|
|
func defaultCalculateVoteResultsAndVotingPower(
|
|
ctx context.Context,
|
|
k Keeper,
|
|
proposalID uint64,
|
|
validators map[string]v1.ValidatorGovInfo,
|
|
) (math.LegacyDec, map[v1.VoteOption]math.LegacyDec, error) {
|
|
totalVP := math.LegacyZeroDec()
|
|
results := createEmptyResults()
|
|
|
|
// iterate over all votes, tally up the voting power of each validator
|
|
rng := collections.NewPrefixedPairRange[uint64, sdk.AccAddress](proposalID)
|
|
votesToRemove := []collections.Pair[uint64, sdk.AccAddress]{}
|
|
if err := k.Votes.Walk(ctx, rng, func(key collections.Pair[uint64, sdk.AccAddress], vote v1.Vote) (bool, error) {
|
|
// if validator, just record it in the map
|
|
voter, err := k.authKeeper.AddressCodec().StringToBytes(vote.Voter)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
valAddrStr, err := k.sk.ValidatorAddressCodec().BytesToString(voter)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if val, ok := validators[valAddrStr]; ok {
|
|
val.Vote = vote.Options
|
|
validators[valAddrStr] = val
|
|
}
|
|
|
|
// iterate over all delegations from voter, deduct from any delegated-to validators
|
|
err = k.sk.IterateDelegations(ctx, voter, func(index int64, delegation sdk.DelegationI) (stop bool) {
|
|
valAddrStr := delegation.GetValidatorAddr()
|
|
|
|
if val, ok := validators[valAddrStr]; ok {
|
|
// There is no need to handle the special case that validator address equal to voter address.
|
|
// Because voter's voting power will tally again even if there will be deduction of voter's voting power from validator.
|
|
val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares())
|
|
validators[valAddrStr] = val
|
|
|
|
// delegation shares * bonded / total shares
|
|
votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares)
|
|
|
|
for _, option := range vote.Options {
|
|
weight, _ := math.LegacyNewDecFromStr(option.Weight)
|
|
subPower := votingPower.Mul(weight)
|
|
results[option.Option] = results[option.Option].Add(subPower)
|
|
}
|
|
|
|
totalVP = totalVP.Add(votingPower)
|
|
}
|
|
|
|
return false
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
votesToRemove = append(votesToRemove, key)
|
|
return false, nil
|
|
}); err != nil {
|
|
return math.LegacyDec{}, nil, err
|
|
}
|
|
|
|
// remove all votes from store
|
|
for _, key := range votesToRemove {
|
|
if err := k.Votes.Remove(ctx, key); err != nil {
|
|
return math.LegacyDec{}, nil, err
|
|
}
|
|
}
|
|
|
|
// iterate over the validators again to tally their voting power
|
|
for _, val := range validators {
|
|
if len(val.Vote) == 0 {
|
|
continue
|
|
}
|
|
|
|
sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions)
|
|
votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares)
|
|
|
|
for _, option := range val.Vote {
|
|
weight, _ := math.LegacyNewDecFromStr(option.Weight)
|
|
subPower := votingPower.Mul(weight)
|
|
results[option.Option] = results[option.Option].Add(subPower)
|
|
}
|
|
totalVP = totalVP.Add(votingPower)
|
|
}
|
|
|
|
return totalVP, results, nil
|
|
}
|
|
|
|
func createEmptyResults() map[v1.VoteOption]math.LegacyDec {
|
|
results := make(map[v1.VoteOption]math.LegacyDec)
|
|
results[v1.OptionYes] = math.LegacyZeroDec()
|
|
results[v1.OptionAbstain] = math.LegacyZeroDec()
|
|
results[v1.OptionNo] = math.LegacyZeroDec()
|
|
results[v1.OptionNoWithVeto] = math.LegacyZeroDec()
|
|
results[v1.OptionSpam] = math.LegacyZeroDec()
|
|
|
|
return results
|
|
}
|