Skip to content
Last update: January 26, 2024

Decoding contract call data

MMI does not send decoded transaction data to custodians; the main reason for this is that MMI (and Metamask) does not have privileged information about the purpose of a transaction. The calldata are usually encoded by a library such as ethers.js or web3.js on the web page (dapp) where the transaction is initiated.

That said, there are situations where it makes sense for custodians to show contract information, parameters and method names to users, in order to

  1. Allow approvers and cosigners the opportunity to verify a transaction before approving it
  2. More easily locate the transaction in a list of transactions, by showing similar information to MMI

This section explains a possible method.

Determining transaction type

This is a rough taxonomy of transaction types and how to detect them:

Type Sub type Data Value To Note
Contract interaction
ERC-20 Approve Yes No Token contract Decode with HST ABI
ERC-20 Transfer Yes No Token contract Decode with HST ABI
ERC-20 Transfer From Yes No Token contract Decode with HST ABI
Other Yes Possible Contract address Check with registry
Simple transfer No Yes Transfer recipient
Contract Deployment Yes Possibly No
No-op No No Any

Tip

Here is the Metamask code which achieves much of this. Please note that this functionality is subject to change or removal.

Token transactions

Many transactions will be calls to standard ERC-20 methods. Therefore, it makes sense to attempt to parse the transaction using the Human Standard Token ABI.

import { ethers } from 'ethers';
import abi from 'human-standard-token-abi';
const hstInterface = new ethers.utils.Interface(abi);

const data = txParams.data

let tokenTransactionTypes = {
  'transfer : 'TOKEN_METHOD_TRANSFER',
  'transferfrom' : 'TOKEN_METHOD_TRANSFER_FROM'
  'approve' : 'TOKEN_METHOD_APPROVE' 
}

let tokenTxType
let parsedTransaction

try {
  let parsedTransaction = hstInterface.parseTransaction({ data })
  tokenTxType = tokenTransactionTypes[parsedTransaction.name]
} catch (e) {
  // This is not a token transaction
}

Example

For this transaction with a to address of 0x6B175474E89094C44Da98b954EedeAC495271d0F (DAI) and a data parameter of 0xa9059cbb00000000000000000000000001e3f80ed12f390b767fe711571784f028726f1b00000000000000000000000000000000000000000000001b1ae4d6e2ef500000 we can see that we end up with a tokenTxType of TOKEN_METHOD_TRANSFER and a parsed transaction containing

  args: [
    '0x01e3F80ED12F390B767fE711571784f028726F1B',
    BigNumber { _hex: '0x1b1ae4d6e2ef500000', _isBigNumber: true },
    _to: '0x01e3F80ED12F390B767fE711571784f028726F1B',
    _value: BigNumber { _hex: '0x1b1ae4d6e2ef500000', _isBigNumber: true }
  ],

From this we can tell that the recipient is 0x01e3F80ED12F390B767fE711571784f028726F1B and the value to be transferred is 0x1b1ae4d6e2ef500000.

Metamask provides a library that you can use to obtain token metadata to provide further information to the user

import contractMap from '@metamask/contract-metadata'
import ethJSUtil from 'ethereumjs-util'
const { toChecksumAddress } = ethJSUtil

const contractMetadata = contractMap[toChecksumAddress(0x6B175474E89094C44Da98b954EedeAC495271d0F)] // The contract address, which is the to address of the TX

/* 
{
  name: 'Dai Stablecoin',
  logo: 'dai.svg',
  erc20: true,
  symbol: 'DAI',
  decimals: 18
}
*/

Based on this, you can divide 0x5348a014c0d08d4b6805c8 by 10**18 (the number of decimals) and see that the transaction would transfer 500 Dai

Other transactions

For other transactions, you will need to look up the method in a method hash database/rainbow table.

One option is to use an on-chain registry such as parity’s. Metamask provides a JS library for doing this. This is the recommended option, with an off-chain database as a fallback. Note that 4byte may return more than one result.

4byte is such an offchain database of contract methods, including ABIs and function signatures, which has a REST API.

You can identify the method being called in a contract interaction transaction by looking at the first 4 bytes of the data parameter.

Let us take the example of this transaction.

Here is the input data

0x7ff36ab50000000000000000000000000000000000000000000000000000000002a2265a00000000000000000000000000000000000000000000000000000000000000800000000000000000000000006f69286d7c51de0f0cf5f3c03c8ca3eda8a322f900000000000000000000000000000000000000000000000000000000613751030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000004ddc2d193948926d02f9b1fe9e1daa0718270ed5

the first 4 bytes of which are 0x7ff36ab5

We can start with a HTTP request to 4byte, as it is illustrative:

curl --silent https://www.4byte.directory/api/v1/signatures/\?hex_signature\=0x7ff36ab5 | jq
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 171811,
      "created_at": "2020-08-09T13:07:42.089878Z",
      "text_signature": "swapExactETHForTokens(uint256,address[],address,uint256)",
      "hex_signature": "0x7ff36ab5",
      "bytes_signature": "\u007fójµ"
    }
  ]
}

Based on this response, you can construct a partial ABI, and use ethers or any other library to decode the parameters.

However, if you have the web3 library available to you, you can skip the construction of the partial ABI, and simply decode the parameters with web3.eth.abi.decodeParameters

First you strip the 4 byte prefix, leaving

0x0000000000000000000000000000000000000000000000000000000002a2265a00000000000000000000000000000000000000000000000000000000000000800000000000000000000000006f69286d7c51de0f0cf5f3c03c8ca3eda8a322f900000000000000000000000000000000000000000000000000000000613751030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000004ddc2d193948926d02f9b1fe9e1daa0718270ed5

and you can construct a parameter array based on the text_signature of the 4byte response, or the args property of the response from the method registry.

const parameters = ['uint256', 'address[]', 'address', 'uint256']


> web3.eth.abi.decodeParameters(parameters, data)
Result {
  '0': '44181082',
  '1': [
    '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
    '0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5'
  ],
  '2': '0x6f69286d7C51DE0f0cF5F3C03c8cA3edA8a322F9',
  '3': '1631015171',
  __length__: 4
}

Note that neither 4bytes nor eth-method-registry include the parameter names. If you wish to display these, you would need a collection of ABIs - we’re currently not aware of one.

Resources