diff --git a/e2e/fee_test.go b/e2e/fee_test.go new file mode 100644 index 0000000..fcd1b44 --- /dev/null +++ b/e2e/fee_test.go @@ -0,0 +1,40 @@ +package e2e + +import ( + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + + "github.com/stretchr/testify/require" +) + +var COLLECTOR = "noble17xpfvakm2amg962yls6f84z3kell8c5lc6kgnn" + +// TestBeginBlocker tests the module's begin blocker logic. +func TestBeginBlocker(t *testing.T) { + t.Parallel() + + var wrapper Wrapper + ctx, _, _ := Suite(t, &wrapper, false) + validator := wrapper.chain.Validators[0] + + oldBalance, err := wrapper.chain.BankQueryAllBalances(ctx, wrapper.owner.FormattedAddress()) + require.NoError(t, err) + + err = validator.BankSend(ctx, wrapper.owner.KeyName(), ibc.WalletAmount{ + Address: wrapper.pendingOwner.FormattedAddress(), + Denom: "uusdc", + Amount: math.NewInt(1_000_000), + }) + require.NoError(t, err) + + balance, err := wrapper.chain.BankQueryAllBalances(ctx, COLLECTOR) + require.NoError(t, err) + require.True(t, balance.IsZero()) + + newBalance, err := wrapper.chain.BankQueryAllBalances(ctx, wrapper.owner.FormattedAddress()) + require.NoError(t, err) + require.Equal(t, oldBalance.Sub(sdk.NewInt64Coin("uusdc", 1_000_000)), newBalance) +} diff --git a/simapp/app.yaml b/simapp/app.yaml index 0b93421..77c2717 100644 --- a/simapp/app.yaml +++ b/simapp/app.yaml @@ -4,7 +4,7 @@ modules: "@type": cosmos.app.runtime.v1alpha1.Module app_name: SimApp pre_blockers: [ upgrade ] - begin_blockers: [ capability, staking, ibc ] + begin_blockers: [ authority, capability, staking, ibc ] end_blockers: [ staking ] init_genesis: [ capability, auth, bank, staking, ibc, genutil, transfer, upgrade, authority ] override_store_keys: diff --git a/utils/mocks/account.go b/utils/mocks/account.go index 2e83d0d..2df64f7 100644 --- a/utils/mocks/account.go +++ b/utils/mocks/account.go @@ -2,7 +2,9 @@ package mocks import ( "cosmossdk.io/core/address" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/codec" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/noble-assets/authority/x/authority/types" ) @@ -13,3 +15,7 @@ type AccountKeeper struct{} func (AccountKeeper) AddressCodec() address.Codec { return codec.NewBech32Codec("noble") } + +func (k AccountKeeper) GetModuleAddress(moduleName string) sdk.AccAddress { + return authtypes.NewModuleAddress(moduleName) +} diff --git a/utils/mocks/authority.go b/utils/mocks/authority.go index 977f4cc..375e64f 100644 --- a/utils/mocks/authority.go +++ b/utils/mocks/authority.go @@ -22,6 +22,10 @@ import ( ) func AuthorityKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { + return AuthorityKeeperWithBank(t, BankKeeper{}) +} + +func AuthorityKeeperWithBank(t testing.TB, bank types.BankKeeper) (*keeper.Keeper, sdk.Context) { logger := log.NewNopLogger() key := storetypes.NewKVStoreKey(types.ModuleName) @@ -40,6 +44,7 @@ func AuthorityKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { runtime.ProvideEventService(), router, AccountKeeper{}, + bank, ), wrapper.Ctx } diff --git a/utils/mocks/bank.go b/utils/mocks/bank.go new file mode 100644 index 0000000..2de1128 --- /dev/null +++ b/utils/mocks/bank.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/codec" + "github.com/noble-assets/authority/x/authority/types" +) + +var cdc = codec.NewBech32Codec("noble") + +var _ types.BankKeeper = BankKeeper{} + +type BankKeeper struct { + Balances map[string]sdk.Coins +} + +func (k BankKeeper) GetAllBalances(_ context.Context, bz sdk.AccAddress) sdk.Coins { + address, _ := cdc.BytesToString(bz) + return k.Balances[address] +} + +func (k BankKeeper) SendCoins(_ context.Context, _, _ sdk.AccAddress, _ sdk.Coins) error { + return nil +} diff --git a/x/authority/keeper/abci.go b/x/authority/keeper/abci.go new file mode 100644 index 0000000..ddc3e60 --- /dev/null +++ b/x/authority/keeper/abci.go @@ -0,0 +1,28 @@ +package keeper + +import ( + "context" + + "cosmossdk.io/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// BeginBlock sends all fees collected in the previous block to the module's owner. +func (k *Keeper) BeginBlock(ctx context.Context) error { + collector := k.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName) + balance := k.bankKeeper.GetAllBalances(ctx, collector) + if balance.IsZero() { + return nil + } + + owner, err := k.Owner.Get(ctx) + if err != nil { + return errors.Wrap(err, "failed to get owner from state") + } + address, err := k.accountKeeper.AddressCodec().StringToBytes(owner) + if err != nil { + return errors.Wrap(err, "failed to decode owner address") + } + + return k.bankKeeper.SendCoins(ctx, collector, address, balance) +} diff --git a/x/authority/keeper/abci_test.go b/x/authority/keeper/abci_test.go new file mode 100644 index 0000000..88d5d80 --- /dev/null +++ b/x/authority/keeper/abci_test.go @@ -0,0 +1,49 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/noble-assets/authority/utils" + "github.com/noble-assets/authority/utils/mocks" + "github.com/stretchr/testify/require" +) + +func TestBeginBlock(t *testing.T) { + bank := mocks.BankKeeper{ + Balances: make(map[string]sdk.Coins), + } + keeper, ctx := mocks.AuthorityKeeperWithBank(t, bank) + + // ACT: Attempt to run begin blocker with empty fee collector. + err := keeper.BeginBlock(ctx) + // ASSERT: The action should've succeeded due to empty account. + require.NoError(t, err) + + // ARRANGE: Give the fee collector some balance. + bank.Balances["noble17xpfvakm2amg962yls6f84z3kell8c5lc6kgnn"] = sdk.NewCoins( + sdk.NewInt64Coin("uusdc", 20_000), + ) + + // ACT: Attempt to run begin blocker with no owner set. + err = keeper.BeginBlock(ctx) + // ASSERT: The action should've failed due to no owner set. + require.ErrorContains(t, err, "failed to get owner from state") + + // ARRANGE: Set an invalid owner in state. + require.NoError(t, keeper.Owner.Set(ctx, "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn")) + + // ACT: Attempt to run begin blocker with invalid owner set. + err = keeper.BeginBlock(ctx) + // ASSERT: The action should've failed due to invalid owner set. + require.ErrorContains(t, err, "failed to decode owner address") + + // ARRANGE: Generate an owner account and set in state. + owner := utils.TestAccount() + require.NoError(t, keeper.Owner.Set(ctx, owner.Address)) + + // ACT: Attempt to run begin blocker. + err = keeper.BeginBlock(ctx) + // ASSERT: The action should've succeeded. + require.NoError(t, err) +} diff --git a/x/authority/keeper/keeper.go b/x/authority/keeper/keeper.go index 3266ee0..d1a9d5d 100644 --- a/x/authority/keeper/keeper.go +++ b/x/authority/keeper/keeper.go @@ -22,6 +22,7 @@ type Keeper struct { router baseapp.MessageRouter accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper } func NewKeeper( @@ -31,6 +32,7 @@ func NewKeeper( eventService event.Service, router baseapp.MessageRouter, accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, ) *Keeper { builder := collections.NewSchemaBuilder(storeService) @@ -45,6 +47,7 @@ func NewKeeper( router: router, accountKeeper: accountKeeper, + bankKeeper: bankKeeper, } schema, err := builder.Build() diff --git a/x/authority/keeper/msg_server_test.go b/x/authority/keeper/msg_server_test.go index d9ec9d1..f6b9d0d 100644 --- a/x/authority/keeper/msg_server_test.go +++ b/x/authority/keeper/msg_server_test.go @@ -93,7 +93,7 @@ func TestTransferOwnership(t *testing.T) { owner := utils.TestAccount() require.NoError(t, k.Owner.Set(ctx, owner.Address)) - // ACT: Attempt to tranfer ownership with invalid signer. + // ACT: Attempt to transfer ownership with invalid signer. _, err := server.TransferOwnership(ctx, &types.MsgTransferOwnership{ Signer: utils.TestAccount().Address, }) diff --git a/x/authority/module.go b/x/authority/module.go index ae8683f..e5428e5 100644 --- a/x/authority/module.go +++ b/x/authority/module.go @@ -33,6 +33,7 @@ const ConsensusVersion = 1 var ( _ module.AppModuleBasic = AppModule{} _ appmodule.AppModule = AppModule{} + _ appmodule.HasBeginBlocker = AppModule{} _ module.HasConsensusVersion = AppModule{} _ module.HasGenesis = AppModule{} _ module.HasServices = AppModule{} @@ -96,6 +97,10 @@ func (AppModule) IsOnePerModuleType() {} func (AppModule) IsAppModule() {} +func (m AppModule) BeginBlock(ctx context.Context) error { + return m.keeper.BeginBlock(ctx) +} + func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion } func (m AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, bz json.RawMessage) { @@ -178,6 +183,7 @@ type ModuleInputs struct { Router baseapp.MessageRouter AccountKeeper types.AccountKeeper + BankKeeper types.BankKeeper } type ModuleOutputs struct { @@ -195,6 +201,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { in.EventService, in.Router, in.AccountKeeper, + in.BankKeeper, ) m := NewAppModule(k, in.AccountKeeper) diff --git a/x/authority/types/expected_keepers.go b/x/authority/types/expected_keepers.go index ac2a31b..e2f378d 100644 --- a/x/authority/types/expected_keepers.go +++ b/x/authority/types/expected_keepers.go @@ -1,7 +1,18 @@ package types -import "cosmossdk.io/core/address" +import ( + "context" + + "cosmossdk.io/core/address" + sdk "github.com/cosmos/cosmos-sdk/types" +) type AccountKeeper interface { AddressCodec() address.Codec + GetModuleAddress(moduleName string) sdk.AccAddress +} + +type BankKeeper interface { + GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins + SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error }