Cryptoassets

Decoding an Ethereum Transaction

Decoding an Ethereum transaction, read part one of Trustology's new decentralised finance (DeFi) blog series.

If you want to do anything interesting in Ethereum, you will have to interact with smart contracts. Whether you want to send ERC20 tokens like LINK or DAI, trade non fungible tokens like digital art, or earn interest on your crypto and interact with other DeFi products, a smart contract is always involved. 

However, smart contracts are becoming increasingly complex. From proxy contracts to allow for upgradeability, to multi-send contracts that allow for the batching of transactions, what we are seeing is a rapid evolution of features that allow for the movement of one's assets. 

You've also got DeFi transactions, they are often not generated by you or your code so you need to verify it's doing what you think it is. Additionally, understanding a method call is useful for:

  • Seeing and knowing that the contract methods calls are what you expect
  • Seeing the parameter types and values that allow you to understand how the contracts work
  • Analysing a contract to produce stats on method calls
  • Tracking interactions with key addresses
  • Writing your own rules to decide on which transactions to sign (more on that later)
  • Having fun, if you like that sort of thing

In part one of our new DeFi blog series, we will demonstrate and introduce what goes on inside an Ethereum transaction and how you can use Trustology’s custodial wallet platform to interact with smart contracts in a much more secure way. This knowledge will provide you with a good foundation in subsequent posts when we talk about our Firewall, Webhooks, and other DeFi services that we offer.

Let's dig in.

We’re going to start with a Gnosis Safe contract and a transaction a user would send that the Gnosis safe Dapp would generate.

Gnosis Safe is a popular wallet contract implemented by Gnosis. A multi-user organisation that wants to use Gnosis Safe will first have to define a list of accounts and a required threshold of signatures needed to send a transaction. Users will submit the transaction to the Safe, which will authorise the execution of the transaction only when the threshold required is reached. This way, the users have a tighter control of their funds.

The Gnosis safe has lots of features including offline signing (which saves on gas fees) but for now we’re going to take a look at the basic method for executing a transaction.

First port of call: Start with Etherscan

Let's start with a transaction as seen by Etherscan and see if we can get the output they do.

Decoding an Ethereum Transaction

The transaction above is from a user that has interacted with the Gnosis Safe smart contract on the Ethereum mainnet and we can see the details directly on Etherscan.

Let’s start by looking at the “data” field. This is the field that Ethereum uses to decide what method to call on the “to” (or “Interacted with” in Etherscan) contract.

Here’s the full data payload below. As you can see, a lot of hex values. This looks difficult to understand but with a little skill and knowledge we can break this up into the important parts.

Decoding an Ethereum Transaction

 

Start at the beginning

All “data” fields on an Ethereum transaction contain the name of the method that is to be called. This is the first thing we should look at. Etherscan shows this for us in the “Input Data” section:

Decoding and Ethereum Transaction

In this section we can see what Etherscan has given us: 

Function: execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 dataGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures)

So, how did they get this?

Well, the first thing we need is to use the first 4 bytes (first 8 hex characters) of the input data, which is: 6a761202

This hex value is derived from taking the method name and its argument types, removing any whitespace, taking the `keccak hash of the result, and then taking the first 4 bytes of it and displaying it in hex. With me so far?

Note: The parameter names are not included in the hash, only the types. This means that different contracts can have the same methodId's but call their parameters differently and potentially have different logic. Thus it is sensible to keep track of the contract which the method belongs to. There is a handy website that tracks these. Check out https://www.4byte.directory/ and enter the hash 6a761202. At time of writing there is only 1 registered method.

We have the source of the Gnosis contract so we know what the method name and the parameters are. We grab them from here.

In JavaScript, the following will output the keccak hash of our method plus parameters:

// import a keccak decoder or write your own
import { keccak } from "../decoder/keccak";

const method = `execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 dataGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures)`

// regex pattern to remove the word before a comma or closing bracket
export const removeArgsFromMethod = (method: string) => {
 return method.replace(/\s\w+(,|\))/g, (_, commaOrBracket) => commaOrBracket);
};

// remove the argument names and remove any spaces
const preparedMethod = removeArgsFromMethod(method).replace(/s/g, "")

// keccak hash of the method
const keccakHashOfMethod = keccak(Buffer.from(preparedMethod))

// first 4 bytes
const methodId = keccakHashOfMethod.slice(0,4).toString("hex") // 6a761202

Note: If you don’t have the source code of the smart contract then you won’t be able to generate the hash to check and it will be more difficult calculate the exact parameter types.

The output from that Javascript is 6a761202 which matches the first 4 bytes (8 hex characters) of the data payload. Great. We’ve identified that the method being called is in fact “execTransaction” with the 10 parameters.

Now, if you go back to Etherscan and click on the `Decode Input Data button you can see how Etherscan has taken the data payload and broken it up into the 10 parameters including the types. Again, how have they done this?

Screenshot 2020-11-28 at 13.59.59

 

Get a handle on parameters

Our next job is to break up all the parameters that have been passed to the execTransaction and see what they are.

Before we do that a quick note on bytes to hex conversion. Very simply 1 byte of data = 2 characters of Hex. So, whenever you see a string of hex characters, you can divide the number of characters by 2 to get the size in bytes. Any "0x" at the beginning is ignored in the actual calculation.

Now, on to decoding the method parameters. 

Let's have a look at the data again but this time we split it into 32 byte (64 Hex characters) strings. Why? Because ethereum uses 32 bytes blocks and nearly all primitive types are 32 bytes. There are a couple of exceptions though, such as the "bytes" type which we'll see later.

So, once we strip off the methodId (0x6a761202) and then break the remaining data into 32 bytes (or 64 characters) chunks we can start to see something more interesting.

MethodID: 0x6a761202

Decoding an Ethereum Transaction

These are the values of the input parameters of the execTransaction function. It's not quite straightforward so let's work through each 32 byte value in turn. Column 3 of the table above shows where each of the values maps to the parameters of the execTransaction function.

For example, the first parameter (the `"to" parameter):

000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7

is of type address.

Ethereum addresses are 20 byte values so it has been zero-padded to fit into a 32 byte value. To get the actual address, we just need to extract the last 20 bytes and prefix 0x. 

In this case it becomes:

`0xdac17f958d2ee523a2206206994597c13d831ec7

(Hmm...this is the Tether contract)

In fact, removing the zero padding is how to decode most of the simple parameters like uint8, unit256, etc... 

The next parameter is the “value”. This, we assume, is how much value needs to be transferred from the Gnosis safe.  Its value is zero.  (This is the value of Ether remember)

The next parameter is interesting, it's of types bytes. bytes is a variable-length so how long is it?  The value is:

0000000000000000000000000000000000000000000000000000000000000140

This value represents the offset in hex of bytes from the `methodId` of where the actual data begins. Think of it as a pointer, redirecting you to where you should look for this information. So we need to convert 0x140 bytes into hex characters.

0x140 (bytes in hex) = 320 bytes in decimal

320 bytes in decimal x 2 = 640 characters in hex

so the data begins 320 bytes from the start, and thus it is 640 hex characters along, or 10 rows along. In JavaScript, it will look as follows:

const numberOfBytesInHex = 140

const numberOfHexCharacters = parseInt(numberOfBytesInHex, 16) * 2; // = 640

Going to the 10th row the first 64 characters represent the length of the data (this requires the same conversion done above). 

0000000000000000000000000000000000000000000000000000000000000044

is decoded into a length of 136 hex characters. 

So finally on the 11th row the data begins and ends after 136 characters. Which means our data field is the following:

a9059cbb000000000000000000000000ba339b8271afea713008314487dd98f8d941720f
00000000000000000000000000000000000000000000000000000002540be400

So in summary, for a bytes field, the first 32 bytes is a pointer to where the data starts, once there, the next 32 bytes tells you how long the data is. Then you read that many bytes. If the data doesn’t end in a 32 byte block, it will be padded so whatever is next, will start in the next 32 byte block.

Looking at the value again:

a9059cbb000000000000000000000000ba339b8271afea713008314487dd98f8d941720f
00000000000000000000000000000000000000000000000000000002540be400

It looks a little familiar... It has a 4 byte methodId and then 2 x 32 byte chunks (2 lots of 64 hex characters). It’s another method call!

In fact, we can recognise immediately that this transaction is in itself an ERC20 transfer transaction as it has the famous a9059cbb method signature.

(Tip: go to https://emn178.github.io/online-tools/keccak_256.html and type in transfer(address,uint256) and you will see the first 8 characters are in fact a9059cbb`)

So, we can see that this Gnosis transaction is to call another contract of address 0xdAC17F958D2ee523a2206206994597C13D831ec7 with the “data” of:

a9059cbb000000000000000000000000ba339b8271afea713008314487dd98f8d941720f
00000000000000000000000000000000000000000000000000000002540be400

Which is an ERC20 “transfer” call. We could then use what we learnt above to recursively calculate the details of this data call by extracting the methodId bytes and breaking up the data and looking at the parameters.

Take a look and see if you can work out the recipient and value of tokens sent to the Tether contract.

Conclusion

There was a lot of information here and we didn't go through every single parameter, but hopefully, you can see it was very useful to be able to break the transaction data down and by working through a data payload methodically you can always understand what's happening. 

We saw how for a Gnosis contract transaction we could break it down into the constituent parts and we saw the methodId and the parameter types and how to deconstruct them into useful arguments.

We also saw how that, in this case, there was effectively a transaction within a transaction. The Gnosis safe transaction was just wrapping an ERC20 transfer. This was effectively moving USDT tokens from the Gnosis safe to somewhere else.

In our next blog, we’ll see how we can take our understanding of Ethereum transactions to break down a Defi contract call to apply rules to decide if we want to sign / send the transaction. This is something that Trustology uses in its new Defi firewall to protect customers from signing / sending transactions that don’t meet certain criteria. The DeFi Firewall is the first in a suite of services Trustology aims to launch for institutional users looking to support new tokens in DeFi or explore yield bearing opportunities. Notifications and flows are next on our list to complement our current firewall solution. Currently, we are the only custodial wallet to integrate with MetaMask, which lets institutions tap DeFi innovation from the security of an insured custodial wallet platform.  

To access the code demonstrated in this blog and more, please click here.

To enquire about our DeFi services chat to us.

About Trustology

Backed by ConsenSys and Two Sigma Ventures, and founded in 2017, Trustology was established on the premise of enabling greater freedom to transact in a fair and efficient manner. By bringing the world of blockchain technology and cryptoassets to new market participants, we believe this will help make this happen.

That’s why we built TrustVault — a fast, user-friendly, insured and highly-secure custodial wallet service for institutions and individuals designed to address the security and ownership shortcomings of existing solutions today. Learn more about us at trustology.io.    

Recommended reading:

If you want to know more about Ethereum transactions there are some good articles, you start with: