-
Notifications
You must be signed in to change notification settings - Fork 208
/
ballot.go
169 lines (142 loc) · 5.65 KB
/
ballot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package keeper
import (
"sort"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/NibiruChain/collections"
"github.com/NibiruChain/nibiru/x/common/asset"
"github.com/NibiruChain/nibiru/x/common/set"
"github.com/NibiruChain/nibiru/x/oracle/types"
)
// groupBallotsByPair groups votes by pair and removes votes that are not part of
// the validator set.
//
// NOTE: **Make abstain votes to have zero vote power**
func (k Keeper) groupBallotsByPair(
ctx sdk.Context,
validatorsPerformance types.ValidatorPerformances,
) (pairBallotsMap map[asset.Pair]types.ExchangeRateBallots) {
pairBallotsMap = map[asset.Pair]types.ExchangeRateBallots{}
for _, value := range k.Votes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).KeyValues() {
voterAddr, aggregateVote := value.Key, value.Value
// organize ballot only for the active validators
if validatorPerformance, exists := validatorsPerformance[aggregateVote.Voter]; exists {
for _, exchangeRateTuple := range aggregateVote.ExchangeRateTuples {
power := validatorPerformance.Power
if !exchangeRateTuple.ExchangeRate.IsPositive() {
// Make the power of abstain vote zero
power = 0
}
pairBallotsMap[exchangeRateTuple.Pair] = append(pairBallotsMap[exchangeRateTuple.Pair],
types.NewExchangeRateBallot(
exchangeRateTuple.ExchangeRate,
exchangeRateTuple.Pair,
voterAddr,
power,
),
)
}
}
}
return
}
// clearVotesAndPreVotes clears all tallied prevotes and votes from the store
func (k Keeper) clearVotesAndPreVotes(ctx sdk.Context, votePeriod uint64) {
// Clear all aggregate prevotes
for _, prevote := range k.Prevotes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).KeyValues() {
valAddr, aggregatePrevote := prevote.Key, prevote.Value
if ctx.BlockHeight() >= int64(aggregatePrevote.SubmitBlock+votePeriod) {
err := k.Prevotes.Delete(ctx, valAddr)
if err != nil {
k.Logger(ctx).Error("failed to delete prevote", "error", err)
}
}
}
// Clear all aggregate votes
for _, valAddr := range k.Votes.Iterate(ctx, collections.Range[sdk.ValAddress]{}).Keys() {
err := k.Votes.Delete(ctx, valAddr)
if err != nil {
k.Logger(ctx).Error("failed to delete vote", "error", err)
}
}
}
// isPassingVoteThreshold ballot is passing the threshold amount of voting power
func isPassingVoteThreshold(ballots types.ExchangeRateBallots, thresholdVotingPower sdkmath.Int, minVoters uint64) bool {
ballotPower := sdk.NewInt(ballots.Power())
if ballotPower.IsZero() {
return false
}
if ballotPower.LT(thresholdVotingPower) {
return false
}
if ballots.NumValidVoters() < minVoters {
return false
}
return true
}
// removeInvalidBallots removes the ballots which have not reached the vote threshold
// or which are not part of the whitelisted pairs anymore: example when params change during a vote period
// but some votes were already made.
//
// ALERT: This function mutates pairBallotMap slice, it removes the ballot for the pair which is not passing the threshold
// or which is not whitelisted anymore.
func (k Keeper) removeInvalidBallots(
ctx sdk.Context,
pairBallotsMap map[asset.Pair]types.ExchangeRateBallots,
) (map[asset.Pair]types.ExchangeRateBallots, set.Set[asset.Pair]) {
whitelistedPairs := set.New(k.GetWhitelistedPairs(ctx)...)
totalBondedPower := sdk.TokensToConsensusPower(k.StakingKeeper.TotalBondedTokens(ctx), k.StakingKeeper.PowerReduction(ctx))
thresholdVotingPower := k.VoteThreshold(ctx).MulInt64(totalBondedPower).RoundInt()
minVoters := k.MinVoters(ctx)
// Iterate through sorted keys for deterministic ordering.
// For more info, see: https://github.com/NibiruChain/nibiru/issues/1374#issue-1715353299
var pairs []string
for pair := range pairBallotsMap {
pairs = append(pairs, pair.String())
}
sort.Strings(pairs)
for _, pairStr := range pairs {
pair := asset.Pair(pairStr)
ballots := pairBallotsMap[pair]
// If pair is not whitelisted, or the ballot for it has failed, then skip
// and remove it from pairBallotsMap for iteration efficiency
if _, exists := whitelistedPairs[pair]; !exists {
delete(pairBallotsMap, pair)
continue
}
// If the ballot is not passed, remove it from the whitelistedPairs set
// to prevent slashing validators who did valid vote.
if !isPassingVoteThreshold(ballots, thresholdVotingPower, minVoters) {
delete(whitelistedPairs, pair)
delete(pairBallotsMap, pair)
continue
}
}
return pairBallotsMap, whitelistedPairs
}
// Tally calculates the median and returns it. Sets the set of voters to be rewarded, i.e. voted within
// a reasonable spread from the weighted median to the store
//
// ALERT: This function mutates validatorPerformances slice based on the votes made by the validators.
func Tally(ballots types.ExchangeRateBallots, rewardBand sdk.Dec, validatorPerformances types.ValidatorPerformances) sdk.Dec {
weightedMedian := ballots.WeightedMedianWithAssertion()
standardDeviation := ballots.StandardDeviation(weightedMedian)
rewardSpread := weightedMedian.Mul(rewardBand.QuoInt64(2))
if standardDeviation.GT(rewardSpread) {
rewardSpread = standardDeviation
}
for _, ballot := range ballots {
// Filter ballot winners & abstain voters
voteInsideSpread := ballot.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) &&
ballot.ExchangeRate.LTE(weightedMedian.Add(rewardSpread))
isAbstainVote := !ballot.ExchangeRate.IsPositive()
if voteInsideSpread || isAbstainVote {
voterAddr := ballot.Voter.String()
validatorPerformance := validatorPerformances[voterAddr]
validatorPerformance.RewardWeight += ballot.Power
validatorPerformance.WinCount++
validatorPerformances[voterAddr] = validatorPerformance
}
}
return weightedMedian
}