Skip to content

Commit

Permalink
feature: ERC1155 decoding, and decoding in CallFrames
Browse files Browse the repository at this point in the history
  • Loading branch information
tyler-smith committed Sep 18, 2023
1 parent 1429529 commit 3f09ca1
Show file tree
Hide file tree
Showing 25 changed files with 2,472 additions and 1,190 deletions.
97 changes: 17 additions & 80 deletions eth/tracers/blocknative/blocknative.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package blocknative

import (
"encoding/json"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers/blocknative/decoder"
Expand All @@ -20,6 +17,7 @@ type Tracer interface {
// TracerOpts configure the tracer to save or ignore various aspects of a transaction execution.
type TracerOpts struct {
Logs bool `json:"logs"`
Decode bool `json:"decode"`
BalanceChanges bool `json:"balanceChanges"`

// DisableBlockContext disables the block context in the trace.
Expand All @@ -30,10 +28,10 @@ type TracerOpts struct {
// Trace contains all the accumulated details of a transaction execution.
type Trace struct {
CallFrame
BlockContext *BlockContext `json:"blockContext,omitempty"`
Logs []CallLog `json:"logs,omitempty"`
Time string `json:"time,omitempty"`
BalanceChanges NetBalanceChanges `json:"balanceChanges"`
BlockContext *BlockContext `json:"blockContext,omitempty"`
Logs []CallLog `json:"logs,omitempty"`
Time string `json:"time,omitempty"`
BalanceChanges decoder.NetBalanceChanges `json:"balanceChanges"`
}

// BlockContext contains information about the block we simulate transactions in.
Expand All @@ -48,17 +46,18 @@ type BlockContext struct {
}

type CallFrame struct {
Type string `json:"type"`
From string `json:"from"`
To string `json:"to,omitempty"`
Value string `json:"value,omitempty"`
Gas string `json:"gas"`
GasUsed string `json:"gasUsed"`
Input string `json:"input"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
ErrorReason string `json:"errorReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty"`
Type string `json:"type"`
From string `json:"from"`
To string `json:"to,omitempty"`
Value string `json:"value,omitempty"`
Gas string `json:"gas"`
GasUsed string `json:"gasUsed"`
Input string `json:"input"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
ErrorReason string `json:"errorReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty"`
Decoded *decoder.CallFrame `json:"decoded,omitempty"`
}

// CallLog represents a single log entry from the receipt of a transaction.
Expand All @@ -72,65 +71,3 @@ type CallLog struct {
// Topics is a slice of up to 4 32byte words provided with the log.
Topics []common.Hash `json:"topics"`
}

// NetBalanceChanges is a list of account balance changes.
type NetBalanceChanges []AccountBalanceChanges

// AccountBalanceChanges is a list of balance changes for a single account.
type AccountBalanceChanges struct {
Address common.Address `json:"address"`
BalanceChanges []BalanceChange `json:"balanceChanges"`
}

// BalanceChange is a change in an account's balance for a single asset.
type BalanceChange struct {
Delta *Amount `json:"delta"`
Asset *decoder.Asset `json:"asset"`
Breakdown []AssetTransferEvent `json:"breakdown"`
}

// AssetTransferEvent is a single transfer of an asset.
type AssetTransferEvent struct {
Counterparty common.Address `json:"counterparty"`
Amount *Amount `json:"amount"`
}

type Amount big.Int

func NewAmount(i *big.Int) *Amount {
return (*Amount)(i)
}

func (a *Amount) Add(x *Amount, y *big.Int) {
a.ToInt().Add(x.ToInt(), y)
}

func (a *Amount) ToInt() *big.Int {
return (*big.Int)(a)
}

func (a *Amount) String() string {
return a.ToInt().String()
}

func (a *Amount) MarshalJSON() ([]byte, error) {
return json.Marshal(a.ToInt().String())
}

func (a *Amount) UnmarshalJSON(data []byte) error {
if data == nil || len(data) == 0 {
*a = *NewAmount(big.NewInt(0))
return nil
}

var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
aInt, ok := new(big.Int).SetString(s, 10)
if !ok {
return fmt.Errorf("failed to convert string to Amount")
}
*a = *(*Amount)(aInt)
return nil
}
61 changes: 61 additions & 0 deletions eth/tracers/blocknative/decoder/abi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package decoder

import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/log"
)

var (
abiTypes = struct {
_string abi.Type
_address abi.Type
_uint256 abi.Type
_uint256Array abi.Type
_bytes abi.Type
}{}

abiArgs = struct {
singleString abi.Arguments
batchTransfer abi.Arguments
}{}
)

func init() {
if err := initABITypes(); err != nil {
log.Error("failed to initialize abi types", "err", err)
}
initABIArgs()
}

// initABITypes initializes all the abiTypes.
func initABITypes() error {
var err error
if abiTypes._string, err = abi.NewType("string", "", nil); err != nil {
return err
}
if abiTypes._address, err = abi.NewType("address", "", nil); err != nil {
return err
}
if abiTypes._uint256, err = abi.NewType("uint256", "", nil); err != nil {
return err
}
if abiTypes._uint256Array, err = abi.NewType("uint256[]", "", nil); err != nil {
return err
}
if abiTypes._bytes, err = abi.NewType("bytes", "", nil); err != nil {
return err
}
return nil
}

// initABIArgs initializes all the abiArgs.
func initABIArgs() {
abiArgs.singleString = abi.Arguments{abi.Argument{Type: abiTypes._string, Name: "name"}}
abiArgs.batchTransfer = abi.Arguments{
abi.Argument{Type: abiTypes._address, Name: "from"},
abi.Argument{Type: abiTypes._address, Name: "to"},
abi.Argument{Type: abiTypes._uint256Array, Name: "tokenIDs"},
abi.Argument{Type: abiTypes._uint256Array, Name: "values"},
abi.Argument{Type: abiTypes._bytes, Name: "data"},
}
}
203 changes: 203 additions & 0 deletions eth/tracers/blocknative/decoder/asset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package decoder

import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"math/big"
)

var (
ethAddress = common.Address{}
EthAssetID = AssetID{ethAddress, nil}
EthAsset = &Asset{
AssetID: EthAssetID,
AssetMetadata: &AssetMetadata{
Type: AssetTypeNative,
Name: "Ether",
Symbol: "ETH",
Decimals: 18,
},
}
)

func DecodeAsset(evmCall EVMCallFn, contract *Contract, assetID AssetID) (*AssetMetadata, error) {
var metadata AssetMetadata
switch {
case contract.IsERC20():
metadata = decodeERC20Metadata(evmCall, assetID.Address)
case contract.IsERC721():
metadata = decodeERC721Metadata(evmCall, assetID.Address, assetID.TokenID)
case contract.IsERC1155():
metadata = decodeERC1155Metadata(evmCall, assetID.Address, assetID.TokenID)
}
return &metadata, nil
}

// decodeERC20Metadata decodes the metadata for an ERC20 token from the EVM.
func decodeERC20Metadata(evmCall EVMCallFn, addr common.Address) AssetMetadata {
var err error
metadata := AssetMetadata{Type: AssetTypeERC20}

if metadata.Name, err = decodeMetadataName(evmCall, addr); err != nil {
log.Trace("failed to decode ERC20 name", "err", err)
}
if metadata.Symbol, err = decodeMetadataSymbol(evmCall, addr); err != nil {
log.Trace("failed to decode ERC20 symbol", "err", err)
}
if metadata.Decimals, err = decodeMetadataDecimals(evmCall, addr); err != nil {
log.Trace("failed to decode ERC20 decimals", "err", err)
}

return metadata
}

// decodeERC721Metadata decodes the metadata for an ERC721 token from the EVM.
func decodeERC721Metadata(evmCall EVMCallFn, addr common.Address, tokenID *big.Int) AssetMetadata {
var err error
metadata := AssetMetadata{Type: AssetTypeERC721}

if metadata.Name, err = decodeMetadataName(evmCall, addr); err != nil {
log.Trace("failed to decode ERC721 name", "err", err)
}
if metadata.Symbol, err = decodeMetadataSymbol(evmCall, addr); err != nil {
log.Trace("failed to decode ERC721 symbol", "err", err)
}
if metadata.URI, err = decodeMetadataTokenURI(evmCall, addr, tokenID); err != nil {
log.Trace("failed to decode ERC721 tokenURI", "err", err)
}

return metadata
}

// decodeERC1155Metadata decodes the metadata for an ERC1155 token from the EVM.
func decodeERC1155Metadata(evmCall EVMCallFn, addr common.Address, tokenID *big.Int) AssetMetadata {
var err error
metadata := AssetMetadata{Type: AssetTypeERC1155}

if metadata.URI, err = decodeMetadataURI(evmCall, addr, tokenID); err != nil {
log.Trace("failed to decode ERC1155 URI", "err", err)
}

return metadata
}

// decodeMetadataName decodes the name of an asset from the EVM.
func decodeMetadataName(evmCall EVMCallFn, addr common.Address) (string, error) {
return callAndDecodeString(evmCall, addr, methodIDName)
}

// decodeMetadataSymbol decodes the symbol of an asset from the EVM.
func decodeMetadataSymbol(evmCall EVMCallFn, addr common.Address) (string, error) {
return callAndDecodeString(evmCall, addr, methodIDSymbol)
}

// decodeMetadataDecimals decodes the decimals of an asset from the EVM.
func decodeMetadataDecimals(evmCall EVMCallFn, addr common.Address) (uint8, error) {
return callAndDecodeUint8(evmCall, addr, methodIDDecimals)
}

// decodeMetadataTokenURI decodes the tokenURI of an asset from the EVM.
func decodeMetadataTokenURI(evmCall EVMCallFn, addr common.Address, tokenID *big.Int) (string, error) {
tokenIDBytes := tokenID.Bytes()
if len(tokenIDBytes) > 32 {
return "", fmt.Errorf("tokenID is too large")
}
common.LeftPadBytes(tokenIDBytes, 32)
input := append(methodIDTokenURI, tokenIDBytes...)
return callAndDecodeString(evmCall, addr, input)
}

// decodeMetadataURI decodes the URI of an asset from the EVM.
func decodeMetadataURI(evmCall EVMCallFn, addr common.Address, tokenID *big.Int) (string, error) {
tokenIDBytes := tokenID.Bytes()
if len(tokenIDBytes) > 32 {
return "", fmt.Errorf("tokenID is too large")
}
common.LeftPadBytes(tokenIDBytes, 32)
input := append(methodIDURI, tokenIDBytes...)
return callAndDecodeString(evmCall, addr, input)
}

// callAndDecodeString calls a method and decodes the result as a string.
func callAndDecodeString(evmCall EVMCallFn, addr common.Address, method []byte) (string, error) {
// Load bytes from the EVM.
stringBytes, err := evmCall(addr, method)
if err != nil {
return "", err
}

// Parse into a string.
stringInterface, err := abiArgs.singleString.Unpack(stringBytes)
if err != nil {
return "", err
}
if len(stringInterface) < len(abiArgs.singleString) {
return "", fmt.Errorf("unexpected decoded size")
}
str, ok := stringInterface[0].(string)
if !ok {
return "", fmt.Errorf("unexpected type for decoded string")
}

return str, nil
}

// callAndDecodeUint8 calls a method and decodes the result as a uint8.
func callAndDecodeUint8(evmCall EVMCallFn, addr common.Address, method []byte) (uint8, error) {
// Load bytes from the EVM.
uint8Bytes, err := evmCall(addr, method)
if err != nil {
return 0, err
}

// Parse into a uint8.
if len(uint8Bytes) < 1 {
return 0, fmt.Errorf("unexpected decoded size")
}
return uint8Bytes[len(uint8Bytes)-1], nil
}

// decodeArgsSafeBatchTransferFrom calls a method and decodes the result as a uint256[].
func decodeArgsSafeBatchTransferFrom(bytes []byte) ([]*Transfer, error) {
args, err := abiArgs.batchTransfer.UnpackValues(bytes)
if err != nil {
return nil, err
}
if len(args) < len(abiArgs.batchTransfer) {
return nil, fmt.Errorf("unexpected decoded size")
}

from, ok := args[0].(common.Address)
if !ok {
return nil, fmt.Errorf("unexpected type for decoded address")
}
to, ok := args[1].(common.Address)
if !ok {
return nil, fmt.Errorf("unexpected type for decoded address")
}
tokenIDs, ok := args[2].([]*big.Int)
if !ok {
return nil, fmt.Errorf("unexpected type for decoded uint265[]")
}
values, ok := args[3].([]*big.Int)
if !ok {
return nil, fmt.Errorf("unexpected type for decoded uint265[]")
}

if len(tokenIDs) != len(values) {
return nil, fmt.Errorf("expected matching array lengths")
}

transfers := make([]*Transfer, len(tokenIDs))
for i := 0; i < len(tokenIDs); i++ {
transfers[i] = &Transfer{
From: from,
To: to,
Value: NewAmount(values[i]),
TokenID: tokenIDs[i],
}
}

return transfers, nil
}
Loading

0 comments on commit 3f09ca1

Please sign in to comment.