diff --git a/abi/revert.go b/abi/revert.go new file mode 100644 index 000000000..43fa4b7df --- /dev/null +++ b/abi/revert.go @@ -0,0 +1,76 @@ +package abi + +import ( + "bytes" + "errors" + "fmt" + "math/big" + + ethabi "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/crypto" +) + +// revertSelector is a special function selector for revert reason unpacking. +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] + +// 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", +} + +// UnpackRevert resolves the abi-encoded revert reason. According to the solidity +// spec https://solidity.readthedocs.io/en/latest/control-structures.html#revert, +// the provided revert reason is abi-encoded as if it were a call to function +// `Error(string)` or `Panic(uint256)`. So it's a special tool for it. +func UnpackRevert(data []byte) (string, error) { + if len(data) < 4 { + return "", errors.New("invalid data for unpacking") + } + switch { + case bytes.Equal(data[:4], revertSelector): + typ, err := ethabi.NewType("string") + if err != nil { + return "", err + } + var unpacked string + if err := (ethabi.Arguments{{Type: typ}}).Unpack(&unpacked, data[4:]); err != nil { + return "", err + } + return unpacked, nil + case bytes.Equal(data[:4], panicSelector): + typ, err := ethabi.NewType("uint256") + if err != nil { + return "", err + } + var pCode *big.Int + if err := (ethabi.Arguments{{Type: typ}}).Unpack(&pCode, data[4:]); err != nil { + return "", err + } + // uint64 safety check for future + // but the code is not bigger than MAX(uint64) now + if pCode.IsUint64() { + if reason, ok := panicReasons[pCode.Uint64()]; ok { + return reason, nil + } + } + return fmt.Sprintf("unknown panic code: %#x", pCode), nil + default: + return "", errors.New("invalid data for unpacking") + } +} diff --git a/abi/revert_test.go b/abi/revert_test.go new file mode 100644 index 000000000..089d7ceef --- /dev/null +++ b/abi/revert_test.go @@ -0,0 +1,43 @@ +package abi + +import ( + "errors" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +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 { + 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) + } + }) + } +} diff --git a/tracers/native/call.go b/tracers/native/call.go index 110989c71..5b6286779 100644 --- a/tracers/native/call.go +++ b/tracers/native/call.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/vechain/thor/v2/abi" "github.com/vechain/thor/v2/tracers" "github.com/vechain/thor/v2/vm" ) @@ -41,16 +42,17 @@ type callLog struct { } type callFrame struct { - Type vm.OpCode `json:"-"` - From common.Address `json:"from"` - Gas uint64 `json:"gas"` - GasUsed uint64 `json:"gasUsed"` - To *common.Address `json:"to,omitempty" rlp:"optional"` - Input []byte `json:"input" rlp:"optional"` - Output []byte `json:"output,omitempty" rlp:"optional"` - Error string `json:"error,omitempty" rlp:"optional"` - Calls []callFrame `json:"calls,omitempty" rlp:"optional"` - Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas uint64 `json:"gas"` + GasUsed uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input []byte `json:"input" rlp:"optional"` + Output []byte `json:"output,omitempty" rlp:"optional"` + Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` + Calls []callFrame `json:"calls,omitempty" rlp:"optional"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` // Placed at end on purpose. The RLP will be decoded to 0 instead of // nil if there are non-empty elements after in the struct. Value *big.Int `json:"value,omitempty" rlp:"optional"` @@ -78,6 +80,12 @@ func (f *callFrame) processOutput(output []byte, err error) { return } f.Output = output + if len(output) < 4 { + return + } + if unpacked, err := abi.UnpackRevert(output); err == nil { + f.RevertReason = unpacked + } } type callFrameMarshaling struct { diff --git a/tracers/native/gen_callframe_json.go b/tracers/native/gen_callframe_json.go index 5df134aa5..accbdc6ac 100644 --- a/tracers/native/gen_callframe_json.go +++ b/tracers/native/gen_callframe_json.go @@ -24,6 +24,7 @@ func (c callFrame) MarshalJSON() ([]byte, error) { Input hexutil.Bytes `json:"input" rlp:"optional"` Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` Calls []callFrame `json:"calls,omitempty" rlp:"optional"` Logs []callLog `json:"logs,omitempty" rlp:"optional"` Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` @@ -38,6 +39,7 @@ func (c callFrame) MarshalJSON() ([]byte, error) { enc.Input = c.Input enc.Output = c.Output enc.Error = c.Error + enc.RevertReason = c.RevertReason enc.Calls = c.Calls enc.Logs = c.Logs enc.Value = (*hexutil.Big)(c.Value) @@ -56,6 +58,7 @@ func (c *callFrame) UnmarshalJSON(input []byte) error { Input *hexutil.Bytes `json:"input" rlp:"optional"` Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"` Error *string `json:"error,omitempty" rlp:"optional"` + RevertReason *string `json:"revertReason,omitempty"` Calls []callFrame `json:"calls,omitempty" rlp:"optional"` Logs []callLog `json:"logs,omitempty" rlp:"optional"` Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` @@ -88,6 +91,9 @@ func (c *callFrame) UnmarshalJSON(input []byte) error { if dec.Error != nil { c.Error = *dec.Error } + if dec.RevertReason != nil { + c.RevertReason = *dec.RevertReason + } if dec.Calls != nil { c.Calls = dec.Calls }