Web3.py Patterns: Revert Reason Lookups

When writing smart contracts, you're encouraged to include human-readable error messages. In Solidity, these can be declared within require or revert statements. For example:

function subtract(uint256 a, uint256 b) public pure returns (uint256) {
    require(b <= a, "(a) must be larger than (b)!");
    uint256 c = a - b;
    return c;
}

Error messages are collected and intelligently displayed within block explorers like Etherscan, but if you need to programmatically find out why a transaction failed, there's a little more legwork required.

If you fetch an Ethereum transaction, you'll find all the relevant information included in the sending of that transaction, e.g., to, from, value, and data. A transaction receipt, on the other hand, will indicate that a transaction failed, but the revert reason itself is not included.

One way to fetch a revert reason is to replay the transaction using the eth_call method. This RPC method is used to execute a transaction locally – nothing will be broadcasted to the network, but you'll see the outcome as if the transaction were really mined.

In order to replay a transaction, you need the context that the transaction was originally executed in, e.g., the balances in the accounts at the time of the transaction. eth_call makes this simple by allowing you to pass in a block number that you want a transaction to be replayed at. The pseudocode looks something like this:

from web3 import Web3

w3 = Web3(<your-provider>)
w3.eth.call(replay_tx, block_number)

There is a catch. By default, Ethereum clients don't store all the historical context required to do lookups like this for older transactions. This role is performed by "archive" nodes, which require significantly more storage space. If you aren't running your own archive node, you'll need to use a provider that does support this capability.

Putting it all together – a transaction hash is all that's required to prepare the replay transaction. Fetching that transaction provides the inputs of that transaction, as well as the block it was mined in. See the full code sample, using a randomly selected Uniswap transaction failure, below:

import os
from web3 import Web3, HTTPProvider 

# instantiate your (archive-capable) provider:
w3 = Web3(HTTPProvider(os.environ['MAINNET_URL']))

# fetch a reverted transaction:
tx = w3.eth.get_transaction('0x2e7aa4314eeb171d4...')

# build a new transaction to replay:
replay_tx = {
    'to': tx['to'],
    'from': tx['from'],
    'value': tx['value'],
    'data': tx['input'],
}

# replay the transaction locally:
try:
    w3.eth.call(replay_tx, tx.blockNumber)
except Exception as e: 
    print(e)
    # execution reverted: UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT