diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index 580d84f2f3..a8aebc4830 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -4,14 +4,19 @@ import ( "errors" "math/big" "strings" + "time" sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/types/tx/signing" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + types5 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/ethereum/go-ethereum/core/types" ethparams "github.com/ethereum/go-ethereum/params" "github.com/evmos/ethermint/tests" @@ -322,6 +327,96 @@ func (suite AnteTestSuite) TestAnteHandler() { return txBuilder.GetTx() }, false, false, true, }, + { + "success- DeliverTx EIP712 create validator", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + amount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + txBuilder := suite.CreateTestEIP712MsgCreateValidator(from, privKey, "ethermint_9000-1", gas, amount) + return txBuilder.GetTx() + }, false, false, true, + }, + { + "success- DeliverTx EIP712 MsgSubmitProposal", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + gasAmount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + //reusing the gasAmount for deposit + deposit := sdk.NewCoins(coinAmount) + txBuilder := suite.CreateTestEIP712SubmitProposal(from, privKey, "ethermint_9000-1", gas, gasAmount, deposit) + return txBuilder.GetTx() + }, false, false, true, + }, + { + "success- DeliverTx EIP712 MsgSubmitProposal not text", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + gasAmount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + //reusing the gasAmount for deposit + deposit := sdk.NewCoins(coinAmount) + proposal := distrtypes.NewCommunityPoolSpendProposal("My proposal", "My description", from, gasAmount) + msgSubmit, err := types5.NewMsgSubmitProposal(proposal, deposit, from) + suite.Require().NoError(err) + return suite.CreateTestEIP712CosmosTxBuilder(from, privKey, "ethermint_9000-1", gas, gasAmount, msgSubmit).GetTx() + }, false, false, true, + }, + { + "success- DeliverTx EIP712 MsgGrant", + func() sdk.Tx { + from := acc.GetAddress() + grantee := sdk.AccAddress("_______grantee______") + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + gasAmount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + blockTime := time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC) + expiresAt := blockTime.Add(time.Hour) + msg, err := authz.NewMsgGrant( + from, grantee, &banktypes.SendAuthorization{SpendLimit: gasAmount}, &expiresAt, + ) + suite.Require().NoError(err) + return suite.CreateTestEIP712CosmosTxBuilder(from, privKey, "ethermint_9000-1", gas, gasAmount, msg).GetTx() + }, false, false, true, + }, + + { + "success- DeliverTx EIP712 MsgGrantAllowance", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + gasAmount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + txBuilder := suite.CreateTestEIP712GrantAllowance(from, privKey, "ethermint_9000-1", gas, gasAmount) + return txBuilder.GetTx() + }, false, false, true, + }, + { + "success- DeliverTx EIP712 edit validator", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + amount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + txBuilder := suite.CreateTestEIP712MsgEditValidator(from, privKey, "ethermint_9000-1", gas, amount) + return txBuilder.GetTx() + }, false, false, true, + }, + { + "success- DeliverTx EIP712 submit evidence", + func() sdk.Tx { + from := acc.GetAddress() + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)) + amount := sdk.NewCoins(coinAmount) + gas := uint64(200000) + txBuilder := suite.CreateTestEIP712MsgEditValidator(from, privKey, "ethermint_9000-1", gas, amount) + return txBuilder.GetTx() + }, false, false, true, + }, { "fails - DeliverTx EIP712 signed Cosmos Tx with wrong Chain ID", func() sdk.Tx { diff --git a/app/ante/utils_test.go b/app/ante/utils_test.go index 7a7b9b8941..de892a95b0 100644 --- a/app/ante/utils_test.go +++ b/app/ante/utils_test.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" types2 "github.com/cosmos/cosmos-sdk/x/bank/types" + evtypes "github.com/cosmos/cosmos-sdk/x/evidence/types" types3 "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/crypto" "github.com/evmos/ethermint/ethereum/eip712" @@ -40,6 +41,9 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + types5 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/tendermint/tendermint/crypto/ed25519" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) @@ -267,6 +271,69 @@ func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgDelegate(from sdk.AccAdd return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgSend) } +func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder { + // Build MsgCreateValidator + valAddr := sdk.ValAddress(from.Bytes()) + + msgCreate, err := types3.NewMsgCreateValidator( + valAddr, + priv.PubKey(), + sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20)), + // TODO: can this values be empty strings? + types3.NewDescription("moniker", "indentity", "website", "security_contract", "details"), + types3.NewCommissionRates(sdk.OneDec(), sdk.OneDec(), sdk.OneDec()), + sdk.OneInt(), + ) + suite.Require().NoError(err) + return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgCreate) +} + +func (suite *AnteTestSuite) CreateTestEIP712SubmitProposal(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins, deposit sdk.Coins) client.TxBuilder { + proposal, ok := types5.ContentFromProposalType("My proposal", "My description", types5.ProposalTypeText) + suite.Require().True(ok) + msgSubmit, err := types5.NewMsgSubmitProposal(proposal, deposit, from) + suite.Require().NoError(err) + return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgSubmit) +} + +func (suite *AnteTestSuite) CreateTestEIP712GrantAllowance(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder { + spendLimit := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 10)) + threeHours := time.Now().Add(3 * time.Hour) + basic := &feegrant.BasicAllowance{ + SpendLimit: spendLimit, + Expiration: &threeHours, + } + granted := tests.GenerateAddress() + grantedAddr := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, granted.Bytes()) + msgGrant, err := feegrant.NewMsgGrantAllowance(basic, from, grantedAddr.GetAddress()) + suite.Require().NoError(err) + return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgGrant) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgEditValidator(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder { + valAddr := sdk.ValAddress(from.Bytes()) + msgEdit := types3.NewMsgEditValidator( + valAddr, + types3.NewDescription("moniker", "identity", "website", "security_contract", "details"), + nil, + nil, + ) + return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgEdit) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgSubmitEvidence(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder { + pk := ed25519.GenPrivKey() + msgEvidence, err := evtypes.NewMsgSubmitEvidence(from, &evtypes.Equivocation{ + Height: 11, + Time: time.Now().UTC(), + Power: 100, + ConsensusAddress: pk.PubKey().Address().String(), + }) + suite.Require().NoError(err) + + return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgEvidence) +} + func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder( from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins, msg sdk.Msg, ) client.TxBuilder { @@ -281,6 +348,10 @@ func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder( // GenerateTypedData TypedData var ethermintCodec codec.ProtoCodecMarshaler + registry := codectypes.NewInterfaceRegistry() + types.RegisterInterfaces(registry) + ethermintCodec = codec.NewProtoCodec(registry) + fee := legacytx.NewStdFee(gas, gasAmount) accNumber := suite.app.AccountKeeper.GetAccount(suite.ctx, from).GetAccountNumber() diff --git a/ethereum/eip712/eip712.go b/ethereum/eip712/eip712.go index 2f5b5eea1b..bb9a5f55e8 100644 --- a/ethereum/eip712/eip712.go +++ b/ethereum/eip712/eip712.go @@ -7,11 +7,12 @@ import ( "math/big" "reflect" "strings" + "time" - sdkmath "cosmossdk.io/math" "golang.org/x/text/cases" "golang.org/x/text/language" + sdkmath "cosmossdk.io/math" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -20,6 +21,10 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/tendermint/tendermint/crypto/sr25519" ) // ComputeTypedDataHash computes keccak hash of typed data for signing. @@ -40,6 +45,24 @@ func ComputeTypedDataHash(typedData apitypes.TypedData) ([]byte, error) { return crypto.Keccak256(rawData), nil } +func getAminoMap(data map[string]interface{}, topFieldName string, aminoMap map[string]bool) { + if _, ok := data["type"]; ok && len(data) == 2 { + aminoMap[topFieldName] = true + } + + // switch field.(type) + for fieldName, field := range data { + switch x := field.(type) { + case map[string]interface{}: + getAminoMap(x, fieldName, aminoMap) + case []map[string]interface{}: + for _, i := range field.([]map[string]interface{}) { + getAminoMap(i, fieldName, aminoMap) + } + } + } +} + // WrapTxToTypedData is an ultimate method that wraps Amino-encoded Cosmos Tx JSON data // into an EIP712-compatible TypedData request. func WrapTxToTypedData( @@ -63,7 +86,14 @@ func WrapTxToTypedData( Salt: "0", } - msgTypes, err := extractMsgTypes(cdc, "MsgValue", msg) + aminoMap := make(map[string]bool) + q := txData["msgs"].([]interface{}) + for _, imsg := range q { + msgInterface := imsg.(map[string]interface{}) + getAminoMap(msgInterface, "msg", aminoMap) + } + + msgTypes, err := extractMsgTypes(cdc, "MsgValue", msg, txData, aminoMap) if err != nil { return apitypes.TypedData{}, err } @@ -98,7 +128,7 @@ type FeeDelegationOptions struct { FeePayer sdk.AccAddress } -func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg) (apitypes.Types, error) { +func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg, txData map[string]interface{}, aminoMap map[string]bool) (apitypes.Types, error) { rootTypes := apitypes.Types{ "EIP712Domain": { { @@ -122,16 +152,6 @@ func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg Type: "string", }, }, - "Tx": { - {Name: "account_number", Type: "string"}, - {Name: "chain_id", Type: "string"}, - {Name: "fee", Type: "Fee"}, - {Name: "memo", Type: "string"}, - {Name: "msgs", Type: "Msg[]"}, - {Name: "sequence", Type: "string"}, - // Note timeout_height was removed because it was not getting filled with the legacyTx - // {Name: "timeout_height", Type: "string"}, - }, "Fee": { {Name: "amount", Type: "Coin[]"}, {Name: "gas", Type: "string"}, @@ -140,14 +160,37 @@ func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg {Name: "denom", Type: "string"}, {Name: "amount", Type: "string"}, }, - "Msg": { + msgTypeName: {}, + } + if _, ok := aminoMap["msg"]; ok { + rootTypes["Tx"] = []apitypes.Type{ + {Name: "account_number", Type: "string"}, + {Name: "chain_id", Type: "string"}, + {Name: "fee", Type: "Fee"}, + {Name: "memo", Type: "string"}, + {Name: "msgs", Type: "Msg[]"}, + {Name: "sequence", Type: "string"}, + // Note timeout_height was removed because it was not getting filled with the legacyTx + // {Name: "timeout_height", Type: "string"}, + } + rootTypes["Msg"] = []apitypes.Type{ {Name: "type", Type: "string"}, {Name: "value", Type: msgTypeName}, - }, - msgTypeName: {}, + } + } else { + rootTypes["Tx"] = []apitypes.Type{ + {Name: "account_number", Type: "string"}, + {Name: "chain_id", Type: "string"}, + {Name: "fee", Type: "Fee"}, + {Name: "memo", Type: "string"}, + {Name: "msgs", Type: msgTypeName + "[]"}, + {Name: "sequence", Type: "string"}, + // Note timeout_height was removed because it was not getting filled with the legacyTx + // {Name: "timeout_height", Type: "string"}, + } } - if err := walkFields(cdc, rootTypes, msgTypeName, msg); err != nil { + if err := walkFields(cdc, rootTypes, msgTypeName, msg, aminoMap); err != nil { return nil, err } @@ -156,7 +199,7 @@ func extractMsgTypes(cdc codectypes.AnyUnpacker, msgTypeName string, msg sdk.Msg const typeDefPrefix = "_" -func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType string, in interface{}) (err error) { +func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType string, in interface{}, aminoMap map[string]bool) (err error) { defer doRecover(&err) t := reflect.TypeOf(in) @@ -174,7 +217,7 @@ func walkFields(cdc codectypes.AnyUnpacker, typeMap apitypes.Types, rootType str break } - return traverseFields(cdc, typeMap, rootType, typeDefPrefix, t, v) + return traverseFields(cdc, typeMap, rootType, typeDefPrefix, t, v, aminoMap) } type cosmosAnyWrapper struct { @@ -189,6 +232,7 @@ func traverseFields( prefix string, t reflect.Type, v reflect.Value, + aminoMap map[string]bool, ) error { n := t.NumField() @@ -213,25 +257,41 @@ func traverseFields( fieldName := jsonNameFromTag(t.Field(i).Tag) if fieldType == cosmosAnyType { + any, ok := field.Interface().(*codectypes.Any) if !ok { return sdkerrors.Wrapf(sdkerrors.ErrPackAny, "%T", field.Interface()) } - anyWrapper := &cosmosAnyWrapper{ - Type: any.TypeUrl, - } + if _, ok := aminoMap[fieldName]; ok { + anyWrapper := &cosmosAnyWrapper{ + Type: any.TypeUrl, + } - if err := cdc.UnpackAny(any, &anyWrapper.Value); err != nil { - return sdkerrors.Wrap(err, "failed to unpack Any in msg struct") - } + if err := cdc.UnpackAny(any, &anyWrapper.Value); err != nil { + return sdkerrors.Wrap(err, "failed to unpack Any in msg struct") + } - fieldType = reflect.TypeOf(anyWrapper) - field = reflect.ValueOf(anyWrapper) + fieldType = reflect.TypeOf(anyWrapper) + field = reflect.ValueOf(anyWrapper) + } else { + + var Value interface{} + if err := cdc.UnpackAny(any, &Value); err != nil { + return sdkerrors.Wrap(err, "failed to unpack Any in msg struct") + } + fieldType = reflect.TypeOf(Value) + field = reflect.ValueOf(Value) + } // then continue as normal } + // If its a nil pointer, do not include in types + if fieldType.Kind() == reflect.Ptr && field.IsNil() { + continue + } + for { if fieldType.Kind() == reflect.Ptr { fieldType = fieldType.Elem() @@ -296,6 +356,11 @@ func traverseFields( ethTyp := typToEth(fieldType) if len(ethTyp) > 0 { + // Support array of uint64 + if isCollection && fieldType.Kind() != reflect.Slice && fieldType.Kind() != reflect.Array { + ethTyp += "[]" + } + if prefix == typeDefPrefix { typeMap[rootType] = append(typeMap[rootType], apitypes.Type{ Name: fieldName, @@ -335,7 +400,7 @@ func traverseFields( }) } - if err := traverseFields(cdc, typeMap, rootType, fieldPrefix, fieldType, field); err != nil { + if err := traverseFields(cdc, typeMap, rootType, fieldPrefix, fieldType, field, aminoMap); err != nil { return err } @@ -381,7 +446,13 @@ var ( addressType = reflect.TypeOf(common.Address{}) bigIntType = reflect.TypeOf(big.Int{}) cosmIntType = reflect.TypeOf(sdkmath.Int{}) + cosmDecType = reflect.TypeOf(sdk.Dec{}) cosmosAnyType = reflect.TypeOf(&codectypes.Any{}) + timeType = reflect.TypeOf(time.Time{}) + + edType = reflect.TypeOf(ed25519.PubKey{}) + secpType = reflect.TypeOf(secp256k1.PubKey{}) + srType = reflect.TypeOf(sr25519.PubKey{}) ) // typToEth supports only basic types and arrays of basic types. @@ -426,6 +497,11 @@ func typToEth(typ reflect.Type) string { } case reflect.Ptr: if typ.Elem().ConvertibleTo(bigIntType) || + typ.Elem().ConvertibleTo(timeType) || + typ.Elem().ConvertibleTo(edType) || + typ.Elem().ConvertibleTo(srType) || + typ.Elem().ConvertibleTo(secpType) || + typ.Elem().ConvertibleTo(cosmDecType) || typ.Elem().ConvertibleTo(cosmIntType) { return str } @@ -433,6 +509,11 @@ func typToEth(typ reflect.Type) string { if typ.ConvertibleTo(hashType) || typ.ConvertibleTo(addressType) || typ.ConvertibleTo(bigIntType) || + typ.ConvertibleTo(timeType) || + typ.ConvertibleTo(edType) || + typ.ConvertibleTo(srType) || + typ.ConvertibleTo(secpType) || + typ.ConvertibleTo(cosmDecType) || typ.ConvertibleTo(cosmIntType) { return str } diff --git a/go.mod b/go.mod index f29cd71873..f5afd515c9 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/tendermint/tendermint v0.34.20 github.com/tendermint/tm-db v0.6.7 github.com/tyler-smith/go-bip39 v1.1.0 + golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 golang.org/x/text v0.3.7 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 google.golang.org/grpc v1.48.0 @@ -167,7 +168,6 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect