diff --git a/x/oracle/abci.go b/x/oracle/abci.go index 4b24e68ee9..4ca5f874be 100644 --- a/x/oracle/abci.go +++ b/x/oracle/abci.go @@ -16,7 +16,7 @@ func isPeriodLastBlock(ctx sdk.Context, blocksPerPeriod uint64) bool { } // EndBlocker is called at the end of every block -func EndBlocker(ctx sdk.Context, k keeper.Keeper) error { +func EndBlocker(ctx sdk.Context, k keeper.Keeper, experimental bool) error { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) params := k.GetParams(ctx) @@ -41,6 +41,11 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) error { k.ClearExchangeRates(ctx) + if isPeriodLastBlock(ctx, params.MedianPeriod) && experimental { + k.ClearMedians(ctx) + k.ClearMedianDeviations(ctx) + } + // NOTE: it filters out inactive or jailed validators ballotDenomSlice := k.OrganizeBallotByDenom(ctx, validatorClaimMap) @@ -56,6 +61,18 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) error { if err = k.SetExchangeRateWithEvent(ctx, ballotDenom.Denom, exchangeRate); err != nil { return err } + + if experimental { + // Stamp rate every stamp period if asset is set to have historic stats tracked + if isPeriodLastBlock(ctx, params.StampPeriod) && params.HistoricAcceptList.Contains(ballotDenom.Denom) { + k.AddHistoricPrice(ctx, ballotDenom.Denom, exchangeRate) + } + + // Set median price every median period if asset is set to have historic stats tracked + if isPeriodLastBlock(ctx, params.MedianPeriod) && params.HistoricAcceptList.Contains(ballotDenom.Denom) { + k.CalcAndSetMedian(ctx, ballotDenom.Denom) + } + } } // update miss counting & slashing @@ -91,6 +108,14 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) error { k.SlashAndResetMissCounters(ctx) } + // Prune historic prices every prune period + if isPeriodLastBlock(ctx, params.PrunePeriod) && experimental { + pruneBlock := uint64(ctx.BlockHeight()) - params.PrunePeriod + for _, v := range params.HistoricAcceptList { + k.DeleteHistoricPrice(ctx, v.String(), pruneBlock) + } + } + return nil } diff --git a/x/oracle/abci_test.go b/x/oracle/abci_test.go new file mode 100644 index 0000000000..8320abf195 --- /dev/null +++ b/x/oracle/abci_test.go @@ -0,0 +1,109 @@ +package oracle_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + "github.com/stretchr/testify/suite" + "github.com/tendermint/tendermint/crypto/secp256k1" + tmrand "github.com/tendermint/tendermint/libs/rand" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + umeeapp "github.com/umee-network/umee/v3/app" + appparams "github.com/umee-network/umee/v3/app/params" + "github.com/umee-network/umee/v3/x/oracle" + "github.com/umee-network/umee/v3/x/oracle/types" +) + +const ( + displayDenom string = appparams.DisplayDenom + bondDenom string = appparams.BondDenom +) + +type IntegrationTestSuite struct { + suite.Suite + + ctx sdk.Context + app *umeeapp.UmeeApp +} + +const ( + initialPower = int64(10000000000) +) + +func (s *IntegrationTestSuite) SetupTest() { + require := s.Require() + isCheckTx := false + app := umeeapp.Setup(s.T(), isCheckTx, 1) + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{ + ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)), + Height: int64(types.DefaultMedianPeriod) - 1, + }) + + oracle.InitGenesis(ctx, app.OracleKeeper, *types.DefaultGenesisState()) + + sh := teststaking.NewHelper(s.T(), ctx, *app.StakingKeeper) + sh.Denom = bondDenom + amt := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction) + + // mint and send coins to validators + require.NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, initCoins)) + require.NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, initCoins)) + require.NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, initCoins)) + require.NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr2, initCoins)) + + sh.CreateValidator(valAddr, valPubKey, amt, true) + sh.CreateValidator(valAddr2, valPubKey2, amt, true) + + staking.EndBlocker(ctx, *app.StakingKeeper) + + s.app = app + s.ctx = ctx +} + +// Test addresses +var ( + valPubKeys = simapp.CreateTestPubKeys(2) + + valPubKey = valPubKeys[0] + pubKey = secp256k1.GenPrivKey().PubKey() + addr = sdk.AccAddress(pubKey.Address()) + valAddr = sdk.ValAddress(pubKey.Address()) + + valPubKey2 = valPubKeys[1] + pubKey2 = secp256k1.GenPrivKey().PubKey() + addr2 = sdk.AccAddress(pubKey2.Address()) + valAddr2 = sdk.ValAddress(pubKey2.Address()) + + initTokens = sdk.TokensFromConsensusPower(initialPower, sdk.DefaultPowerReduction) + initCoins = sdk.NewCoins(sdk.NewCoin(bondDenom, initTokens)) +) + +func (s *IntegrationTestSuite) TestEndblockerExperimentalFlag() { + app, ctx := s.app, s.ctx + + // add historic price and calcSet median stats + app.OracleKeeper.AddHistoricPrice(s.ctx, displayDenom, sdk.MustNewDecFromStr("1.0")) + app.OracleKeeper.CalcAndSetMedian(s.ctx, displayDenom) + + // with experimental flag off median stats don't get cleared + oracle.EndBlocker(ctx, app.OracleKeeper, false) + median, err := app.OracleKeeper.GetMedian(s.ctx, displayDenom) + s.Require().NoError(err) + s.Require().Equal(sdk.MustNewDecFromStr("1.0"), median) + + // with experimental flag on median stats get cleared + oracle.EndBlocker(ctx, app.OracleKeeper, true) + median, err = app.OracleKeeper.GetMedian(s.ctx, displayDenom) + s.Require().Error(err) + s.Require().Equal(sdk.ZeroDec(), median) +} + +func TestOracleTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/x/oracle/keeper/historic_price.go b/x/oracle/keeper/historic_price.go index 339fbd5bcd..70da90ebb0 100644 --- a/x/oracle/keeper/historic_price.go +++ b/x/oracle/keeper/historic_price.go @@ -220,8 +220,7 @@ func (k Keeper) DeleteHistoricPrice( store.Delete(types.KeyHistoricPrice(denom, blockNum)) } -// DeleteMedian deletes a given denom's median price in the last prune -// period since a given block. +// DeleteMedian deletes a given denom's median price. func (k Keeper) DeleteMedian( ctx sdk.Context, denom string, @@ -231,7 +230,7 @@ func (k Keeper) DeleteMedian( } // DeleteMedianDeviation deletes a given denom's standard deviation around -// its median price in the last prune period since a given block. +// its median price. func (k Keeper) DeleteMedianDeviation( ctx sdk.Context, denom string, @@ -239,3 +238,24 @@ func (k Keeper) DeleteMedianDeviation( store := ctx.KVStore(k.storeKey) store.Delete(types.KeyMedianDeviation(denom)) } + +// ClearMedians iterates through all medians in the store and deletes them. +func (k Keeper) ClearMedians(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.KeyPrefixMedian) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} + +// ClearMedianDeviations iterates through all median deviations in the store +// and deletes them. +func (k Keeper) ClearMedianDeviations(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.KeyPrefixMedianDeviation) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} diff --git a/x/oracle/keeper/historic_price_test.go b/x/oracle/keeper/historic_price_test.go index 19d8793f3c..ff99872941 100644 --- a/x/oracle/keeper/historic_price_test.go +++ b/x/oracle/keeper/historic_price_test.go @@ -12,11 +12,12 @@ func (s *IntegrationTestSuite) TestSetHistoraclePricing() { // add multiple historic prices to store exchangeRates := []string{"1.0", "1.2", "1.1", "1.4"} - for _, exchangeRate := range exchangeRates { - app.OracleKeeper.AddHistoricPrice(ctx, displayDenom, sdk.MustNewDecFromStr(exchangeRate)) - + for i, exchangeRate := range exchangeRates { // update blockheight - ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + int64(i)) + + app.OracleKeeper.AddHistoricPrice(ctx, displayDenom, sdk.MustNewDecFromStr(exchangeRate)) + app.OracleKeeper.CalcAndSetMedian(ctx, displayDenom) } // set and check median and median standard deviation diff --git a/x/oracle/module.go b/x/oracle/module.go index bff3d0c686..6a19dfcb0d 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -182,7 +182,7 @@ func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} // EndBlock executes all ABCI EndBlock logic respective to the x/oracle module. // It returns no validator updates. func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - if err := EndBlocker(ctx, am.keeper); err != nil { + if err := EndBlocker(ctx, am.keeper, am.experimental); err != nil { panic(err) } diff --git a/x/oracle/types/params.go b/x/oracle/types/params.go index c5f6a9c6c4..e31284f3af 100644 --- a/x/oracle/types/params.go +++ b/x/oracle/types/params.go @@ -194,6 +194,10 @@ func (p Params) Validate() error { return fmt.Errorf("oracle parameter MedianPeriod must be greater than or equal with StampPeriod") } + if p.StampPeriod%p.VotePeriod != 0 || p.MedianPeriod%p.VotePeriod != 0 || p.PrunePeriod%p.VotePeriod != 0 { + return fmt.Errorf("oracle parameters StampPeriod, MedianPeriod, and PrunePeriod must be exact multiples of VotePeiod") + } + for _, denom := range p.AcceptList { if len(denom.BaseDenom) == 0 { return fmt.Errorf("oracle parameter AcceptList Denom must have BaseDenom") diff --git a/x/oracle/types/params_test.go b/x/oracle/types/params_test.go index fd6a5ebe80..2f37ae28f0 100644 --- a/x/oracle/types/params_test.go +++ b/x/oracle/types/params_test.go @@ -241,21 +241,34 @@ func TestParamsEqual(t *testing.T) { err = p10.Validate() require.Error(t, err) - // empty name + // StampPeriod, MedianPeriod, PrunePeriod are multiples of VotePeriod p11 := DefaultParams() - p11.AcceptList[0].BaseDenom = "" - p11.AcceptList[0].SymbolDenom = "ATOM" + p11.StampPeriod = 10 + p11.VotePeriod = 3 + err = p11.Validate() + require.Error(t, err) + p11.MedianPeriod = 10 + err = p11.Validate() + require.Error(t, err) + p11.PrunePeriod = 10 err = p11.Validate() require.Error(t, err) - // empty + // empty name p12 := DefaultParams() - p12.AcceptList[0].BaseDenom = "uatom" - p12.AcceptList[0].SymbolDenom = "" + p12.AcceptList[0].BaseDenom = "" + p12.AcceptList[0].SymbolDenom = "ATOM" err = p12.Validate() require.Error(t, err) + // empty p13 := DefaultParams() - require.NotNil(t, p13.ParamSetPairs()) - require.NotNil(t, p13.String()) + p13.AcceptList[0].BaseDenom = "uatom" + p13.AcceptList[0].SymbolDenom = "" + err = p13.Validate() + require.Error(t, err) + + p14 := DefaultParams() + require.NotNil(t, p14.ParamSetPairs()) + require.NotNil(t, p14.String()) }