diff --git a/x/rollup/keeper/deposits.go b/x/rollup/keeper/deposits.go index 76a4254f..553def92 100644 --- a/x/rollup/keeper/deposits.go +++ b/x/rollup/keeper/deposits.go @@ -264,9 +264,9 @@ func (k *Keeper) mintETH( types.EventTypeMintETH, sdk.NewAttribute(types.AttributeKeyL1DepositTxType, types.L1UserDepositTxType), sdk.NewAttribute(types.AttributeKeyMintCosmosAddress, mintAddr), - sdk.NewAttribute(types.AttributeKeyMint, hexutil.Encode(remainingCoins.BigInt().Bytes())), + sdk.NewAttribute(types.AttributeKeyMint, hexutil.EncodeBig(remainingCoins.BigInt())), sdk.NewAttribute(types.AttributeKeyToCosmosAddress, recipientAddr), - sdk.NewAttribute(types.AttributeKeyValue, hexutil.Encode(transferAmount.BigInt().Bytes())), + sdk.NewAttribute(types.AttributeKeyValue, hexutil.EncodeBig(transferAmount.BigInt())), ) return &mintEvent, nil @@ -297,7 +297,7 @@ func (k *Keeper) mintERC20( sdk.NewAttribute(types.AttributeKeyL1DepositTxType, types.L1UserDepositTxType), sdk.NewAttribute(types.AttributeKeyToCosmosAddress, userAddr), sdk.NewAttribute(types.AttributeKeyERC20Address, erc20addr), - sdk.NewAttribute(types.AttributeKeyValue, hexutil.Encode(amount.BigInt().Bytes())), + sdk.NewAttribute(types.AttributeKeyValue, hexutil.EncodeBig(amount.BigInt())), ) return &mintEvent, nil diff --git a/x/rollup/keeper/keeper.go b/x/rollup/keeper/keeper.go index e2595691..8634e078 100644 --- a/x/rollup/keeper/keeper.go +++ b/x/rollup/keeper/keeper.go @@ -12,10 +12,11 @@ import ( ) type Keeper struct { - cdc codec.BinaryCodec - storeService store.KVStoreService - rollupCfg *rollup.Config - bankkeeper types.BankKeeper + cdc codec.BinaryCodec + storeService store.KVStoreService + rollupCfg *rollup.Config + bankkeeper types.BankKeeper + accountkeeper types.AccountKeeper } func NewKeeper( @@ -23,12 +24,14 @@ func NewKeeper( storeService store.KVStoreService, // dependencies bankKeeper types.BankKeeper, + accountKeeper types.AccountKeeper, ) *Keeper { return &Keeper{ - cdc: cdc, - storeService: storeService, - bankkeeper: bankKeeper, - rollupCfg: &rollup.Config{}, + cdc: cdc, + storeService: storeService, + bankkeeper: bankKeeper, + accountkeeper: accountKeeper, + rollupCfg: &rollup.Config{}, } } diff --git a/x/rollup/keeper/keeper_test.go b/x/rollup/keeper/keeper_test.go index 9c53c70a..7281cff3 100644 --- a/x/rollup/keeper/keeper_test.go +++ b/x/rollup/keeper/keeper_test.go @@ -4,11 +4,13 @@ import ( "context" "testing" + "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/runtime" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/polymerdao/monomer/x/rollup/keeper" rolluptestutil "github.com/polymerdao/monomer/x/rollup/testutil" "github.com/polymerdao/monomer/x/rollup/types" @@ -18,11 +20,12 @@ import ( type KeeperTestSuite struct { suite.Suite - ctx context.Context - rollupKeeper *keeper.Keeper - bankKeeper *rolluptestutil.MockBankKeeper - rollupStore storetypes.KVStore - eventManger sdk.EventManagerI + ctx context.Context + rollupKeeper *keeper.Keeper + bankKeeper *rolluptestutil.MockBankKeeper + accountKeeper *rolluptestutil.MockAccountKeeper + rollupStore storetypes.KVStore + eventManger sdk.EventManagerI } func TestKeeperTestSuite(t *testing.T) { @@ -35,11 +38,13 @@ func (s *KeeperTestSuite) SetupSubTest() { s.T(), storeKey, storetypes.NewTransientStoreKey("transient_test")).Ctx + s.accountKeeper = rolluptestutil.NewMockAccountKeeper(gomock.NewController(s.T())) s.bankKeeper = rolluptestutil.NewMockBankKeeper(gomock.NewController(s.T())) s.rollupKeeper = keeper.NewKeeper( moduletestutil.MakeTestEncodingConfig().Codec, runtime.NewKVStoreService(storeKey), s.bankKeeper, + s.accountKeeper, ) sdkCtx := sdk.UnwrapSDKContext(s.ctx) s.rollupStore = sdkCtx.KVStore(storeKey) @@ -47,11 +52,19 @@ func (s *KeeperTestSuite) SetupSubTest() { } func (s *KeeperTestSuite) mockBurnETH() { - s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes() - s.bankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes() + s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes() + s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(nil).AnyTimes() } func (s *KeeperTestSuite) mockMintETH() { - s.bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes() - s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + s.bankKeeper.EXPECT().MintCoins(s.ctx, types.ModuleName, gomock.Any()).Return(nil).AnyTimes() + s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.ctx, types.ModuleName, gomock.Any(), gomock.Any()).Return(nil).AnyTimes() +} + +func (s *KeeperTestSuite) mockFeeCollector() { + mockFeeCollectorAddress := sdk.AccAddress("fee_collector") + s.accountKeeper.EXPECT().GetModuleAddress(authtypes.FeeCollectorName).Return(mockFeeCollectorAddress).AnyTimes() + s.bankKeeper.EXPECT().GetBalance(s.ctx, mockFeeCollectorAddress, types.WEI).Return(sdk.NewCoin(types.WEI, math.NewInt(100_000))).AnyTimes() + s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, authtypes.FeeCollectorName, types.ModuleName, gomock.Any()).Return(nil).AnyTimes() + s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(nil).AnyTimes() } diff --git a/x/rollup/keeper/msg_server.go b/x/rollup/keeper/msg_server.go index 3e31ce22..e60520cf 100644 --- a/x/rollup/keeper/msg_server.go +++ b/x/rollup/keeper/msg_server.go @@ -2,8 +2,11 @@ package keeper import ( "context" + "math/big" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/polymerdao/monomer/x/rollup/types" "github.com/samber/lo" @@ -62,7 +65,7 @@ func (k *Keeper) InitiateWithdrawal( return nil, types.WrapError(types.ErrBurnETH, "failed to burn ETH for cosmosAddress: %v; err: %v", cosmAddr, err) } - withdrawalValueHex := hexutil.Encode(msg.Value.BigInt().Bytes()) + withdrawalValueHex := hexutil.EncodeBig(msg.Value.BigInt()) k.EmitEvents(ctx, sdk.Events{ sdk.NewEvent( types.EventTypeWithdrawalInitiated, @@ -83,3 +86,71 @@ func (k *Keeper) InitiateWithdrawal( return &types.MsgInitiateWithdrawalResponse{}, nil } + +func (k *Keeper) InitiateFeeWithdrawal( + goCtx context.Context, + _ *types.MsgInitiateFeeWithdrawal, +) (*types.MsgInitiateFeeWithdrawalResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + // TODO: make minWithdrawalAmount and l1recipientAddr configurable once a genesis state is added + const ( + minWithdrawalAmount = 100 + l1recipientAddr = "0x63d93aC6FA6B4021527e967ac3Eb29F2B3E52B96" + feeWithdrawalGasLimit = 400_000 + ) + + feeCollectorAddr := k.accountkeeper.GetModuleAddress(authtypes.FeeCollectorName) + if feeCollectorAddr == nil { + ctx.Logger().Error("Failed to get fee collector address") + return nil, types.WrapError(types.ErrInitiateFeeWithdrawal, "failed to get fee collector address") + } + + feeCollectorBalance := k.bankkeeper.GetBalance(ctx, feeCollectorAddr, types.WEI) + if feeCollectorBalance.Amount.LT(math.NewInt(minWithdrawalAmount)) { + ctx.Logger().Error("Fee collector balance is below the minimum withdrawal amount", "balance", feeCollectorBalance.String()) + return nil, types.WrapError( + types.ErrInitiateFeeWithdrawal, + "fee collector balance is below the minimum withdrawal amount: %v", feeCollectorBalance.String(), + ) + } + + ctx.Logger().Debug("Withdrawing L2 fees", "amount", feeCollectorBalance.String(), "recipient", l1recipientAddr) + + // Burn the withdrawn fees from the fee collector account on L2. To avoid needing to enable burn permissions for the + // FeeCollector module account, they will first be sent to the rollup module account before being burned. + fees := sdk.NewCoins(feeCollectorBalance) + if err := k.bankkeeper.SendCoinsFromModuleToModule(ctx, authtypes.FeeCollectorName, types.ModuleName, fees); err != nil { + ctx.Logger().Error("Failed to send withdrawn fees from fee collector account to rollup module", "err", err) + return nil, types.WrapError( + types.ErrInitiateFeeWithdrawal, + "failed to send withdrawn fees from fee collector account to rollup module: %v", err, + ) + } + if err := k.bankkeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(feeCollectorBalance)); err != nil { + ctx.Logger().Error("Failed to burn withdrawn fees from rollup module", "err", err) + return nil, types.WrapError(types.ErrInitiateFeeWithdrawal, "failed to burn withdrawn fees from rollup module: %v", err) + } + + withdrawalValueHex := hexutil.EncodeBig(feeCollectorBalance.Amount.BigInt()) + feeCollectorAddrStr := feeCollectorAddr.String() + k.EmitEvents(ctx, sdk.Events{ + sdk.NewEvent( + types.EventTypeWithdrawalInitiated, + sdk.NewAttribute(types.AttributeKeySender, feeCollectorAddrStr), + sdk.NewAttribute(types.AttributeKeyL1Target, l1recipientAddr), + sdk.NewAttribute(types.AttributeKeyValue, withdrawalValueHex), + sdk.NewAttribute(types.AttributeKeyGasLimit, hexutil.EncodeBig(big.NewInt(feeWithdrawalGasLimit))), + sdk.NewAttribute(types.AttributeKeyData, "0x"), + // The nonce attribute will be set by Monomer + ), + sdk.NewEvent( + types.EventTypeBurnETH, + sdk.NewAttribute(types.AttributeKeyL2FeeWithdrawalTx, types.EventTypeWithdrawalInitiated), + sdk.NewAttribute(types.AttributeKeyFromCosmosAddress, feeCollectorAddrStr), + sdk.NewAttribute(types.AttributeKeyValue, withdrawalValueHex), + ), + }) + + return &types.MsgInitiateFeeWithdrawalResponse{}, nil +} diff --git a/x/rollup/keeper/msg_server_test.go b/x/rollup/keeper/msg_server_test.go index e1cf0f2f..f64e0aa1 100644 --- a/x/rollup/keeper/msg_server_test.go +++ b/x/rollup/keeper/msg_server_test.go @@ -4,6 +4,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum-optimism/optimism/op-service/eth" gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/golang/mock/gomock" @@ -87,14 +88,14 @@ func (s *KeeperTestSuite) TestApplyL1Txs() { "bank keeper mint coins failure": { txBytes: [][]byte{l1AttributesTxBz, depositTxBz}, setupMocks: func() { - s.bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnauthorized) + s.bankKeeper.EXPECT().MintCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnauthorized) }, shouldError: true, }, "bank keeper send coins failure": { txBytes: [][]byte{l1AttributesTxBz, depositTxBz}, setupMocks: func() { - s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).Return(sdkerrors.ErrUnknownRequest) + s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.ctx, types.ModuleName, gomock.Any(), gomock.Any()).Return(sdkerrors.ErrUnknownRequest) }, shouldError: true, }, @@ -161,14 +162,14 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() { }, "bank keeper insufficient funds failure": { setupMocks: func() { - s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).Return(types.ErrBurnETH) + s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(types.ErrBurnETH) }, sender: sender, shouldError: true, }, "bank keeper burn coins failure": { setupMocks: func() { - s.bankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest) + s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest) }, sender: sender, shouldError: true, @@ -208,3 +209,69 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() { }) } } + +func (s *KeeperTestSuite) TestInitiateFeeWithdrawal() { + tests := map[string]struct { + setupMocks func() + shouldError bool + }{ + "successful message": { + shouldError: false, + }, + "fee collector address not found": { + setupMocks: func() { + s.accountKeeper.EXPECT().GetModuleAddress(authtypes.FeeCollectorName).Return(nil) + }, + shouldError: true, + }, + "fee collector balance below minimum withdrawal amount": { + setupMocks: func() { + s.bankKeeper.EXPECT().GetBalance(s.ctx, gomock.Any(), types.WEI).Return(sdk.NewCoin(types.WEI, math.NewInt(1))) + }, + shouldError: true, + }, + "bank keeper send coins failure": { + setupMocks: func() { + s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, authtypes.FeeCollectorName, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest) + }, + shouldError: true, + }, + "bank keeper burn coins failure": { + setupMocks: func() { + s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest) + }, + shouldError: true, + }, + } + + for name, test := range tests { + s.Run(name, func() { + if test.setupMocks != nil { + test.setupMocks() + } + s.mockFeeCollector() + + resp, err := s.rollupKeeper.InitiateFeeWithdrawal(s.ctx, &types.MsgInitiateFeeWithdrawal{ + Sender: sdk.AccAddress("addr").String(), + }) + + if test.shouldError { + s.Require().Error(err) + s.Require().Nil(resp) + } else { + s.Require().NoError(err) + s.Require().NotNil(resp) + + // Verify that the expected event types are emitted + expectedEventTypes := []string{ + sdk.EventTypeMessage, + types.EventTypeWithdrawalInitiated, + types.EventTypeBurnETH, + } + for i, event := range s.eventManger.Events() { + s.Require().Equal(expectedEventTypes[i], event.Type) + } + } + }) + } +} diff --git a/x/rollup/module.go b/x/rollup/module.go index fdbb0d0b..2d69381d 100644 --- a/x/rollup/module.go +++ b/x/rollup/module.go @@ -12,6 +12,7 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" protov1 "github.com/golang/protobuf/proto" //nolint:staticcheck "github.com/gorilla/mux" @@ -27,9 +28,10 @@ import ( type ModuleInputs struct { depinject.In - Codec codec.Codec - StoreService store.KVStoreService - BankKeeper bankkeeper.Keeper + Codec codec.Codec + StoreService store.KVStoreService + BankKeeper bankkeeper.Keeper + AccountKeeper authkeeper.AccountKeeper } type ModuleOutputs struct { @@ -53,8 +55,8 @@ func ProvideCustomGetSigner() signing.CustomGetSigner { } } -func ProvideModule(in ModuleInputs) ModuleOutputs { - k := keeper.NewKeeper(in.Codec, in.StoreService, in.BankKeeper) +func ProvideModule(in ModuleInputs) ModuleOutputs { //nolint:gocritic // hugeParam + k := keeper.NewKeeper(in.Codec, in.StoreService, in.BankKeeper, in.AccountKeeper) return ModuleOutputs{ Keeper: k, Module: NewAppModule(in.Codec, k), diff --git a/x/rollup/tests/integration/rollup_test.go b/x/rollup/tests/integration/rollup_test.go index e757d4f8..878451eb 100644 --- a/x/rollup/tests/integration/rollup_test.go +++ b/x/rollup/tests/integration/rollup_test.go @@ -31,8 +31,10 @@ import ( "github.com/stretchr/testify/require" ) +const initialFeeCollectorBalance = 1_000_000 + func TestRollup(t *testing.T) { - integrationApp := setupIntegrationApp(t) + integrationApp, feeCollectorAddr := setupIntegrationApp(t) queryClient := banktypes.NewQueryClient(integrationApp.QueryHelper()) erc20tokenAddr := common.HexToAddress("0xabcdef123456") @@ -54,19 +56,19 @@ func TestRollup(t *testing.T) { mintAddr, err := monomer.CosmosETHAddress(from).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) - recipientAddr, err := monomer.CosmosETHAddress(*depositTx.To()).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) + userCosmosAddr, err := monomer.CosmosETHAddress(*depositTx.To()).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) // query the mint address ETH balance and assert it's zero - require.Equal(t, math.ZeroInt(), queryUserETHBalance(t, queryClient, mintAddr, integrationApp)) + require.Equal(t, math.ZeroInt(), queryETHBalance(t, queryClient, mintAddr, integrationApp)) // query the recipient address ETH balance and assert it's zero - require.Equal(t, math.ZeroInt(), queryUserETHBalance(t, queryClient, recipientAddr, integrationApp)) + require.Equal(t, math.ZeroInt(), queryETHBalance(t, queryClient, userCosmosAddr, integrationApp)) // query the user's ERC20 balance and assert it's zero erc20userCosmosAddr, err := monomer.CosmosETHAddress(erc20userAddr).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) - require.Equal(t, math.ZeroInt(), queryUserERC20Balance(t, queryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp)) + require.Equal(t, math.ZeroInt(), queryERC20Balance(t, queryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp)) // send an invalid MsgApplyL1Txs and assert error _, err = integrationApp.RunMsg(&rolluptypes.MsgApplyL1Txs{ @@ -81,13 +83,13 @@ func TestRollup(t *testing.T) { require.NoError(t, err) // query the mint address ETH balance and assert it's equal to the mint amount minus the transfer amount - require.Equal(t, new(big.Int).Sub(mintAmount, transferAmount), queryUserETHBalance(t, queryClient, mintAddr, integrationApp).BigInt()) + require.Equal(t, new(big.Int).Sub(mintAmount, transferAmount), queryETHBalance(t, queryClient, mintAddr, integrationApp).BigInt()) // query the recipient address ETH balance and assert it's equal to the transfer amount - require.Equal(t, transferAmount, queryUserETHBalance(t, queryClient, recipientAddr, integrationApp).BigInt()) + require.Equal(t, transferAmount, queryETHBalance(t, queryClient, userCosmosAddr, integrationApp).BigInt()) // query the user's ERC20 balance and assert it's equal to the deposit amount - require.Equal(t, erc20depositAmount, queryUserERC20Balance(t, queryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp).BigInt()) + require.Equal(t, erc20depositAmount, queryERC20Balance(t, queryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp).BigInt()) // try to withdraw more than deposited and assert error _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateWithdrawal{ @@ -101,7 +103,7 @@ func TestRollup(t *testing.T) { // send a successful MsgInitiateWithdrawal _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateWithdrawal{ - Sender: recipientAddr, + Sender: userCosmosAddr, Target: l1WithdrawalAddr, Value: math.NewIntFromBigInt(transferAmount), GasLimit: new(big.Int).SetUint64(100_000_000).Bytes(), @@ -110,10 +112,22 @@ func TestRollup(t *testing.T) { require.NoError(t, err) // query the recipient address ETH balance and assert it's zero - require.Equal(t, math.ZeroInt(), queryUserETHBalance(t, queryClient, recipientAddr, integrationApp)) + require.Equal(t, math.ZeroInt(), queryETHBalance(t, queryClient, userCosmosAddr, integrationApp)) + + // query the fee collector's ETH balance and assert it's equal to the initial mint amount + require.Equal(t, math.NewInt(initialFeeCollectorBalance), queryETHBalance(t, queryClient, feeCollectorAddr, integrationApp)) + + // send a successful MsgInitiateFeeWithdrawal + _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateFeeWithdrawal{ + Sender: userCosmosAddr, + }) + require.NoError(t, err) + + // query the fee collector's ETH balance and assert it's zero + require.Equal(t, math.ZeroInt(), queryETHBalance(t, queryClient, feeCollectorAddr, integrationApp)) } -func setupIntegrationApp(t *testing.T) *integration.App { +func setupIntegrationApp(t *testing.T) (*integration.App, string) { encodingCfg := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, rollup.AppModuleBasic{}) keys := storetypes.NewKVStoreKeys(authtypes.StoreKey, banktypes.StoreKey, rolluptypes.StoreKey) authority := authtypes.NewModuleAddress("gov").String() @@ -126,7 +140,10 @@ func setupIntegrationApp(t *testing.T) *integration.App { encodingCfg.Codec, runtime.NewKVStoreService(keys[authtypes.StoreKey]), authtypes.ProtoBaseAccount, - map[string][]string{rolluptypes.ModuleName: {authtypes.Minter, authtypes.Burner}}, + map[string][]string{ + authtypes.FeeCollectorName: {}, + rolluptypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + }, addresscodec.NewBech32Codec("cosmos"), "cosmos", authority, @@ -143,14 +160,21 @@ func setupIntegrationApp(t *testing.T) *integration.App { encodingCfg.Codec, runtime.NewKVStoreService(keys[rolluptypes.StoreKey]), bankKeeper, + accountKeeper, ) authModule := auth.NewAppModule(encodingCfg.Codec, accountKeeper, authsims.RandomGenesisAccounts, nil) bankModule := bank.NewAppModule(encodingCfg.Codec, bankKeeper, accountKeeper, nil) rollupModule := rollup.NewAppModule(encodingCfg.Codec, rollupKeeper) + // Start the integration test with funds in the fee collector account since fees are disabled in the simulated integration app + ctx := sdk.NewContext(cms, cmtproto.Header{}, false, logger) + initialFees := sdk.NewCoins(sdk.NewCoin(rolluptypes.WEI, math.NewInt(initialFeeCollectorBalance))) + require.NoError(t, bankKeeper.MintCoins(ctx, rolluptypes.ModuleName, initialFees)) + require.NoError(t, bankKeeper.SendCoinsFromModuleToModule(ctx, rolluptypes.ModuleName, authtypes.FeeCollectorName, initialFees)) + integrationApp := integration.NewIntegrationApp( - sdk.NewContext(cms, cmtproto.Header{}, false, logger), + ctx, logger, keys, encodingCfg.Codec, @@ -163,22 +187,22 @@ func setupIntegrationApp(t *testing.T) *integration.App { rolluptypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), rollupKeeper) banktypes.RegisterQueryServer(integrationApp.QueryHelper(), bankkeeper.NewQuerier(&bankKeeper)) - return integrationApp + return integrationApp, accountKeeper.GetModuleAddress(authtypes.FeeCollectorName).String() } -func queryUserBalance(t *testing.T, queryClient banktypes.QueryClient, userAddr, denom string, app *integration.App) math.Int { +func queryBalance(t *testing.T, queryClient banktypes.QueryClient, addr, denom string, app *integration.App) math.Int { resp, err := queryClient.Balance(app.Context(), &banktypes.QueryBalanceRequest{ - Address: userAddr, + Address: addr, Denom: denom, }) require.NoError(t, err) return resp.Balance.Amount } -func queryUserETHBalance(t *testing.T, queryClient banktypes.QueryClient, userAddr string, app *integration.App) math.Int { - return queryUserBalance(t, queryClient, userAddr, rolluptypes.WEI, app) +func queryETHBalance(t *testing.T, queryClient banktypes.QueryClient, addr string, app *integration.App) math.Int { + return queryBalance(t, queryClient, addr, rolluptypes.WEI, app) } -func queryUserERC20Balance(t *testing.T, queryClient banktypes.QueryClient, userAddr string, erc20addr common.Address, app *integration.App) math.Int { - return queryUserBalance(t, queryClient, userAddr, "erc20/"+erc20addr.String()[2:], app) +func queryERC20Balance(t *testing.T, queryClient banktypes.QueryClient, addr string, erc20addr common.Address, app *integration.App) math.Int { + return queryBalance(t, queryClient, addr, "erc20/"+erc20addr.String()[2:], app) } diff --git a/x/rollup/testutil/expected_keepers_mocks.go b/x/rollup/testutil/expected_keepers_mocks.go index b03ca085..ee6e1734 100644 --- a/x/rollup/testutil/expected_keepers_mocks.go +++ b/x/rollup/testutil/expected_keepers_mocks.go @@ -54,6 +54,20 @@ func (mr *MockBankKeeperMockRecorder) BurnCoins(ctx, moduleName, amt any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BurnCoins", reflect.TypeOf((*MockBankKeeper)(nil).BurnCoins), ctx, moduleName, amt) } +// GetBalance mocks base method. +func (m *MockBankKeeper) GetBalance(ctx context.Context, addr types.AccAddress, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) +} + // MintCoins mocks base method. func (m *MockBankKeeper) MintCoins(ctx context.Context, name string, amt types.Coins) error { m.ctrl.T.Helper() @@ -96,6 +110,20 @@ func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderMo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) } +// SendCoinsFromModuleToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToModule", ctx, senderModule, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, amt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderModule, recipientModule, amt) +} + // MockAccountKeeper is a mock of AccountKeeper interface. type MockAccountKeeper struct { ctrl *gomock.Controller @@ -119,28 +147,16 @@ func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { return m.recorder } -// NewAccountWithAddress mocks base method. -func (m *MockAccountKeeper) NewAccountWithAddress(arg0 context.Context, arg1 types.AccAddress) types.AccountI { +// GetModuleAddress mocks base method. +func (m *MockAccountKeeper) GetModuleAddress(moduleName string) types.AccAddress { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewAccountWithAddress", arg0, arg1) - ret0, _ := ret[0].(types.AccountI) + ret := m.ctrl.Call(m, "GetModuleAddress", moduleName) + ret0, _ := ret[0].(types.AccAddress) return ret0 } -// NewAccountWithAddress indicates an expected call of NewAccountWithAddress. -func (mr *MockAccountKeeperMockRecorder) NewAccountWithAddress(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAccountWithAddress", reflect.TypeOf((*MockAccountKeeper)(nil).NewAccountWithAddress), arg0, arg1) -} - -// SetAccount mocks base method. -func (m *MockAccountKeeper) SetAccount(arg0 context.Context, arg1 types.AccountI) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetAccount", arg0, arg1) -} - -// SetAccount indicates an expected call of SetAccount. -func (mr *MockAccountKeeperMockRecorder) SetAccount(arg0, arg1 any) *gomock.Call { +// GetModuleAddress indicates an expected call of GetModuleAddress. +func (mr *MockAccountKeeperMockRecorder) GetModuleAddress(moduleName any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccount", reflect.TypeOf((*MockAccountKeeper)(nil).SetAccount), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAddress", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAddress), moduleName) } diff --git a/x/rollup/types/errors.go b/x/rollup/types/errors.go index 48595e7c..2d2b53f3 100644 --- a/x/rollup/types/errors.go +++ b/x/rollup/types/errors.go @@ -17,6 +17,7 @@ var ( ErrL1BlockInfo = registerErr("L1 block info") ErrProcessL1UserDepositTxs = registerErr("failed to process L1 user deposit txs") ErrProcessL1SystemDepositTx = registerErr("failed to process L1 system deposit tx") + ErrInitiateFeeWithdrawal = registerErr("failed to initiate fee withdrawal") ) // register new errors without hard-coding error codes diff --git a/x/rollup/types/events.go b/x/rollup/types/events.go index 6e9cee7d..2e604cc2 100644 --- a/x/rollup/types/events.go +++ b/x/rollup/types/events.go @@ -3,6 +3,7 @@ package types const ( AttributeKeyL1DepositTxType = "l1_deposit_tx_type" AttributeKeyL2WithdrawalTx = "l2_withdrawal_tx" + AttributeKeyL2FeeWithdrawalTx = "l2_fee_withdrawal_tx" AttributeKeyFromCosmosAddress = "from_address" AttributeKeyToCosmosAddress = "to_address" AttributeKeyMintCosmosAddress = "mint_address" diff --git a/x/rollup/types/expected_keepers.go b/x/rollup/types/expected_keepers.go index 8819e24d..d6a56527 100644 --- a/x/rollup/types/expected_keepers.go +++ b/x/rollup/types/expected_keepers.go @@ -10,12 +10,14 @@ import ( type BankKeeper interface { SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error MintCoins(ctx context.Context, name string, amt sdk.Coins) error BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + + GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin } type AccountKeeper interface { - NewAccountWithAddress(context.Context, sdk.AccAddress) sdk.AccountI - SetAccount(context.Context, sdk.AccountI) + GetModuleAddress(moduleName string) sdk.AccAddress }