Skip to content

Commit

Permalink
l2geth/rpc: support solidity custom errors (#97)
Browse files Browse the repository at this point in the history
* l2geth/rpc: support solidity custom errors

* Add execution reverted error
  • Loading branch information
ericlee42 authored Jul 9, 2024
1 parent 58b47c0 commit 784dbd8
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 23 deletions.
55 changes: 46 additions & 9 deletions l2geth/accounts/abi/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"io"
"math/big"

"github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum-optimism/optimism/l2geth/crypto"
Expand Down Expand Up @@ -189,13 +190,34 @@ func (abi *ABI) EventByID(topic common.Hash) (*Event, error) {
return nil, fmt.Errorf("no event with id: %#x", topic.Hex())
}

// panicReasons map is for readable panic codes
// see this linkage for the details
// https://docs.soliditylang.org/en/v0.8.21/control-structures.html#panic-via-assert-and-error-via-require
// the reason string list is copied from ether.js
// https://github.com/ethers-io/ethers.js/blob/fa3a883ff7c88611ce766f58bdd4b8ac90814470/src.ts/abi/interface.ts#L207-L218
var panicReasons = map[uint64]string{
0x00: "generic panic",
0x01: "assert(false)",
0x11: "arithmetic underflow or overflow",
0x12: "division or modulo by zero",
0x21: "enum overflow",
0x22: "invalid encoded storage byte array accessed",
0x31: "out-of-bounds array access; popping on an empty array",
0x32: "out-of-bounds access of an array or bytesN",
0x41: "out of memory",
0x51: "uninitialized function",
}

// UsingOVM
// Both RevertSelector and UnpackRevert were pulled from upstream
// geth as they were not present in the version of geth that this
// codebase was forked from. These are useful for displaying revert
// messages to users when they use `eth_call`
// RevertSelector is a special function selector for revert reason unpacking.
var RevertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]
var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]

// panicSelector is a special function selector for panic reason unpacking.
var panicSelector = crypto.Keccak256([]byte("Panic(uint256)"))[:4]

// UnpackRevert resolves the abi-encoded revert reason. According to the solidity
// docs https://docs.soliditylang.org/en/v0.8.4/control-structures.html#revert,
Expand All @@ -205,13 +227,28 @@ func UnpackRevert(data []byte) (string, error) {
if len(data) < 4 {
return "", errors.New("invalid data for unpacking")
}
if !bytes.Equal(data[:4], RevertSelector) {
return "", errors.New("invalid data for unpacking")
}
typ, _ := NewType("string", "", nil)
unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:])
if err != nil {
return "", err
switch {
case bytes.Equal(data[:4], revertSelector):
typ, _ := NewType("string", "", nil)
unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:])
if err != nil {
return "", err
}
return unpacked[0].(string), nil
case bytes.Equal(data[:4], panicSelector):
typ, _ := NewType("uint256", "", nil)
unpacked, err := (Arguments{{Type: typ}}).UnpackValues(data[4:])
if err != nil {
return "", err
}
pCode := unpacked[0].(*big.Int)
if pCode.IsUint64() {
if reason, ok := panicReasons[pCode.Uint64()]; ok {
return reason, nil
}
}
return fmt.Sprintf("unknown panic code: %#x", pCode), nil
}
return unpacked[0].(string), nil

return "", errors.New("invalid data for unpacking")
}
36 changes: 36 additions & 0 deletions l2geth/accounts/abi/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package abi
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"math/big"
"reflect"
Expand Down Expand Up @@ -1054,3 +1055,38 @@ func TestDoubleDuplicateMethodNames(t *testing.T) {
t.Fatalf("Should not have found extra method")
}
}

func TestUnpackRevert(t *testing.T) {
t.Parallel()

var cases = []struct {
input string
expect string
expectErr error
}{
{"", "", errors.New("invalid data for unpacking")},
{"08c379a1", "", errors.New("invalid data for unpacking")},
{"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000", "revert reason", nil},
{"4e487b710000000000000000000000000000000000000000000000000000000000000000", "generic panic", nil},
{"4e487b7100000000000000000000000000000000000000000000000000000000000000ff", "unknown panic code: 0xff", nil},
}
for index, c := range cases {
index, c := index, c
t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) {
t.Parallel()
got, err := UnpackRevert(common.Hex2Bytes(c.input))
if c.expectErr != nil {
if err == nil {
t.Fatalf("Expected non-nil error")
}
if err.Error() != c.expectErr.Error() {
t.Fatalf("Expected error mismatch, want %v, got %v", c.expectErr, err)
}
return
}
if c.expect != got {
t.Fatalf("Output mismatch, want %v, got %v", c.expect, got)
}
})
}
}
17 changes: 3 additions & 14 deletions l2geth/internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (

"github.com/davecgh/go-spew/spew"
"github.com/ethereum-optimism/optimism/l2geth/accounts"
"github.com/ethereum-optimism/optimism/l2geth/accounts/abi"
"github.com/ethereum-optimism/optimism/l2geth/accounts/keystore"
"github.com/ethereum-optimism/optimism/l2geth/accounts/scwallet"
"github.com/ethereum-optimism/optimism/l2geth/common"
Expand Down Expand Up @@ -978,12 +977,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
return nil, err
}
if failed {
reason, errUnpack := abi.UnpackRevert(result)
err := errors.New("execution reverted")
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %v", reason)
}
return (hexutil.Bytes)(result), err
return (hexutil.Bytes)(result), newRevertError(result)
}
return (hexutil.Bytes)(result), err
}
Expand Down Expand Up @@ -1092,13 +1086,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
if hi == cap {
ok, res, _ := executable(hi)
if !ok {
if len(res) >= 4 && bytes.Equal(res[:4], abi.RevertSelector) {
reason, errUnpack := abi.UnpackRevert(res)
err := errors.New("execution reverted")
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %v", reason)
}
return 0, err
if len(res) >= 4 {
return 0, newRevertError(res)
}
return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)
}
Expand Down
41 changes: 41 additions & 0 deletions l2geth/internal/ethapi/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ethapi

import (
"errors"
"fmt"

"github.com/ethereum-optimism/optimism/l2geth/accounts/abi"
"github.com/ethereum-optimism/optimism/l2geth/common/hexutil"
)

// revertError is an API error that encompasses an EVM revert with JSON error
// code and a binary data blob.
type revertError struct {
error
reason string // revert reason hex encoded
}

// ErrorCode returns the JSON error code for a revert.
// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal
func (e *revertError) ErrorCode() int {
return 3
}

// ErrorData returns the hex encoded revert reason.
func (e *revertError) ErrorData() interface{} {
return e.reason
}

// newRevertError creates a revertError instance with the provided revert data.
func newRevertError(revert []byte) *revertError {
err := errors.New("execution reverted")

reason, errUnpack := abi.UnpackRevert(revert)
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %s", reason)
}
return &revertError{
error: err,
reason: hexutil.Encode(revert),
}
}
4 changes: 4 additions & 0 deletions l2geth/rpc/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func errorMessage(err error) *jsonrpcMessage {
if ok {
msg.Error.Code = ec.ErrorCode()
}
de, ok := err.(ErrorWithData)
if ok {
msg.Error.Data = de.ErrorData()
}
return msg
}

Expand Down
5 changes: 5 additions & 0 deletions l2geth/rpc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type Error interface {
ErrorCode() int // returns the code
}

type ErrorWithData interface {
Error() string
ErrorData() interface{}
}

// ServerCodec implements reading, parsing and writing RPC messages for the server side of
// a RPC session. Implementations must be go-routine safe since the codec can be called in
// multiple go-routines concurrently.
Expand Down

0 comments on commit 784dbd8

Please sign in to comment.