diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index 15cc95338e..369b29a3f2 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -798,7 +798,7 @@ func (suite AnteTestSuite) TestAnteHandler() { signing.SignMode_SIGN_MODE_DIRECT, msg, "ethermint_9000-1", - 2000, + 2000000, "EIP-712", ) @@ -831,7 +831,7 @@ func (suite AnteTestSuite) TestAnteHandler() { signing.SignMode_SIGN_MODE_DIRECT, msg, "ethermint_9000-1", - 2000, + 2000000, "EIP-712", ) @@ -860,12 +860,48 @@ func (suite AnteTestSuite) TestAnteHandler() { signing.SignMode_SIGN_MODE_DIRECT, msg, "ethermint_9000-1", - 2000, + 2000000, "EIP-712", ) txBuilder.SetMsgs(msg, msg) + return txBuilder.GetTx() + }, false, false, false, + }, + { + "Fails - Authz Exec with unauthorized message", + func() sdk.Tx { + ethTx := evmtypes.NewTx( + suite.app.EvmKeeper.ChainID(), + 1, + &to, + big.NewInt(10), + 100000, + big.NewInt(150), + big.NewInt(200), + nil, + nil, + nil, + ) + ethTx.From = addr.Hex() + + msg := authz.NewMsgExec( + sdk.AccAddress(privKey.PubKey().Address()), + []sdk.Msg{ethTx}, + ) + + txBuilder := suite.CreateTestSingleSignedTx( + privKey, + signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, + &msg, + "ethermint_9000-1", + 200000, + "", + ) + + txBuilder.SetMsgs(&msg) + return txBuilder.GetTx() }, false, false, false, }, diff --git a/app/ante/authz.go b/app/ante/authz.go new file mode 100644 index 0000000000..3909d0b3e1 --- /dev/null +++ b/app/ante/authz.go @@ -0,0 +1,101 @@ +// Copyright 2021 Evmos Foundation +// This file is part of Evmos' Ethermint library. +// +// The Ethermint library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Ethermint library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE +package ante + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// maxNestedMsgs defines a cap for the number of nested messages on a MsgExec message +const maxNestedMsgs = 6 + +// AuthzLimiterDecorator blocks certain msg types from being granted or executed +// within the authorization module. +type AuthzLimiterDecorator struct { + // disabledMsgs is a set that contains type urls of unauthorized msgs. + disabledMsgs map[string]struct{} +} + +// NewAuthzLimiterDecorator creates a decorator to block certain msg types +// from being granted or executed within authz. +func NewAuthzLimiterDecorator(disabledMsgTypes []string) AuthzLimiterDecorator { + disabledMsgs := make(map[string]struct{}) + for _, url := range disabledMsgTypes { + disabledMsgs[url] = struct{}{} + } + + return AuthzLimiterDecorator{ + disabledMsgs: disabledMsgs, + } +} + +func (ald AuthzLimiterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + if err := ald.checkDisabledMsgs(tx.GetMsgs(), false, 0); err != nil { + return ctx, errorsmod.Wrapf(errortypes.ErrUnauthorized, err.Error()) + } + return next(ctx, tx, simulate) +} + +// checkDisabledMsgs iterates through the msgs and returns an error if it finds any unauthorized msgs. +// +// This method is recursive as MsgExec's can wrap other MsgExecs. nestedMsgs sets a reasonable limit on +// the total messages, regardless of how they are nested. +func (ald AuthzLimiterDecorator) checkDisabledMsgs(msgs []sdk.Msg, isAuthzInnerMsg bool, nestedMsgs int) error { + if nestedMsgs >= maxNestedMsgs { + return fmt.Errorf("found more nested msgs than permitted. Limit is : %d", maxNestedMsgs) + } + for _, msg := range msgs { + switch msg := msg.(type) { + case *authz.MsgExec: + innerMsgs, err := msg.GetMessages() + if err != nil { + return err + } + nestedMsgs++ + if err := ald.checkDisabledMsgs(innerMsgs, true, nestedMsgs); err != nil { + return err + } + case *authz.MsgGrant: + authorization, err := msg.GetAuthorization() + if err != nil { + return err + } + + url := authorization.MsgTypeURL() + if ald.isDisabledMsg(url) { + return fmt.Errorf("found disabled msg type: %s", url) + } + default: + url := sdk.MsgTypeURL(msg) + if isAuthzInnerMsg && ald.isDisabledMsg(url) { + return fmt.Errorf("found disabled msg type: %s", url) + } + } + } + return nil +} + +// isDisabledMsg returns true if the given message is in the set of restricted +// messages from the AnteHandler. +func (ald AuthzLimiterDecorator) isDisabledMsg(msgTypeURL string) bool { + _, ok := ald.disabledMsgs[msgTypeURL] + return ok +} diff --git a/app/ante/authz_test.go b/app/ante/authz_test.go new file mode 100644 index 0000000000..054f411531 --- /dev/null +++ b/app/ante/authz_test.go @@ -0,0 +1,445 @@ +package ante_test + +import ( + "fmt" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + utiltx "github.com/evmos/ethermint/testutil/tx" + + "github.com/evmos/ethermint/app/ante" + + "github.com/evmos/ethermint/crypto/ethsecp256k1" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +func (suite *AnteTestSuite) TestAuthzLimiterDecorator() { + _, testAddresses, err := generatePrivKeyAddressPairs(5) + suite.Require().NoError(err) + + validator := sdk.ValAddress(testAddresses[4]) + stakingAuthDelegate, err := stakingtypes.NewStakeAuthorization( + []sdk.ValAddress{validator}, + nil, + stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, + nil, + ) + suite.Require().NoError(err) + + stakingAuthUndelegate, err := stakingtypes.NewStakeAuthorization( + []sdk.ValAddress{validator}, + nil, + stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_UNDELEGATE, + nil, + ) + suite.Require().NoError(err) + + decorator := ante.NewAuthzLimiterDecorator( + []string{ + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&stakingtypes.MsgUndelegate{}), + }, + ) + + testMsgSend := createMsgSend(testAddresses) + testMsgEthereumTx := &evmtypes.MsgEthereumTx{} + + testCases := []struct { + name string + msgs []sdk.Msg + expectedErr error + }{ + { + "enabled msg - non blocked msg", + []sdk.Msg{ + testMsgSend, + }, + nil, + }, + { + "enabled msg MsgEthereumTx - blocked msg not wrapped in MsgExec", + []sdk.Msg{ + testMsgEthereumTx, + }, + nil, + }, + { + "enabled msg - blocked msg not wrapped in MsgExec", + []sdk.Msg{ + &stakingtypes.MsgUndelegate{}, + }, + nil, + }, + { + "enabled msg - MsgGrant contains a non blocked msg", + []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(&banktypes.MsgSend{}), + ), + }, + nil, + }, + { + "enabled msg - MsgGrant contains a non blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses, + stakingAuthDelegate, + ), + }, + nil, + }, + { + "disabled msg - MsgGrant contains a blocked msg", + []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(testMsgEthereumTx), + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - MsgGrant contains a blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses, + stakingAuthUndelegate, + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "allowed msg - when a MsgExec contains a non blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + testMsgSend, + }, + ), + }, + nil, + }, + { + "disabled msg - MsgExec contains a blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + testMsgEthereumTx, + }, + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - surrounded by valid msgs", + []sdk.Msg{ + newMsgGrant( + testAddresses, + stakingAuthDelegate, + ), + newMsgExec( + testAddresses[1], + []sdk.Msg{ + testMsgSend, + testMsgEthereumTx, + }, + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgExec containing a blocked msg", + []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 2, + []sdk.Msg{ + testMsgEthereumTx, + }, + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgGrant containing a blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(testMsgEthereumTx), + ), + }, + ), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgExec NOT containing a blocked msg but has more nesting levels than the allowed", + []sdk.Msg{ + createNestedExecMsgSend(testAddresses, 6), + }, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - two multiple nested MsgExec messages NOT containing a blocked msg over the limit", + []sdk.Msg{ + createNestedExecMsgSend(testAddresses, 5), + createNestedExecMsgSend(testAddresses, 5), + }, + sdkerrors.ErrUnauthorized, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + tx, err := suite.createTx(suite.priv, tc.msgs...) + suite.Require().NoError(err) + + _, err = decorator.AnteHandle(suite.ctx, tx, false, NextFn) + if tc.expectedErr != nil { + suite.Require().Error(err) + suite.Require().ErrorIs(err, tc.expectedErr) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *AnteTestSuite) TestRejectDeliverMsgsInAuthz() { + _, testAddresses, err := generatePrivKeyAddressPairs(10) + suite.Require().NoError(err) + + testcases := []struct { + name string + msgs []sdk.Msg + expectedCode uint32 + isEIP712 bool + }{ + { + name: "a MsgGrant with MsgEthereumTx typeURL on the authorization field is blocked", + msgs: []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgGrant with MsgCreateVestingAccount typeURL on the authorization field is blocked", + msgs: []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgGrant with MsgEthereumTx typeURL on the authorization field included on EIP712 tx is blocked", + msgs: []sdk.Msg{ + newGenericMsgGrant( + testAddresses, + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + isEIP712: true, + }, + { + name: "a MsgExec with nested messages (valid: MsgSend and invalid: MsgEthereumTx) is blocked", + msgs: []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + createMsgSend(testAddresses), + &evmtypes.MsgEthereumTx{}, + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgExec with nested MsgExec messages that has invalid messages is blocked", + msgs: []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 2, + []sdk.Msg{ + &evmtypes.MsgEthereumTx{}, + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgExec with more nested MsgExec messages than allowed and with valid messages is blocked", + msgs: []sdk.Msg{ + createNestedExecMsgSend(testAddresses, 6), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "two MsgExec messages NOT containing a blocked msg but between the two have more nesting than the allowed. Then, is blocked", + msgs: []sdk.Msg{ + createNestedExecMsgSend(testAddresses, 5), + createNestedExecMsgSend(testAddresses, 5), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + } + + for _, tc := range testcases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + var ( + tx sdk.Tx + err error + ) + + if tc.isEIP712 { + tx, err = suite.createEIP712Tx(suite.priv, tc.msgs...) + } else { + tx, err = suite.createTx(suite.priv, tc.msgs...) + } + suite.Require().NoError(err) + + txEncoder := suite.clientCtx.TxConfig.TxEncoder() + bz, err := txEncoder(tx) + suite.Require().NoError(err) + + resCheckTx := suite.app.CheckTx( + abci.RequestCheckTx{ + Tx: bz, + Type: abci.CheckTxType_New, + }, + ) + suite.Require().Equal(resCheckTx.Code, tc.expectedCode, resCheckTx.Log) + + resDeliverTx := suite.app.DeliverTx( + abci.RequestDeliverTx{ + Tx: bz, + }, + ) + suite.Require().Equal(resDeliverTx.Code, tc.expectedCode, resDeliverTx.Log) + }) + } +} + +func generatePrivKeyAddressPairs(accCount int) ([]*ethsecp256k1.PrivKey, []sdk.AccAddress, error) { + var ( + err error + testPrivKeys = make([]*ethsecp256k1.PrivKey, accCount) + testAddresses = make([]sdk.AccAddress, accCount) + ) + + for i := range testPrivKeys { + testPrivKeys[i], err = ethsecp256k1.GenerateKey() + if err != nil { + return nil, nil, err + } + testAddresses[i] = testPrivKeys[i].PubKey().Address().Bytes() + } + return testPrivKeys, testAddresses, nil +} + +func newMsgGrant(testAddresses []sdk.AccAddress, auth authz.Authorization) *authz.MsgGrant { + expiration := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + msg, err := authz.NewMsgGrant(testAddresses[0], testAddresses[1], auth, &expiration) + if err != nil { + panic(err) + } + return msg +} + +func newGenericMsgGrant(testAddresses []sdk.AccAddress, typeUrl string) *authz.MsgGrant { + auth := authz.NewGenericAuthorization(typeUrl) + return newMsgGrant(testAddresses, auth) +} + +func newMsgExec(grantee sdk.AccAddress, msgs []sdk.Msg) *authz.MsgExec { + msg := authz.NewMsgExec(grantee, msgs) + return &msg +} + +func createNestedExecMsgSend(testAddresses []sdk.AccAddress, depth int) *authz.MsgExec { + return createNestedMsgExec( + testAddresses[1], + depth, + []sdk.Msg{ + createMsgSend(testAddresses), + }, + ) +} + +func createMsgSend(testAddresses []sdk.AccAddress) *banktypes.MsgSend { + return banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 1e8)), + ) +} + +func createNestedMsgExec(grantee sdk.AccAddress, numLevels int, msgsToExec []sdk.Msg) *authz.MsgExec { + msgs := make([]*authz.MsgExec, numLevels) + for i := range msgs { + if i == 0 { + msgs[i] = newMsgExec(grantee, msgsToExec) + continue + } + msgs[i] = newMsgExec(grantee, []sdk.Msg{msgs[i-1]}) + } + return msgs[numLevels-1] +} + +func (suite *AnteTestSuite) createTx(priv cryptotypes.PrivKey, msgs ...sdk.Msg) (sdk.Tx, error) { + addr := sdk.AccAddress(priv.PubKey().Address().Bytes()) + args := utiltx.CosmosTxArgs{ + TxCfg: suite.clientCtx.TxConfig, + Priv: priv, + Gas: 1000000, + FeeGranter: addr, + Msgs: msgs, + } + + return utiltx.PrepareCosmosTx(suite.ctx, suite.app, args) +} + +func (suite *AnteTestSuite) createEIP712Tx(priv cryptotypes.PrivKey, msgs ...sdk.Msg) (sdk.Tx, error) { + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + fees := sdk.NewCoins(coinAmount) + cosmosTxArgs := utiltx.CosmosTxArgs{ + TxCfg: suite.clientCtx.TxConfig, + Priv: suite.priv, + ChainID: suite.ctx.ChainID(), + Gas: 200000, + Fees: fees, + Msgs: msgs, + } + + return utiltx.CreateEIP712CosmosTx( + suite.ctx, + suite.app, + utiltx.EIP712TxArgs{ + CosmosTxArgs: cosmosTxArgs, + UseLegacyExtension: true, + }, + ) +} diff --git a/app/ante/eip712.go b/app/ante/eip712.go index 2ea7f6c21a..f5a9bca72c 100644 --- a/app/ante/eip712.go +++ b/app/ante/eip712.go @@ -53,6 +53,8 @@ func init() { func NewLegacyCosmosAnteHandlerEip712(options HandlerOptions) sdk.AnteHandler { return sdk.ChainAnteDecorators( RejectMessagesDecorator{}, // reject MsgEthereumTxs + // disable the Msg types that cannot be included on an authz.MsgExec msgs field + NewAuthzLimiterDecorator(options.DisabledAuthzMsgs), authante.NewSetUpContextDecorator(), authante.NewValidateBasicDecorator(), authante.NewTxTimeoutHeightDecorator(), diff --git a/app/ante/handler_options.go b/app/ante/handler_options.go index b9df6f8289..ab5c1ca179 100644 --- a/app/ante/handler_options.go +++ b/app/ante/handler_options.go @@ -44,6 +44,7 @@ type HandlerOptions struct { MaxTxGasWanted uint64 ExtensionOptionChecker ante.ExtensionOptionChecker TxFeeChecker ante.TxFeeChecker + DisabledAuthzMsgs []string } func (options HandlerOptions) validate() error { @@ -84,6 +85,8 @@ func newEthAnteHandler(options HandlerOptions) sdk.AnteHandler { func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler { return sdk.ChainAnteDecorators( RejectMessagesDecorator{}, // reject MsgEthereumTxs + // disable the Msg types that cannot be included on an authz.MsgExec msgs field + NewAuthzLimiterDecorator(options.DisabledAuthzMsgs), ante.NewSetUpContextDecorator(), ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), ante.NewValidateBasicDecorator(), diff --git a/app/ante/utils_test.go b/app/ante/utils_test.go index 838ad63f7f..8db74635a4 100644 --- a/app/ante/utils_test.go +++ b/app/ante/utils_test.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/evmos/ethermint/ethereum/eip712" + "github.com/evmos/ethermint/testutil" "github.com/evmos/ethermint/types" "github.com/ethereum/go-ethereum/common" @@ -43,6 +44,7 @@ import ( "github.com/evmos/ethermint/crypto/ethsecp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" evtypes "github.com/cosmos/cosmos-sdk/x/evidence/types" "github.com/cosmos/cosmos-sdk/x/feegrant" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" @@ -65,6 +67,7 @@ type AnteTestSuite struct { app *app.EthermintApp clientCtx client.Context anteHandler sdk.AnteHandler + priv cryptotypes.PrivKey ethSigner ethtypes.Signer enableFeemarket bool enableLondonHF bool @@ -79,6 +82,9 @@ func (suite *AnteTestSuite) StateDB() *statedb.StateDB { func (suite *AnteTestSuite) SetupTest() { checkTx := false + priv, err := ethsecp256k1.GenerateKey() + suite.Require().NoError(err) + suite.priv = priv suite.app = app.Setup(checkTx, func(app *app.EthermintApp, genesis simapp.GenesisState) simapp.GenesisState { if suite.enableFeemarket { @@ -109,7 +115,7 @@ func (suite *AnteTestSuite) SetupTest() { return genesis }) - suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{Height: 2, ChainID: "ethermint_9000-1", Time: time.Now().UTC()}) + suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{Height: 2, ChainID: testutil.TestnetChainID + "-1", Time: time.Now().UTC()}) suite.ctx = suite.ctx.WithMinGasPrices(sdk.NewDecCoins(sdk.NewDecCoin(evmtypes.DefaultEVMDenom, sdk.OneInt()))) suite.ctx = suite.ctx.WithBlockGasMeter(sdk.NewGasMeter(1000000000000000000)) suite.app.EvmKeeper.WithChainID(suite.ctx) @@ -117,6 +123,10 @@ func (suite *AnteTestSuite) SetupTest() { infCtx := suite.ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) suite.app.AccountKeeper.SetParams(infCtx, authtypes.DefaultParams()) + addr := sdk.AccAddress(priv.PubKey().Address().Bytes()) + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + encodingConfig := encoding.MakeConfig(app.ModuleBasics) // We're using TestMsg amino encoding in some tests, so register it here. encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) @@ -133,11 +143,30 @@ func (suite *AnteTestSuite) SetupTest() { FeeMarketKeeper: suite.app.FeeMarketKeeper, SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + DisabledAuthzMsgs: []string{ + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&vestingtypes.MsgCreateVestingAccount{}), + }, }) suite.Require().NoError(err) suite.anteHandler = anteHandler suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID()) + + // fund signer acc to pay for tx fees + amt := sdk.NewInt(int64(math.Pow10(18) * 2)) + err = testutil.FundAccount( + suite.app.BankKeeper, + suite.ctx, + suite.priv.PubKey().Address().Bytes(), + sdk.NewCoins(sdk.NewCoin(testutil.BaseDenom, amt)), + ) + suite.Require().NoError(err) + + header := suite.ctx.BlockHeader() + suite.ctx = suite.ctx.WithBlockHeight(header.Height - 1) + suite.ctx, err = testutil.Commit(suite.ctx, suite.app, time.Second*0, nil) + suite.Require().NoError(err) } func TestAnteTestSuite(t *testing.T) { diff --git a/app/app.go b/app/app.go index dd744e25dd..1223274df5 100644 --- a/app/app.go +++ b/app/app.go @@ -666,6 +666,10 @@ func (app *EthermintApp) setAnteHandler(txConfig client.TxConfig, maxGasWanted u MaxTxGasWanted: maxGasWanted, ExtensionOptionChecker: ethermint.HasDynamicFeeExtensionOption, TxFeeChecker: ante.NewDynamicFeeChecker(app.EvmKeeper), + DisabledAuthzMsgs: []string{ + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&vestingtypes.MsgCreateVestingAccount{}), + }, }) if err != nil { panic(err) diff --git a/testutil/abci.go b/testutil/abci.go new file mode 100644 index 0000000000..f744ea7937 --- /dev/null +++ b/testutil/abci.go @@ -0,0 +1,64 @@ +package testutil + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/evmos/ethermint/app" +) + +// Commit commits a block at a given time. Reminder: At the end of each +// Tendermint Consensus round the following methods are run +// 1. BeginBlock +// 2. DeliverTx +// 3. EndBlock +// 4. Commit +func Commit(ctx sdk.Context, app *app.EthermintApp, t time.Duration, vs *tmtypes.ValidatorSet) (sdk.Context, error) { + header := ctx.BlockHeader() + + if vs != nil { + res := app.EndBlock(abci.RequestEndBlock{Height: header.Height}) + + nextVals, err := applyValSetChanges(vs, res.ValidatorUpdates) + if err != nil { + return ctx, err + } + header.ValidatorsHash = vs.Hash() + header.NextValidatorsHash = nextVals.Hash() + } else { + app.EndBlocker(ctx, abci.RequestEndBlock{Height: header.Height}) + } + + _ = app.Commit() + + header.Height++ + header.Time = header.Time.Add(t) + header.AppHash = app.LastCommitID().Hash + + app.BeginBlock(abci.RequestBeginBlock{ + Header: header, + }) + + return ctx.WithBlockHeader(header), nil +} + +// applyValSetChanges takes in tmtypes.ValidatorSet and []abci.ValidatorUpdate and will return a new tmtypes.ValidatorSet which has the +// provided validator updates applied to the provided validator set. +func applyValSetChanges(valSet *tmtypes.ValidatorSet, valUpdates []abci.ValidatorUpdate) (*tmtypes.ValidatorSet, error) { + updates, err := tmtypes.PB2TM.ValidatorUpdates(valUpdates) + if err != nil { + return nil, err + } + + // must copy since validator set will mutate with UpdateWithChangeSet + newVals := valSet.Copy() + err = newVals.UpdateWithChangeSet(updates) + if err != nil { + return nil, err + } + + return newVals, nil +} diff --git a/testutil/constants.go b/testutil/constants.go new file mode 100644 index 0000000000..ad209729e2 --- /dev/null +++ b/testutil/constants.go @@ -0,0 +1,8 @@ +package testutil + +const ( + // TestnetChainID defines the Evmos EIP155 chain ID for testnet + TestnetChainID = "ethermint_9000" + // BaseDenom defines the Evmos mainnet denomination + BaseDenom = "aphoton" +) diff --git a/testutil/fund.go b/testutil/fund.go index 94be7349f7..e4f3652570 100644 --- a/testutil/fund.go +++ b/testutil/fund.go @@ -18,6 +18,7 @@ package testutil import ( sdk "github.com/cosmos/cosmos-sdk/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" ) @@ -25,18 +26,18 @@ import ( // sending the coins to the address. This should be used for testing purposes // only! func FundAccount(bankKeeper bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, amounts sdk.Coins) error { - if err := bankKeeper.MintCoins(ctx, evmtypes.ModuleName, amounts); err != nil { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { return err } - return bankKeeper.SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, addr, amounts) + return bankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, amounts) } // FundModuleAccount is a utility function that funds a module account by // minting and sending the coins to the address. This should be used for testing // purposes only! func FundModuleAccount(bankKeeper bankkeeper.Keeper, ctx sdk.Context, recipientMod string, amounts sdk.Coins) error { - if err := bankKeeper.MintCoins(ctx, evmtypes.ModuleName, amounts); err != nil { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { return err } diff --git a/testutil/tx/cosmos.go b/testutil/tx/cosmos.go new file mode 100644 index 0000000000..1e2ad4e250 --- /dev/null +++ b/testutil/tx/cosmos.go @@ -0,0 +1,143 @@ +// Copyright 2021 Evmos Foundation +// This file is part of Evmos' Ethermint library. +// +// The Ethermint library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Ethermint library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE +package tx + +import ( + "math" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/evmos/ethermint/app" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +var ( + feeAmt = math.Pow10(16) + DefaultFee = sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewIntFromUint64(uint64(feeAmt))) +) + +// CosmosTxArgs contains the params to create a cosmos tx +type CosmosTxArgs struct { + // TxCfg is the client transaction config + TxCfg client.TxConfig + // Priv is the private key that will be used to sign the tx + Priv cryptotypes.PrivKey + // ChainID is the chain's id on cosmos format, e.g. 'ethermint_9000-1' + ChainID string + // Gas to be used on the tx + Gas uint64 + // GasPrice to use on tx + GasPrice *sdkmath.Int + // Fees is the fee to be used on the tx (amount and denom) + Fees sdk.Coins + // FeeGranter is the account address of the fee granter + FeeGranter sdk.AccAddress + // Msgs slice of messages to include on the tx + Msgs []sdk.Msg +} + +// PrepareCosmosTx creates a cosmos tx and signs it with the provided messages and private key. +// It returns the signed transaction and an error +func PrepareCosmosTx( + ctx sdk.Context, + appEthermint *app.EthermintApp, + args CosmosTxArgs, +) (authsigning.Tx, error) { + txBuilder := args.TxCfg.NewTxBuilder() + + txBuilder.SetGasLimit(args.Gas) + + var fees sdk.Coins + if args.GasPrice != nil { + fees = sdk.Coins{{Denom: evmtypes.DefaultEVMDenom, Amount: args.GasPrice.MulRaw(int64(args.Gas))}} + } else { + fees = sdk.Coins{DefaultFee} + } + + txBuilder.SetFeeAmount(fees) + if err := txBuilder.SetMsgs(args.Msgs...); err != nil { + return nil, err + } + + txBuilder.SetFeeGranter(args.FeeGranter) + + return signCosmosTx( + ctx, + appEthermint, + args, + txBuilder, + ) +} + +// signCosmosTx signs the cosmos transaction on the txBuilder provided using +// the provided private key +func signCosmosTx( + ctx sdk.Context, + appEthermint *app.EthermintApp, + args CosmosTxArgs, + txBuilder client.TxBuilder, +) (authsigning.Tx, error) { + addr := sdk.AccAddress(args.Priv.PubKey().Address().Bytes()) + seq, err := appEthermint.AccountKeeper.GetSequence(ctx, addr) + if err != nil { + return nil, err + } + + // First round: we gather all the signer infos. We use the "set empty + // signature" hack to do that. + sigV2 := signing.SignatureV2{ + PubKey: args.Priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: args.TxCfg.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: seq, + } + + sigsV2 := []signing.SignatureV2{sigV2} + + if err := txBuilder.SetSignatures(sigsV2...); err != nil { + return nil, err + } + + // Second round: all signer infos are set, so each signer can sign. + accNumber := appEthermint.AccountKeeper.GetAccount(ctx, addr).GetAccountNumber() + signerData := authsigning.SignerData{ + ChainID: args.ChainID, + AccountNumber: accNumber, + Sequence: seq, + } + sigV2, err = tx.SignWithPrivKey( + args.TxCfg.SignModeHandler().DefaultMode(), + signerData, + txBuilder, args.Priv, args.TxCfg, + seq, + ) + if err != nil { + return nil, err + } + + sigsV2 = []signing.SignatureV2{sigV2} + if err = txBuilder.SetSignatures(sigsV2...); err != nil { + return nil, err + } + return txBuilder.GetTx(), nil +} diff --git a/testutil/tx/eip712.go b/testutil/tx/eip712.go new file mode 100644 index 0000000000..2aa130c846 --- /dev/null +++ b/testutil/tx/eip712.go @@ -0,0 +1,272 @@ +// Copyright 2021 Evmos Foundation +// This file is part of Evmos' Ethermint library. +// +// The Ethermint library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Ethermint library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE +package tx + +import ( + "errors" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/crypto" + cryptocodec "github.com/evmos/ethermint/crypto/codec" + + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/evmos/ethermint/ethereum/eip712" + + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/evmos/ethermint/types" + + "github.com/evmos/ethermint/app" +) + +type EIP712TxArgs struct { + CosmosTxArgs CosmosTxArgs + UseLegacyExtension bool +} + +type typedDataArgs struct { + chainID uint64 + data []byte + legacyFeePayer sdk.AccAddress + legacyMsg sdk.Msg +} + +type legacyWeb3ExtensionArgs struct { + feePayer string + chainID uint64 + signature []byte +} + +type signatureV2Args struct { + pubKey cryptotypes.PubKey + signature []byte + nonce uint64 +} + +// CreateEIP712CosmosTx creates a cosmos tx for typed data according to EIP712. +// Also, signs the tx with the provided messages and private key. +// It returns the signed transaction and an error +func CreateEIP712CosmosTx( + ctx sdk.Context, + appEthermint *app.EthermintApp, + args EIP712TxArgs, +) (sdk.Tx, error) { + builder, err := PrepareEIP712CosmosTx( + ctx, + appEthermint, + args, + ) + return builder.GetTx(), err +} + +// PrepareEIP712CosmosTx creates a cosmos tx for typed data according to EIP712. +// Also, signs the tx with the provided messages and private key. +// It returns the tx builder with the signed transaction and an error +func PrepareEIP712CosmosTx( + ctx sdk.Context, + appEthermint *app.EthermintApp, + args EIP712TxArgs, +) (client.TxBuilder, error) { + txArgs := args.CosmosTxArgs + + pc, err := types.ParseChainID(txArgs.ChainID) + if err != nil { + return nil, err + } + chainIDNum := pc.Uint64() + + fmt.Println("args ", txArgs.Priv) + from := sdk.AccAddress(txArgs.Priv.PubKey().Address().Bytes()) + fmt.Println("from ", from) + acc := appEthermint.AccountKeeper.GetAccount(ctx, from) + + fmt.Println("acc: ", acc) + accNumber := acc.GetAccountNumber() + + nonce, err := appEthermint.AccountKeeper.GetSequence(ctx, from) + if err != nil { + return nil, err + } + + fee := legacytx.NewStdFee(txArgs.Gas, txArgs.Fees) //nolint: staticcheck + + msgs := txArgs.Msgs + data := legacytx.StdSignBytes(ctx.ChainID(), accNumber, nonce, 0, fee, msgs, "", nil) + + typedDataArgs := typedDataArgs{ + chainID: chainIDNum, + data: data, + legacyFeePayer: from, + legacyMsg: msgs[0], + } + + typedData, err := createTypedData(typedDataArgs) + if err != nil { + return nil, err + } + + txBuilder := txArgs.TxCfg.NewTxBuilder() + builder, ok := txBuilder.(authtx.ExtensionOptionsTxBuilder) + if !ok { + return nil, errors.New("txBuilder could not be casted to authtx.ExtensionOptionsTxBuilder type") + } + + builder.SetFeeAmount(fee.Amount) + builder.SetGasLimit(txArgs.Gas) + + err = builder.SetMsgs(txArgs.Msgs...) + if err != nil { + return nil, err + } + + return signCosmosEIP712Tx( + ctx, + appEthermint, + args, + builder, + chainIDNum, + typedData, + ) +} + +// createTypedData creates the TypedData object corresponding to +// the arguments. +func createTypedData(args typedDataArgs) (apitypes.TypedData, error) { + registry := codectypes.NewInterfaceRegistry() + types.RegisterInterfaces(registry) + cryptocodec.RegisterInterfaces(registry) + evmosCodec := codec.NewProtoCodec(registry) + + feeDelegation := &eip712.FeeDelegationOptions{ + FeePayer: args.legacyFeePayer, + } + + return eip712.WrapTxToTypedData( + evmosCodec, + args.chainID, + args.legacyMsg, + args.data, + feeDelegation, + ) +} + +// signCosmosEIP712Tx signs the cosmos transaction on the txBuilder provided using +// the provided private key and the typed data +func signCosmosEIP712Tx( + ctx sdk.Context, + appEvmos *app.EthermintApp, + args EIP712TxArgs, + builder authtx.ExtensionOptionsTxBuilder, + chainID uint64, + data apitypes.TypedData, +) (client.TxBuilder, error) { + priv := args.CosmosTxArgs.Priv + + from := sdk.AccAddress(priv.PubKey().Address().Bytes()) + nonce, err := appEvmos.AccountKeeper.GetSequence(ctx, from) + if err != nil { + return nil, err + } + + sigHash, _, err := apitypes.TypedDataAndHash(data) + if err != nil { + return nil, err + } + + keyringSigner := NewSigner(priv) + signature, pubKey, err := keyringSigner.SignByAddress(from, sigHash) + if err != nil { + return nil, err + } + signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + + if args.UseLegacyExtension { + if err := setBuilderLegacyWeb3Extension( + builder, + legacyWeb3ExtensionArgs{ + feePayer: from.String(), + chainID: chainID, + signature: signature, + }); err != nil { + return nil, err + } + } + + sigsV2 := getTxSignatureV2( + signatureV2Args{ + pubKey: pubKey, + signature: signature, + nonce: nonce, + }, + args.UseLegacyExtension, + ) + + err = builder.SetSignatures(sigsV2) + if err != nil { + return nil, err + } + + return builder, nil +} + +// getTxSignatureV2 returns the SignatureV2 object corresponding to +// the arguments, using the legacy implementation as needed. +func getTxSignatureV2(args signatureV2Args, useLegacyExtension bool) signing.SignatureV2 { + if useLegacyExtension { + return signing.SignatureV2{ + PubKey: args.pubKey, + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, + }, + Sequence: args.nonce, + } + } + + // Must use SIGN_MODE_DIRECT, since Amino has some trouble parsing certain Any values from a SignDoc + // with the Legacy EIP-712 TypedData encodings. This is not an issue with the latest encoding. + return signing.SignatureV2{ + PubKey: args.pubKey, + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + Signature: args.signature, + }, + Sequence: args.nonce, + } +} + +// setBuilderLegacyWeb3Extension creates a legacy ExtensionOptionsWeb3Tx and +// appends it to the builder options. +func setBuilderLegacyWeb3Extension(builder authtx.ExtensionOptionsTxBuilder, args legacyWeb3ExtensionArgs) error { + option, err := codectypes.NewAnyWithValue(&types.ExtensionOptionsWeb3Tx{ + FeePayer: args.feePayer, + TypedDataChainID: args.chainID, + FeePayerSig: args.signature, + }) + if err != nil { + return err + } + + builder.SetExtensionOptions(option) + return nil +} diff --git a/testutil/tx/signer.go b/testutil/tx/signer.go new file mode 100644 index 0000000000..2d0d6a4417 --- /dev/null +++ b/testutil/tx/signer.go @@ -0,0 +1,67 @@ +// Copyright 2021 Evmos Foundation +// This file is part of Evmos' Ethermint library. +// +// The Ethermint library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Ethermint library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE +package tx + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/evmos/ethermint/crypto/ethsecp256k1" +) + +var _ keyring.Signer = &Signer{} + +// Signer defines a type that is used on testing for signing MsgEthereumTx +type Signer struct { + privKey cryptotypes.PrivKey +} + +func NewSigner(sk cryptotypes.PrivKey) keyring.Signer { + return &Signer{ + privKey: sk, + } +} + +// Sign signs the message using the underlying private key +func (s Signer) Sign(_ string, msg []byte) ([]byte, cryptotypes.PubKey, error) { + if s.privKey.Type() != ethsecp256k1.KeyType { + return nil, nil, fmt.Errorf( + "invalid private key type for signing ethereum tx; expected %s, got %s", + ethsecp256k1.KeyType, + s.privKey.Type(), + ) + } + + sig, err := s.privKey.Sign(msg) + if err != nil { + return nil, nil, err + } + + return sig, s.privKey.PubKey(), nil +} + +// SignByAddress sign byte messages with a user key providing the address. +func (s Signer) SignByAddress(address sdk.Address, msg []byte) ([]byte, cryptotypes.PubKey, error) { + signer := sdk.AccAddress(s.privKey.PubKey().Address()) + if !signer.Equals(address) { + return nil, nil, fmt.Errorf("address mismatch: signer %s ≠ given address %s", signer, address) + } + + return s.Sign("", msg) +}