diff --git a/ante/ante.go b/ante/ante.go new file mode 100644 index 00000000..6cd061c9 --- /dev/null +++ b/ante/ante.go @@ -0,0 +1,85 @@ +package ante + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + ibcante "github.com/cosmos/ibc-go/v7/modules/core/ante" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + + migaloofeeante "github.com/White-Whale-Defi-Platform/migaloo-chain/v3/x/globalfee/ante" +) + +// HandlerOptions extend the SDK's AnteHandler options by requiring the IBC +// channel keeper. +type HandlerOptions struct { + ante.HandlerOptions + Codec codec.BinaryCodec + GovKeeper *govkeeper.Keeper + IBCkeeper *ibckeeper.Keeper + ExtensionOptionChecker authante.ExtensionOptionChecker + BypassMinFeeMsgTypes []string + GlobalFeeSubspace paramtypes.Subspace + StakingSubspace paramtypes.Subspace + TxFeeChecker authante.TxFeeChecker +} + +func NewAnteHandler(opts HandlerOptions) (sdk.AnteHandler, error) { + if opts.AccountKeeper == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "account keeper is required for AnteHandler") + } + if opts.BankKeeper == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "bank keeper is required for AnteHandler") + } + if opts.SignModeHandler == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for AnteHandler") + } + if opts.IBCkeeper == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "IBC keeper is required for AnteHandler") + } + if opts.GlobalFeeSubspace.Name() == "" { + return nil, sdkerrors.Wrap(sdkerrors.ErrNotFound, "globalfee param store is required for AnteHandler") + } + if opts.StakingSubspace.Name() == "" { + return nil, sdkerrors.Wrap(sdkerrors.ErrNotFound, "staking param store is required for AnteHandler") + } + if opts.GovKeeper == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "gov keeper is required for AnteHandler") + } + + sigGasConsumer := opts.SigGasConsumer + if sigGasConsumer == nil { + sigGasConsumer = ante.DefaultSigVerificationGasConsumer + } + + // maxBypassMinFeeMsgGasUsage is the maximum gas usage per message + // so that a transaction that contains only message types that can + // bypass the minimum fee can be accepted with a zero fee. + // For details, see gaiafeeante.NewFeeDecorator() + var maxBypassMinFeeMsgGasUsage uint64 = 200_000 + + anteDecorators := []sdk.AnteDecorator{ + ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + ante.NewExtensionOptionsDecorator(opts.ExtensionOptionChecker), + ante.NewValidateBasicDecorator(), + ante.NewTxTimeoutHeightDecorator(), + ante.NewValidateMemoDecorator(opts.AccountKeeper), + ante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper), + NewGovPreventSpamDecorator(opts.Codec, opts.GovKeeper), + migaloofeeante.NewFeeDecorator(opts.BypassMinFeeMsgTypes, opts.GlobalFeeSubspace, opts.StakingSubspace, maxBypassMinFeeMsgGasUsage), + + ante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker), + ante.NewSetPubKeyDecorator(opts.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators + ante.NewValidateSigCountDecorator(opts.AccountKeeper), + ante.NewSigGasConsumeDecorator(opts.AccountKeeper, sigGasConsumer), + ante.NewSigVerificationDecorator(opts.AccountKeeper, opts.SignModeHandler), + ante.NewIncrementSequenceDecorator(opts.AccountKeeper), + ibcante.NewRedundantRelayDecorator(opts.IBCkeeper), + } + + return sdk.ChainAnteDecorators(anteDecorators...), nil +} diff --git a/ante/gov_ante.go b/ante/gov_ante.go new file mode 100644 index 00000000..d4bc11f6 --- /dev/null +++ b/ante/gov_ante.go @@ -0,0 +1,96 @@ +package ante + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// initial deposit must be greater than or equal to 10% of the minimum deposit +var minInitialDepositFraction = sdk.NewDecWithPrec(10, 2) + +type GovPreventSpamDecorator struct { + govKeeper *govkeeper.Keeper + cdc codec.BinaryCodec +} + +func NewGovPreventSpamDecorator(cdc codec.BinaryCodec, govKeeper *govkeeper.Keeper) GovPreventSpamDecorator { + return GovPreventSpamDecorator{ + govKeeper: govKeeper, + cdc: cdc, + } +} + +func (g GovPreventSpamDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, + simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + // run checks only on CheckTx or simulate + if !ctx.IsCheckTx() || simulate { + return next(ctx, tx, simulate) + } + + msgs := tx.GetMsgs() + if err = g.ValidateGovMsgs(ctx, msgs); err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +// validateGovMsgs checks if the InitialDeposit amounts are greater than the minimum initial deposit amount +func (g GovPreventSpamDecorator) ValidateGovMsgs(ctx sdk.Context, msgs []sdk.Msg) error { + validMsg := func(m sdk.Msg) error { + if msg, ok := m.(*govv1.MsgSubmitProposal); ok { + // prevent messages with insufficient initial deposit amount + depositParams := g.govKeeper.GetParams(ctx) + minInitialDeposit := g.calcMinInitialDeposit(depositParams.MinDeposit) + initialDeposit := sdk.NewCoins(msg.InitialDeposit...) + if initialDeposit.IsAllLT(minInitialDeposit) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "insufficient initial deposit amount - required: %v", minInitialDeposit) + } + } + + return nil + } + + validAuthz := func(execMsg *authz.MsgExec) error { + for _, v := range execMsg.Msgs { + var innerMsg sdk.Msg + if err := g.cdc.UnpackAny(v, &innerMsg); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "cannot unmarshal authz exec msgs") + } + if err := validMsg(innerMsg); err != nil { + return err + } + } + + return nil + } + + for _, m := range msgs { + if msg, ok := m.(*authz.MsgExec); ok { + if err := validAuthz(msg); err != nil { + return err + } + continue + } + + // validate normal msgs + if err := validMsg(m); err != nil { + return err + } + } + return nil +} + +func (g GovPreventSpamDecorator) calcMinInitialDeposit(minDeposit sdk.Coins) (minInitialDeposit sdk.Coins) { + for _, coin := range minDeposit { + minInitialCoins := minInitialDepositFraction.MulInt(coin.Amount).RoundInt() + minInitialDeposit = minInitialDeposit.Add(sdk.NewCoin(coin.Denom, minInitialCoins)) + } + return +} diff --git a/ante/gov_ante_test.go b/ante/gov_ante_test.go new file mode 100644 index 00000000..10e7440e --- /dev/null +++ b/ante/gov_ante_test.go @@ -0,0 +1,92 @@ +package ante_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/stretchr/testify/suite" + + "github.com/White-Whale-Defi-Platform/migaloo-chain/v3/ante" + tmrand "github.com/cometbft/cometbft/libs/rand" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + + migalooapp "github.com/White-Whale-Defi-Platform/migaloo-chain/v3/app" +) + +var ( + insufficientCoins = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 100)) + minCoins = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000)) + moreThanMinCoins = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 2500000)) + testAddr = sdk.AccAddress("test1") +) + +type GovAnteHandlerTestSuite struct { + suite.Suite + + app *migalooapp.MigalooApp + ctx sdk.Context + clientCtx client.Context +} + +func (s *GovAnteHandlerTestSuite) SetupTest() { + app := migalooapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{ + ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)), + Height: 1, + }) + + encodingConfig := migalooapp.MakeTestEncodingConfig() + encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) + testdata.RegisterInterfaces(encodingConfig.InterfaceRegistry) + + s.app = app + s.ctx = ctx + s.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) +} + +func TestGovSpamPreventionSuite(t *testing.T) { + suite.Run(t, new(GovAnteHandlerTestSuite)) +} + +func (s *GovAnteHandlerTestSuite) TestGlobalFeeMinimumGasFeeAnteHandler() { + // setup test + s.SetupTest() + tests := []struct { + title, description string + proposalType string + proposerAddr sdk.AccAddress + initialDeposit sdk.Coins + expectPass bool + }{ + {"Passing proposal 1", "the purpose of this proposal is to pass", govv1beta1.ProposalTypeText, testAddr, minCoins, true}, + {"Passing proposal 2", "the purpose of this proposal is to pass with more coins than minimum", govv1beta1.ProposalTypeText, testAddr, moreThanMinCoins, true}, + {"Failing proposal", "the purpose of this proposal is to fail", govv1beta1.ProposalTypeText, testAddr, insufficientCoins, false}, + } + + decorator := ante.NewGovPreventSpamDecorator(s.app.AppCodec(), &s.app.GovKeeper) + + for _, tc := range tests { + content, ok := govv1beta1.ContentFromProposalType(tc.title, tc.description, tc.proposalType) + s.Require().True(ok) + s.Require().NotNil(content) + + msg, err := govv1beta1.NewMsgSubmitProposal( + content, + tc.initialDeposit, + tc.proposerAddr, + ) + + s.Require().NoError(err) + + err = decorator.ValidateGovMsgs(s.ctx, []sdk.Msg{msg}) + if tc.expectPass { + s.Require().NoError(err, "expected %v to pass", tc.title) + } else { + s.Require().Error(err, "expected %v to fail", tc.title) + } + } +}