Skip to content

Commit

Permalink
Merge pull request #6836 from onflow/mpeter/evm-events-abi-decode-ass…
Browse files Browse the repository at this point in the history
…ert-errors

Include human-friendly error messages on EVM transaction events
  • Loading branch information
janezpodhostnik authored Jan 13, 2025
2 parents 5ba9d90 + e682cd2 commit d50ad45
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 1 deletion.
2 changes: 1 addition & 1 deletion fvm/evm/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (p *transactionEvent) ToCadence(chainID flow.ChainID) (cadence.Event, error
cadence.NewUInt8(p.Result.TxType),
bytesToCadenceUInt8ArrayValue(p.Payload),
cadence.NewUInt16(uint16(p.Result.ResultSummary().ErrorCode)),
cadence.String(p.Result.ErrorMsg()),
cadence.String(p.Result.ErrorMessageWithRevertReason()),
cadence.NewUInt64(p.Result.GasConsumed),
cadence.String(p.Result.DeployedContractAddressString()),
bytesToCadenceUInt8ArrayValue(encodedLogs),
Expand Down
168 changes: 168 additions & 0 deletions fvm/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,174 @@ func TestEVMRun(t *testing.T) {
assert.Equal(t, num, last.Big().Int64())
})
})

t.Run("testing EVM.run execution reverted with assert error", func(t *testing.T) {

t.Parallel()

RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
sc := systemcontracts.SystemContractsForChain(chain.ChainID())
code := []byte(fmt.Sprintf(
`
import EVM from %s
transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){
prepare(account: &Account) {
let coinbase = EVM.EVMAddress(bytes: coinbaseBytes)
let res = EVM.run(tx: tx, coinbase: coinbase)
assert(res.status == EVM.Status.failed, message: "unexpected status")
assert(res.errorCode == 306, message: "unexpected error code")
assert(res.deployedContract == nil, message: "unexpected deployed contract")
}
}
`,
sc.EVMContract.Address.HexWithPrefix(),
))

coinbaseAddr := types.Address{1, 2, 3}
coinbaseBalance := getEVMAccountBalance(t, ctx, vm, snapshot, coinbaseAddr)
require.Zero(t, types.BalanceToBigInt(coinbaseBalance).Uint64())

innerTxBytes := testAccount.PrepareSignAndEncodeTx(t,
testContract.DeployedAt.ToCommon(),
testContract.MakeCallData(t, "assertError"),
big.NewInt(0),
uint64(100_000),
big.NewInt(1),
)

innerTx := cadence.NewArray(
ConvertToCadence(innerTxBytes),
).WithType(stdlib.EVMTransactionBytesCadenceType)

coinbase := cadence.NewArray(
ConvertToCadence(coinbaseAddr.Bytes()),
).WithType(stdlib.EVMAddressBytesCadenceType)

tx := fvm.Transaction(
flow.NewTransactionBody().
SetScript(code).
AddAuthorizer(sc.FlowServiceAccount.Address).
AddArgument(json.MustEncode(innerTx)).
AddArgument(json.MustEncode(coinbase)),
0)

state, output, err := vm.Run(
ctx,
tx,
snapshot,
)
require.NoError(t, err)
require.NoError(t, output.Err)
require.NotEmpty(t, state.WriteSet)

// assert event fields are correct
require.Len(t, output.Events, 2)
txEvent := output.Events[0]
txEventPayload := TxEventToPayload(t, txEvent, sc.EVMContract.Address)
require.NoError(t, err)

assert.Equal(
t,
"execution reverted: Assert Error Message",
txEventPayload.ErrorMessage,
)
},
)
})

t.Run("testing EVM.run execution reverted with custom error", func(t *testing.T) {

t.Parallel()

RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
sc := systemcontracts.SystemContractsForChain(chain.ChainID())
code := []byte(fmt.Sprintf(
`
import EVM from %s
transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){
prepare(account: &Account) {
let coinbase = EVM.EVMAddress(bytes: coinbaseBytes)
let res = EVM.run(tx: tx, coinbase: coinbase)
assert(res.status == EVM.Status.failed, message: "unexpected status")
assert(res.errorCode == 306, message: "unexpected error code")
assert(res.deployedContract == nil, message: "unexpected deployed contract")
}
}
`,
sc.EVMContract.Address.HexWithPrefix(),
))

coinbaseAddr := types.Address{1, 2, 3}
coinbaseBalance := getEVMAccountBalance(t, ctx, vm, snapshot, coinbaseAddr)
require.Zero(t, types.BalanceToBigInt(coinbaseBalance).Uint64())

innerTxBytes := testAccount.PrepareSignAndEncodeTx(t,
testContract.DeployedAt.ToCommon(),
testContract.MakeCallData(t, "customError"),
big.NewInt(0),
uint64(100_000),
big.NewInt(1),
)

innerTx := cadence.NewArray(
ConvertToCadence(innerTxBytes),
).WithType(stdlib.EVMTransactionBytesCadenceType)

coinbase := cadence.NewArray(
ConvertToCadence(coinbaseAddr.Bytes()),
).WithType(stdlib.EVMAddressBytesCadenceType)

tx := fvm.Transaction(
flow.NewTransactionBody().
SetScript(code).
AddAuthorizer(sc.FlowServiceAccount.Address).
AddArgument(json.MustEncode(innerTx)).
AddArgument(json.MustEncode(coinbase)),
0)

state, output, err := vm.Run(
ctx,
tx,
snapshot,
)
require.NoError(t, err)
require.NoError(t, output.Err)
require.NotEmpty(t, state.WriteSet)

// assert event fields are correct
require.Len(t, output.Events, 2)
txEvent := output.Events[0]
txEventPayload := TxEventToPayload(t, txEvent, sc.EVMContract.Address)
require.NoError(t, err)

// Unlike assert errors, custom errors cannot be further examined
// or ABI decoded, as we do not have access to the contract's ABI.
assert.Equal(
t,
"execution reverted",
txEventPayload.ErrorMessage,
)
},
)
})
}

func TestEVMBatchRun(t *testing.T) {
Expand Down
20 changes: 20 additions & 0 deletions fvm/evm/types/result.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package types

import (
"fmt"

"github.com/onflow/go-ethereum/accounts/abi"
gethCommon "github.com/onflow/go-ethereum/common"
gethTypes "github.com/onflow/go-ethereum/core/types"
gethVM "github.com/onflow/go-ethereum/core/vm"
"github.com/onflow/go-ethereum/rlp"
)

Expand Down Expand Up @@ -144,6 +148,22 @@ func (res *Result) ErrorMsg() string {
return errorMsg
}

// ErrorMessageWithRevertReason returns the error message, if any VM or Validation
// error occurred. Execution reverts coming from `assert` or `require` Solidity
// statements, are parsed into their human-friendly representation.
func (res *Result) ErrorMessageWithRevertReason() string {
errorMessage := res.ErrorMsg()

if res.ResultSummary().ErrorCode == ExecutionErrCodeExecutionReverted {
reason, errUnpack := abi.UnpackRevert(res.ReturnedData)
if errUnpack == nil {
errorMessage = fmt.Sprintf("%v: %v", gethVM.ErrExecutionReverted.Error(), reason)
}
}

return errorMessage
}

// RLPEncodedLogs returns the rlp encoding of the logs
func (res *Result) RLPEncodedLogs() ([]byte, error) {
var encodedLogs []byte
Expand Down

0 comments on commit d50ad45

Please sign in to comment.