Skip to content

Commit

Permalink
Merge pull request #521 from oasisprotocol/pro-wh/feature/abitoken
Browse files Browse the repository at this point in the history
 runtime: use abiparse in VisitEVMEvent
  • Loading branch information
pro-wh authored Oct 19, 2023
2 parents 9e33fae + 3b93bac commit 63cd391
Show file tree
Hide file tree
Showing 12 changed files with 1,360 additions and 186 deletions.
14 changes: 7 additions & 7 deletions analyzer/evmabi/evmabi.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
)

func mustUnmarshalABI(artifactJSON []byte) *abi.ABI {
func MustUnmarshalABI(artifactJSON []byte) *abi.ABI {
var artifact struct {
ABI *abi.ABI
}
Expand All @@ -19,24 +19,24 @@ func mustUnmarshalABI(artifactJSON []byte) *abi.ABI {

//go:embed contracts/artifacts/ERC20.json
var artifactERC20JSON []byte
var ERC20 = mustUnmarshalABI(artifactERC20JSON)
var ERC20 = MustUnmarshalABI(artifactERC20JSON)

//go:embed contracts/artifacts/ERC165.json
var artifactERC165JSON []byte
var ERC165 = mustUnmarshalABI(artifactERC165JSON)
var ERC165 = MustUnmarshalABI(artifactERC165JSON)

//go:embed contracts/artifacts/ERC721.json
var artifactERC721JSON []byte
var ERC721 = mustUnmarshalABI(artifactERC721JSON)
var ERC721 = MustUnmarshalABI(artifactERC721JSON)

//go:embed contracts/artifacts/ERC721TokenReceiver.json
var artifactERC721TokenReceiverJSON []byte
var ERC721TokenReceiver = mustUnmarshalABI(artifactERC721TokenReceiverJSON)
var ERC721TokenReceiver = MustUnmarshalABI(artifactERC721TokenReceiverJSON)

//go:embed contracts/artifacts/ERC721Metadata.json
var artifactERC721MetadataJSON []byte
var ERC721Metadata = mustUnmarshalABI(artifactERC721MetadataJSON)
var ERC721Metadata = MustUnmarshalABI(artifactERC721MetadataJSON)

//go:embed contracts/artifacts/ERC721Enumerable.json
var artifactERC721EnumerableJSON []byte
var ERC721Enumerable = mustUnmarshalABI(artifactERC721EnumerableJSON)
var ERC721Enumerable = MustUnmarshalABI(artifactERC721EnumerableJSON)
156 changes: 156 additions & 0 deletions analyzer/runtime/abiparse/abiparse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package abiparse

import (
"fmt"
"reflect"

"github.com/ethereum/go-ethereum/accounts/abi"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)

// evmPreMarshal converts v to a type that gives us the JSON serialization that we like:
// - large integers are JSON strings instead of JSON numbers
// - byte array types are JSON strings of base64 instead of JSON arrays of numbers
// Contrived dot for godot linter: .
func evmPreMarshal(v interface{}, t abi.Type) interface{} {
switch t.T {
case abi.IntTy, abi.UintTy:
if t.Size > 32 {
return fmt.Sprint(v)
}
case abi.SliceTy, abi.ArrayTy:
rv := reflect.ValueOf(v)
slice := make([]interface{}, 0, rv.Len())
for i := 0; i < rv.Len(); i++ {
slice = append(slice, evmPreMarshal(rv.Index(i).Interface(), *t.Elem))
}
return slice
case abi.TupleTy:
rv := reflect.ValueOf(v)
m := map[string]interface{}{}
for i, fieldName := range t.TupleRawNames {
m[fieldName] = evmPreMarshal(rv.Field(i).Interface(), *t.TupleElems[i])
}
case abi.FixedBytesTy, abi.FunctionTy:
c := reflect.New(t.GetType()).Elem()
c.Set(reflect.ValueOf(v))
return hexutil.Encode(c.Bytes())
case abi.BytesTy:
return hexutil.Encode(reflect.ValueOf(v).Bytes())
}
return v
}

// ParseData parses call data into the method and its arguments.
func ParseData(data []byte, contractABI *abi.ABI) (*abi.Method, []interface{}, error) {
if len(data) < 4 {
return nil, nil, fmt.Errorf("data (%dB) too short to have method ID", len(data))
}
methodID := data[:4]
packedArgs := data[4:]
method, err := contractABI.MethodById(methodID)
if err != nil {
return nil, nil, fmt.Errorf("contract ABI MethodById: %w", err)
}
args, err := method.Inputs.Unpack(packedArgs)
if err != nil {
return nil, nil, fmt.Errorf("method inputs Unpack: %w", err)
}
return method, args, nil
}

func ParseResult(result []byte, method *abi.Method) ([]interface{}, error) {
args, err := method.Outputs.Unpack(result)
if err != nil {
return nil, fmt.Errorf("method outputs Unpack: %w", err)
}
return args, nil
}

func parseOneSimple(data []byte, t abi.Type) (interface{}, error) {
oneIn := abi.Arguments{abi.Argument{Type: t}}
oneArg, err := oneIn.Unpack(data)
if err != nil {
return nil, err
}
return oneArg[0], nil
}

func ParseEvent(topics [][]byte, data []byte, contractABI *abi.ABI) (*abi.Event, []interface{}, error) {
if len(topics) < 1 {
return nil, nil, fmt.Errorf("topics (%d) too short to have event signature", len(topics))
}
topicsEC := make([]ethCommon.Hash, 0, len(topics))
for _, topic := range topics {
topicsEC = append(topicsEC, ethCommon.BytesToHash(topic))
}
event, err := contractABI.EventByID(topicsEC[0])
if err != nil {
return nil, nil, fmt.Errorf("contract ABI EventByID: %w", err)
}

argsFromData, err := event.Inputs.Unpack(data)
if err != nil {
return nil, nil, fmt.Errorf("event inputs Unpack: %w", err)
}

args := make([]interface{}, len(event.Inputs))
nextTopicIndex := 0
if !event.Anonymous {
// Non-anonymous events use the hash of the event signature as the
// first topic value. That topic is implicit; the value is not
// associated with any argument, so skip over it.
nextTopicIndex = 1
}
nextDataIndex := 0

for i, in := range event.Inputs {
if in.Indexed {
switch in.Type.T {
case abi.StringTy, abi.SliceTy, abi.ArrayTy, abi.TupleTy, abi.BytesTy:
// https://docs.soliditylang.org/en/latest/abi-spec.html
// > However, for all “complex” types or types of dynamic
// > length, including all arrays, string, bytes and structs,
// > EVENT_INDEXED_ARGS will contain the Keccak hash of a
// > special in-place encoded value ..., rather than the
// > encoded value directly.
// Make a little wrapper to help viewers know it's only the hash.
args[i] = map[string]ethCommon.Hash{"hash": topicsEC[nextTopicIndex]}
nextTopicIndex++
default:
// > For all types of length at most 32 bytes, the
// > EVENT_INDEXED_ARGS array contains the value directly,
// > padded or sign-extended (for signed integers) to 32
// > bytes, just as for regular ABI encoding.
args[i], err = parseOneSimple(topics[nextTopicIndex], in.Type)
if err != nil {
return nil, nil, fmt.Errorf("event input %d topic %d Unpack: %w", i, nextTopicIndex, err)
}
nextTopicIndex++
}
} else {
args[i] = argsFromData[nextDataIndex]
nextDataIndex++
}
}
return event, args, nil
}

func ParseError(data []byte, contractABI *abi.ABI) (*abi.Error, []interface{}, error) {
if len(data) < 4 {
return nil, nil, fmt.Errorf("data (%dB) too short to have error ID", len(data))
}
var errorID [4]byte
copy(errorID[:], data[:4])
packedArgs := data[4:]
abiError, err := contractABI.ErrorByID(errorID)
if err != nil {
return nil, nil, fmt.Errorf("contract ABI ErrorById: %w", err)
}
args, err := abiError.Inputs.Unpack(packedArgs)
if err != nil {
return nil, nil, fmt.Errorf("error inputs Unpack: %w", err)
}
return abiError, args, nil
}
100 changes: 80 additions & 20 deletions api/logic/evm_test.go → analyzer/runtime/abiparse/abiparse_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package logic
package abiparse

import (
_ "embed"
Expand All @@ -7,7 +7,6 @@ import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

Expand All @@ -16,23 +15,17 @@ import (

//go:embed test_contracts/artifacts/Varied.json
var artifactVariedJSON []byte
var Varied *abi.ABI
var Varied = evmabi.MustUnmarshalABI(artifactVariedJSON)

func init() {
type artifact struct {
ABI *abi.ABI
}
var artifactVaried artifact
if err := json.Unmarshal(artifactVariedJSON, &artifactVaried); err != nil {
panic(err)
}
Varied = artifactVaried.ABI
}
//go:embed test_contracts/artifacts/CustomErrors.json
var artifactCustomErrorsJSON []byte
var CustomErrors = evmabi.MustUnmarshalABI(artifactCustomErrorsJSON)

func TestEVMParseTypes(t *testing.T) {
func TestParseTypes(t *testing.T) {
// -1,1,-1,1,true,0x0101010101010101010101010101010101010101010101010101010101010101,0x0101010101010101010101010101010101010101,0x010101010101010101010101010101010101010102020202,[1,1],[1,1],0x01,"a",[1],[1],[1,"a"]
data, err := hex.DecodeString("b23a194fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010101010101010102020202000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
method, args, err := EVMParseData(data, Varied)
method, args, err := ParseData(data, Varied)
require.NoError(t, err)
require.Equal(t, Varied.Methods["test"], *method)
jsonExpected := []string{
Expand Down Expand Up @@ -60,20 +53,73 @@ func TestEVMParseTypes(t *testing.T) {
}
}

func TestEVMParseData(t *testing.T) {
func TestParseTxUnnamed(t *testing.T) {
data, err := hex.DecodeString("ee799b1800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002")
require.NoError(t, err)
result, err := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004")
require.NoError(t, err)
method, args, err := ParseData(data, Varied)
require.NoError(t, err)
require.Equal(t, Varied.Methods["testUnnamed"], *method)
require.Equal(t, []interface{}{
uint16(1),
uint16(2),
}, args)
outArgs, err := ParseResult(result, method)
require.NoError(t, err)
require.Equal(t, []interface{}{
uint16(3),
uint16(4),
}, outArgs)
}

func TestParseEventUnnamed(t *testing.T) {
topicsHex := []string{
"54789e7ea9ad7e0cf2c8e336a0e24206777d784a11ec566b33ae6402ee7c2518",
"0000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000002",
}
topics := make([][]byte, 0, len(topicsHex))
for _, topicHex := range topicsHex {
topic, err := hex.DecodeString(topicHex)
require.NoError(t, err)
topics = append(topics, topic)
}
data, err := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004")
require.NoError(t, err)
event, args, err := ParseEvent(topics, data, Varied)
require.NoError(t, err)
require.Equal(t, Varied.Events["TestUnnamed"], *event)
require.Equal(t, []interface{}{
uint16(1),
uint16(2),
uint16(3),
uint16(4),
}, args)
}

func TestParseTx(t *testing.T) {
// https://explorer.emerald.oasis.dev/tx/0x1ac7521df4cda38c87cff56b1311ee9362168bd794230415a37f2aff3a554a5f/internal-transactions
data, err := hex.DecodeString("095ea7b3000000000000000000000000250d48c5e78f1e85f7ab07fec61e93ba703ae6680000000000000000000000000000000000000000000000003782dace9d900000")
require.NoError(t, err)
method, args, err := EVMParseData(data, evmabi.ERC20)
// oasis.misc.toHex(oasis.misc.fromCBOR(await networks.mainnet.nic.runtimeClientGetTransactionsWithResults({runtime_id: networks.mainnet.runtimes.emerald.id, round: 5709967})[1].result).ok)
result, err := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000001")
require.NoError(t, err)
method, inArgs, err := ParseData(data, evmabi.ERC20)
require.NoError(t, err)
require.Equal(t, evmabi.ERC20.Methods["approve"], *method)
require.Equal(t, []interface{}{
ethCommon.HexToAddress("0x250d48c5e78f1e85f7ab07fec61e93ba703ae668"),
big.NewInt(4000000000000000000),
}, args)
}, inArgs)
outArgs, err := ParseResult(result, method)
require.NoError(t, err)
require.Equal(t, []interface{}{
true,
}, outArgs)
}

func TestEVMParseEvent(t *testing.T) {
func TestParseEvent(t *testing.T) {
// https://explorer.emerald.oasis.dev/tx/0x1ac7521df4cda38c87cff56b1311ee9362168bd794230415a37f2aff3a554a5f/logs
topicsHex := []string{
"8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
Expand All @@ -88,10 +134,24 @@ func TestEVMParseEvent(t *testing.T) {
}
data, err := hex.DecodeString("0000000000000000000000000000000000000000000000003782dace9d900000")
require.NoError(t, err)
event, args, err := EVMParseEvent(topics, data, evmabi.ERC20)
event, args, err := ParseEvent(topics, data, evmabi.ERC20)
require.NoError(t, err)
require.Equal(t, evmabi.ERC20.Events["Approval"], *event)
require.Equal(t, []interface{}{
ethCommon.HexToAddress("0ecf5262e5b864e1612875f8fc18f151315b5e91"),
ethCommon.HexToAddress("250d48c5e78f1e85f7ab07fec61e93ba703ae668"),
big.NewInt(4000000000000000000),
}, args)
}

func TestParseError(t *testing.T) {
revertBytes, err := hex.DecodeString("41a0efd30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
abiError, args, err := ParseError(revertBytes, CustomErrors)
require.NoError(t, err)
require.Equal(t, CustomErrors.Errors["E"], *abiError)
require.Equal(t, []interface{}{
uint16(1),
"a",
}, args)
}
11 changes: 11 additions & 0 deletions analyzer/runtime/abiparse/test_contracts/CustomErrors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.18;

error E(uint16 n, string s);

contract CustomErrors {
function test() public pure {
revert E(1, "a");
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.21;
pragma solidity ^0.8.18;

struct O {
uint16 n;
string s;
}

error E(address a, uint16 n);

interface Varied {
function test(
int8 i8,
Expand All @@ -28,4 +30,6 @@ interface Varied {
uint8[] calldata l8,
O calldata o
) external;
function testUnnamed(uint16, uint16) external returns (uint16, uint16);
event TestUnnamed(uint16 indexed, uint16 indexed, uint16, uint16);
}
Loading

0 comments on commit 63cd391

Please sign in to comment.