From 7cc73b4f6167143aad8a9dd1e7c22cc442a8c2ff Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:46:39 -0500 Subject: [PATCH] fix(oracle)!: count vote omission as abstain for less slashing + more stability (#1565) * refactor(liquidate_test): use named imports * refactor(amm_test): use named imports * refactor(admin_test, calc_test, grpc_query_test): use named imports * refactor(perp/keeper): use named imports * refactor(perp/keeper): use named imports | Complete * changelog * fix(oracle): miss outside reward band but not on abstain * fix(oracle): #wip checkpoint for omit == abstain * checkout x/perp from master * change log * test(oracle/types): more unit tests * refactor: use types. instead of New for names args * rm error.txt * del zombie code * variable renaming * rename exchange rate ballot to exchange rate vote * rename ballot to vote * rename variables * rename variables * rename variables * fix TestWhitelistedPairs * rename variables * rename ballots to votes --------- Co-authored-by: Kevin Yang <5478483+k-yang@users.noreply.github.com> --- CHANGELOG.md | 113 ++++++++-- x/oracle/keeper/ballot.go | 149 +++++++------ x/oracle/keeper/ballot_test.go | 154 ++++++------- x/oracle/keeper/msg_server_test.go | 2 +- x/oracle/keeper/params.go | 6 +- x/oracle/keeper/reward.go | 6 +- x/oracle/keeper/reward_test.go | 2 +- x/oracle/keeper/slash_test.go | 103 ++++----- x/oracle/keeper/test_utils.go | 23 +- x/oracle/keeper/update_exchange_rates.go | 105 +++++---- x/oracle/keeper/update_exchange_rates_test.go | 210 ++++++++++-------- x/oracle/keeper/whitelist.go | 4 +- x/oracle/keeper/whitelist_test.go | 6 +- x/oracle/types/ballot.go | 80 ++++--- x/oracle/types/ballot_test.go | 78 ++++--- x/oracle/types/test_utils.go | 2 +- 16 files changed, 618 insertions(+), 425 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e358bcec..678c4e689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,29 +42,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features -* [#1596](https://github.com/NibiruChain/nibiru/pull/1596) - epic(tokenfactory): - State transitions, collections, genesis import and export, and app wiring - * [#1607](https://github.com/NibiruChain/nibiru/pull/1607) - Token factory - transaction messages for CreateDenom, ChangeAdmin, and UpdateModuleParams - * [#1620](https://github.com/NibiruChain/nibiru/pull/1620) - Token factory - transaction messages for Mint and Burn +* [#1596](https://github.com/NibiruChain/nibiru/pull/1596) - epic(tokenfactory): State transitions, collections, genesis import and export, and app wiring +* [#1607](https://github.com/NibiruChain/nibiru/pull/1607) - Token factory transaction messages for CreateDenom, ChangeAdmin, and UpdateModuleParams +* [#1620](https://github.com/NibiruChain/nibiru/pull/1620) - Token factory transaction messages for Mint and Burn * [#1573](https://github.com/NibiruChain/nibiru/pull/1573) - feat(perp): Close markets and compute settlement price ### State Machine Breaking * [#1609](https://github.com/NibiruChain/nibiru/pull/1609) - refactor(app)!: Remove x/stablecoin module. * [#1613](https://github.com/NibiruChain/nibiru/pull/1613) - feat(app)!: enforce min commission by changing default and genesis validation -* [#1615](https://github.com/NibiruChain/nibiru/pull/1613) - feat(ante)!: Ante - handler to add a maximum commission rate of 25% for validators. -* [#1616](https://github.com/NibiruChain/nibiru/pull/1616) - fix(app)!: - Add custom wasm snapshotter for proper state exports -* [#1617](https://github.com/NibiruChain/nibiru/pull/1617) - fix(app)!: - non-nil snapshot manager is not guarantted in testapp +* [#1615](https://github.com/NibiruChain/nibiru/pull/1613) - feat(ante)!: Ante handler to add a maximum commission rate of 25% for validators. +* [#1616](https://github.com/NibiruChain/nibiru/pull/1616) - fix(app)!: Add custom wasm snapshotter for proper state exports +* [#1617](https://github.com/NibiruChain/nibiru/pull/1617) - fix(app)!: non-nil snapshot manager is not guaranteed in testapp ### Improvements -* [#1610](https://github.com/NibiruChain/nibiru/pull/1610) - refactor(app): - Simplify app.go with less redundant imports using struct embedding. +* [#1610](https://github.com/NibiruChain/nibiru/pull/1610) - refactor(app): Simplify app.go with less redundant imports using struct embedding. * [#1614](https://github.com/NibiruChain/nibiru/pull/1614) - refactor(proto): Use explicit namespacing on proto imports for #1608 * [#1630](https://github.com/NibiruChain/nibiru/pull/1630) - refactor(wasm): clean up wasmbinding/ folder structure @@ -93,7 +86,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#1559](https://github.com/NibiruChain/nibiru/pull/1559) - feat: add versions to markets to allow to disable them * [#1585](https://github.com/NibiruChain/nibiru/pull/1585) - feat: include flag versioned in query markets to allow to query disabled markets * [#1594](https://github.com/NibiruChain/nibiru/pull/1594) - feat: add user discounts - +* [#1463](https://github.com/NibiruChain/nibiru/pull/1463) - feat(oracle): add genesis pricefeeder delegation +* [#1479](https://github.com/NibiruChain/nibiru/pull/1479) - feat(perp): implement `PartialClose` +* [#1498](https://github.com/NibiruChain/nibiru/pull/1498) - feat: add cli to change root sudo command +* [#1501](https://github.com/NibiruChain/nibiru/pull/1501) - feat(localnet.sh): (1) Make it possible to run while offline. (2) Implement --no-build option to use the script with the current `nibid` installed. +* [#1501](https://github.com/NibiruChain/nibiru/pull/1501) - feat(proto): add Python buf generation logic for py-sdk +* [#1503](https://github.com/NibiruChain/nibiru/pull/1503) - feat(wasm): add Oracle Exchange Rate query for wasm +* [#1543](https://github.com/NibiruChain/nibiru/pull/1543) - epic(devgas): devgas module for incentivizing smart contract +* ### Improvements * [#1466](https://github.com/NibiruChain/nibiru/pull/1466) - refactor(perp): `PositionLiquidatedEvent` @@ -116,6 +116,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#1555](https://github.com/NibiruChain/nibiru/pull/1555) - feat(devgas): Convert legacy ABCI events to typed proto events * [#1558](https://github.com/NibiruChain/nibiru/pull/1558) - feat(perp): paginated query to read the position store * [#1554](https://github.com/NibiruChain/nibiru/pull/1554) - refactor: runs gofumpt formatter, which has nice conventions: go install mvdan.cc/gofumpt@latest + +### Bug Fixes + +* [#1459](https://github.com/NibiruChain/nibiru/pull/1459) - fix(spot): wire `x/spot` msgService into app router +* [#1467](https://github.com/NibiruChain/nibiru/pull/1467) - fix(oracle): make `calcTwap` safer +* [#1464](https://github.com/NibiruChain/nibiru/pull/1464) - fix(gov): wire legacy proposal handlers +* [#1565](https://github.com/NibiruChain/nibiru/pull/1565) - fix(oracle)!: Count vote omission as abstain for less slashing + more stability + +### State Machine Breaking + +* [#1473](https://github.com/NibiruChain/nibiru/pull/1473) - refactor(perp)!: rename `OpenPosition` to `MarketOrder` +* [#1477](https://github.com/NibiruChain/nibiru/pull/1477) - refactor(oracle)!: Move away from deprecated events to typed events in x/oracle + +### Dependencies + +- Bump `robinraju/release-downloader` from 1.6 to 1.8 (#1326) +- Bump `pozetroninc/github-action-get-latest-release` from 0.6.0 to 0.7.0 (#1325) +- Bump `technote-space/get-diff-action` from 4 to 6 (#1327) +- Bump `actions/setup-go` from 3 to 4 (#1324) +- Bump `github.com/docker/distribution` from 2.8.1+incompatible to 2.8.2+incompatible (#1339) +- Bump `github.com/CosmWasm/wasmvm` from 1.2.1 to 1.3.0 (#1354, #1507) +- Bump `github.com/spf13/cast` from 1.5.0 to 1.5.1 (#1358) +- Bump `github.com/stretchr/testify` from 1.8.2 to 1.8.4 (#1384, #1435) +- Bump `cosmossdk.io/math` from 1.0.0-beta.6 to 1.1.2 (#1394, [#1547](https://github.com/NibiruChain/nibiru/pull/1547)) +- Bump `google.golang.org/grpc` from 1.53.0 to 1.57.0 (#1395, #1437, #1443, #1497, [#1525](https://github.com/NibiruChain/nibiru/pull/1525)) +- Bump `github.com/gin-gonic/gin` from 1.8.1 to 1.9.1 (#1409) +- Bump `github.com/spf13/viper` from 1.15.0 to 1.16.0 (#1436) +- Bump `github.com/prometheus/client_golang` from 1.15.1 to 1.16.0 (#1431) +- Bump `github.com/cosmos/ibc-go/v7` from 7.1.0 to 7.3.0 (#1445, [#1562](https://github.com/NibiruChain/nibiru/pull/1562)) +- Bump `bufbuild/buf-setup-action` from 1.21.0 to 1.26.1 (#1449, #1469, #1505, #1510, [#1537](https://github.com/NibiruChain/nibiru/pull/1537), [#1540](https://github.com/NibiruChain/nibiru/pull/1540), [#1544](https://github.com/NibiruChain/nibiru/pull/1544)) +- Bump `google.golang.org/protobuf` from 1.30.0 to 1.31.0 (#1450) +- Bump `cosmossdk.io/errors` from 1.0.0-beta.7 to 1.0.0 (#1499) +- Bump `github.com/holiman/uint256` from 1.2.2 to 1.2.3 (#1504) +- Bump `actions/checkout` from 3 to 4 ([#1563](https://github.com/NibiruChain/nibiru/pull/1563)) + +### Breaking + +* [#1380](https://github.com/NibiruChain/nibiru/pull/1380) - feat(wasm): Add CreateMarket admin call for the controller contract +* [#1359](https://github.com/NibiruChain/nibiru/pull/1359) - feat(perp): Add InsuranceFundWithdraw admin call with corresponding smart contract +* [#1356](https://github.com/NibiruChain/nibiru/pull/1356) - build: Regress wasmvm (v1.1.1), tendermint (v0.34.24), and Cosmos-SDK (v0.45.14) dependencies +* [#1346](https://github.com/NibiruChain/nibiru/pull/1346) - build: Upgrade wasmvm (v1.2.1), tendermint (v0.34.26), and Cosmos-SDK (v0.45.14) dependencies +* [#1317](https://github.com/NibiruChain/nibiru/pull/1317) - feat(sudo): Implement and test CLI commands for tx and queries. +* [#1307](https://github.com/NibiruChain/nibiru/pull/1307) - feat(sudo): Create the x/sudo module + integration tests +* [#1299](https://github.com/NibiruChain/nibiru/pull/1299) - feat(wasm): Add peg shift bindings +* [#1292](https://github.com/NibiruChain/nibiru/pull/1292) - feat(wasm): Add module bindings for execute calls in x/perp: OpenPosition, ClosePosition, AddMargin, RemoveMargin. +* [#1287](https://github.com/NibiruChain/nibiru/pull/1287) - feat(wasm): Add module bindings for custom queries in x/perp: Reserves, AllMarkets, BasePrice, PremiumFraction, Metrics, PerpParams, PerpModuleAccounts +* [#1282](https://github.com/NibiruChain/nibiru/pull/1282) - feat(inflation)!: add inflation module +* [#1270](https://github.com/NibiruChain/nibiru/pull/1270) - refactor(proto)!: lint protos and standardize versioning +* [#1271](https://github.com/NibiruChain/nibiru/pull/1271) - refactor(perp)!: vpool → perp/amm #2 | imports and renames +* [#1269](https://github.com/NibiruChain/nibiru/pull/1269) - refactor(perp)!: merge x/util with x/perp +* [#1267](https://github.com/NibiruChain/nibiru/pull/1267) - refactor(perp)!: vpool → perp/amm #1 | Moves types, keeper, and cli +* [#1243](https://github.com/NibiruChain/nibiru/pull/1243) - feat(vpool): sqrt of liquidity depth tracked on pool +* [#1220](https://github.com/NibiruChain/nibiru/pull/1220) - feat: reduce gas fees when posting price +* [#1229](https://github.com/NibiruChain/nibiru/pull/1229) - feat: upgrade ibc to v4.2.0 and wasm v0.30.0 +* [#1254](https://github.com/NibiruChain/nibiru/pull/1254) - feat: add bias field into vpool +* [#1255](https://github.com/NibiruChain/nibiru/pull/1255) - feat: add peg multiplier field into vpool, which for now defaults to 1 +* [#1281](https://github.com/NibiruChain/nibiru/pull/1281) - feat: add peg multiplier to the pricing logic +* [#1291](https://github.com/NibiruChain/nibiru/pull/1291) - refactor(perp)!: add perp v2 state protos +* [#1296](https://github.com/NibiruChain/nibiru/pull/1296) - refactor(perp)!: update perp v2 state protos +* [#1298](https://github.com/NibiruChain/nibiru/pull/1298) - refactor(perp)!: remove `MaxOracleSpreadRatio` from Perpv2 +* [#1302](https://github.com/NibiruChain/nibiru/pull/1302) - refactor(oracle)!: price snapshot start time inclusive +* [#1301](https://github.com/NibiruChain/nibiru/pull/1301) - fix(epochs)!: correct epoch start time +* [#1304](https://github.com/NibiruChain/nibiru/pull/1304) - feat: db backend - rocksdb +* [#1305](https://github.com/NibiruChain/nibiru/pull/1305) - refactor(perp!): Remove unnecessary protos +* [#1312](https://github.com/NibiruChain/nibiru/pull/1312) - feat(wasm): wire depth shift handler to the wasm router +* [#1306](https://github.com/NibiruChain/nibiru/pull/1306) - feat(perp): complete perp v2 types +* [#1309](https://github.com/NibiruChain/nibiru/pull/1309) - feat: minimum swap amount set to $1 +* [#1336](https://github.com/NibiruChain/nibiru/pull/1336) - feat: move oracle params out of params subspace and onto the keeper +* [#1315](https://github.com/NibiruChain/nibiru/pull/1315) - feat: oracle rewards distribution every week +* [#1342](https://github.com/NibiruChain/nibiru/pull/1342) - feat(perp): market not enabled can only be used to close out existing positions +* [#1367](https://github.com/NibiruChain/nibiru/pull/1367) - feat: wire enable market to wasm +* [#1382](https://github.com/NibiruChain/nibiru/pull/1382) - refactor(perp)!: remove `perpv1` +* [#1385](https://github.com/NibiruChain/nibiru/pull/1385) - test(perp): add clearing house negative tests +* [#1388](https://github.com/NibiruChain/nibiru/pull/1388) - refactor(perp)!: idempotent position changed event +* [#1387](https://github.com/NibiruChain/nibiru/pull/1387) - feat: upgrade to Cosmos SDK v0.46.10 +* [#1413](https://github.com/NibiruChain/nibiru/pull/1413) - fix(perp): provide descriptive errors when all liquidations fail in MultiLiquidate +* [#1427](https://github.com/NibiruChain/nibiru/pull/1427) - refactor(perp)!: PositionChangedEvent `MarginToUser` +* [#1407](https://github.com/NibiruChain/nibiru/pull/1407) - feat!: upgrade to Cosmos SDK v0.47.3 + +### Improvements + * [#1574](https://github.com/NibiruChain/nibiru/pull/1574) - chore(goreleaser): update wasmvm to v1.4.0 * [#1463](https://github.com/NibiruChain/nibiru/pull/1463) - feat(oracle): add genesis pricefeeder delegation * [#1466](https://github.com/NibiruChain/nibiru/pull/1466) - refactor(perp): `PositionLiquidatedEvent` @@ -687,4 +768,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Testing * [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests. -* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods. \ No newline at end of file +* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods. diff --git a/x/oracle/keeper/ballot.go b/x/oracle/keeper/ballot.go index 1c7da53ad..7af68d545 100644 --- a/x/oracle/keeper/ballot.go +++ b/x/oracle/keeper/ballot.go @@ -13,48 +13,52 @@ import ( "github.com/NibiruChain/nibiru/x/oracle/types" ) -// groupBallotsByPair takes a collection of votes and organizes them by their +// groupVotesByPair takes a collection of votes and groups them by their // associated asset pair. This method only considers votes from active validators // and disregards votes from validators that are not in the provided validator set. // // Note that any abstain votes (votes with a non-positive exchange rate) are // assigned zero vote power. This function then returns a map where each -// asset pair is associated with its collection of ExchangeRateBallots. -func (k Keeper) groupBallotsByPair( +// asset pair is associated with its collection of ExchangeRateVotes. +func (k Keeper) groupVotesByPair( ctx sdk.Context, - validatorsPerformance types.ValidatorPerformances, -) (pairBallotsMap map[asset.Pair]types.ExchangeRateBallots) { - pairBallotsMap = map[asset.Pair]types.ExchangeRateBallots{} + validatorPerformances types.ValidatorPerformances, +) (pairVotes map[asset.Pair]types.ExchangeRateVotes) { + pairVotes = map[asset.Pair]types.ExchangeRateVotes{} 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, - ), - ) + // skip votes from inactive validators + validatorPerformance, exists := validatorPerformances[aggregateVote.Voter] + if !exists { + continue + } + + for _, tuple := range aggregateVote.ExchangeRateTuples { + power := validatorPerformance.Power + if !tuple.ExchangeRate.IsPositive() { + // Make the power of abstain vote zero + power = 0 } + + pairVotes[tuple.Pair] = append( + pairVotes[tuple.Pair], + types.NewExchangeRateVote( + tuple.ExchangeRate, + tuple.Pair, + voterAddr, + power, + ), + ) } } return } -// clearVotesAndPreVotes clears all tallied prevotes and votes from the store -func (k Keeper) clearVotesAndPreVotes(ctx sdk.Context, votePeriod uint64) { +// 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 @@ -75,95 +79,104 @@ func (k Keeper) clearVotesAndPreVotes(ctx sdk.Context, votePeriod uint64) { } } -// isPassingVoteThreshold ballot is passing the threshold amount of voting power +// isPassingVoteThreshold votes is passing the threshold amount of voting power func isPassingVoteThreshold( - ballots types.ExchangeRateBallots, thresholdVotingPower sdkmath.Int, minVoters uint64, + votes types.ExchangeRateVotes, thresholdVotingPower sdkmath.Int, minVoters uint64, ) bool { - ballotPower := sdk.NewInt(ballots.Power()) - if ballotPower.IsZero() { + totalPower := sdk.NewInt(votes.Power()) + if totalPower.IsZero() { return false } - if ballotPower.LT(thresholdVotingPower) { + if totalPower.LT(thresholdVotingPower) { return false } - if ballots.NumValidVoters() < minVoters { + if votes.NumValidVoters() < minVoters { return false } return true } -// removeInvalidBallots removes the ballots which have not reached the vote +// removeInvalidVotes removes the votes 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 +// ALERT: This function mutates the pairVotes map, it removes the votes for // the pair which is not passing the threshold or which is not whitelisted // anymore. -func (k Keeper) removeInvalidBallots( +func (k Keeper) removeInvalidVotes( ctx sdk.Context, - pairBallotsMap map[asset.Pair]types.ExchangeRateBallots, -) (map[asset.Pair]types.ExchangeRateBallots, set.Set[asset.Pair]) { - whitelistedPairs := set.New(k.GetWhitelistedPairs(ctx)...) - + pairVotes map[asset.Pair]types.ExchangeRateVotes, + whitelistedPairs set.Set[asset.Pair], +) { 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. - orderedBallotsMap := omap.OrderedMap_Pair[types.ExchangeRateBallots](pairBallotsMap) - for pair := range orderedBallotsMap.Range() { - ballots := pairBallotsMap[pair] - // If pair is not whitelisted, or the ballot for it has failed, then skip + orderedPairVotes := omap.OrderedMap_Pair[types.ExchangeRateVotes](pairVotes) + for pair := range orderedPairVotes.Range() { + // If pair is not whitelisted, or the votes for it has failed, then skip // and remove it from pairBallotsMap for iteration efficiency - if _, exists := whitelistedPairs[pair]; !exists { - delete(pairBallotsMap, pair) - continue + if !whitelistedPairs.Has(pair) { + delete(pairVotes, pair) } - // If the ballot is not passed, remove it from the whitelistedPairs set + // If the votes is not passed, remove it from the whitelistedPairs set // to prevent slashing validators who did valid vote. - if !isPassingVoteThreshold(ballots, thresholdVotingPower, minVoters) { + if !isPassingVoteThreshold( + pairVotes[pair], + k.VoteThreshold(ctx).MulInt64(totalBondedPower).RoundInt(), + k.MinVoters(ctx), + ) { delete(whitelistedPairs, pair) - delete(pairBallotsMap, pair) + delete(pairVotes, 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 +// 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) +// ALERT: This function mutates validatorPerformances slice based on the votes +// made by the validators. +func Tally( + votes types.ExchangeRateVotes, + rewardBand sdk.Dec, + validatorPerformances types.ValidatorPerformances, +) sdk.Dec { + weightedMedian := votes.WeightedMedianWithAssertion() + standardDeviation := votes.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() + for _, v := range votes { + // Filter votes winners & abstain voters + isInsideSpread := v.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) && + v.ExchangeRate.LTE(weightedMedian.Add(rewardSpread)) + isAbstainVote := !v.ExchangeRate.IsPositive() // strictly less than zero, don't want to include zero + isMiss := !isInsideSpread && !isAbstainVote - if voteInsideSpread || isAbstainVote { - voterAddr := ballot.Voter.String() + validatorPerformance := validatorPerformances[v.Voter.String()] - validatorPerformance := validatorPerformances[voterAddr] - validatorPerformance.RewardWeight += ballot.Power + switch { + case isInsideSpread: + validatorPerformance.RewardWeight += v.Power validatorPerformance.WinCount++ - validatorPerformances[voterAddr] = validatorPerformance + case isMiss: + validatorPerformance.MissCount++ + case isAbstainVote: + validatorPerformance.AbstainCount++ } + + validatorPerformances[v.Voter.String()] = validatorPerformance } return weightedMedian diff --git a/x/oracle/keeper/ballot_test.go b/x/oracle/keeper/ballot_test.go index 68e4c5b63..b66e78033 100644 --- a/x/oracle/keeper/ballot_test.go +++ b/x/oracle/keeper/ballot_test.go @@ -21,7 +21,7 @@ import ( "github.com/NibiruChain/nibiru/x/oracle/types" ) -func TestGroupBallotsByPair(t *testing.T) { +func TestGroupVotesByPair(t *testing.T) { fixture := CreateTestFixture(t) power := int64(100) @@ -37,25 +37,27 @@ func TestGroupBallotsByPair(t *testing.T) { require.NoError(t, err) staking.EndBlocker(fixture.Ctx, &fixture.StakingKeeper) - btcBallots := types.ExchangeRateBallots{ - types.NewExchangeRateBallot(sdk.NewDec(17), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[0], power), - types.NewExchangeRateBallot(sdk.NewDec(10), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[1], power), - types.NewExchangeRateBallot(sdk.NewDec(6), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[2], power), + pairBtc := asset.Registry.Pair(denoms.BTC, denoms.NUSD) + pairEth := asset.Registry.Pair(denoms.ETH, denoms.NUSD) + btcVotes := types.ExchangeRateVotes{ + {Pair: pairBtc, ExchangeRate: sdk.NewDec(17), Voter: ValAddrs[0], Power: power}, + {Pair: pairBtc, ExchangeRate: sdk.NewDec(10), Voter: ValAddrs[1], Power: power}, + {Pair: pairBtc, ExchangeRate: sdk.NewDec(6), Voter: ValAddrs[2], Power: power}, } - ethBallots := types.ExchangeRateBallots{ - types.NewExchangeRateBallot(sdk.NewDec(1000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[0], power), - types.NewExchangeRateBallot(sdk.NewDec(1300), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[1], power), - types.NewExchangeRateBallot(sdk.NewDec(2000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[2], power), + ethVotes := types.ExchangeRateVotes{ + {Pair: pairEth, ExchangeRate: sdk.NewDec(1_000), Voter: ValAddrs[0], Power: power}, + {Pair: pairEth, ExchangeRate: sdk.NewDec(1_300), Voter: ValAddrs[1], Power: power}, + {Pair: pairEth, ExchangeRate: sdk.NewDec(2_000), Voter: ValAddrs[2], Power: power}, } - for i, ballot := range btcBallots { + for i, v := range btcVotes { fixture.OracleKeeper.Votes.Insert( fixture.Ctx, ValAddrs[i], types.NewAggregateExchangeRateVote( types.ExchangeRateTuples{ - {Pair: ballot.Pair, ExchangeRate: ballot.ExchangeRate}, - {Pair: ethBallots[i].Pair, ExchangeRate: ethBallots[i].ExchangeRate}, + {Pair: v.Pair, ExchangeRate: v.ExchangeRate}, + {Pair: ethVotes[i].Pair, ExchangeRate: ethVotes[i].ExchangeRate}, }, ValAddrs[i], ), @@ -63,7 +65,7 @@ func TestGroupBallotsByPair(t *testing.T) { } // organize votes by pair - ballotMap := fixture.OracleKeeper.groupBallotsByPair(fixture.Ctx, types.ValidatorPerformances{ + pairVotes := fixture.OracleKeeper.groupVotesByPair(fixture.Ctx, types.ValidatorPerformances{ ValAddrs[0].String(): { Power: power, WinCount: 0, @@ -81,17 +83,17 @@ func TestGroupBallotsByPair(t *testing.T) { }, }) - // sort each ballot for comparison - sort.Sort(btcBallots) - sort.Sort(ethBallots) - sort.Sort(ballotMap[asset.Registry.Pair(denoms.BTC, denoms.NUSD)]) - sort.Sort(ballotMap[asset.Registry.Pair(denoms.ETH, denoms.NUSD)]) + // sort each votes for comparison + sort.Sort(btcVotes) + sort.Sort(ethVotes) + sort.Sort(pairVotes[asset.Registry.Pair(denoms.BTC, denoms.NUSD)]) + sort.Sort(pairVotes[asset.Registry.Pair(denoms.ETH, denoms.NUSD)]) - require.Equal(t, btcBallots, ballotMap[asset.Registry.Pair(denoms.BTC, denoms.NUSD)]) - require.Equal(t, ethBallots, ballotMap[asset.Registry.Pair(denoms.ETH, denoms.NUSD)]) + require.Equal(t, btcVotes, pairVotes[asset.Registry.Pair(denoms.BTC, denoms.NUSD)]) + require.Equal(t, ethVotes, pairVotes[asset.Registry.Pair(denoms.ETH, denoms.NUSD)]) } -func TestClearBallots(t *testing.T) { +func TestClearVotesAndPrevotes(t *testing.T) { fixture := CreateTestFixture(t) power := int64(100) @@ -107,18 +109,18 @@ func TestClearBallots(t *testing.T) { require.NoError(t, err) staking.EndBlocker(fixture.Ctx, &fixture.StakingKeeper) - btcBallot := types.ExchangeRateBallots{ - types.NewExchangeRateBallot(sdk.NewDec(17), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[0], power), - types.NewExchangeRateBallot(sdk.NewDec(10), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[1], power), - types.NewExchangeRateBallot(sdk.NewDec(6), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[2], power), + btcVotes := types.ExchangeRateVotes{ + types.NewExchangeRateVote(sdk.NewDec(17), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[0], power), + types.NewExchangeRateVote(sdk.NewDec(10), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[1], power), + types.NewExchangeRateVote(sdk.NewDec(6), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[2], power), } - ethBallot := types.ExchangeRateBallots{ - types.NewExchangeRateBallot(sdk.NewDec(1000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[0], power), - types.NewExchangeRateBallot(sdk.NewDec(1300), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[1], power), - types.NewExchangeRateBallot(sdk.NewDec(2000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[2], power), + ethVotes := types.ExchangeRateVotes{ + types.NewExchangeRateVote(sdk.NewDec(1000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[0], power), + types.NewExchangeRateVote(sdk.NewDec(1300), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[1], power), + types.NewExchangeRateVote(sdk.NewDec(2000), asset.Registry.Pair(denoms.ETH, denoms.NUSD), ValAddrs[2], power), } - for i := range btcBallot { + for i := range btcVotes { fixture.OracleKeeper.Prevotes.Insert(fixture.Ctx, ValAddrs[i], types.AggregateExchangeRatePrevote{ Hash: "", Voter: ValAddrs[i].String(), @@ -127,12 +129,12 @@ func TestClearBallots(t *testing.T) { fixture.OracleKeeper.Votes.Insert(fixture.Ctx, ValAddrs[i], types.NewAggregateExchangeRateVote(types.ExchangeRateTuples{ - {Pair: btcBallot[i].Pair, ExchangeRate: btcBallot[i].ExchangeRate}, - {Pair: ethBallot[i].Pair, ExchangeRate: ethBallot[i].ExchangeRate}, + {Pair: btcVotes[i].Pair, ExchangeRate: btcVotes[i].ExchangeRate}, + {Pair: ethVotes[i].Pair, ExchangeRate: ethVotes[i].ExchangeRate}, }, ValAddrs[i])) } - fixture.OracleKeeper.clearVotesAndPreVotes(fixture.Ctx, 10) + fixture.OracleKeeper.clearVotesAndPrevotes(fixture.Ctx, 10) prevoteCounter := len(fixture.OracleKeeper.Prevotes.Iterate(fixture.Ctx, collections.Range[sdk.ValAddress]{}).Keys()) voteCounter := len(fixture.OracleKeeper.Votes.Iterate(fixture.Ctx, collections.Range[sdk.ValAddress]{}).Keys()) @@ -141,7 +143,7 @@ func TestClearBallots(t *testing.T) { require.Equal(t, voteCounter, 0) // vote period starts at b=10, clear the votes at b=0 and below. - fixture.OracleKeeper.clearVotesAndPreVotes(fixture.Ctx.WithBlockHeight(fixture.Ctx.BlockHeight()+10), 10) + fixture.OracleKeeper.clearVotesAndPrevotes(fixture.Ctx.WithBlockHeight(fixture.Ctx.BlockHeight()+10), 10) prevoteCounter = len(fixture.OracleKeeper.Prevotes.Iterate(fixture.Ctx, collections.Range[sdk.ValAddress]{}).Keys()) require.Equal(t, prevoteCounter, 0) } @@ -167,18 +169,18 @@ func TestFuzzTally(t *testing.T) { (*e)[validator] = types.NewValidatorPerformance(power, addr) } }, - func(e *types.ExchangeRateBallots, c fuzz.Continue) { - ballot := types.ExchangeRateBallots{} + func(e *types.ExchangeRateVotes, c fuzz.Continue) { + votes := types.ExchangeRateVotes{} for addr, power := range validators { addr, _ := sdk.ValAddressFromBech32(addr) var rate sdk.Dec c.Fuzz(&rate) - ballot = append(ballot, types.NewExchangeRateBallot(rate, asset.NewPair(c.RandString(), c.RandString()), addr, power)) + votes = append(votes, types.NewExchangeRateVote(rate, asset.NewPair(c.RandString(), c.RandString()), addr, power)) } - *e = ballot + *e = votes }, ) @@ -188,18 +190,18 @@ func TestFuzzTally(t *testing.T) { claimMap := types.ValidatorPerformances{} f.Fuzz(&claimMap) - ballot := types.ExchangeRateBallots{} - f.Fuzz(&ballot) + votes := types.ExchangeRateVotes{} + f.Fuzz(&votes) var rewardBand sdk.Dec f.Fuzz(&rewardBand) require.NotPanics(t, func() { - Tally(ballot, rewardBand, claimMap) + Tally(votes, rewardBand, claimMap) }) } -type VoteMap = map[asset.Pair]types.ExchangeRateBallots +type VoteMap = map[asset.Pair]types.ExchangeRateVotes func TestRemoveInvalidBallots(t *testing.T) { testCases := []struct { @@ -207,51 +209,51 @@ func TestRemoveInvalidBallots(t *testing.T) { voteMap VoteMap }{ { - name: "empty key, empty ballot", + name: "empty key, empty votes", voteMap: VoteMap{ - "": types.ExchangeRateBallots{}, + "": types.ExchangeRateVotes{}, }, }, { - name: "nonempty key, empty ballot", + name: "nonempty key, empty votes", voteMap: VoteMap{ - "xxx": types.ExchangeRateBallots{}, + "xxx": types.ExchangeRateVotes{}, }, }, { - name: "nonempty keys, empty ballot", + name: "nonempty keys, empty votes", voteMap: VoteMap{ - "xxx": types.ExchangeRateBallots{}, - "abc123": types.ExchangeRateBallots{}, + "xxx": types.ExchangeRateVotes{}, + "abc123": types.ExchangeRateVotes{}, }, }, { - name: "mixed empty keys, empty ballot", + name: "mixed empty keys, empty votes", voteMap: VoteMap{ - "xxx": types.ExchangeRateBallots{}, - "": types.ExchangeRateBallots{}, - "abc123": types.ExchangeRateBallots{}, - "0x": types.ExchangeRateBallots{}, + "xxx": types.ExchangeRateVotes{}, + "": types.ExchangeRateVotes{}, + "abc123": types.ExchangeRateVotes{}, + "0x": types.ExchangeRateVotes{}, }, }, { - name: "empty key, nonempty ballot, not whitelisted", + name: "empty key, nonempty votes, not whitelisted", voteMap: VoteMap{ - "": types.ExchangeRateBallots{ + "": types.ExchangeRateVotes{ {Pair: "", ExchangeRate: sdk.ZeroDec(), Voter: sdk.ValAddress{}, Power: 0}, }, }, }, { - name: "nonempty key, nonempty ballot, whitelisted", + name: "nonempty key, nonempty votes, whitelisted", voteMap: VoteMap{ - "x": types.ExchangeRateBallots{ + "x": types.ExchangeRateVotes{ {Pair: "x", ExchangeRate: sdk.Dec{}, Voter: sdk.ValAddress{123}, Power: 5}, }, - asset.Registry.Pair(denoms.BTC, denoms.NUSD): types.ExchangeRateBallots{ + asset.Registry.Pair(denoms.BTC, denoms.NUSD): types.ExchangeRateVotes{ {Pair: asset.Registry.Pair(denoms.BTC, denoms.NUSD), ExchangeRate: sdk.Dec{}, Voter: sdk.ValAddress{123}, Power: 5}, }, - asset.Registry.Pair(denoms.ETH, denoms.NUSD): types.ExchangeRateBallots{ + asset.Registry.Pair(denoms.ETH, denoms.NUSD): types.ExchangeRateVotes{ {Pair: asset.Registry.Pair(denoms.BTC, denoms.NUSD), ExchangeRate: sdk.Dec{}, Voter: sdk.ValAddress{123}, Power: 5}, }, }, @@ -263,7 +265,10 @@ func TestRemoveInvalidBallots(t *testing.T) { t.Run(tc.name, func(t *testing.T) { fixture, _ := Setup(t) assert.NotPanics(t, func() { - fixture.OracleKeeper.removeInvalidBallots(fixture.Ctx, tc.voteMap) + fixture.OracleKeeper.removeInvalidVotes(fixture.Ctx, tc.voteMap, set.New[asset.Pair]( + asset.NewPair(denoms.BTC, denoms.NUSD), + asset.NewPair(denoms.ETH, denoms.NUSD), + )) }, "voteMap: %v", tc.voteMap) }) } @@ -299,12 +304,12 @@ func TestFuzzPickReferencePair(t *testing.T) { (*e)[sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()).String()] = int64(c.Intn(100) + 1) } }, - func(e *map[asset.Pair]types.ExchangeRateBallots, c fuzz.Continue) { + func(e *map[asset.Pair]types.ExchangeRateVotes, c fuzz.Continue) { validators := map[string]int64{} c.Fuzz(&validators) for _, pair := range pairs { - ballots := types.ExchangeRateBallots{} + votes := types.ExchangeRateVotes{} for addr, power := range validators { addr, _ := sdk.ValAddressFromBech32(addr) @@ -312,10 +317,10 @@ func TestFuzzPickReferencePair(t *testing.T) { var rate sdk.Dec c.Fuzz(&rate) - ballots = append(ballots, types.NewExchangeRateBallot(rate, pair, addr, power)) + votes = append(votes, types.NewExchangeRateVote(rate, pair, addr, power)) } - (*e)[pair] = ballots + (*e)[pair] = votes } }, ) @@ -328,28 +333,27 @@ func TestFuzzPickReferencePair(t *testing.T) { // test OracleKeeper.Pairs.Insert voteTargets := set.Set[asset.Pair]{} f.Fuzz(&voteTargets) - whitelistedPairs := make(set.Set[string]) + whitelistedPairs := make(set.Set[asset.Pair]) for key := range voteTargets { - input.OracleKeeper.WhitelistedPairs.Insert(input.Ctx, key) - whitelistedPairs.Add(key.String()) + whitelistedPairs.Add(key) } // test OracleKeeper.RemoveInvalidBallots - voteMap := map[asset.Pair]types.ExchangeRateBallots{} + voteMap := map[asset.Pair]types.ExchangeRateVotes{} f.Fuzz(&voteMap) assert.NotPanics(t, func() { - input.OracleKeeper.removeInvalidBallots(input.Ctx, voteMap) + input.OracleKeeper.removeInvalidVotes(input.Ctx, voteMap, whitelistedPairs) }, "voteMap: %v", voteMap) } func TestZeroBallotPower(t *testing.T) { - btcBallots := types.ExchangeRateBallots{ - types.NewExchangeRateBallot(sdk.NewDec(17), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[0], 0), - types.NewExchangeRateBallot(sdk.NewDec(10), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[1], 0), - types.NewExchangeRateBallot(sdk.NewDec(6), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[2], 0), + btcVotess := types.ExchangeRateVotes{ + types.NewExchangeRateVote(sdk.NewDec(17), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[0], 0), + types.NewExchangeRateVote(sdk.NewDec(10), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[1], 0), + types.NewExchangeRateVote(sdk.NewDec(6), asset.Registry.Pair(denoms.BTC, denoms.NUSD), ValAddrs[2], 0), } - assert.False(t, isPassingVoteThreshold(btcBallots, sdk.ZeroInt(), 0)) + assert.False(t, isPassingVoteThreshold(btcVotess, sdk.ZeroInt(), 0)) } diff --git a/x/oracle/keeper/msg_server_test.go b/x/oracle/keeper/msg_server_test.go index 5337cab7a..d8f392439 100644 --- a/x/oracle/keeper/msg_server_test.go +++ b/x/oracle/keeper/msg_server_test.go @@ -17,7 +17,7 @@ func TestFeederDelegation(t *testing.T) { exchangeRates := types.ExchangeRateTuples{ { Pair: asset.Registry.Pair(denoms.BTC, denoms.NUSD), - ExchangeRate: randomExchangeRate, + ExchangeRate: testExchangeRate, }, } diff --git a/x/oracle/keeper/params.go b/x/oracle/keeper/params.go index 3724d46bc..f45f7b3fb 100644 --- a/x/oracle/keeper/params.go +++ b/x/oracle/keeper/params.go @@ -18,20 +18,20 @@ func (k Keeper) VotePeriod(ctx sdk.Context) (res uint64) { return params.VotePeriod } -// VoteThreshold returns the minimum percentage of votes that must be received for a ballot to pass. +// VoteThreshold returns the minimum percentage of votes that must be received for a votes to pass. func (k Keeper) VoteThreshold(ctx sdk.Context) (res sdk.Dec) { params, _ := k.Params.Get(ctx) return params.VoteThreshold } -// MinVoters returns the minimum percentage of votes that must be received for a ballot to pass. +// MinVoters returns the minimum percentage of votes that must be received for a votes to pass. func (k Keeper) MinVoters(ctx sdk.Context) (res uint64) { params, _ := k.Params.Get(ctx) return params.MinVoters } // RewardBand returns a maxium divergence that a price vote can have from the -// weighted median in the ballot. If a vote lies within the valid range +// weighted median in the votes. If a vote lies within the valid range // defined by: // // μ := weightedMedian, diff --git a/x/oracle/keeper/reward.go b/x/oracle/keeper/reward.go index 89a9f65dd..75c1fe751 100644 --- a/x/oracle/keeper/reward.go +++ b/x/oracle/keeper/reward.go @@ -25,13 +25,13 @@ func (k Keeper) AllocateRewards(ctx sdk.Context, funderModule string, totalCoins return k.bankKeeper.SendCoinsFromModuleToModule(ctx, funderModule, types.ModuleName, totalCoins) } -// rewardBallotWinners gives out a portion of spread fees collected in the +// rewardWinners gives out a portion of spread fees collected in the // oracle reward pool to the oracle voters that voted faithfully. -func (k Keeper) rewardBallotWinners( +func (k Keeper) rewardWinners( ctx sdk.Context, validatorPerformances types.ValidatorPerformances, ) { - totalRewardWeight := validatorPerformances.GetTotalRewardWeight() + totalRewardWeight := validatorPerformances.TotalRewardWeight() if totalRewardWeight == 0 { return } diff --git a/x/oracle/keeper/reward_test.go b/x/oracle/keeper/reward_test.go index c0861f6c8..8b75c3e2a 100644 --- a/x/oracle/keeper/reward_test.go +++ b/x/oracle/keeper/reward_test.go @@ -40,7 +40,7 @@ func TestKeeperRewardsDistributionMultiVotePeriods(t *testing.T) { MakeAggregatePrevoteAndVote(t, fixture, msgServer, fixture.Ctx.BlockHeight(), types.ExchangeRateTuples{ { Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), - ExchangeRate: randomExchangeRate, + ExchangeRate: testExchangeRate, }, }, valIndex) } diff --git a/x/oracle/keeper/slash_test.go b/x/oracle/keeper/slash_test.go index b4bd13077..6a99b8e89 100644 --- a/x/oracle/keeper/slash_test.go +++ b/x/oracle/keeper/slash_test.go @@ -105,22 +105,22 @@ func TestInvalidVotesSlashing(t *testing.T) { // Account 1, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 0) // Account 2, govstable, miss vote MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Add(sdk.NewDec(100000000000000))}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Add(sdk.NewDec(100000000000000))}, }, 1) // Account 3, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 2) // Account 4, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 3) input.OracleKeeper.UpdateExchangeRates(input.Ctx) @@ -131,27 +131,27 @@ func TestInvalidVotesSlashing(t *testing.T) { } validator := input.StakingKeeper.Validator(input.Ctx, ValAddrs[1]) - require.Equal(t, stakingAmt, validator.GetBondedTokens()) + require.Equal(t, testStakingAmt, validator.GetBondedTokens()) // one more miss vote will inccur ValAddrs[1] slashing // Account 1, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 0) // Account 2, govstable, miss vote MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Add(sdk.NewDec(100000000000000))}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Add(sdk.NewDec(100000000000000))}, }, 1) // Account 3, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 2) // Account 4, govstable MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 3) input.Ctx = input.Ctx.WithBlockHeight(votePeriodsPerWindow - 1) @@ -160,46 +160,47 @@ func TestInvalidVotesSlashing(t *testing.T) { // input.OracleKeeper.UpdateExchangeRates(input.Ctx) validator = input.StakingKeeper.Validator(input.Ctx, ValAddrs[1]) - require.Equal(t, sdk.OneDec().Sub(slashFraction).MulInt(stakingAmt).TruncateInt(), validator.GetBondedTokens()) + require.Equal(t, sdk.OneDec().Sub(slashFraction).MulInt(testStakingAmt).TruncateInt(), validator.GetBondedTokens()) } +// TestWhitelistSlashing: Creates a scenario where one valoper (valIdx 0) does +// not vote throughout an entire vote window, while valopers 1 and 2 do. func TestWhitelistSlashing(t *testing.T) { - input, h := Setup(t) + input, msgServer := Setup(t) - votePeriodsPerWindow := sdk.NewDec(int64(input.OracleKeeper.SlashWindow(input.Ctx))).QuoInt64(int64(input.OracleKeeper.VotePeriod(input.Ctx))).TruncateInt64() - slashFraction := input.OracleKeeper.SlashFraction(input.Ctx) - minValidPerWindow := input.OracleKeeper.MinValidPerWindow(input.Ctx) - - for i := uint64(0); i < uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64()); i++ { - input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + votePeriodsPerSlashWindow := sdk.NewDec(int64(input.OracleKeeper.SlashWindow(input.Ctx))).QuoInt64(int64(input.OracleKeeper.VotePeriod(input.Ctx))).TruncateInt64() + minValidVotePeriodsPerWindow := input.OracleKeeper.MinValidPerWindow(input.Ctx) - // Account 2, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 1) - // Account 3, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) - - input.OracleKeeper.UpdateExchangeRates(input.Ctx) - // input.OracleKeeper.SlashAndResetMissCounters(input.Ctx) - // input.OracleKeeper.UpdateExchangeRates(input.Ctx) - require.Equal(t, i+1, input.OracleKeeper.MissCounters.GetOr(input.Ctx, ValAddrs[0], 0)) + pair := asset.Registry.Pair(denoms.NIBI, denoms.NUSD) + priceVoteFromVal := func(valIdx int, block int64, erate sdk.Dec) { + MakeAggregatePrevoteAndVote(t, input, msgServer, block, + types.ExchangeRateTuples{{Pair: pair, ExchangeRate: erate}}, + valIdx) + } + input.OracleKeeper.WhitelistedPairs.Insert(input.Ctx, pair) + perfs := input.OracleKeeper.UpdateExchangeRates(input.Ctx) + require.EqualValues(t, 0, perfs.TotalRewardWeight()) + + allowedMissPct := sdk.OneDec().Sub(minValidVotePeriodsPerWindow) + allowedMissVotePeriods := allowedMissPct.MulInt64(votePeriodsPerSlashWindow). + TruncateInt64() + t.Logf("For %v blocks, valoper0 does not vote, while 1 and 2 do.", allowedMissVotePeriods) + for idxMissPeriod := uint64(0); idxMissPeriod < uint64(allowedMissVotePeriods); idxMissPeriod++ { + block := input.Ctx.BlockHeight() + 1 + input.Ctx = input.Ctx.WithBlockHeight(block) + + valIdx := 0 // Valoper doesn't vote (abstain) + priceVoteFromVal(valIdx+1, block, testExchangeRate) + priceVoteFromVal(valIdx+2, block, testExchangeRate) + + perfs := input.OracleKeeper.UpdateExchangeRates(input.Ctx) + missCount := input.OracleKeeper.MissCounters.GetOr(input.Ctx, ValAddrs[0], 0) + require.EqualValues(t, 0, missCount, perfs.String()) } + t.Log("valoper0 should not be slashed") validator := input.StakingKeeper.Validator(input.Ctx, ValAddrs[0]) - require.Equal(t, stakingAmt, validator.GetBondedTokens()) - - // one more miss vote will inccur Account 1 slashing - - // Account 2, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 1) - // Account 3, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) - - input.Ctx = input.Ctx.WithBlockHeight(votePeriodsPerWindow - 1) - input.OracleKeeper.UpdateExchangeRates(input.Ctx) - input.OracleKeeper.SlashAndResetMissCounters(input.Ctx) - // input.OracleKeeper.UpdateExchangeRates(input.Ctx) - validator = input.StakingKeeper.Validator(input.Ctx, ValAddrs[0]) - require.Equal(t, sdk.OneDec().Sub(slashFraction).MulInt(stakingAmt).TruncateInt(), validator.GetBondedTokens()) + require.Equal(t, testStakingAmt, validator.GetBondedTokens()) } func TestNotPassedBallotSlashing(t *testing.T) { @@ -218,7 +219,7 @@ func TestNotPassedBallotSlashing(t *testing.T) { input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) // Account 1, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 0) + MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}}, 0) input.OracleKeeper.UpdateExchangeRates(input.Ctx) input.OracleKeeper.SlashAndResetMissCounters(input.Ctx) @@ -230,12 +231,12 @@ func TestNotPassedBallotSlashing(t *testing.T) { func TestAbstainSlashing(t *testing.T) { input, h := Setup(t) + + // reset whitelisted pairs params, err := input.OracleKeeper.Params.Get(input.Ctx) require.NoError(t, err) params.Whitelist = []asset.Pair{asset.Registry.Pair(denoms.NIBI, denoms.NUSD)} input.OracleKeeper.Params.Set(input.Ctx, params) - - // clear tobin tax to reset vote targets for _, p := range input.OracleKeeper.WhitelistedPairs.Iterate(input.Ctx, collections.Range[asset.Pair]{}).Keys() { input.OracleKeeper.WhitelistedPairs.Delete(input.Ctx, p) } @@ -247,14 +248,14 @@ func TestAbstainSlashing(t *testing.T) { for i := uint64(0); i <= uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64()); i++ { input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) - // Account 1, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 0) + // Account 1, NIBI/NUSD + MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}}, 0) - // Account 2, govstable, abstain vote - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: sdk.ZeroDec()}}, 1) + // Account 2, NIBI/NUSD, abstain vote + MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: sdk.OneDec().Neg()}}, 1) - // Account 3, govstable - MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) + // Account 3, NIBI/NUSD + MakeAggregatePrevoteAndVote(t, input, h, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}}, 2) input.OracleKeeper.UpdateExchangeRates(input.Ctx) input.OracleKeeper.SlashAndResetMissCounters(input.Ctx) @@ -263,5 +264,5 @@ func TestAbstainSlashing(t *testing.T) { } validator := input.StakingKeeper.Validator(input.Ctx, ValAddrs[1]) - require.Equal(t, stakingAmt, validator.GetBondedTokens()) + require.Equal(t, testStakingAmt, validator.GetBondedTokens()) } diff --git a/x/oracle/keeper/test_utils.go b/x/oracle/keeper/test_utils.go index c08bb1797..5cef19d0e 100644 --- a/x/oracle/keeper/test_utils.go +++ b/x/oracle/keeper/test_utils.go @@ -285,8 +285,8 @@ func AllocateRewards(t *testing.T, input TestFixture, rewards sdk.Coins, votePer } var ( - stakingAmt = sdk.TokensFromConsensusPower(10, sdk.DefaultPowerReduction) - randomExchangeRate = sdk.NewDec(1700) + testStakingAmt = sdk.TokensFromConsensusPower(10, sdk.DefaultPowerReduction) + testExchangeRate = sdk.NewDec(1700) ) func Setup(t *testing.T) (TestFixture, types.MsgServer) { @@ -304,22 +304,29 @@ func Setup(t *testing.T) (TestFixture, types.MsgServer) { sh := stakingkeeper.NewMsgServerImpl(&fixture.StakingKeeper) // Validator created - _, err := sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[0], ValPubKeys[0], stakingAmt)) + _, err := sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[0], ValPubKeys[0], testStakingAmt)) require.NoError(t, err) - _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[1], ValPubKeys[1], stakingAmt)) + _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[1], ValPubKeys[1], testStakingAmt)) require.NoError(t, err) - _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[2], ValPubKeys[2], stakingAmt)) + _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[2], ValPubKeys[2], testStakingAmt)) require.NoError(t, err) - _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[3], ValPubKeys[3], stakingAmt)) + _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[3], ValPubKeys[3], testStakingAmt)) require.NoError(t, err) - _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[4], ValPubKeys[4], stakingAmt)) + _, err = sh.CreateValidator(fixture.Ctx, NewTestMsgCreateValidator(ValAddrs[4], ValPubKeys[4], testStakingAmt)) require.NoError(t, err) staking.EndBlocker(fixture.Ctx, &fixture.StakingKeeper) return fixture, h } -func MakeAggregatePrevoteAndVote(t *testing.T, input TestFixture, msgServer types.MsgServer, height int64, rates types.ExchangeRateTuples, valIdx int) { +func MakeAggregatePrevoteAndVote( + t *testing.T, + input TestFixture, + msgServer types.MsgServer, + height int64, + rates types.ExchangeRateTuples, + valIdx int, +) { salt := "1" ratesStr, err := rates.ToString() require.NoError(t, err) diff --git a/x/oracle/keeper/update_exchange_rates.go b/x/oracle/keeper/update_exchange_rates.go index a8fd8f9d1..99a1251b1 100644 --- a/x/oracle/keeper/update_exchange_rates.go +++ b/x/oracle/keeper/update_exchange_rates.go @@ -12,82 +12,99 @@ import ( ) // UpdateExchangeRates updates the ExchangeRates, this is supposed to be executed on EndBlock. -func (k Keeper) UpdateExchangeRates(ctx sdk.Context) { +func (k Keeper) UpdateExchangeRates(ctx sdk.Context) types.ValidatorPerformances { k.Logger(ctx).Info("processing validator price votes") - validatorPerformances := k.newValidatorPerformances(ctx) - pairBallotsMap, whitelistedPairs := k.getPairBallotsMapAndWhitelistedPairs(ctx, validatorPerformances) + whitelistedPairs := set.New[asset.Pair](k.GetWhitelistedPairs(ctx)...) + + pairVotes := k.getPairVotes(ctx, validatorPerformances, whitelistedPairs) + + k.clearExchangeRates(ctx, pairVotes) + k.tallyVotesAndUpdatePrices(ctx, pairVotes, validatorPerformances) - k.resetExchangeRates(ctx, pairBallotsMap) - k.countVotesAndUpdateExchangeRates(ctx, pairBallotsMap, validatorPerformances) + k.incrementMissCounters(ctx, whitelistedPairs, validatorPerformances) + k.incrementAbstainsByOmission(ctx, len(whitelistedPairs), validatorPerformances) - k.registerMissedVotes(ctx, whitelistedPairs, validatorPerformances) - k.rewardBallotWinners(ctx, validatorPerformances) + k.rewardWinners(ctx, validatorPerformances) params, _ := k.Params.Get(ctx) - k.clearVotesAndPreVotes(ctx, params.VotePeriod) - k.updateWhitelist(ctx, params.Whitelist, whitelistedPairs) + k.clearVotesAndPrevotes(ctx, params.VotePeriod) + k.refreshWhitelist(ctx, params.Whitelist, whitelistedPairs) + return validatorPerformances } -// registerMissedVotes it parses all validators performance and increases the missed vote of those that did not vote. -func (k Keeper) registerMissedVotes(ctx sdk.Context, whitelistedPairs set.Set[asset.Pair], validatorPerformances types.ValidatorPerformances) { +// incrementMissCounters it parses all validators performance and increases the +// missed vote of those that did not vote. +func (k Keeper) incrementMissCounters( + ctx sdk.Context, + whitelistedPairs set.Set[asset.Pair], + validatorPerformances types.ValidatorPerformances, +) { for _, validatorPerformance := range validatorPerformances { - if int(validatorPerformance.WinCount) == len(whitelistedPairs) { - continue + if int(validatorPerformance.MissCount) > 0 { + k.MissCounters.Insert( + ctx, validatorPerformance.ValAddress, + k.MissCounters.GetOr(ctx, validatorPerformance.ValAddress, 0)+uint64(validatorPerformance.MissCount), + ) + + k.Logger(ctx).Info("vote miss", "validator", validatorPerformance.ValAddress.String()) } + } +} - k.MissCounters.Insert(ctx, validatorPerformance.ValAddress, k.MissCounters.GetOr(ctx, validatorPerformance.ValAddress, 0)+1) - k.Logger(ctx).Info("vote miss", "validator", validatorPerformance.ValAddress.String()) +func (k Keeper) incrementAbstainsByOmission( + ctx sdk.Context, + numPairs int, + validatorPerformances types.ValidatorPerformances, +) { + for valAddr, performance := range validatorPerformances { + omitCount := int64(numPairs) - (performance.WinCount + performance.AbstainCount + performance.MissCount) + if omitCount > 0 { + performance.AbstainCount += omitCount + validatorPerformances[valAddr] = performance + } } } -// countVotesAndUpdateExchangeRates processes the votes and updates the ExchangeRates based on the results. -func (k Keeper) countVotesAndUpdateExchangeRates( +// tallyVotesAndUpdatePrices processes the votes and updates the ExchangeRates based on the results. +func (k Keeper) tallyVotesAndUpdatePrices( ctx sdk.Context, - pairBallotsMap map[asset.Pair]types.ExchangeRateBallots, + pairVotes map[asset.Pair]types.ExchangeRateVotes, validatorPerformances types.ValidatorPerformances, ) { rewardBand := k.RewardBand(ctx) - // Iterate through sorted keys for deterministic ordering. - orderedBallotsMap := omap.OrderedMap_Pair[types.ExchangeRateBallots](pairBallotsMap) - for pair := range orderedBallotsMap.Range() { - ballots := pairBallotsMap[pair] - exchangeRate := Tally(ballots, rewardBand, validatorPerformances) - + orderedPairVotes := omap.OrderedMap_Pair[types.ExchangeRateVotes](pairVotes) + for pair := range orderedPairVotes.Range() { + exchangeRate := Tally(pairVotes[pair], rewardBand, validatorPerformances) k.SetPrice(ctx, pair, exchangeRate) - - _ = ctx.EventManager().EmitTypedEvent(&types.EventPriceUpdate{ - Pair: pair.String(), - Price: exchangeRate, - TimestampMs: ctx.BlockTime().UnixMilli(), - }) } } -// getPairBallotsMapAndWhitelistedPairs returns a map of pairs and ballots excluding invalid Ballots -// and a map with all whitelisted pairs. -func (k Keeper) getPairBallotsMapAndWhitelistedPairs( +// getPairVotes returns a map of pairs and votes excluding abstained votes and votes that don't meet the threshold criteria +func (k Keeper) getPairVotes( ctx sdk.Context, validatorPerformances types.ValidatorPerformances, -) (pairBallotsMap map[asset.Pair]types.ExchangeRateBallots, whitelistedPairsMap set.Set[asset.Pair]) { - pairBallotsMap = k.groupBallotsByPair(ctx, validatorPerformances) + whitelistedPairs set.Set[asset.Pair], +) (pairVotes map[asset.Pair]types.ExchangeRateVotes) { + pairVotes = k.groupVotesByPair(ctx, validatorPerformances) + + k.removeInvalidVotes(ctx, pairVotes, whitelistedPairs) - return k.removeInvalidBallots(ctx, pairBallotsMap) + return pairVotes } -// resetExchangeRates removes all exchange rates from the state -// We remove the price for pair with expired prices or valid ballots -func (k Keeper) resetExchangeRates(ctx sdk.Context, pairBallotsMap map[asset.Pair]types.ExchangeRateBallots) { +// clearExchangeRates removes all exchange rates from the state +// We remove the price for pair with expired prices or valid votes +func (k Keeper) clearExchangeRates(ctx sdk.Context, pairVotes map[asset.Pair]types.ExchangeRateVotes) { params, _ := k.Params.Get(ctx) - expirationBlocks := params.ExpirationBlocks for _, key := range k.ExchangeRates.Iterate(ctx, collections.Range[asset.Pair]{}).Keys() { - _, validBallot := pairBallotsMap[key] - exchangeRate, _ := k.ExchangeRates.Get(ctx, key) - isExpired := exchangeRate.CreatedBlock+expirationBlocks <= uint64(ctx.BlockHeight()) + _, isValid := pairVotes[key] + previousExchangeRate, _ := k.ExchangeRates.Get(ctx, key) + isExpired := previousExchangeRate.CreatedBlock+params.ExpirationBlocks <= uint64(ctx.BlockHeight()) - if validBallot || isExpired { + if isValid || isExpired { err := k.ExchangeRates.Delete(ctx, key) if err != nil { k.Logger(ctx).Error("failed to delete exchange rate", "pair", key.String(), "error", err) diff --git a/x/oracle/keeper/update_exchange_rates_test.go b/x/oracle/keeper/update_exchange_rates_test.go index b2660addb..074d68d7c 100644 --- a/x/oracle/keeper/update_exchange_rates_test.go +++ b/x/oracle/keeper/update_exchange_rates_test.go @@ -24,7 +24,7 @@ func TestOracleThreshold(t *testing.T) { exchangeRates := types.ExchangeRateTuples{ { Pair: asset.Registry.Pair(denoms.BTC, denoms.NUSD), - ExchangeRate: randomExchangeRate, + ExchangeRate: testExchangeRate, }, } exchangeRateStr, err := exchangeRates.ToString() @@ -68,12 +68,12 @@ func TestOracleThreshold(t *testing.T) { fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) rate, err := fixture.OracleKeeper.ExchangeRates.Get(fixture.Ctx, exchangeRates[0].Pair) require.NoError(t, err) - assert.Equal(t, randomExchangeRate, rate.ExchangeRate) + assert.Equal(t, testExchangeRate, rate.ExchangeRate) // Case 3. // Increase voting power of absent validator, exchange rate consensus fails val, _ := fixture.StakingKeeper.GetValidator(fixture.Ctx, ValAddrs[4]) - _, _ = fixture.StakingKeeper.Delegate(fixture.Ctx.WithBlockHeight(0), Addrs[4], stakingAmt.MulRaw(8), stakingtypes.Unbonded, val, false) + _, _ = fixture.StakingKeeper.Delegate(fixture.Ctx.WithBlockHeight(0), Addrs[4], testStakingAmt.MulRaw(8), stakingtypes.Unbonded, val, false) for i := 0; i < 4; i++ { salt := fmt.Sprintf("%d", i) @@ -95,8 +95,8 @@ func TestResetExchangeRates(t *testing.T) { pair := asset.Registry.Pair(denoms.BTC, denoms.NUSD) fixture, _ := Setup(t) - emptyBallot := map[asset.Pair]types.ExchangeRateBallots{} - validBallot := map[asset.Pair]types.ExchangeRateBallots{pair: {}} + emptyVotes := map[asset.Pair]types.ExchangeRateVotes{} + validVotes := map[asset.Pair]types.ExchangeRateVotes{pair: {}} // Set expiration blocks to 10 params, _ := fixture.OracleKeeper.Params.Get(fixture.Ctx) @@ -104,25 +104,25 @@ func TestResetExchangeRates(t *testing.T) { fixture.OracleKeeper.Params.Set(fixture.Ctx, params) // Post a price at block 1 - fixture.OracleKeeper.SetPrice(fixture.Ctx.WithBlockHeight(1), pair, randomExchangeRate) + fixture.OracleKeeper.SetPrice(fixture.Ctx.WithBlockHeight(1), pair, testExchangeRate) // reset exchange rates at block 2 // Price should still be there because not expired yet - fixture.OracleKeeper.resetExchangeRates(fixture.Ctx.WithBlockHeight(2), emptyBallot) + fixture.OracleKeeper.clearExchangeRates(fixture.Ctx.WithBlockHeight(2), emptyVotes) _, err := fixture.OracleKeeper.ExchangeRates.Get(fixture.Ctx, pair) assert.NoError(t, err) - // reset exchange rates at block 3 but pair is in ballot - // Price should be removed there because there was a valid ballot - fixture.OracleKeeper.resetExchangeRates(fixture.Ctx.WithBlockHeight(3), validBallot) + // reset exchange rates at block 3 but pair is in votes + // Price should be removed there because there was a valid votes + fixture.OracleKeeper.clearExchangeRates(fixture.Ctx.WithBlockHeight(3), validVotes) _, err = fixture.OracleKeeper.ExchangeRates.Get(fixture.Ctx, pair) assert.Error(t, err) // Post a price at block 69 // reset exchange rates at block 79 // Price should not be there anymore because expired - fixture.OracleKeeper.SetPrice(fixture.Ctx.WithBlockHeight(69), pair, randomExchangeRate) - fixture.OracleKeeper.resetExchangeRates(fixture.Ctx.WithBlockHeight(79), emptyBallot) + fixture.OracleKeeper.SetPrice(fixture.Ctx.WithBlockHeight(69), pair, testExchangeRate) + fixture.OracleKeeper.clearExchangeRates(fixture.Ctx.WithBlockHeight(79), emptyVotes) _, err = fixture.OracleKeeper.ExchangeRates.Get(fixture.Ctx, pair) assert.Error(t, err) @@ -131,7 +131,7 @@ func TestResetExchangeRates(t *testing.T) { func TestOracleTally(t *testing.T) { fixture, _ := Setup(t) - ballot := types.ExchangeRateBallots{} + votes := types.ExchangeRateVotes{} rates, valAddrs, stakingKeeper := types.GenerateRandomTestCase() fixture.OracleKeeper.StakingKeeper = stakingKeeper h := NewMsgServerImpl(fixture.OracleKeeper) @@ -153,14 +153,14 @@ func TestOracleTally(t *testing.T) { require.NoError(t, err1) require.NoError(t, err2) - power := stakingAmt.QuoRaw(int64(6)).Int64() + power := testStakingAmt.QuoRaw(int64(6)).Int64() if decExchangeRate.IsZero() { power = int64(0) } - vote := types.NewExchangeRateBallot( + vote := types.NewExchangeRateVote( decExchangeRate, asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddrs[i], power) - ballot = append(ballot, vote) + votes = append(votes, vote) // change power of every three validator if i%3 == 0 { @@ -168,50 +168,51 @@ func TestOracleTally(t *testing.T) { } } - validatorClaimMap := make(types.ValidatorPerformances) + validatorPerformances := make(types.ValidatorPerformances) for _, valAddr := range valAddrs { - validatorClaimMap[valAddr.String()] = types.ValidatorPerformance{ - Power: stakingKeeper.Validator(fixture.Ctx, valAddr).GetConsensusPower(sdk.DefaultPowerReduction), - RewardWeight: int64(0), - WinCount: int64(0), - ValAddress: valAddr, - } + validatorPerformances[valAddr.String()] = types.NewValidatorPerformance( + stakingKeeper.Validator(fixture.Ctx, valAddr).GetConsensusPower(sdk.DefaultPowerReduction), + valAddr, + ) } - sort.Sort(ballot) - weightedMedian := ballot.WeightedMedianWithAssertion() - standardDeviation := ballot.StandardDeviation(weightedMedian) + sort.Sort(votes) + weightedMedian := votes.WeightedMedianWithAssertion() + standardDeviation := votes.StandardDeviation(weightedMedian) maxSpread := weightedMedian.Mul(fixture.OracleKeeper.RewardBand(fixture.Ctx).QuoInt64(2)) if standardDeviation.GT(maxSpread) { maxSpread = standardDeviation } - expectedValidatorClaimMap := make(types.ValidatorPerformances) + expectedValidatorPerformances := make(types.ValidatorPerformances) for _, valAddr := range valAddrs { - expectedValidatorClaimMap[valAddr.String()] = types.ValidatorPerformance{ - Power: stakingKeeper.Validator(fixture.Ctx, valAddr).GetConsensusPower(sdk.DefaultPowerReduction), - RewardWeight: int64(0), - WinCount: int64(0), - ValAddress: valAddr, - } + expectedValidatorPerformances[valAddr.String()] = types.NewValidatorPerformance( + stakingKeeper.Validator(fixture.Ctx, valAddr).GetConsensusPower(sdk.DefaultPowerReduction), + valAddr, + ) } - for _, vote := range ballot { - if (vote.ExchangeRate.GTE(weightedMedian.Sub(maxSpread)) && - vote.ExchangeRate.LTE(weightedMedian.Add(maxSpread))) || - !vote.ExchangeRate.IsPositive() { - key := vote.Voter.String() - claim := expectedValidatorClaimMap[key] - claim.RewardWeight += vote.Power - claim.WinCount++ - expectedValidatorClaimMap[key] = claim + for _, vote := range votes { + key := vote.Voter.String() + validatorPerformance := expectedValidatorPerformances[key] + if vote.ExchangeRate.GTE(weightedMedian.Sub(maxSpread)) && + vote.ExchangeRate.LTE(weightedMedian.Add(maxSpread)) { + validatorPerformance.RewardWeight += vote.Power + validatorPerformance.WinCount++ + } else if !vote.ExchangeRate.IsPositive() { + validatorPerformance.AbstainCount++ + } else { + validatorPerformance.MissCount++ } + expectedValidatorPerformances[key] = validatorPerformance } - tallyMedian := Tally(ballot, fixture.OracleKeeper.RewardBand(fixture.Ctx), validatorClaimMap) + tallyMedian := Tally( + votes, fixture.OracleKeeper.RewardBand(fixture.Ctx), validatorPerformances) - assert.Equal(t, expectedValidatorClaimMap, validatorClaimMap) + assert.Equal(t, expectedValidatorPerformances, validatorPerformances) assert.Equal(t, tallyMedian.MulInt64(100).TruncateInt(), weightedMedian.MulInt64(100).TruncateInt()) + assert.NotEqualValues(t, 0, validatorPerformances.TotalRewardWeight(), validatorPerformances.String()) } func TestOracleRewardBand(t *testing.T) { @@ -228,26 +229,26 @@ func TestOracleRewardBand(t *testing.T) { } fixture.OracleKeeper.WhitelistedPairs.Insert(fixture.Ctx, asset.Registry.Pair(denoms.NIBI, denoms.NUSD)) - rewardSpread := randomExchangeRate.Mul(fixture.OracleKeeper.RewardBand(fixture.Ctx).QuoInt64(2)) + rewardSpread := testExchangeRate.Mul(fixture.OracleKeeper.RewardBand(fixture.Ctx).QuoInt64(2)) // Account 1, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Sub(rewardSpread)}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Sub(rewardSpread)}, }, 0) // Account 2, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 1) // Account 3, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 2) // Account 4, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Add(rewardSpread)}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Add(rewardSpread)}, }, 3) fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) @@ -260,22 +261,22 @@ func TestOracleRewardBand(t *testing.T) { // Account 1 will miss the vote due to raward band condition // Account 1, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Sub(rewardSpread.Add(sdk.OneDec()))}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Sub(rewardSpread.Add(sdk.OneDec()))}, }, 0) // Account 2, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 1) // Account 3, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}, }, 2) // Account 4, nibi:nusd MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{ - {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate.Add(rewardSpread)}, + {Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate.Add(rewardSpread)}, }, 3) fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) @@ -403,62 +404,87 @@ func TestWhitelistedPairs(t *testing.T) { params, err := fixture.OracleKeeper.Params.Get(fixture.Ctx) require.NoError(t, err) + t.Log("whitelist ONLY nibi:nusd") for _, p := range fixture.OracleKeeper.WhitelistedPairs.Iterate(fixture.Ctx, collections.Range[asset.Pair]{}).Keys() { fixture.OracleKeeper.WhitelistedPairs.Delete(fixture.Ctx, p) } fixture.OracleKeeper.WhitelistedPairs.Insert(fixture.Ctx, asset.Registry.Pair(denoms.NIBI, denoms.NUSD)) - // nibi:nusd - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 0) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 1) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 3) + t.Log("vote and prevote from all vals on nibi:nusd") + priceVoteFromVal := func(valIdx int, block int64) { + MakeAggregatePrevoteAndVote(t, fixture, msgServer, block, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: testExchangeRate}}, valIdx) + } + block := int64(0) + priceVoteFromVal(0, block) + priceVoteFromVal(1, block) + priceVoteFromVal(2, block) + priceVoteFromVal(3, block) - // add btc:nusd for next vote period + t.Log("whitelist btc:nusd for next vote period") params.Whitelist = []asset.Pair{asset.Registry.Pair(denoms.NIBI, denoms.NUSD), asset.Registry.Pair(denoms.BTC, denoms.NUSD)} fixture.OracleKeeper.Params.Set(fixture.Ctx, params) fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) - // no missing current + t.Log("assert: no miss counts for all vals") assert.Equal(t, uint64(0), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[0], 0)) assert.Equal(t, uint64(0), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[1], 0)) assert.Equal(t, uint64(0), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[2], 0)) assert.Equal(t, uint64(0), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[3], 0)) - // whitelisted pairs are {nibi:nusd, btc:nusd} - assert.Equal(t, []asset.Pair{asset.Registry.Pair(denoms.BTC, denoms.NUSD), asset.Registry.Pair(denoms.NIBI, denoms.NUSD)}, fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) + t.Log("whitelisted pairs are {nibi:nusd, btc:nusd}") + assert.Equal(t, + []asset.Pair{ + asset.Registry.Pair(denoms.BTC, denoms.NUSD), + asset.Registry.Pair(denoms.NIBI, denoms.NUSD), + }, + fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) - // nibi:nusd, missing btc:nusd - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 0) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 1) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 3) + t.Log("vote from vals 0-3 on nibi:nusd (but not btc:nusd)") + priceVoteFromVal(0, block) + priceVoteFromVal(1, block) + priceVoteFromVal(2, block) + priceVoteFromVal(3, block) - // delete btc:nusd for next vote period + t.Log("delete btc:nusd for next vote period") params.Whitelist = []asset.Pair{asset.Registry.Pair(denoms.NIBI, denoms.NUSD)} fixture.OracleKeeper.Params.Set(fixture.Ctx, params) - fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) - - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[0], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[1], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[2], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[3], 0)) - - // btc:nusd must be deleted - assert.Equal(t, []asset.Pair{asset.Registry.Pair(denoms.NIBI, denoms.NUSD)}, fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) - require.False(t, fixture.OracleKeeper.WhitelistedPairs.Has(fixture.Ctx, asset.Registry.Pair(denoms.BTC, denoms.NUSD))) - - // nibi:nusd, no missing - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 0) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 1) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 2) - MakeAggregatePrevoteAndVote(t, fixture, msgServer, 0, types.ExchangeRateTuples{{Pair: asset.Registry.Pair(denoms.NIBI, denoms.NUSD), ExchangeRate: randomExchangeRate}}, 3) - - fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) - - // validators keep miss counters from last vote period - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[0], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[1], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[2], 0)) - assert.Equal(t, uint64(1), fixture.OracleKeeper.MissCounters.GetOr(fixture.Ctx, ValAddrs[2], 0)) + perfs := fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) + + t.Log("validators 0-3 all voted -> expect win") + for valIdx := 0; valIdx < 4; valIdx++ { + perf := perfs[ValAddrs[valIdx].String()] + assert.EqualValues(t, 1, perf.WinCount) + assert.EqualValues(t, 1, perf.AbstainCount) + assert.EqualValues(t, 0, perf.MissCount) + } + t.Log("validators 4 didn't vote -> expect abstain") + perf := perfs[ValAddrs[4].String()] + assert.EqualValues(t, 0, perf.WinCount) + assert.EqualValues(t, 2, perf.AbstainCount) + assert.EqualValues(t, 0, perf.MissCount) + + t.Log("btc:nusd must be deleted") + assert.Equal(t, []asset.Pair{asset.Registry.Pair(denoms.NIBI, denoms.NUSD)}, + fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) + require.False(t, fixture.OracleKeeper.WhitelistedPairs.Has( + fixture.Ctx, asset.Registry.Pair(denoms.BTC, denoms.NUSD))) + + t.Log("vote from vals 0-3 on nibi:nusd") + priceVoteFromVal(0, block) + priceVoteFromVal(1, block) + priceVoteFromVal(2, block) + priceVoteFromVal(3, block) + perfs = fixture.OracleKeeper.UpdateExchangeRates(fixture.Ctx) + + t.Log("Although validators 0-2 voted, it's for the same period -> expect abstains for everyone") + for valIdx := 0; valIdx < 4; valIdx++ { + perf := perfs[ValAddrs[valIdx].String()] + assert.EqualValues(t, 1, perf.WinCount) + assert.EqualValues(t, 0, perf.AbstainCount) + assert.EqualValues(t, 0, perf.MissCount) + } + perf = perfs[ValAddrs[4].String()] + assert.EqualValues(t, 0, perf.WinCount) + assert.EqualValues(t, 1, perf.AbstainCount) + assert.EqualValues(t, 0, perf.MissCount) } diff --git a/x/oracle/keeper/whitelist.go b/x/oracle/keeper/whitelist.go index 636506701..f1662c19e 100644 --- a/x/oracle/keeper/whitelist.go +++ b/x/oracle/keeper/whitelist.go @@ -19,9 +19,9 @@ func (k Keeper) GetWhitelistedPairs(ctx sdk.Context) []asset.Pair { return k.WhitelistedPairs.Iterate(ctx, collections.Range[asset.Pair]{}).Keys() } -// updateWhitelist updates the whitelist by detecting possible changes between +// refreshWhitelist updates the whitelist by detecting possible changes between // the current vote targets and the current updated whitelist. -func (k Keeper) updateWhitelist(ctx sdk.Context, nextWhitelist []asset.Pair, currentWhitelist set.Set[asset.Pair]) { +func (k Keeper) refreshWhitelist(ctx sdk.Context, nextWhitelist []asset.Pair, currentWhitelist set.Set[asset.Pair]) { updateRequired := false if len(currentWhitelist) != len(nextWhitelist) { diff --git a/x/oracle/keeper/whitelist_test.go b/x/oracle/keeper/whitelist_test.go index 95dbed286..6061d73e3 100644 --- a/x/oracle/keeper/whitelist_test.go +++ b/x/oracle/keeper/whitelist_test.go @@ -106,7 +106,7 @@ func TestUpdateWhitelist(t *testing.T) { sort.Slice(whitelistSlice, func(i, j int) bool { return whitelistSlice[i].String() < whitelistSlice[j].String() }) - fixture.OracleKeeper.updateWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) + fixture.OracleKeeper.refreshWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) assert.Equal(t, whitelistSlice, fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) // len update (fast path) @@ -115,7 +115,7 @@ func TestUpdateWhitelist(t *testing.T) { sort.Slice(whitelistSlice, func(i, j int) bool { return whitelistSlice[i].String() < whitelistSlice[j].String() }) - fixture.OracleKeeper.updateWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) + fixture.OracleKeeper.refreshWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) assert.Equal(t, whitelistSlice, fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) // diff update (slow path) @@ -124,6 +124,6 @@ func TestUpdateWhitelist(t *testing.T) { sort.Slice(whitelistSlice, func(i, j int) bool { return whitelistSlice[i].String() < whitelistSlice[j].String() }) - fixture.OracleKeeper.updateWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) + fixture.OracleKeeper.refreshWhitelist(fixture.Ctx, whitelistSlice, currentWhitelist) assert.Equal(t, whitelistSlice, fixture.OracleKeeper.GetWhitelistedPairs(fixture.Ctx)) } diff --git a/x/oracle/types/ballot.go b/x/oracle/types/ballot.go index f4ed8b953..e0784ef21 100644 --- a/x/oracle/types/ballot.go +++ b/x/oracle/types/ballot.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "fmt" "math" "sort" @@ -14,17 +15,17 @@ import ( // NOTE: we don't need to implement proto interface on this file // these are not used in store or rpc response -// ExchangeRateBallot is a convenience wrapper to reduce redundant lookup cost -type ExchangeRateBallot struct { +// ExchangeRateVote is a convenience wrapper to reduce redundant lookup cost +type ExchangeRateVote struct { Pair asset.Pair ExchangeRate sdk.Dec // aka price Voter sdk.ValAddress Power int64 // how much tendermint consensus power this vote should have } -// NewExchangeRateBallot returns a new ExchangeRateBallot instance -func NewExchangeRateBallot(rate sdk.Dec, pair asset.Pair, voter sdk.ValAddress, power int64) ExchangeRateBallot { - return ExchangeRateBallot{ +// NewExchangeRateVote returns a new ExchangeRateVote instance +func NewExchangeRateVote(rate sdk.Dec, pair asset.Pair, voter sdk.ValAddress, power int64) ExchangeRateVote { + return ExchangeRateVote{ ExchangeRate: rate, Pair: pair, Voter: voter, @@ -32,11 +33,11 @@ func NewExchangeRateBallot(rate sdk.Dec, pair asset.Pair, voter sdk.ValAddress, } } -// ExchangeRateBallots is a convenience wrapper around a ExchangeRateVote slice -type ExchangeRateBallots []ExchangeRateBallot +// ExchangeRateVotes is a convenience wrapper around a ExchangeRateVote slice +type ExchangeRateVotes []ExchangeRateVote // ToMap return organized exchange rate map by validator -func (pb ExchangeRateBallots) ToMap() map[string]sdk.Dec { +func (pb ExchangeRateVotes) ToMap() map[string]sdk.Dec { validatorExchangeRateMap := make(map[string]sdk.Dec) for _, vote := range pb { if vote.ExchangeRate.IsPositive() { @@ -47,8 +48,8 @@ func (pb ExchangeRateBallots) ToMap() map[string]sdk.Dec { return validatorExchangeRateMap } -// ToCrossRate return cross_rate(base/exchange_rate) ballot -func (pb ExchangeRateBallots) ToCrossRate(bases map[string]sdk.Dec) (cb ExchangeRateBallots) { +// ToCrossRate return cross_rate(base/exchange_rate) votes +func (pb ExchangeRateVotes) ToCrossRate(bases map[string]sdk.Dec) (cb ExchangeRateVotes) { for i := range pb { vote := pb[i] @@ -67,20 +68,20 @@ func (pb ExchangeRateBallots) ToCrossRate(bases map[string]sdk.Dec) (cb Exchange } // NumValidVoters returns the number of voters who actually voted (i.e. did not abstain from voting for a pair). -func (b ExchangeRateBallots) NumValidVoters() uint64 { +func (v ExchangeRateVotes) NumValidVoters() uint64 { count := 0 - for _, ballot := range b { - if ballot.ExchangeRate.IsPositive() { + for _, vote := range v { + if vote.ExchangeRate.IsPositive() { count++ } } return uint64(count) } -// Power returns the total amount of voting power in the ballot -func (b ExchangeRateBallots) Power() int64 { +// Power returns the total amount of voting power in the votes +func (v ExchangeRateVotes) Power() int64 { totalPower := int64(0) - for _, vote := range b { + for _, vote := range v { totalPower += vote.Power } @@ -88,12 +89,12 @@ func (b ExchangeRateBallots) Power() int64 { } // WeightedMedian returns the median weighted by the power of the ExchangeRateVote. -// CONTRACT: ballot must be sorted -func (pb ExchangeRateBallots) WeightedMedian() sdk.Dec { - totalPower := pb.Power() - if pb.Len() > 0 { +// CONTRACT: votes must be sorted +func (votes ExchangeRateVotes) WeightedMedian() sdk.Dec { + totalPower := votes.Power() + if votes.Len() > 0 { pivot := int64(0) - for _, v := range pb { + for _, v := range votes { votePower := v.Power pivot += votePower @@ -106,7 +107,7 @@ func (pb ExchangeRateBallots) WeightedMedian() sdk.Dec { } // WeightedMedianWithAssertion returns the median weighted by the power of the ExchangeRateVote. -func (pb ExchangeRateBallots) WeightedMedianWithAssertion() sdk.Dec { +func (pb ExchangeRateVotes) WeightedMedianWithAssertion() sdk.Dec { sort.Sort(pb) totalPower := pb.Power() if pb.Len() > 0 { @@ -124,7 +125,7 @@ func (pb ExchangeRateBallots) WeightedMedianWithAssertion() sdk.Dec { } // StandardDeviation returns the standard deviation by the power of the ExchangeRateVote. -func (pb ExchangeRateBallots) StandardDeviation(median sdk.Dec) (standardDeviation sdk.Dec) { +func (pb ExchangeRateVotes) StandardDeviation(median sdk.Dec) (standardDeviation sdk.Dec) { if len(pb) == 0 { return sdk.ZeroDec() } @@ -156,26 +157,31 @@ func (pb ExchangeRateBallots) StandardDeviation(median sdk.Dec) (standardDeviati } // Len implements sort.Interface -func (pb ExchangeRateBallots) Len() int { +func (pb ExchangeRateVotes) Len() int { return len(pb) } // Less reports whether the element with // index i should sort before the element with index j. -func (pb ExchangeRateBallots) Less(i, j int) bool { +func (pb ExchangeRateVotes) Less(i, j int) bool { return pb[i].ExchangeRate.LT(pb[j].ExchangeRate) } // Swap implements sort.Interface. -func (pb ExchangeRateBallots) Swap(i, j int) { +func (pb ExchangeRateVotes) Swap(i, j int) { pb[i], pb[j] = pb[j], pb[i] } // ValidatorPerformance keeps track of a validator performance in the voting period. type ValidatorPerformance struct { - Power int64 // tendermint consensus power - RewardWeight int64 // how much of the rewards this validator should receive, units of consensus power - WinCount int64 + // Tendermint consensus voting power + Power int64 + // RewardWeight: Weight of rewards the validator should receive in units of + // consensus power. + RewardWeight int64 + WinCount int64 // Number of valid votes for which the validator will be rewarded + AbstainCount int64 // Number of abstained votes for which there will be no reward or punishment + MissCount int64 // Number of invalid/punishable votes ValAddress sdk.ValAddress } @@ -185,14 +191,16 @@ func NewValidatorPerformance(power int64, recipient sdk.ValAddress) ValidatorPer Power: power, RewardWeight: 0, WinCount: 0, + AbstainCount: 0, + MissCount: 0, ValAddress: recipient, } } type ValidatorPerformances map[string]ValidatorPerformance -// GetTotalRewardWeight returns the sum of the reward weight of all the validators included in the map -func (vp ValidatorPerformances) GetTotalRewardWeight() int64 { +// TotalRewardWeight returns the sum of the reward weight of all the validators included in the map +func (vp ValidatorPerformances) TotalRewardWeight() int64 { totalRewardWeight := int64(0) for _, validator := range vp { totalRewardWeight += validator.RewardWeight @@ -200,3 +208,13 @@ func (vp ValidatorPerformances) GetTotalRewardWeight() int64 { return totalRewardWeight } + +func (vp ValidatorPerformances) String() string { + jsonBz, _ := json.MarshalIndent(vp, "", " ") + return string(jsonBz) +} + +func (vp ValidatorPerformance) String() string { + jsonBz, _ := json.MarshalIndent(vp, "", " ") + return string(jsonBz) +} diff --git a/x/oracle/types/ballot_test.go b/x/oracle/types/ballot_test.go index df05fd677..58de574bc 100644 --- a/x/oracle/types/ballot_test.go +++ b/x/oracle/types/ballot_test.go @@ -20,12 +20,12 @@ import ( "github.com/NibiruChain/nibiru/x/oracle/types" ) -func TestToMap(t *testing.T) { +func TestExchangeRateVotesToMap(t *testing.T) { tests := struct { - votes []types.ExchangeRateBallot + votes []types.ExchangeRateVote isValid []bool }{ - []types.ExchangeRateBallot{ + []types.ExchangeRateVote{ { Voter: sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()), Pair: asset.Registry.Pair(denoms.BTC, denoms.NUSD), @@ -48,7 +48,7 @@ func TestToMap(t *testing.T) { []bool{true, false, true}, } - pb := types.ExchangeRateBallots(tests.votes) + pb := types.ExchangeRateVotes(tests.votes) mapData := pb.ToMap() for i, vote := range tests.votes { exchangeRate, ok := mapData[string(vote.Voter)] @@ -59,6 +59,9 @@ func TestToMap(t *testing.T) { require.False(t, ok) } } + require.NotPanics(t, func() { + types.ExchangeRateVotes(tests.votes).NumValidVoters() + }) } func TestToCrossRate(t *testing.T) { @@ -84,26 +87,26 @@ func TestToCrossRate(t *testing.T) { }, } - pbBase := types.ExchangeRateBallots{} - pbQuote := types.ExchangeRateBallots{} - cb := types.ExchangeRateBallots{} + pbBase := types.ExchangeRateVotes{} + pbQuote := types.ExchangeRateVotes{} + cb := types.ExchangeRateVotes{} for _, data := range data { valAddr := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()) if !data.base.IsZero() { - pbBase = append(pbBase, types.NewExchangeRateBallot(data.base, asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) + pbBase = append(pbBase, types.NewExchangeRateVote(data.base, asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) } - pbQuote = append(pbQuote, types.NewExchangeRateBallot(data.quote, asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) + pbQuote = append(pbQuote, types.NewExchangeRateVote(data.quote, asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) if !data.base.IsZero() && !data.quote.IsZero() { - cb = append(cb, types.NewExchangeRateBallot(data.base.Quo(data.quote), asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) + cb = append(cb, types.NewExchangeRateVote(data.base.Quo(data.quote), asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 100)) } else { - cb = append(cb, types.NewExchangeRateBallot(sdk.ZeroDec(), asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 0)) + cb = append(cb, types.NewExchangeRateVote(sdk.ZeroDec(), asset.Registry.Pair(denoms.BTC, denoms.NUSD), valAddr, 0)) } } - baseMapBallot := pbBase.ToMap() - require.Equal(t, cb, pbQuote.ToCrossRate(baseMapBallot)) + basePairPrices := pbBase.ToMap() + require.Equal(t, cb, pbQuote.ToCrossRate(basePairPrices)) sort.Sort(cb) } @@ -123,12 +126,12 @@ func TestSqrt(t *testing.T) { func TestPBPower(t *testing.T) { ctx := sdk.NewContext(nil, tmproto.Header{}, false, nil) _, valAccAddrs, sk := types.GenerateRandomTestCase() - pb := types.ExchangeRateBallots{} - ballotPower := int64(0) + pb := types.ExchangeRateVotes{} + totalPower := int64(0) for i := 0; i < len(sk.Validators()); i++ { power := sk.Validator(ctx, valAccAddrs[i]).GetConsensusPower(sdk.DefaultPowerReduction) - vote := types.NewExchangeRateBallot( + vote := types.NewExchangeRateVote( sdk.ZeroDec(), asset.Registry.Pair(denoms.ETH, denoms.NUSD), valAccAddrs[i], @@ -139,15 +142,15 @@ func TestPBPower(t *testing.T) { require.NotEqual(t, int64(0), vote.Power) - ballotPower += vote.Power + totalPower += vote.Power } - require.Equal(t, ballotPower, pb.Power()) + require.Equal(t, totalPower, pb.Power()) // Mix in a fake validator, the total power should not have changed. pubKey := secp256k1.GenPrivKey().PubKey() faceValAddr := sdk.ValAddress(pubKey.Address()) - fakeVote := types.NewExchangeRateBallot( + fakeVote := types.NewExchangeRateVote( sdk.OneDec(), asset.Registry.Pair(denoms.ETH, denoms.NUSD), faceValAddr, @@ -155,7 +158,7 @@ func TestPBPower(t *testing.T) { ) pb = append(pb, fakeVote) - require.Equal(t, ballotPower, pb.Power()) + require.Equal(t, totalPower, pb.Power()) } func TestPBWeightedMedian(t *testing.T) { @@ -203,7 +206,7 @@ func TestPBWeightedMedian(t *testing.T) { } for _, tc := range tests { - pb := types.ExchangeRateBallots{} + pb := types.ExchangeRateVotes{} for i, input := range tc.inputs { valAddr := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()) @@ -212,7 +215,7 @@ func TestPBWeightedMedian(t *testing.T) { power = 0 } - vote := types.NewExchangeRateBallot( + vote := types.NewExchangeRateVote( sdk.NewDec(int64(input)), asset.Registry.Pair(denoms.ETH, denoms.NUSD), valAddr, @@ -273,7 +276,7 @@ func TestPBStandardDeviation(t *testing.T) { base := math.Pow10(types.OracleDecPrecision) for _, tc := range tests { - pb := types.ExchangeRateBallots{} + pb := types.ExchangeRateVotes{} for i, input := range tc.inputs { valAddr := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()) @@ -282,7 +285,7 @@ func TestPBStandardDeviation(t *testing.T) { power = 0 } - vote := types.NewExchangeRateBallot( + vote := types.NewExchangeRateVote( sdk.NewDecWithPrec(int64(input*base), int64(types.OracleDecPrecision)), asset.Registry.Pair(denoms.ETH, denoms.NUSD), valAddr, @@ -301,12 +304,12 @@ func TestPBStandardDeviationOverflow(t *testing.T) { exchangeRate, err := sdk.NewDecFromStr("100000000000000000000000000000000000000000000000000000000.0") require.NoError(t, err) - pb := types.ExchangeRateBallots{types.NewExchangeRateBallot( + pb := types.ExchangeRateVotes{types.NewExchangeRateVote( sdk.ZeroDec(), asset.Registry.Pair(denoms.ETH, denoms.NUSD), valAddr, 2, - ), types.NewExchangeRateBallot( + ), types.NewExchangeRateVote( exchangeRate, asset.Registry.Pair(denoms.ETH, denoms.NUSD), valAddr, @@ -334,3 +337,26 @@ func TestNewClaim(t *testing.T) { ValAddress: addr, }, claim) } + +func TestValidatorPerformances(t *testing.T) { + power := int64(42) + valNames := []string{"val0", "val1", "val2", "val3"} + perfList := []types.ValidatorPerformance{ + types.NewValidatorPerformance(power, sdk.ValAddress([]byte(valNames[0]))), + types.NewValidatorPerformance(power, sdk.ValAddress([]byte(valNames[1]))), + types.NewValidatorPerformance(power, sdk.ValAddress([]byte(valNames[2]))), + types.NewValidatorPerformance(power, sdk.ValAddress([]byte(valNames[3]))), + } + perfs := make(types.ValidatorPerformances) + for idx, perf := range perfList { + perfs[valNames[idx]] = perf + } + + require.NotPanics(t, func() { + out := perfs.String() + require.NotEmpty(t, out) + + out = perfs[valNames[0]].String() + require.NotEmpty(t, out) + }) +} diff --git a/x/oracle/types/test_utils.go b/x/oracle/types/test_utils.go index 7832bfcb7..67b51d075 100644 --- a/x/oracle/types/test_utils.go +++ b/x/oracle/types/test_utils.go @@ -48,7 +48,7 @@ func GenerateRandomTestCase() (rates []float64, valAddrs []sdk.ValAddress, staki var _ StakingKeeper = DummyStakingKeeper{} -// DummyStakingKeeper dummy staking keeper to test ballot +// DummyStakingKeeper dummy staking keeper to test votes type DummyStakingKeeper struct { validators []MockValidator }