diff --git a/evmd/tests/ibc/ics20_precompile_transfer_test.go b/evmd/tests/ibc/ics20_precompile_transfer_test.go index 623c3ae0f..4cc013636 100644 --- a/evmd/tests/ibc/ics20_precompile_transfer_test.go +++ b/evmd/tests/ibc/ics20_precompile_transfer_test.go @@ -16,9 +16,9 @@ import ( "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/precompiles/ics20" + chainutil "github.com/cosmos/evm/testutil" evmibctesting "github.com/cosmos/evm/testutil/ibc" evmante "github.com/cosmos/evm/x/vm/ante" - evmtypes "github.com/cosmos/evm/x/vm/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" @@ -299,8 +299,8 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { "INVALID-DENOM-HASH", ) suite.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) - revertErr := evmtypes.NewExecErrorWithReason(evmRes.Ret) - suite.Require().Contains(revertErr.ErrorData(), "invalid denom trace hash") + revertErr := chainutil.DecodeRevertReason(*evmRes) + suite.Require().Contains(revertErr.Error(), "invalid denom trace hash") // denomHash query method evmRes, err = evmAppB.EVMKeeper.CallEVM( @@ -347,8 +347,8 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { "", ) suite.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) - revertErr = evmtypes.NewExecErrorWithReason(evmRes.Ret) - suite.Require().Contains(revertErr.ErrorData(), "invalid denomination for cross-chain transfer") + revertErr = chainutil.DecodeRevertReason(*evmRes) + suite.Require().Contains(revertErr.Error(), "invalid denomination for cross-chain transfer") }) } } diff --git a/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go b/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go index db504e7f9..df0e6a021 100644 --- a/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go +++ b/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go @@ -17,9 +17,9 @@ import ( "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/precompiles/ics20" + chainutil "github.com/cosmos/evm/testutil" evmibctesting "github.com/cosmos/evm/testutil/ibc" evmante "github.com/cosmos/evm/x/vm/ante" - evmtypes "github.com/cosmos/evm/x/vm/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" @@ -305,8 +305,8 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { "INVALID-DENOM-HASH", ) suite.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) - revertErr := evmtypes.NewExecErrorWithReason(evmRes.Ret) - suite.Require().Contains(revertErr.ErrorData(), "invalid denom trace hash") + revertErr := chainutil.DecodeRevertReason(*evmRes) + suite.Require().Contains(revertErr.Error(), "invalid denom trace hash") // denomHash query method evmRes, err = evmAppB.EVMKeeper.CallEVM( @@ -353,8 +353,8 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { "", ) suite.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) - revertErr = evmtypes.NewExecErrorWithReason(evmRes.Ret) - suite.Require().Contains(revertErr.ErrorData(), "invalid denomination for cross-chain transfer") + revertErr = chainutil.DecodeRevertReason(*evmRes) + suite.Require().Contains(revertErr.Error(), "invalid denomination for cross-chain transfer") }) } } diff --git a/testutil/integration/evm/factory/factory.go b/testutil/integration/evm/factory/factory.go index 46541d473..6db538b4e 100644 --- a/testutil/integration/evm/factory/factory.go +++ b/testutil/integration/evm/factory/factory.go @@ -12,6 +12,7 @@ import ( abcitypes "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/evm/precompiles/testutil" + chainutil "github.com/cosmos/evm/testutil" basefactory "github.com/cosmos/evm/testutil/integration/base/factory" "github.com/cosmos/evm/testutil/integration/evm/grpc" "github.com/cosmos/evm/testutil/integration/evm/network" @@ -209,8 +210,7 @@ func (tf *IntegrationTxFactory) checkEthTxResponse(res *abcitypes.ExecTxResult) } if evmRes.Failed() { - revertErr := evmtypes.NewExecErrorWithReason(evmRes.Ret) - return fmt.Errorf("tx failed with VmError: %v: %s", evmRes.VmError, revertErr.ErrorData()) + return chainutil.DecodeRevertReason(evmRes) } return nil } diff --git a/testutil/util.go b/testutil/util.go index 2a86ef032..fc5835ce4 100644 --- a/testutil/util.go +++ b/testutil/util.go @@ -1,10 +1,18 @@ package testutil import ( + "bytes" "context" + "fmt" "time" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/cosmos/evm/crypto/ethsecp256k1" + evmtypes "github.com/cosmos/evm/x/vm/types" + + errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" @@ -108,3 +116,39 @@ func CreateTx(ctx context.Context, txCfg client.TxConfig, priv cryptotypes.PrivK return txBuilder.GetTx(), nil } + +// DecodeRevertReason extracts and decodes the human-readable revert reason from an EVM transaction response. +// It processes the raw return data (Ret field) from a failed EVM transaction and attempts to decode +// any ABI-encoded revert messages into readable error strings. +// +// Returns: +// - error: A formatted error containing either: +// - "tx failed with VmError: : " for successfully decoded reverts +// - "tx failed with VmError: : " for non-decodable data +// - "failed to decode revert data: " if decoding fails +// +// Example usage: +// +// res, err := executeTransaction(...) +// if res.VmError != "" { +// decodedErr := DecodeRevertReason(res) +// // decodedErr might be: "tx failed with VmError: execution reverted: ERC20: insufficient allowance" +// } +func DecodeRevertReason(evmRes evmtypes.MsgEthereumTxResponse) error { + revertErr := evmtypes.NewExecErrorWithReason(evmRes.Ret) + hexData, ok := revertErr.ErrorData().(string) + if ok { + decodedBytes, err := hexutil.Decode(hexData) + if err == nil { + if len(decodedBytes) >= 4 && bytes.Equal(decodedBytes[:4], evmtypes.RevertSelector) { + var reason string + reason, err = abi.UnpackRevert(decodedBytes) + if err == nil { + return fmt.Errorf("tx failed with VmError: %v: %s", evmRes.VmError, reason) + } + } + } + return errorsmod.Wrap(err, "failed to decode revert data") + } + return fmt.Errorf("tx failed with VmError: %v: %s", evmRes.VmError, revertErr.ErrorData()) +} diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index 565c3626e..041ef2342 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -444,11 +444,6 @@ func (k *Keeper) ApplyMessageWithConfig( // reset leftoverGas, to be used by the tracer leftoverGas = msg.GasLimit - gasUsed - // if the execution reverted, we return the revert reason as the return data - if vmError == vm.ErrExecutionReverted.Error() { - ret = evm.Interpreter().ReturnData() - } - return &types.MsgEthereumTxResponse{ GasUsed: gasUsed, VmError: vmError, diff --git a/x/vm/types/errors.go b/x/vm/types/errors.go index 007ac35d9..aba448436 100644 --- a/x/vm/types/errors.go +++ b/x/vm/types/errors.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" errorsmod "cosmossdk.io/errors" @@ -122,7 +123,7 @@ func NewExecErrorWithReason(revertReason []byte) *RevertError { } return &RevertError{ error: err, - reason: reason, + reason: hexutil.Encode(result), } } diff --git a/x/vm/types/errors_test.go b/x/vm/types/errors_test.go new file mode 100644 index 000000000..f2e173f88 --- /dev/null +++ b/x/vm/types/errors_test.go @@ -0,0 +1,51 @@ +package types_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" + + "github.com/cosmos/evm/x/vm/types" +) + +func TestNewExecErrorWithReason(t *testing.T) { + testCases := []struct { + name string + errorMessage string + revertReason []byte + data string + }{ + { + "Empty reason", + "execution reverted", + nil, + "0x", + }, + { + "With unpackable reason", + "execution reverted", + []byte("a"), + "0x61", + }, + { + "With packable reason but empty reason", + "execution reverted", + types.RevertSelector, + "0x08c379a0", + }, + { + "With packable reason with reason", + "execution reverted: COUNTER_TOO_LOW", + hexutil.MustDecode("0x08C379A00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000F434F554E5445525F544F4F5F4C4F570000000000000000000000000000000000"), + "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f434f554e5445525f544f4f5f4c4f570000000000000000000000000000000000", + }, + } + + for _, tc := range testCases { + errWithReason := types.NewExecErrorWithReason(tc.revertReason) + require.Equal(t, tc.errorMessage, errWithReason.Error()) + require.Equal(t, tc.data, errWithReason.ErrorData()) + require.Equal(t, 3, errWithReason.ErrorCode()) + } +}