How to Parse an EVM Smart Contract in Go

I was recently looking for a working solution to parsing Smart Contracts in the EVM chains. I couldn’t find a simple to follow, working solution based on Go. This post aims to fill that gap.

In this post, we’d parse Polygon bridge transactions. Let’s look at this particular transaction. This txn belonging to block 31740481 in Polygon L2 chain is interacting with a smart contract here.

Get a Polygon Endpoint

The first thing we want to do is to grab a JSON-RPC endpoint to Polygon. I got one from quicknode.com. Once you have it, test the endpoint by running curl.

# Remember to replace the URL with yours.
$ URL="https://virulent-fluent-isle.matic.quiknode.pro/625d8404f145726303e9637515d142dc12c630a8/"

$ curl $URL -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["0x84c162b0293329804cf92280c0ec92c95ef0ce457f8e1ca09368eb89fe419af6"],"id":1}' | jq
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "blockHash": "0x38d7be0f715918bb231d69002e2b2ddea5211623483122bceb073f7e44d0d280",
    "blockNumber": "0x1e45241",
    "from": "0x22e68ae864c4be029c8be5499669f1850966ed4c",
    "gas": "0xa68f",
    "gasPrice": "0x2bcd140083",
    "maxFeePerGas": "0x2bcd140083",
    "maxPriorityFeePerGas": "0x2bcd132c55",
    "hash": "0x84c162b0293329804cf92280c0ec92c95ef0ce457f8e1ca09368eb89fe419af6",
    "input": "0xa9059cbb000000000000000000000000cb5076e438d46d532bee1bfba2594e6fdd65cf96000000000000000000000000000000000000000000000000e25fbeb9aff7cf54",
    "nonce": "0x139",
    "to": "0x0000000000000000000000000000000000001010",
    "transactionIndex": "0x60",
    "value": "0xe25fbeb9aff7cf54",
    "type": "0x2",
    "accessList": [],
    "chainId": "0x89",
    "v": "0x0",
    "r": "0x9342b409b2e1870170dedc3e7745f30f151490284a3e46a6dcf17f0d7c546a0e",
    "s": "0x4e4ad203e4412c67afeef06c79f14b9e93c25b1add24176a418ad9e6685228fb"
  }
}

In this txn, to field has the contract address, and input field has the data that we want to parse.

Download Contract ABI

We’d need an ABI for to parse the transaction. The simplest way to do this would be to download the JSON ABI from polygonscan. You can access the contract from the Contract” tab.

Polygon Smart Contract

You can scroll down to Contract ABI, click Export ABI > JSON Format”. Polygon Smart Contract

Alternative: Compile .sol to .abi

You can also use solidity compiler to compile the contract into the ABI. For that, download the .sol contract file, and run solc --abi MRC20.sol. Note that the solc compiler must be the same version as defined in the .sol file. For example, Polygon contract requires the compiler with version 0.5.2.

$ head poly.sol
/**
 *Submitted for verification at polygonscan.com on 2021-12-16
*/

// File: openzeppelin-solidity/contracts/math/SafeMath.sol

pragma solidity ^0.5.2;

/**
 * @title SafeMath

To install the correct solc, you can either use npm install g solc@0.5.2 (which didn’t work for me for some strange reason), or you can just download a precompiled binary from Github here.

Parse ABI file

Store the ABI you just downloaded in mrc20.abi file. We can now read and parse the ABI from Go:

fdata, err := ioutil.ReadFile("mrc20.abi")
Check(err)

// `abi` library is from `github.com/ethereum/go-ethereum/accounts/abi`
contractAbi, err := abi.JSON(bytes.NewReader(fdata))
Check(err)

List All Methods

We can now query all the methods available in the ABI like so:

for m := range contractAbi.Methods {
  fmt.Printf("Found method: %s\n", m)
}

Parse Transaction Input

Now, we need to parse the input field in the transaction. The first 4 bytes of the txn represent the ID of the method in the ABI. We can query the method by using contractAbi.MethodById.

bno := new(big.Int).SetInt64(*blockId)
block, err := client.BlockByNumber(context.Background(), bno)
Check(err)

fmt.Printf("Found %d transactions in block: %d\n\n", len(block.Transactions()), *blockId)
var valid int
for _, tx := range block.Transactions() {
  input := tx.Data() // "input" field above
  if len(input) < 4 {
      continue
  }
  method := input[:4]
  m, err := contractAbi.MethodById(method)
  Check(err)
  fmt.Printf("Method name: %s\n", m.Name)
}

We can now use the method’s Inputs field to parse the inputs to this method. The inputs are encoded in the rest of the txn input field (after the first 4 bytes).

in := make(map[string]interface{})
err = m.Inputs.UnpackIntoMap(in, data[4:])
Check(err)

fmt.Printf("Parsed in: %+v\n", in)

Putting it All Together

The full Go code along with the ABI and go.mod is located in this gist.

$ go build . && ./poly --rpc https://virulent-fluent-isle.matic.quiknode.pro/625d8404f145726303e9637515d142dc12c630a8/ --block 31742819
Chain ID: 137 Latest: 31747503
Found method: name
Found method: owner
Found method: transfer
Found method: disabledHashes
Found method: parentOwner
Found method: networkId
Found method: getTokenTransferOrderHash
Found method: withdraw
Found method: initialize
Found method: CHAINID
Found method: parent
Found method: renounceOwnership
Found method: symbol
Found method: token
Found method: transferWithSig
Found method: balanceOf
Found method: ecrecovery
Found method: EIP712_DOMAIN_SCHEMA_HASH
Found method: currentSupply
Found method: isOwner
Found method: EIP712_DOMAIN_HASH
Found method: totalSupply
Found method: EIP712_TOKEN_TRANSFER_ORDER_SCHEMA_HASH
Found method: setParent
Found method: decimals
Found method: deposit
Found method: transferOwnership
All methods found

Found 65 transactions in block: 31742819

Txn Hash: 0x14bfd28ca9c17b60126d9f48f59789b19afc1c117dc6bbb291ff0a961e72db7c Method: withdraw Inputs: map[amount:+50000000000000000000]

Done parsing Block: 31742819. Found 1 txns involving contract.

Go Parse Some Smart Contract Transactions

This concludes the post. Now you know how to parse an EVM smart contract transaction in Go.



Date
August 10, 2022