Skip to content

Commit

Permalink
Add InitiateFeeWithdrawal msg_server impl
Browse files Browse the repository at this point in the history
Adds a message server implementation for the InitiateFeeWithdrawal
message to mimic the behavior of the OPStack's FeeVault contracts.

Users can permissionlessly triggers fee withdrawals on L2 if the
FeeCollector module account balance is above the predefined minimum
withdrawal amount. The fees are then able to be withdrawn to the
predefined target address on L1.
  • Loading branch information
natebeauregard committed Nov 1, 2024
1 parent 0090e7e commit 87ab1af
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 71 deletions.
6 changes: 3 additions & 3 deletions x/rollup/keeper/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 11 additions & 8 deletions x/rollup/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,26 @@ 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(
cdc codec.BinaryCodec,
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{},
}
}

Expand Down
31 changes: 22 additions & 9 deletions x/rollup/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand All @@ -35,23 +38,33 @@ 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)
s.eventManger = sdkCtx.EventManager()
}

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()
}
73 changes: 72 additions & 1 deletion x/rollup/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
75 changes: 71 additions & 4 deletions x/rollup/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
})
}
}
12 changes: 7 additions & 5 deletions x/rollup/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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),
Expand Down
Loading

0 comments on commit 87ab1af

Please sign in to comment.