From 52116122920b8d458e5161836feb7daaf1a429d1 Mon Sep 17 00:00:00 2001 From: matthiasmatt Date: Sun, 12 Jan 2025 14:59:35 +0100 Subject: [PATCH] feat: reduce gas for first vote by validators --- app/ante.go | 3 + app/ante/handler_opts.go | 6 + app/app.go | 9 +- x/common/testutil/testapp/testapp.go | 38 +++ x/oracle/ante/fee_discount.go | 101 ++++++++ x/oracle/ante/fee_discount_test.go | 342 +++++++++++++++++++++++++++ x/oracle/ante/keeper_interface.go | 17 ++ x/oracle/keeper/ballot.go | 5 + 8 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 x/oracle/ante/fee_discount.go create mode 100644 x/oracle/ante/fee_discount_test.go create mode 100644 x/oracle/ante/keeper_interface.go diff --git a/app/ante.go b/app/ante.go index 79c2d5adf..8cbbc5e69 100644 --- a/app/ante.go +++ b/app/ante.go @@ -9,6 +9,8 @@ import ( authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + oracleante "github.com/NibiruChain/nibiru/v2/x/oracle/ante" + "github.com/NibiruChain/nibiru/v2/app/ante" "github.com/NibiruChain/nibiru/v2/app/evmante" devgasante "github.com/NibiruChain/nibiru/v2/x/devgas/v1/ante" @@ -71,6 +73,7 @@ func NewAnteHandlerNonEVM( // ticket: https://github.com/NibiruChain/nibiru/issues/1915 authante.NewExtensionOptionsDecorator(opts.ExtensionOptionChecker), authante.NewValidateBasicDecorator(), + oracleante.NewVoteFeeDiscountDecorator(opts.OracleKeeper, opts.StakingKeeper), authante.NewTxTimeoutHeightDecorator(), authante.NewValidateMemoDecorator(opts.AccountKeeper), ante.AnteDecoratorEnsureSinglePostPriceMessage{}, diff --git a/app/ante/handler_opts.go b/app/ante/handler_opts.go index 9c1d88301..ccb27e537 100644 --- a/app/ante/handler_opts.go +++ b/app/ante/handler_opts.go @@ -10,9 +10,12 @@ import ( authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + devgasante "github.com/NibiruChain/nibiru/v2/x/devgas/v1/ante" devgaskeeper "github.com/NibiruChain/nibiru/v2/x/devgas/v1/keeper" evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper" + oraclekeeper "github.com/NibiruChain/nibiru/v2/x/oracle/keeper" ) type AnteHandlerOptions struct { @@ -23,6 +26,9 @@ type AnteHandlerOptions struct { EvmKeeper *evmkeeper.Keeper AccountKeeper authkeeper.AccountKeeper + OracleKeeper oraclekeeper.Keeper + StakingKeeper stakingkeeper.Keeper + TxCounterStoreKey types.StoreKey WasmConfig *wasmtypes.WasmConfig MaxTxGasWanted uint64 diff --git a/app/app.go b/app/app.go index 2bc2618de..a56c38d76 100644 --- a/app/app.go +++ b/app/app.go @@ -216,9 +216,10 @@ func NewNibiruApp( app.SetBeginBlocker(app.BeginBlocker) anteHandler := NewAnteHandler(app.AppKeepers, ante.AnteHandlerOptions{ HandlerOptions: authante.HandlerOptions{ - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - FeegrantKeeper: app.FeeGrantKeeper, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + FeegrantKeeper: app.FeeGrantKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), SigGasConsumer: authante.DefaultSigVerificationGasConsumer, ExtensionOptionChecker: func(*codectypes.Any) bool { return true }, @@ -228,6 +229,8 @@ func NewNibiruApp( WasmConfig: &wasmConfig, DevGasKeeper: &app.DevGasKeeper, DevGasBankKeeper: app.BankKeeper, + StakingKeeper: *app.StakingKeeper, + OracleKeeper: app.OracleKeeper, // TODO: feat(evm): enable app/server/config flag for Evm MaxTxGasWanted. MaxTxGasWanted: DefaultMaxTxGasWanted, EvmKeeper: app.EvmKeeper, diff --git a/x/common/testutil/testapp/testapp.go b/x/common/testutil/testapp/testapp.go index d8507b099..d7ad33559 100644 --- a/x/common/testutil/testapp/testapp.go +++ b/x/common/testutil/testapp/testapp.go @@ -10,6 +10,7 @@ import ( "github.com/cometbft/cometbft/libs/log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" auth "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -156,6 +157,43 @@ func NewNibiruTestApp(gen app.GenesisState, baseAppOptions ...func(*baseapp.Base return app } +// AddTestAddrsIncremental generates `numAddrs` new addresses in a simple +// incremental manner, adds them as BaseAccounts in the AccountKeeper, and +// optionally funds them with `coins`. It returns their addresses as +// `[]sdk.ValAddress`, which you can treat as potential validators if needed. +func AddTestAddrsIncremental( + nibiruApp *app.NibiruApp, + ctx sdk.Context, + numAddrs int, + coins sdk.Coins, +) []sdk.ValAddress { + var valAddrs []sdk.ValAddress + + for i := 0; i < numAddrs; i++ { + // 1. Generate a new private key + pk := secp256k1.GenPrivKey() + + // 2. Derive an account address from the pubkey + accAddr := sdk.AccAddress(pk.PubKey().Address()) + + // 3. Create a new base account object + acc := nibiruApp.AccountKeeper.NewAccountWithAddress(ctx, accAddr) + nibiruApp.AccountKeeper.SetAccount(ctx, acc) + + // 4. If the function call passed a non-empty `coins`, fund the new account + if !coins.IsZero() { + if err := FundAccount(nibiruApp.BankKeeper, ctx, accAddr, coins); err != nil { + panic(err) + } + } + + // 5. Convert to ValAddress for convenience + valAddrs = append(valAddrs, sdk.ValAddress(accAddr)) + } + + return valAddrs +} + // FundAccount is a utility function that funds an account by minting and // sending the coins to the address. This should be used for testing purposes // only! diff --git a/x/oracle/ante/fee_discount.go b/x/oracle/ante/fee_discount.go new file mode 100644 index 000000000..3b0ff44b3 --- /dev/null +++ b/x/oracle/ante/fee_discount.go @@ -0,0 +1,101 @@ +package ante + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + + "github.com/NibiruChain/nibiru/v2/x/oracle/types" +) + +// VoteFeeDiscountDecorator checks if the Tx signer is a validator and +// has or hasn't voted in the current voting period. If it's their first time, +// we apply a discount on fees or gas. Otherwise, normal cost applies. +// +// In real code, you'll likely store more config, such as a reference to the +// staking keeper to fetch validator info, or track the current epoch. +type VoteFeeDiscountDecorator struct { + oracleKeeper OracleKeeperI + stakingKeeper StakingKeeperI +} + +// NewVoteFeeDiscountDecorator is the constructor. +func NewVoteFeeDiscountDecorator( + oracleKeeper OracleKeeperI, + stakingKeeper StakingKeeperI, +) VoteFeeDiscountDecorator { + return VoteFeeDiscountDecorator{ + oracleKeeper: oracleKeeper, + stakingKeeper: stakingKeeper, + } +} + +// AnteHandle implements sdk.AnteDecorator. The discount logic is +// purely demonstrative; adapt to your own logic. +func (vfd VoteFeeDiscountDecorator) AnteHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + // 1. We check if there's exactly one signer (typical for Oracle votes). + // If your chain supports multiple signers, adapt accordingly. + sigTx, ok := tx.(authsigning.SigVerifiableTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrTxDecode, "invalid tx type %T", tx) + } + + sigs, err := sigTx.GetSignaturesV2() + if err != nil { + return next(ctx, tx, simulate) + } + if len(sigs) != 1 { + // passthrough if not exactly one signer + return next(ctx, tx, simulate) + } + + // ensure all messages are prevoting or voting messages + for _, msg := range tx.GetMsgs() { + if _, ok := msg.(*types.MsgAggregateExchangeRatePrevote); !ok { + if _, ok := msg.(*types.MsgAggregateExchangeRateVote); !ok { + return next(ctx, tx, simulate) + } + } + } + + // 2. Check if the signer is a validator + valAddr := sdk.ValAddress(sigTx.GetSigners()[0]) + validator, found := vfd.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + return next(ctx, tx, simulate) + } + + if validator.Jailed { + return next(ctx, tx, simulate) + } + + // needs to have at least 0.5% of the supply bonded + totalBonded := vfd.stakingKeeper.TotalBondedTokens(ctx) + currentlyBonded := validator.Tokens + + if currentlyBonded.LT(totalBonded.Mul(math.NewInt(50)).Quo(math.NewInt(10000))) { + return next(ctx, tx, simulate) + } + + // 3. If validator, we check whether they've posted a vote this period + hasVoted := vfd.oracleKeeper.HasVotedInCurrentPeriod(ctx, valAddr) + if !hasVoted { + // 4. If first time, let's say we reduce gas cost. + minGasPrices := ctx.MinGasPrices() + var discounted []sdk.DecCoin + for _, mgp := range minGasPrices { + discounted = append(discounted, sdk.NewDecCoinFromDec(mgp.Denom, mgp.Amount.QuoInt64(69_420))) + } + // We'll create a new context with the updated MinGasPrices + ctx = ctx.WithMinGasPrices(discounted) + } + + // 5. Keep going in the AnteHandler chain + return next(ctx, tx, simulate) +} diff --git a/x/oracle/ante/fee_discount_test.go b/x/oracle/ante/fee_discount_test.go new file mode 100644 index 000000000..8d4456c5e --- /dev/null +++ b/x/oracle/ante/fee_discount_test.go @@ -0,0 +1,342 @@ +package ante_test + +import ( + "testing" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/suite" + + testapp "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" + + "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/x/oracle/ante" + oracletypes "github.com/NibiruChain/nibiru/v2/x/oracle/types" +) + +// MockStakingKeeperI implements ante.StakingKeeperI +type MockStakingKeeperI struct { + validators map[string]stakingtypes.Validator + totalBonded sdk.Int +} + +func (m MockStakingKeeperI) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (stakingtypes.Validator, bool) { + v, ok := m.validators[addr.String()] + return v, ok +} +func (m MockStakingKeeperI) TotalBondedTokens(ctx sdk.Context) sdk.Int { + return m.totalBonded +} + +// MockOracleKeeperI implements ante.OracleKeeperI +type MockOracleKeeperI struct { + votedMap map[string]bool +} + +func (m MockOracleKeeperI) HasVotedInCurrentPeriod(ctx sdk.Context, valAddr sdk.ValAddress) bool { + return m.votedMap[valAddr.String()] +} + +// Test suite +type VoteFeeDiscountTestSuite struct { + suite.Suite + + ctx sdk.Context + app *app.NibiruApp + dec ante.VoteFeeDiscountDecorator + oracle *MockOracleKeeperI + stake *MockStakingKeeperI +} + +// buildTestTx creates a transaction recognized by the standard SDK Tx decoder. +// - msgs: the messages the TX will carry +// - signers: the addresses that will appear as signers in `GetSigners()` +// NOTE: We create placeholder signatures for each signer so the TX won't fail +// any 'signer-length' checks internally. +func (s *VoteFeeDiscountTestSuite) buildTestTx(msgs []sdk.Msg, signers []sdk.AccAddress) (sdk.Tx, error) { + // 1) Create a new TxBuilder from the app's TxConfig + txBuilder := s.app.GetTxConfig().NewTxBuilder() + + // 2) Set the messages + err := txBuilder.SetMsgs(msgs...) + if err != nil { + return nil, err + } + + // (Optional) Set any fees, memo, timeouts, etc. We can do zero fees here. + txBuilder.SetFeeAmount(sdk.NewCoins()) // no fees + txBuilder.SetGasLimit(0) // gas=0 just for tests + txBuilder.SetMemo("test-tx") + // etc. as needed + + // 3) We add placeholder signatures for each signer + // + // The chain expects len(signatures) == len(signers). Even if the + // signatures are empty or all zeros, we need them to match the number + // of signers so that the Tx's `GetSigners()` aligns with what + // the AnteHandler expects. + + sigs := make([]signing.SignatureV2, len(signers)) + for i := range signers { + // We'll just produce a random private key each time, or you could + // keep a single ephemeral key. The key isn't used in a real sig, + // but the presence of the signature tells the SDK "this is a valid signer." + privKey := ed25519.GenPrivKey() + + // Make an empty signature + sigData := signing.SingleSignatureData{ + SignMode: s.app.GetTxConfig().SignModeHandler().DefaultMode(), + Signature: nil, // no actual signature bytes + } + sigV2 := signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: &sigData, + Sequence: 0, + } + sigs[i] = sigV2 + } + + err = txBuilder.SetSignatures(sigs...) + if err != nil { + return nil, err + } + + return txBuilder.GetTx(), nil +} + +func TestVoteFeeDiscountTestSuite(t *testing.T) { + suite.Run(t, new(VoteFeeDiscountTestSuite)) +} + +func (s *VoteFeeDiscountTestSuite) SetupTest() { + // Use a fresh Nibiru app to get a fresh context or make a BasicContext + nibiruApp, ctx := testapp.NewNibiruTestAppAndContext() + s.app = nibiruApp + s.ctx = ctx + + s.stake = &MockStakingKeeperI{ + validators: map[string]types.Validator{}, + totalBonded: math.NewInt(100_000_000), + } + s.oracle = &MockOracleKeeperI{ + votedMap: map[string]bool{}, + } + + s.dec = ante.NewVoteFeeDiscountDecorator(s.oracle, s.stake) +} + +func dummyNext(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + return ctx, nil +} + +func (s *VoteFeeDiscountTestSuite) TestVoteFeeDiscount() { + valAddr := sdk.ValAddress([]byte("validator----------------")) + addressBytes, err := sdk.GetFromBech32(valAddr.String(), sdk.GetConfig().GetBech32ValidatorAddrPrefix()) + if err != nil { + panic(err) + } + // Convert the address bytes to an account address + accAddress := sdk.AccAddress(addressBytes) + + // By default, no validator is found + s.stake.validators[valAddr.String()] = types.Validator{ + OperatorAddress: valAddr.String(), + Status: types.Bonded, // non-jailed + Tokens: math.NewInt(1_000_000), // enough tokens + Jailed: false, + } + + s.Run("Happy path", func() { + // We'll track the minGasPrice in the context. The discount is 1/69,420 + // if triggered. Let's see if it changes or not. + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDecWithPrec(1, 0))} // "1stake" + newCtx := s.ctx.WithMinGasPrices(mgp) + // two signers + msgs := []sdk.Msg{&oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }} + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + // Should pass directly to next. No discount. + s.Require().NotEqual(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should be updated") + }) + + s.Run("Invalid Tx Type => returns error", func() { + badTx := sdk.Tx(nil) // not SigVerifiableTx + ctx, err := s.dec.AnteHandle(s.ctx, badTx, false, dummyNext) + s.Require().Error(err) + s.Require().Contains(err.Error(), sdkerrors.ErrTxDecode.Error()) + s.Require().Equal(s.ctx, ctx) // original context + }) + + s.Run("Multiple signers => pass to next with no discount", func() { + // We'll track the minGasPrice in the context. The discount is 1/69,420 + // if triggered. Let's see if it changes or not. + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDecWithPrec(1, 0))} // "1stake" + newCtx := s.ctx.WithMinGasPrices(mgp) + // two signers + msgs := []sdk.Msg{&oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }} + signers := []sdk.AccAddress{sdk.AccAddress("signer1"), sdk.AccAddress("signer2")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + // Should pass directly to next. No discount. + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + }) + + s.Run("Msg is not a prevote or vote => pass to next (no discount)", func() { + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDec(3))} // 3stake + newCtx := s.ctx.WithMinGasPrices(mgp) + + msgs := []sdk.Msg{&oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }, &banktypes.MsgSend{FromAddress: "from", ToAddress: "to", Amount: sdk.NewCoins(sdk.NewInt64Coin("stake", 1))}} + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + }) + + // ---------------------- + s.Run("Validator not found => pass to next, no discount", func() { + // Remove from stake validators so the address won't be found + delete(s.stake.validators, valAddr.String()) + + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDec(5))} + newCtx := s.ctx.WithMinGasPrices(mgp) + + msgs := []sdk.Msg{ + &oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }, + } + + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + + // Put it back + s.stake.validators[valAddr.String()] = types.Validator{ + OperatorAddress: valAddr.String(), + Status: types.Bonded, + Tokens: math.NewInt(1_000_000), + Jailed: false, + } + }) + + s.Run("Validator is jailed => no discount", func() { + // Mark jailed + v := s.stake.validators[valAddr.String()] + v.Jailed = true + s.stake.validators[valAddr.String()] = v + + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDec(2))} + newCtx := s.ctx.WithMinGasPrices(mgp) + + msgs := []sdk.Msg{ + &oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }, + } + + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + + // unjail for next tests + v.Jailed = false + s.stake.validators[valAddr.String()] = v + }) + + s.Run("Validator does not meet 0.5% threshold => no discount", func() { + // Suppose totalBonded = 1_000_000_000, + // We need at least 0.5% = 5_000_000 to qualify. + // This validator has 1_000_000 => fails the threshold + val := s.stake.validators[valAddr.String()] + val.Tokens = math.NewInt(1_000) + s.stake.validators[valAddr.String()] = val + + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDec(2))} + newCtx := s.ctx.WithMinGasPrices(mgp) + + msgs := []sdk.Msg{ + &oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }, + } + + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + + // bump tokens so we can test discount in next subtest + val.Tokens = math.NewInt(5_000_000) + s.stake.validators[valAddr.String()] = val + }) + + s.Run("Validator meets threshold but has voted => no discount", func() { + // Mark that they've voted + s.oracle.votedMap[valAddr.String()] = true + + mgp := []sdk.DecCoin{sdk.NewDecCoinFromDec("stake", sdk.NewDecWithPrec(100, 1))} // 10 stake + newCtx := s.ctx.WithMinGasPrices(mgp) + + msgs := []sdk.Msg{ + &oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: "hash", + Feeder: accAddress.String(), + Validator: valAddr.String(), + }, + } + + signers := []sdk.AccAddress{sdk.AccAddress("signer1")} + tx, err := s.buildTestTx(msgs, signers) + s.Require().NoError(err) + + ctx2, err := s.dec.AnteHandle(newCtx, tx, false, dummyNext) + s.Require().NoError(err) + // should remain the same + s.Require().Equal(sdk.DecCoins(mgp), ctx2.MinGasPrices(), "should not be updated") + }) +} diff --git a/x/oracle/ante/keeper_interface.go b/x/oracle/ante/keeper_interface.go new file mode 100644 index 000000000..3942549fb --- /dev/null +++ b/x/oracle/ante/keeper_interface.go @@ -0,0 +1,17 @@ +package ante + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// We only need these two methods from StakingKeeper in the AnteDecorator +type StakingKeeperI interface { + GetValidator(ctx sdk.Context, addr sdk.ValAddress) (stakingtypes.Validator, bool) + TotalBondedTokens(ctx sdk.Context) sdk.Int +} + +// We only need this method from the oracle keeper in the AnteDecorator. +type OracleKeeperI interface { + HasVotedInCurrentPeriod(ctx sdk.Context, valAddr sdk.ValAddress) bool +} diff --git a/x/oracle/keeper/ballot.go b/x/oracle/keeper/ballot.go index 134523136..2d2303e2a 100644 --- a/x/oracle/keeper/ballot.go +++ b/x/oracle/keeper/ballot.go @@ -101,6 +101,11 @@ func isPassingVoteThreshold( return true } +func (k Keeper) HasVotedInCurrentPeriod(ctx sdk.Context, valAddr sdk.ValAddress) bool { + _, err := k.Votes.Get(ctx, valAddr) + return err == nil +} + // 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.