From 985c1c8712f41c2ca8a54df5571215c0d0dda470 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Mon, 6 Nov 2023 18:27:43 -0700 Subject: [PATCH] feat: add a BeforeSend hook to the bank module (#278) --- go.mod | 2 +- x/bank/keeper/hooks.go | 19 +++ x/bank/keeper/hooks_test.go | 165 ++++++++++++++++++++++ x/bank/keeper/internal_test.go | 11 ++ x/bank/keeper/keeper.go | 43 +++++- x/bank/keeper/send.go | 83 ++++++++++- x/bank/testutil/expected_keepers_mocks.go | 37 +++++ x/bank/types/expected_keepers.go | 11 ++ x/bank/types/hooks.go | 26 ++++ x/gov/testutil/expected_keepers_mocks.go | 28 ++++ 10 files changed, 417 insertions(+), 8 deletions(-) create mode 100644 x/bank/keeper/hooks.go create mode 100644 x/bank/keeper/hooks_test.go create mode 100644 x/bank/keeper/internal_test.go create mode 100644 x/bank/types/hooks.go diff --git a/go.mod b/go.mod index 634be8afc6e1..0662b4cc6a55 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e google.golang.org/grpc v1.58.1 google.golang.org/protobuf v1.31.0 + gopkg.in/yaml.v2 v2.4.0 gotest.tools/v3 v3.5.0 pgregory.net/rapid v1.1.0 sigs.k8s.io/yaml v1.3.0 @@ -157,7 +158,6 @@ require ( google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.6 // indirect ) diff --git a/x/bank/keeper/hooks.go b/x/bank/keeper/hooks.go new file mode 100644 index 000000000000..c0006c94da93 --- /dev/null +++ b/x/bank/keeper/hooks.go @@ -0,0 +1,19 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// Implements StakingHooks interface +var _ types.BankHooks = BaseSendKeeper{} + +// BeforeSend executes the BeforeSend hook if registered. +func (k BaseSendKeeper) BeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + if k.hooks != nil { + return k.hooks.BeforeSend(ctx, from, to, amount) + } + return nil +} diff --git a/x/bank/keeper/hooks_test.go b/x/bank/keeper/hooks_test.go new file mode 100644 index 000000000000..25a17c5d30e8 --- /dev/null +++ b/x/bank/keeper/hooks_test.go @@ -0,0 +1,165 @@ +package keeper_test + +import ( + "context" + "fmt" + "testing" + + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/bank/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" + "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +var _ types.BankHooks = &MockBankHooksReceiver{} + +var ( + priv1 = secp256k1.GenPrivKey() + addr1 = sdk.AccAddress(priv1.PubKey().Address()) +) + +type testingSuite struct { + BankKeeper bankkeeper.Keeper + AccountKeeper types.AccountKeeper + StakingKeeper stakingkeeper.Keeper + App *runtime.App +} + +func createTestSuite(t *testing.T, genesisAccounts []authtypes.GenesisAccount) testingSuite { + res := testingSuite{} + + var genAccounts []simtestutil.GenesisAccount + for _, acc := range genesisAccounts { + genAccounts = append(genAccounts, simtestutil.GenesisAccount{GenesisAccount: acc}) + } + + startupCfg := simtestutil.DefaultStartUpConfig() + startupCfg.GenesisAccounts = genAccounts + + app, err := simtestutil.SetupWithConfiguration( + depinject.Configs( + configurator.NewAppConfig( + configurator.ParamsModule(), + configurator.AuthModule(), + configurator.StakingModule(), + configurator.TxModule(), + configurator.ConsensusModule(), + configurator.BankModule(), + configurator.GovModule(), + configurator.DistributionModule(), + configurator.AuthModule(), + ), + depinject.Supply(log.NewNopLogger()), + ), + startupCfg, &res.BankKeeper, &res.AccountKeeper, &res.StakingKeeper) + + res.App = app + + require.NoError(t, err) + return res +} + +// BankHooks event hooks for bank (noalias) +type MockBankHooksReceiver struct{} + +// Mock BeforeSend bank hook that doesn't allow the sending of exactly 100 coins of any denom. +func (h *MockBankHooksReceiver) BeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + for _, coin := range amount { + if coin.Amount.Equal(math.NewInt(100)) { + return fmt.Errorf("not allowed; expected %v, got: %v", 100, coin.Amount) + } + } + + return nil +} + +func TestHooks(t *testing.T) { + acc := &authtypes.BaseAccount{ + Address: addr1.String(), + } + + genAccs := []authtypes.GenesisAccount{acc} + app := createTestSuite(t, genAccs) + baseApp := app.App.BaseApp + ctx := baseApp.NewContext(false) + bondDenom, err := app.StakingKeeper.BondDenom(ctx) + require.NoError(t, err) + + addrs := simtestutil.AddTestAddrs(app.BankKeeper, app.StakingKeeper, ctx, 2, math.NewInt(1000)) + banktestutil.FundModuleAccount(ctx, app.BankKeeper, stakingtypes.BondedPoolName, sdk.NewCoins(sdk.NewCoin(bondDenom, math.NewInt(1000)))) + + // create a valid send amount which is 1 coin, and an invalidSendAmount which is 100 coins + validSendAmount := sdk.NewCoins(sdk.NewCoin(bondDenom, math.NewInt(1))) + invalidSendAmount := sdk.NewCoins(sdk.NewCoin(bondDenom, math.NewInt(100))) + + // setup our mock bank hooks receiver that prevents the send of 100 coins + bankHooksReceiver := MockBankHooksReceiver{} + baseBankKeeper, ok := app.BankKeeper.(keeper.BaseKeeper) + require.True(t, ok) + keeper.UnsafeSetHooks( + &baseBankKeeper, types.NewMultiBankHooks(&bankHooksReceiver), + ) + app.BankKeeper = baseBankKeeper + + // try sending a validSendAmount and it should work + err = app.BankKeeper.SendCoins(ctx, addrs[0], addrs[1], validSendAmount) + require.NoError(t, err) + + // try sending an invalidSendAmount and it should not work + err = app.BankKeeper.SendCoins(ctx, addrs[0], addrs[1], invalidSendAmount) + require.Error(t, err) + + // try doing SendManyCoins and make sure if even a single subsend is invalid, the entire function fails + err = app.BankKeeper.SendManyCoins(ctx, addrs[0], []sdk.AccAddress{addrs[0], addrs[1]}, []sdk.Coins{invalidSendAmount, validSendAmount}) + require.Error(t, err) + + // make sure that account to module doesn't bypass hook + err = app.BankKeeper.SendCoinsFromAccountToModule(ctx, addrs[0], stakingtypes.BondedPoolName, validSendAmount) + require.NoError(t, err) + err = app.BankKeeper.SendCoinsFromAccountToModule(ctx, addrs[0], stakingtypes.BondedPoolName, invalidSendAmount) + require.Error(t, err) + + // make sure that module to account doesn't bypass hook + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addrs[0], validSendAmount) + require.NoError(t, err) + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, stakingtypes.BondedPoolName, addrs[0], invalidSendAmount) + require.Error(t, err) + + // make sure that module to module doesn't bypass hook + err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, validSendAmount) + require.NoError(t, err) + err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, invalidSendAmount) + require.Error(t, err) + + // make sure that module to many accounts doesn't bypass hook + err = app.BankKeeper.SendCoinsFromModuleToManyAccounts(ctx, stakingtypes.BondedPoolName, []sdk.AccAddress{addrs[0], addrs[1]}, []sdk.Coins{validSendAmount, validSendAmount}) + require.NoError(t, err) + err = app.BankKeeper.SendCoinsFromModuleToManyAccounts(ctx, stakingtypes.BondedPoolName, []sdk.AccAddress{addrs[0], addrs[1]}, []sdk.Coins{validSendAmount, invalidSendAmount}) + require.Error(t, err) + + // make sure that DelegateCoins doesn't bypass the hook + err = app.BankKeeper.DelegateCoins(ctx, addrs[0], app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), validSendAmount) + require.NoError(t, err) + err = app.BankKeeper.DelegateCoins(ctx, addrs[0], app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), invalidSendAmount) + require.Error(t, err) + + // make sure that UndelegateCoins doesn't bypass the hook + err = app.BankKeeper.UndelegateCoins(ctx, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addrs[0], validSendAmount) + require.NoError(t, err) + err = app.BankKeeper.UndelegateCoins(ctx, app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName), addrs[0], invalidSendAmount) + require.Error(t, err) +} diff --git a/x/bank/keeper/internal_test.go b/x/bank/keeper/internal_test.go new file mode 100644 index 000000000000..3408248f1a98 --- /dev/null +++ b/x/bank/keeper/internal_test.go @@ -0,0 +1,11 @@ +package keeper + +import "github.com/cosmos/cosmos-sdk/x/bank/types" + +// UnsafeSetHooks updates the x/bank keeper's hooks, overriding any potential +// pre-existing hooks. +// +// WARNING: this function should only be used in tests. +func UnsafeSetHooks(k *BaseKeeper, h types.BankHooks) { + k.hooks = h +} diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index 057332d9b5c5..45d6dec04c79 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -45,6 +45,9 @@ type Keeper interface { IterateAllDenomMetaData(ctx context.Context, cb func(types.Metadata) bool) SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromModuleToManyAccounts( + ctx context.Context, senderModule string, recipientAddrs []sdk.AccAddress, amts []sdk.Coins, + ) error SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error @@ -143,6 +146,12 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } + // call the BeforeSend hooks + err := k.BeforeSend(ctx, delegatorAddr, moduleAccAddr, amt) + if err != nil { + return err + } + balances := sdk.NewCoins() for _, coin := range amt { @@ -169,7 +178,7 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA types.NewCoinSpentEvent(delegatorAddr, amt), ) - err := k.addCoins(ctx, moduleAccAddr, amt) + err = k.addCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -192,7 +201,13 @@ func (k BaseKeeper) UndelegateCoins(ctx context.Context, moduleAccAddr, delegato return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } - err := k.subUnlockedCoins(ctx, moduleAccAddr, amt) + // call the BeforeSend hooks + err := k.BeforeSend(ctx, moduleAccAddr, delegatorAddr, amt) + if err != nil { + return err + } + + err = k.subUnlockedCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -283,6 +298,30 @@ func (k BaseKeeper) SendCoinsFromModuleToAccount( return k.SendCoins(ctx, senderAddr, recipientAddr, amt) } +// SendCoinsFromModuleToManyAccounts transfers coins from a ModuleAccount to multiple AccAddresses. +// It will panic if the module account does not exist. An error is returned if +// the recipient address is black-listed or if sending the tokens fails. +func (k BaseKeeper) SendCoinsFromModuleToManyAccounts( + ctx context.Context, senderModule string, recipientAddrs []sdk.AccAddress, amts []sdk.Coins, +) error { + if len(recipientAddrs) != len(amts) { + panic(fmt.Errorf("addresses and amounts numbers does not match")) + } + + senderAddr := k.ak.GetModuleAddress(senderModule) + if senderAddr == nil { + panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", senderModule)) + } + + for _, recipientAddr := range recipientAddrs { + if k.BlockedAddr(recipientAddr) { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr) + } + } + + return k.SendManyCoins(ctx, senderAddr, recipientAddrs, amts) +} + // SendCoinsFromModuleToModule transfers coins from a ModuleAccount to another. // It will panic if either module account does not exist. func (k BaseKeeper) SendCoinsFromModuleToModule( diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index 3bcc6d315c33..7e17b057c999 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -28,6 +28,7 @@ type SendKeeper interface { InputOutputCoins(ctx context.Context, input types.Input, outputs []types.Output) error SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error + SendManyCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddrs []sdk.AccAddress, amts []sdk.Coins) error GetParams(ctx context.Context) types.Params SetParams(ctx context.Context, params types.Params) error @@ -60,6 +61,7 @@ type BaseSendKeeper struct { ak types.AccountKeeper storeService store.KVStoreService logger log.Logger + hooks types.BankHooks // list of addresses that are restricted from receiving transactions blockedAddrs map[string]bool @@ -115,6 +117,17 @@ func (k BaseSendKeeper) GetAuthority() string { return k.authority } +// Set the bank hooks +func (k *BaseSendKeeper) SetHooks(bh types.BankHooks) *BaseSendKeeper { + if k.hooks != nil { + panic("cannot set bank hooks twice") + } + + k.hooks = bh + + return k +} + // GetParams returns the total set of bank parameters. func (k BaseSendKeeper) GetParams(ctx context.Context) (params types.Params) { p, _ := k.Params.Get(ctx) @@ -205,18 +218,21 @@ func (k BaseSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, // SendCoins transfers amt coins from a sending account to a receiving account. // An error is returned upon failure. -func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { - var err error - err = k.subUnlockedCoins(ctx, fromAddr, amt) +func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error { + // call the BeforeSend hooks + sdkCtx := sdk.UnwrapSDKContext(ctx) + err := k.BeforeSend(sdkCtx, fromAddr, toAddr, amt) if err != nil { return err } - toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + err = k.subUnlockedCoins(ctx, fromAddr, amt) if err != nil { return err } + toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + err = k.addCoins(ctx, toAddr, amt) if err != nil { return err @@ -234,7 +250,7 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA // bech32 encoding is expensive! Only do it once for fromAddr fromAddrString := fromAddr.String() - sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdk.UnwrapSDKContext(ctx) sdkCtx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeTransfer, @@ -251,6 +267,63 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA return nil } +// SendManyCoins transfer multiple amt coins from a sending account to multiple receiving accounts. +// An error is returned upon failure. +func (k BaseSendKeeper) SendManyCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddrs []sdk.AccAddress, amts []sdk.Coins) error { + if len(toAddrs) != len(amts) { + return fmt.Errorf("addresses and amounts numbers does not match") + } + + totalAmt := sdk.Coins{} + for i, amt := range amts { + sdkCtx := sdk.UnwrapSDKContext(ctx) + // make sure to trigger the BeforeSend hooks for all the sends that are about to occur + err := k.BeforeSend(sdkCtx, fromAddr, toAddrs[i], amts[i]) + if err != nil { + return err + } + totalAmt = sdk.Coins.Add(totalAmt, amt...) + } + + err := k.subUnlockedCoins(ctx, fromAddr, totalAmt) + if err != nil { + return err + } + + fromAddrString := fromAddr.String() + for i, toAddr := range toAddrs { + amt := amts[i] + + err := k.addCoins(ctx, toAddr, amt) + if err != nil { + return err + } + + acc := k.ak.GetAccount(ctx, toAddr) + if acc == nil { + defer telemetry.IncrCounter(1, "new", "account") + k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()), + sdk.NewAttribute(types.AttributeKeySender, fromAddrString), + sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()), + ), + }) + + } + + return nil +} + // subUnlockedCoins removes the unlocked amt coins of the given account. An error is // returned if the resulting balance is negative or the initial amount is invalid. // A coin_spent event is emitted after. diff --git a/x/bank/testutil/expected_keepers_mocks.go b/x/bank/testutil/expected_keepers_mocks.go index fcdfd8a472e6..199a07a3f98b 100644 --- a/x/bank/testutil/expected_keepers_mocks.go +++ b/x/bank/testutil/expected_keepers_mocks.go @@ -242,3 +242,40 @@ func (mr *MockAccountKeeperMockRecorder) ValidatePermissions(macc interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePermissions", reflect.TypeOf((*MockAccountKeeper)(nil).ValidatePermissions), macc) } + +// MockBankHooks is a mock of BankHooks interface. +type MockBankHooks struct { + ctrl *gomock.Controller + recorder *MockBankHooksMockRecorder +} + +// MockBankHooksMockRecorder is the mock recorder for MockBankHooks. +type MockBankHooksMockRecorder struct { + mock *MockBankHooks +} + +// NewMockBankHooks creates a new mock instance. +func NewMockBankHooks(ctrl *gomock.Controller) *MockBankHooks { + mock := &MockBankHooks{ctrl: ctrl} + mock.recorder = &MockBankHooksMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBankHooks) EXPECT() *MockBankHooksMockRecorder { + return m.recorder +} + +// BeforeSend mocks base method. +func (m *MockBankHooks) BeforeSend(ctx context.Context, from, to types.AccAddress, amount types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeforeSend", ctx, from, to, amount) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeforeSend indicates an expected call of BeforeSend. +func (mr *MockBankHooksMockRecorder) BeforeSend(ctx, from, to, amount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeforeSend", reflect.TypeOf((*MockBankHooks)(nil).BeforeSend), ctx, from, to, amount) +} diff --git a/x/bank/types/expected_keepers.go b/x/bank/types/expected_keepers.go index ebd5eb9a657e..099b5edb704f 100644 --- a/x/bank/types/expected_keepers.go +++ b/x/bank/types/expected_keepers.go @@ -33,3 +33,14 @@ type AccountKeeper interface { SetModuleAccount(ctx context.Context, macc sdk.ModuleAccountI) GetModulePermissions() map[string]types.PermissionsForAddress } + +// Event Hooks +// These can be utilized to communicate between a bank keeper and another +// keeper which must take particular actions when sends happen. +// The second keeper must implement this interface, which then the +// bank keeper can call. + +// BankHooks event hooks for bank sends +type BankHooks interface { + BeforeSend(ctx context.Context, from sdk.AccAddress, to sdk.AccAddress, amount sdk.Coins) error // Must be before any send is executed +} diff --git a/x/bank/types/hooks.go b/x/bank/types/hooks.go new file mode 100644 index 000000000000..05a269b00fbc --- /dev/null +++ b/x/bank/types/hooks.go @@ -0,0 +1,26 @@ +package types + +import ( + context "context" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MultiBankHooks combine multiple bank hooks, all hook functions are run in array sequence +type MultiBankHooks []BankHooks + +// NewMultiBankHooks takes a list of BankHooks and returns a MultiBankHooks +func NewMultiBankHooks(hooks ...BankHooks) MultiBankHooks { + return hooks +} + +// BeforeSend runs the BeforeSend hooks in order for each BankHook in a MultiBankHooks struct +func (h MultiBankHooks) BeforeSend(ctx context.Context, from, to sdk.AccAddress, amount sdk.Coins) error { + for i := range h { + err := h[i].BeforeSend(ctx, from, to, amount) + if err != nil { + return err + } + } + return nil +} diff --git a/x/gov/testutil/expected_keepers_mocks.go b/x/gov/testutil/expected_keepers_mocks.go index eda926027ad7..4c5f783d9206 100644 --- a/x/gov/testutil/expected_keepers_mocks.go +++ b/x/gov/testutil/expected_keepers_mocks.go @@ -857,6 +857,20 @@ func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderMo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) } +// SendCoinsFromModuleToManyAccounts mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToManyAccounts(ctx context.Context, senderModule string, recipientAddrs []types.AccAddress, amts []types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToManyAccounts", ctx, senderModule, recipientAddrs, amts) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToManyAccounts indicates an expected call of SendCoinsFromModuleToManyAccounts. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToManyAccounts(ctx, senderModule, recipientAddrs, amts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToManyAccounts", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToManyAccounts), ctx, senderModule, recipientAddrs, amts) +} + // SendCoinsFromModuleToModule mocks base method. func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt types.Coins) error { m.ctrl.T.Helper() @@ -886,6 +900,20 @@ func (mr *MockBankKeeperMockRecorder) SendEnabled(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEnabled", reflect.TypeOf((*MockBankKeeper)(nil).SendEnabled), arg0, arg1) } +// SendManyCoins mocks base method. +func (m *MockBankKeeper) SendManyCoins(ctx context.Context, fromAddr types.AccAddress, toAddrs []types.AccAddress, amts []types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendManyCoins", ctx, fromAddr, toAddrs, amts) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendManyCoins indicates an expected call of SendManyCoins. +func (mr *MockBankKeeperMockRecorder) SendManyCoins(ctx, fromAddr, toAddrs, amts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendManyCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendManyCoins), ctx, fromAddr, toAddrs, amts) +} + // SetAllSendEnabled mocks base method. func (m *MockBankKeeper) SetAllSendEnabled(ctx context.Context, sendEnableds []*types0.SendEnabled) { m.ctrl.T.Helper()