Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,33 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
}

voteTargets := make(map[string]types.Denom)
totalTargets := 0
k.IterateVoteTargets(ctx, func(denom string, denomInfo types.Denom) bool {
voteTargets[denom] = denomInfo
totalTargets++
return false
})

// Clear all exchange rates
k.IterateBaseExchangeRates(ctx, func(denom string, _ sdk.Dec) (stop bool) {
k.DeleteBaseExchangeRate(ctx, denom)
// TODO: replace this with an indicator of staleness
// k.DeleteBaseExchangeRate(ctx, denom)
return false
})

// Organize votes to ballot by denom
// NOTE: **Filter out inactive or jailed validators**
// NOTE: **Make abstain votes to have zero vote power**
voteMap := k.OrganizeBallotByDenom(ctx, validatorClaimMap)
// belowThresholdVoteMap has assets that failed to meet threshold
referenceDenom, belowThresholdVoteMap := pickReferenceDenom(ctx, k, voteTargets, voteMap)

if referenceDenom := pickReferenceDenom(ctx, k, voteTargets, voteMap); referenceDenom != "" {
if referenceDenom != "" {
// make voteMap of Reference denom to calculate cross exchange rates
ballotRD := voteMap[referenceDenom]
voteMapRD := ballotRD.ToMap()

var exchangeRateRD sdk.Dec

exchangeRateRD = ballotRD.WeightedMedianWithAssertion()
var exchangeRateRD sdk.Dec = ballotRD.WeightedMedianWithAssertion()

// Iterate through ballots and update exchange rates; drop if not enough votes have been achieved.
for denom, ballot := range voteMap {
Expand All @@ -84,14 +87,26 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
// Set the exchange rate, emit ABCI event
k.SetBaseExchangeRateWithEvent(ctx, denom, exchangeRate)
}

for _, ballot := range belowThresholdVoteMap {
// perform tally for below threshold assets to calculate total win count
Tally(ctx, ballot, params.RewardBand, validatorClaimMap)
}
} else {
// in this case, all assets would be in the belowThresholdVoteMap
for _, ballot := range belowThresholdVoteMap {
// perform tally for below threshold assets to calculate total win count
Tally(ctx, ballot, params.RewardBand, validatorClaimMap)
}
}

//---------------------------
// Do miss counting & slashing
voteTargetsLen := len(voteTargets)
for _, claim := range validatorClaimMap {
// Skip abstain & valid voters
if int(claim.WinCount) == voteTargetsLen {
// we require validator to have submitted in-range data
// for all assets to not be counted as a miss
if int(claim.WinCount) == totalTargets {
continue
}

Expand All @@ -111,6 +126,4 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
if utils.IsPeriodLastBlock(ctx, params.SlashWindow) {
k.SlashAndResetMissCounters(ctx)
}

return
}
138 changes: 131 additions & 7 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ func TestOracleThreshold(t *testing.T) {
oracle.EndBlocker(input.Ctx.WithBlockHeight(1), input.OracleKeeper)

_, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx.WithBlockHeight(1), utils.MicroAtomDenom)
require.Error(t, err)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)
// TODO: add check for staleness
}

func TestOracleDrop(t *testing.T) {
Expand All @@ -118,8 +120,10 @@ func TestOracleDrop(t *testing.T) {
// Immediately swap halt after an illiquid oracle vote
oracle.EndBlocker(input.Ctx, input.OracleKeeper)

_, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.Error(t, err)
rate, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)
// TODO: add check for staleness
}

func TestOracleTally(t *testing.T) {
Expand Down Expand Up @@ -326,8 +330,124 @@ func TestNotPassedBallotSlashing(t *testing.T) {

oracle.EndBlocker(input.Ctx, input.OracleKeeper)
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2]))
}

func TestNotPassedBallotSlashingInvalidVotes(t *testing.T) {
input, h := setupN(t, 7)
params := input.OracleKeeper.GetParams(input.Ctx)
params.Whitelist = types.DenomList{{Name: utils.MicroAtomDenom}}
input.OracleKeeper.SetParams(input.Ctx, params)

input.OracleKeeper.ClearVoteTargets(input.Ctx)
input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroAtomDenom)

input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

// Account 1
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 0)
// Account 2
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 1)
// Account 3
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate.Add(sdk.NewDec(100000000000000))}}, 2)

oracle.EndBlocker(input.Ctx, input.OracleKeeper)

// 4-7 should be missed due to not voting
// 3 should be missed due to out of bounds
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6]))
}

func TestInvalidVoteOnAssetUnderThresholdMisses(t *testing.T) {
input, h := setupN(t, 7)
params := input.OracleKeeper.GetParams(input.Ctx)
params.Whitelist = types.DenomList{{Name: utils.MicroAtomDenom}, {Name: utils.MicroEthDenom}}
input.OracleKeeper.SetParams(input.Ctx, params)

input.OracleKeeper.ClearVoteTargets(input.Ctx)
input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroAtomDenom)
input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroEthDenom)

input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

// Account 1
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 0)
// Account 2
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 1)
// Account 3
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 2)

// rest of accounts
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 3)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 4)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 5)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 6)

oracle.EndBlocker(input.Ctx, input.OracleKeeper)

// 6 and 7 should be missed due to not voting on second asset
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6]))

input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

rate, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)

rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroEthDenom)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)

input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

// Account 1
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate}}, 0)
// Account 2
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate}}, 1)
// Account 3
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate.Add(sdk.NewDec(100000000000000))}}, 2)

// rest of accounts meet threshold only for one asset
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 3)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 4)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 5)
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 6)

oracle.EndBlocker(input.Ctx, input.OracleKeeper)

// 4-7 should be missed due to not voting on second asset
// 3 should have missed due to out of bounds value even though it didnt meet voting threshold
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0]))
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3]))
require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4]))
require.Equal(t, uint64(2), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5]))
require.Equal(t, uint64(2), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6]))

input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.NoError(t, err)
require.Equal(t, anotherRandomExchangeRate, rate)

// the old value should be persisted because asset didnt meet ballot threshold
rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroEthDenom)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)
}

func TestAbstainSlashing(t *testing.T) {
Expand All @@ -341,25 +461,29 @@ func TestAbstainSlashing(t *testing.T) {

votePeriodsPerWindow := sdk.NewDec(int64(input.OracleKeeper.SlashWindow(input.Ctx))).QuoInt64(int64(input.OracleKeeper.VotePeriod(input.Ctx))).TruncateInt64()
minValidPerWindow := input.OracleKeeper.MinValidPerWindow(input.Ctx)
slashFraction := input.OracleKeeper.SlashFraction(input.Ctx)

for i := uint64(0); i <= uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64()); i++ {
limit := uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64())
for i := uint64(0); i <= limit; i++ {
input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1)

// Account 1, KRW
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 0)

// Account 2, KRW, abstain vote
// Account 2, KRW, abstain vote - should count as miss
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.ZeroDec()}}, 1)

// Account 3, KRW
makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 2)

oracle.EndBlocker(input.Ctx, input.OracleKeeper)
require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
require.Equal(t, uint64(i+1%limit), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1]))
}

input.Ctx = input.Ctx.WithBlockHeight(votePeriodsPerWindow - 1)
oracle.EndBlocker(input.Ctx, input.OracleKeeper)
validator := input.StakingKeeper.Validator(input.Ctx, keeper.ValAddrs[1])
require.Equal(t, stakingAmt, validator.GetBondedTokens())
require.Equal(t, sdk.OneDec().Sub(slashFraction).MulInt(stakingAmt).TruncateInt(), validator.GetBondedTokens())
}

func TestVoteTargets(t *testing.T) {
Expand Down
22 changes: 22 additions & 0 deletions x/oracle/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,25 @@ func setupVal5(t *testing.T) (keeper.TestInput, sdk.Handler) {

return input, h
}

func setupN(t *testing.T, num int) (keeper.TestInput, sdk.Handler) {
input := keeper.CreateTestInput(t)
params := input.OracleKeeper.GetParams(input.Ctx)
params.VotePeriod = 1
params.SlashWindow = 100
input.OracleKeeper.SetParams(input.Ctx, params)
h := oracle.NewHandler(input.OracleKeeper)

sh := staking.NewHandler(input.StakingKeeper)

require.LessOrEqual(t, num, len(keeper.ValAddrs))

// Validator created
for i := 0; i < num; i++ {
_, err := sh(input.Ctx, keeper.NewTestMsgCreateValidator(keeper.ValAddrs[i], keeper.ValPubKeys[i], stakingAmt))
require.NoError(t, err)
}
staking.EndBlocker(input.Ctx, input.StakingKeeper)

return input, h
}
8 changes: 7 additions & 1 deletion x/oracle/keeper/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,16 @@ func MakeEncodingConfig(_ *testing.T) simparams.EncodingConfig {

// Test addresses
var (
ValPubKeys = simapp.CreateTestPubKeys(5)
ValPubKeys = simapp.CreateTestPubKeys(7)

pubKeys = []crypto.PubKey{
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
secp256k1.GenPrivKey().PubKey(),
}

Addrs = []sdk.AccAddress{
Expand All @@ -99,6 +101,8 @@ var (
sdk.AccAddress(pubKeys[2].Address()),
sdk.AccAddress(pubKeys[3].Address()),
sdk.AccAddress(pubKeys[4].Address()),
sdk.AccAddress(pubKeys[5].Address()),
sdk.AccAddress(pubKeys[6].Address()),
}

ValAddrs = []sdk.ValAddress{
Expand All @@ -107,6 +111,8 @@ var (
sdk.ValAddress(pubKeys[2].Address()),
sdk.ValAddress(pubKeys[3].Address()),
sdk.ValAddress(pubKeys[4].Address()),
sdk.ValAddress(pubKeys[5].Address()),
sdk.ValAddress(pubKeys[6].Address()),
}

InitTokens = sdk.TokensFromConsensusPower(200, sdk.DefaultPowerReduction)
Expand Down
17 changes: 10 additions & 7 deletions x/oracle/tally.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ func Tally(ctx sdk.Context, pb types.ExchangeRateBallot, rewardBand sdk.Dec, val
}

for _, vote := range pb {
// Filter ballot winners & abstain voters
if (vote.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) &&
vote.ExchangeRate.LTE(weightedMedian.Add(rewardSpread))) ||
!vote.ExchangeRate.IsPositive() {
// Filter ballot winners
// abstaining counts as out of range and will be eventually penalized
if vote.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) &&
vote.ExchangeRate.LTE(weightedMedian.Add(rewardSpread)) {

key := vote.Voter.String()
claim := validatorClaimMap[key]
Expand All @@ -46,9 +46,10 @@ func ballotIsPassing(ballot types.ExchangeRateBallot, thresholdVotes sdk.Int) (s
// choose reference denom with the highest voter turnout
// If the voting power of the two denominations is the same,
// select reference denom in alphabetical order.
func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string]types.Denom, voteMap map[string]types.ExchangeRateBallot) string {
func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string]types.Denom, voteMap map[string]types.ExchangeRateBallot) (referenceDenom string, belowThresholdVoteMap map[string]types.ExchangeRateBallot) {
largestBallotPower := int64(0)
referenceDenom := ""
referenceDenom = ""
belowThresholdVoteMap = map[string]types.ExchangeRateBallot{}

totalBondedPower := sdk.TokensToConsensusPower(k.StakingKeeper.TotalBondedTokens(ctx), k.StakingKeeper.PowerReduction(ctx))
voteThreshold := k.VoteThreshold(ctx)
Expand All @@ -69,6 +70,8 @@ func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string
if power, ok := ballotIsPassing(ballot, thresholdVotes); ok {
ballotPower = power.Int64()
} else {
// add assets below threshold to separate map for tally evaluation
belowThresholdVoteMap[denom] = voteMap[denom]
delete(voteTargets, denom)
delete(voteMap, denom)
continue
Expand All @@ -82,5 +85,5 @@ func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string
}
}

return referenceDenom
return
}