fear(eip712): Add EIP-712 encoding for multiple messages of the same type (#1483)

* Add EIP-712 encoding for multiple messages of the same type

* Fix Protobuf encoding bug

* Add ante tests

* Refactor naming and minor implementation details

* Test empty transaction coverage

* Address revisions for code clarity

* Move aminoMessage type definition
This commit is contained in:
Austin Chandra 2022-11-23 04:38:23 -08:00 committed by GitHub
parent f0f3810bf4
commit 9c41edb674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 219 additions and 75 deletions

View File

@ -387,7 +387,7 @@ func (suite AnteTestSuite) TestAnteHandler() {
from, grantee, &banktypes.SendAuthorization{SpendLimit: gasAmount}, &expiresAt,
)
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, privKey, "ethermint_9000-1", gas, gasAmount, msg).GetTx()
return suite.CreateTestEIP712SingleMessageTxBuilder(from, privKey, "ethermint_9000-1", gas, gasAmount, msg).GetTx()
}, false, false, true,
},
@ -457,6 +457,28 @@ func (suite AnteTestSuite) TestAnteHandler() {
return txBuilder.GetTx()
}, false, false, true,
},
{
"success- DeliverTx EIP712 Multiple MsgSend",
func() sdk.Tx {
from := acc.GetAddress()
coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20))
amount := sdk.NewCoins(coinAmount)
gas := uint64(200000)
txBuilder := suite.CreateTestEIP712MultipleMsgSend(from, privKey, "ethermint_9000-1", gas, amount)
return txBuilder.GetTx()
}, false, false, true,
},
{
"fails - DeliverTx EIP712 Multiple Signers",
func() sdk.Tx {
from := acc.GetAddress()
coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdk.NewInt(20))
amount := sdk.NewCoins(coinAmount)
gas := uint64(200000)
txBuilder := suite.CreateTestEIP712MultipleSignerMsgs(from, privKey, "ethermint_9000-1", gas, amount)
return txBuilder.GetTx()
}, false, false, false,
},
{
"fails - DeliverTx EIP712 signed Cosmos Tx with wrong Chain ID",
func() sdk.Tx {

View File

@ -272,7 +272,7 @@ func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgSend(from sdk.AccAddress
// Build MsgSend
recipient := sdk.AccAddress(common.Address{}.Bytes())
msgSend := types2.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(1))))
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgSend)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, msgSend)
}
func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgDelegate(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
@ -280,7 +280,7 @@ func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgDelegate(from sdk.AccAdd
valEthAddr := tests.GenerateAddress()
valAddr := sdk.ValAddress(valEthAddr.Bytes())
msgSend := types3.NewMsgDelegate(from, valAddr, sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20)))
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgSend)
return suite.CreateTestEIP712SingleMessageTxBuilder(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 {
@ -296,7 +296,7 @@ func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator(from sdk.AccAddre
sdk.OneInt(),
)
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgCreate)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, msgCreate)
}
func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator2(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
@ -313,7 +313,7 @@ func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator2(from sdk.AccAddr
sdk.OneInt(),
)
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgCreate)
return suite.CreateTestEIP712SingleMessageTxBuilder(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 {
@ -321,7 +321,7 @@ func (suite *AnteTestSuite) CreateTestEIP712SubmitProposal(from sdk.AccAddress,
suite.Require().True(ok)
msgSubmit, err := types5.NewMsgSubmitProposal(proposal, deposit, from)
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgSubmit)
return suite.CreateTestEIP712SingleMessageTxBuilder(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 {
@ -335,7 +335,7 @@ func (suite *AnteTestSuite) CreateTestEIP712GrantAllowance(from sdk.AccAddress,
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)
return suite.CreateTestEIP712SingleMessageTxBuilder(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 {
@ -346,7 +346,7 @@ func (suite *AnteTestSuite) CreateTestEIP712MsgEditValidator(from sdk.AccAddress
nil,
nil,
)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgEdit)
return suite.CreateTestEIP712SingleMessageTxBuilder(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 {
@ -359,12 +359,12 @@ func (suite *AnteTestSuite) CreateTestEIP712MsgSubmitEvidence(from sdk.AccAddres
})
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgEvidence)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, msgEvidence)
}
func (suite *AnteTestSuite) CreateTestEIP712MsgVoteV1(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
msgVote := govtypes.NewMsgVote(from, 1, govtypes.VoteOption_VOTE_OPTION_YES, "")
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgVote)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, msgVote)
}
func (suite *AnteTestSuite) CreateTestEIP712SubmitProposalV1(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
@ -403,14 +403,28 @@ func (suite *AnteTestSuite) CreateTestEIP712SubmitProposalV1(from sdk.AccAddress
suite.Require().NoError(err)
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, msgProposal)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, msgProposal)
}
func (suite *AnteTestSuite) CreateTestEIP712MsgExec(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
recipient := sdk.AccAddress(common.Address{}.Bytes())
msgSend := types2.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(1))))
msgExec := authz.NewMsgExec(from, []sdk.Msg{msgSend})
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, &msgExec)
return suite.CreateTestEIP712SingleMessageTxBuilder(from, priv, chainId, gas, gasAmount, &msgExec)
}
func (suite *AnteTestSuite) CreateTestEIP712MultipleMsgSend(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
recipient := sdk.AccAddress(common.Address{}.Bytes())
msgSend := types2.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(1))))
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, []sdk.Msg{msgSend, msgSend, msgSend})
}
// Fails
func (suite *AnteTestSuite) CreateTestEIP712MultipleSignerMsgs(from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins) client.TxBuilder {
recipient := sdk.AccAddress(common.Address{}.Bytes())
msgSend1 := types2.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(1))))
msgSend2 := types2.NewMsgSend(recipient, from, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(1))))
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, []sdk.Msg{msgSend1, msgSend2})
}
// StdSignBytes returns the bytes to sign for a transaction.
@ -451,8 +465,14 @@ func StdSignBytes(cdc *codec.LegacyAmino, chainID string, accnum uint64, sequenc
return sdk.MustSortJSON(bz)
}
func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder(
func (suite *AnteTestSuite) CreateTestEIP712SingleMessageTxBuilder(
from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins, msg sdk.Msg,
) client.TxBuilder {
return suite.CreateTestEIP712CosmosTxBuilder(from, priv, chainId, gas, gasAmount, []sdk.Msg{msg})
}
func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder(
from sdk.AccAddress, priv cryptotypes.PrivKey, chainId string, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg,
) client.TxBuilder {
var err error
@ -473,8 +493,8 @@ func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder(
fee := legacytx.NewStdFee(gas, gasAmount)
accNumber := suite.app.AccountKeeper.GetAccount(suite.ctx, from).GetAccountNumber()
data := legacytx.StdSignBytes(chainId, accNumber, nonce, 0, fee, []sdk.Msg{msg}, "", nil)
typedData, err := eip712.WrapTxToTypedData(ethermintCodec, ethChainId, msg, data, &eip712.FeeDelegationOptions{
data := legacytx.StdSignBytes(chainId, accNumber, nonce, 0, fee, msgs, "", nil)
typedData, err := eip712.WrapTxToTypedData(ethermintCodec, ethChainId, msgs[0], data, &eip712.FeeDelegationOptions{
FeePayer: from,
})
suite.Require().NoError(err)
@ -517,7 +537,7 @@ func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder(
err = builder.SetSignatures(sigsV2)
suite.Require().NoError(err)
err = builder.SetMsgs(msg)
err = builder.SetMsgs(msgs...)
suite.Require().NoError(err)
return builder

View File

@ -93,6 +93,9 @@ func (suite *EIP712TestSuite) TestEIP712SignatureVerification() {
signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
}
// Fixed test address
testAddress := suite.createTestAddress()
testCases := []struct {
title string
chainId string
@ -176,7 +179,30 @@ func (suite *EIP712TestSuite) TestEIP712SignatureVerification() {
expectSuccess: true,
},
{
title: "Fails - Two MsgVotes",
title: "Succeeds - Two Single-Signer MsgDelegate",
fee: txtypes.Fee{
Amount: suite.makeCoins("aphoton", math.NewInt(2000)),
GasLimit: 20000,
},
memo: "",
msgs: []sdk.Msg{
stakingtypes.NewMsgDelegate(
testAddress,
sdk.ValAddress(suite.createTestAddress()),
suite.makeCoins("photon", math.NewInt(1))[0],
),
stakingtypes.NewMsgDelegate(
testAddress,
sdk.ValAddress(suite.createTestAddress()),
suite.makeCoins("photon", math.NewInt(5))[0],
),
},
accountNumber: 25,
sequence: 78,
expectSuccess: true,
},
{
title: "Fails - Two MsgVotes with Different Signers",
fee: txtypes.Fee{
Amount: suite.makeCoins("aphoton", math.NewInt(2000)),
GasLimit: 20000,
@ -196,10 +222,22 @@ func (suite *EIP712TestSuite) TestEIP712SignatureVerification() {
},
accountNumber: 25,
sequence: 78,
expectSuccess: false, // Multiple messages are currently not allowed
expectSuccess: false,
},
{
title: "Fails - MsgSend + MsgVote",
title: "Fails - Empty transaction",
fee: txtypes.Fee{
Amount: suite.makeCoins("aphoton", math.NewInt(2000)),
GasLimit: 20000,
},
memo: "",
msgs: []sdk.Msg{},
accountNumber: 25,
sequence: 78,
expectSuccess: false,
},
{
title: "Fails - Single-Signer MsgSend + MsgVote",
fee: txtypes.Fee{
Amount: suite.makeCoins("aphoton", math.NewInt(2000)),
GasLimit: 20000,
@ -207,12 +245,12 @@ func (suite *EIP712TestSuite) TestEIP712SignatureVerification() {
memo: "",
msgs: []sdk.Msg{
govtypes.NewMsgVote(
suite.createTestAddress(),
testAddress,
5,
govtypes.OptionNo,
),
banktypes.NewMsgSend(
suite.createTestAddress(),
testAddress,
suite.createTestAddress(),
suite.makeCoins("photon", math.NewInt(50)),
),

View File

@ -1,13 +1,14 @@
package eip712
import (
"encoding/json"
"errors"
"fmt"
"github.com/cosmos/cosmos-sdk/simapp/params"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"
cosmosTypes "github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
txTypes "github.com/cosmos/cosmos-sdk/types/tx"
apitypes "github.com/ethereum/go-ethereum/signer/core/apitypes"
@ -16,9 +17,14 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
)
type aminoMessage struct {
Type string `json:"type"`
Value interface{} `json:"value"`
}
var (
ethermintProtoCodec codec.ProtoCodecMarshaler
ethermintAminoCodec *codec.LegacyAmino
protoCodec codec.ProtoCodecMarshaler
aminoCodec *codec.LegacyAmino
)
// SetEncodingConfig set the encoding config to the singleton codecs (Amino and Protobuf).
@ -26,8 +32,8 @@ var (
// populated with all relevant message types. As a result, we must call this method on app
// initialization with the app's encoding config.
func SetEncodingConfig(cfg params.EncodingConfig) {
ethermintAminoCodec = cfg.Amino
ethermintProtoCodec = codec.NewProtoCodec(cfg.InterfaceRegistry)
aminoCodec = cfg.Amino
protoCodec = codec.NewProtoCodec(cfg.InterfaceRegistry)
}
// Get the EIP-712 object hash for the given SignDoc bytes by first decoding the bytes into
@ -59,67 +65,69 @@ func GetEIP712TypedDataForMsg(signDocBytes []byte) (apitypes.TypedData, error) {
// Attempt to decode as both Amino and Protobuf since the message format is unknown.
// If either decode works, we can move forward with the corresponding typed data.
typedDataAmino, errAmino := decodeAminoSignDoc(signDocBytes)
if errAmino == nil && verifyEIP712Payload(typedDataAmino) {
if errAmino == nil && isValidEIP712Payload(typedDataAmino) {
return typedDataAmino, nil
}
typedDataProtobuf, errProtobuf := decodeProtobufSignDoc(signDocBytes)
if errProtobuf == nil && verifyEIP712Payload(typedDataProtobuf) {
if errProtobuf == nil && isValidEIP712Payload(typedDataProtobuf) {
return typedDataProtobuf, nil
}
return apitypes.TypedData{}, fmt.Errorf("could not decode sign doc as either Amino or Protobuf. amino: %v protobuf: %v", errAmino, errProtobuf)
return apitypes.TypedData{}, fmt.Errorf("could not decode sign doc as either Amino or Protobuf.\n amino: %v\n protobuf: %v", errAmino, errProtobuf)
}
// verifyEIP712Payload ensures that the given TypedData does not contain empty fields from
// isValidEIP712Payload ensures that the given TypedData does not contain empty fields from
// an improper initialization.
func verifyEIP712Payload(typedData apitypes.TypedData) bool {
func isValidEIP712Payload(typedData apitypes.TypedData) bool {
return len(typedData.Message) != 0 && len(typedData.Types) != 0 && typedData.PrimaryType != "" && typedData.Domain != apitypes.TypedDataDomain{}
}
// Attempt to decode the SignDoc bytes as an Amino SignDoc and return an error on failure
// decodeAminoSignDoc attempts to decode the provided sign doc (bytes) as an Amino payload
// and returns a signable EIP-712 TypedData object.
func decodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
var aminoDoc legacytx.StdSignDoc
if err := ethermintAminoCodec.UnmarshalJSON(signDocBytes, &aminoDoc); err != nil {
if err := aminoCodec.UnmarshalJSON(signDocBytes, &aminoDoc); err != nil {
return apitypes.TypedData{}, err
}
// Unwrap fees
var fees legacytx.StdFee
if err := ethermintAminoCodec.UnmarshalJSON(aminoDoc.Fee, &fees); err != nil {
if err := aminoCodec.UnmarshalJSON(aminoDoc.Fee, &fees); err != nil {
return apitypes.TypedData{}, err
}
if len(aminoDoc.Msgs) != 1 {
return apitypes.TypedData{}, fmt.Errorf("invalid number of messages in SignDoc, expected 1 but got %v", len(aminoDoc.Msgs))
// Validate payload messages
msgs := make([]sdk.Msg, len(aminoDoc.Msgs))
for i, jsonMsg := range aminoDoc.Msgs {
var m sdk.Msg
if err := aminoCodec.UnmarshalJSON(jsonMsg, &m); err != nil {
return apitypes.TypedData{}, fmt.Errorf("failed to unmarshal sign doc message: %w", err)
}
msgs[i] = m
}
var msg cosmosTypes.Msg
if err := ethermintAminoCodec.UnmarshalJSON(aminoDoc.Msgs[0], &msg); err != nil {
return apitypes.TypedData{}, fmt.Errorf("failed to unmarshal first message: %w", err)
if err := validatePayloadMessages(msgs); err != nil {
return apitypes.TypedData{}, err
}
// By default, use first address in list of signers to cover fee
// Currently, support only one signer
if len(msg.GetSigners()) != 1 {
return apitypes.TypedData{}, errors.New("expected exactly one signer for message")
}
// Use first message for fee payer and type inference
msg := msgs[0]
// By convention, the fee payer is the first address in the list of signers.
feePayer := msg.GetSigners()[0]
feeDelegation := &FeeDelegationOptions{
FeePayer: feePayer,
}
// Parse ChainID
chainID, err := ethermint.ParseChainID(aminoDoc.ChainID)
if err != nil {
return apitypes.TypedData{}, errors.New("invalid chain ID passed as argument")
}
typedData, err := WrapTxToTypedData(
ethermintProtoCodec,
protoCodec,
chainID.Uint64(),
msg,
signDocBytes, // Amino StdSignDocBytes
signDocBytes,
feeDelegation,
)
if err != nil {
@ -129,21 +137,19 @@ func decodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
return typedData, nil
}
// Attempt to decode the SignDoc bytes as a Protobuf SignDoc and return an error on failure
// decodeProtobufSignDoc attempts to decode the provided sign doc (bytes) as a Protobuf payload
// and returns a signable EIP-712 TypedData object.
func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
// Decode sign doc
signDoc := &txTypes.SignDoc{}
if err := signDoc.Unmarshal(signDocBytes); err != nil {
return apitypes.TypedData{}, err
}
// Decode auth info
authInfo := &txTypes.AuthInfo{}
if err := authInfo.Unmarshal(signDoc.AuthInfoBytes); err != nil {
return apitypes.TypedData{}, err
}
// Decode body
body := &txTypes.TxBody{}
if err := body.Unmarshal(signDoc.BodyBytes); err != nil {
return apitypes.TypedData{}, err
@ -154,60 +160,60 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
return apitypes.TypedData{}, errors.New("body contains unsupported fields: TimeoutHeight, ExtensionOptions, or NonCriticalExtensionOptions")
}
// Verify single message
if len(body.Messages) != 1 {
return apitypes.TypedData{}, fmt.Errorf("invalid number of messages, expected 1 got %v", len(body.Messages))
if len(authInfo.SignerInfos) != 1 {
return apitypes.TypedData{}, fmt.Errorf("invalid number of signer infos provided, expected 1 got %v", len(authInfo.SignerInfos))
}
// Decode signer info (single signer for now)
// Validate payload messages
msgs := make([]sdk.Msg, len(body.Messages))
for i, protoMsg := range body.Messages {
var m sdk.Msg
if err := protoCodec.UnpackAny(protoMsg, &m); err != nil {
return apitypes.TypedData{}, fmt.Errorf("could not unpack message object with error %w", err)
}
msgs[i] = m
}
if err := validatePayloadMessages(msgs); err != nil {
return apitypes.TypedData{}, err
}
// Use first message for fee payer and type inference
msg := msgs[0]
signerInfo := authInfo.SignerInfos[0]
// Parse ChainID
chainID, err := ethermint.ParseChainID(signDoc.ChainId)
if err != nil {
return apitypes.TypedData{}, fmt.Errorf("invalid chain ID passed as argument: %w", err)
}
// Create StdFee
stdFee := &legacytx.StdFee{
Amount: authInfo.Fee.Amount,
Gas: authInfo.Fee.GasLimit,
}
// Parse Message (single message only)
var msg cosmosTypes.Msg
if err := ethermintProtoCodec.UnpackAny(body.Messages[0], &msg); err != nil {
return apitypes.TypedData{}, fmt.Errorf("could not unpack message object with error %w", err)
}
// Verify single signer (single signer for now)
if len(msg.GetSigners()) != 1 {
return apitypes.TypedData{}, fmt.Errorf("invalid number of signers, expected 1 got %v", len(authInfo.SignerInfos))
}
// Init fee payer
feePayer := msg.GetSigners()[0]
feeDelegation := &FeeDelegationOptions{
FeePayer: feePayer,
}
// Get tip
tip := authInfo.Tip
// Create Legacy SignBytes (expected type for WrapTxToTypedData)
// WrapTxToTypedData expects the payload as an Amino Sign Doc
signBytes := legacytx.StdSignBytes(
signDoc.ChainId,
signDoc.AccountNumber,
signerInfo.Sequence,
body.TimeoutHeight,
*stdFee,
[]cosmosTypes.Msg{msg},
msgs,
body.Memo,
tip,
)
typedData, err := WrapTxToTypedData(
ethermintProtoCodec,
protoCodec,
chainID.Uint64(),
msg,
signBytes,
@ -219,3 +225,61 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
return typedData, nil
}
// validatePayloadMessages ensures that the transaction messages can be represented in an EIP-712
// encoding by checking that messages exist, are of the same type, and share a single signer.
func validatePayloadMessages(msgs []sdk.Msg) error {
if len(msgs) == 0 {
return errors.New("unable to build EIP-712 payload: transaction does contain any messages")
}
var msgType string
var msgSigner sdk.AccAddress
for i, m := range msgs {
t, err := getMsgType(m)
if err != nil {
return err
}
if len(m.GetSigners()) != 1 {
return errors.New("unable to build EIP-712 payload: expect exactly 1 signer")
}
if i == 0 {
msgType = t
msgSigner = m.GetSigners()[0]
continue
}
if t != msgType {
return errors.New("unable to build EIP-712 payload: different types of messages detected")
}
if !msgSigner.Equals(m.GetSigners()[0]) {
return errors.New("unable to build EIP-712 payload: multiple signers detected")
}
}
return nil
}
// getMsgType returns the message type prefix for the given Cosmos SDK Msg
func getMsgType(msg sdk.Msg) (string, error) {
jsonBytes, err := aminoCodec.MarshalJSON(msg)
if err != nil {
return "", err
}
var jsonMsg aminoMessage
if err := json.Unmarshal(jsonBytes, &jsonMsg); err != nil {
return "", err
}
// Verify Type was successfully filled in
if jsonMsg.Type == "" {
return "", errors.New("could not decode message: type is missing")
}
return jsonMsg.Type, nil
}