Inspecting and Debugging Transactions

The TransactionReceipt object provides information about a transaction, as well as various methods to aid in debugging.

>>> tx = Token[0].transfer(accounts[1], 1e18, {'from': accounts[0]})

Transaction sent: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
Token.transfer confirmed - block: 2   gas used: 51019 (33.78%)

>>> tx
<Transaction object '0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b'>

To view human-readable information on a transaction, call the TransactionReceipt.info method.

>>> tx.info()

Transaction was Mined
---------------------
Tx Hash: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
From: 0x4FE357AdBdB4C6C37164C54640851D6bff9296C8
To: 0xDd18d6475A7C71Ee33CEBE730a905DbBd89945a1
Value: 0
Function: Token.transfer
Block: 2
Gas Used: 51019 / 151019 (33.8%)

Events In This Transaction
--------------------------
Transfer
    from: 0x4fe357adbdb4c6c37164c54640851d6bff9296c8
    to: 0xfae9bc8a468ee0d8c84ec00c8345377710e0f0bb
    value: 1000000000000000000

Event Data

Data about events is available as TransactionReceipt.events. It is stored in an EventDict object; a hybrid container with both dict-like and list-like properties.

Hint

You can also view events that were emitted in a reverted transaction. When debugging it can be useful to create temporary events to examine local variables during the execution of a failed transaction.

>>> tx.events
{
    'CountryModified': [
        {
            'country': 1,
            'limits': (0, 0, 0, 0, 0, 0, 0, 0),
            'minrating': 1,
            'permitted': True
        },
        {
            'country': 2,
            'limits': (0, 0, 0, 0, 0, 0, 0, 0),
            'minrating': 1,
            'permitted': True
        }
    ],
    'MultiSigCallApproved': [
        {
            'callHash': "0x0013ae2e37373648c5161d81ca78d84e599f6207ad689693d6e5938c3ae4031d",
            'callSignature': "0xa513efa4",
            'caller': "0xF9c1fd2f0452FA1c60B15f29cA3250DfcB1081b9",
            'id': "0x8be1198d7f1848ebeddb3f807146ce7d26e63d3b6715f27697428ddb52db9b63"
        }
    ]
}

Use it as a dictionary for looking at specific events when the sequence they are fired in does not matter:

>>> len(tx.events)
3
>>> len(tx.events['CountryModified'])
2
>>> 'MultiSigCallApproved' in tx.events
True
>>> tx.events['MultiSigCallApproved']
{
    'callHash': "0x0013ae2e37373648c5161d81ca78d84e599f6207ad689693d6e5938c3ae4031d",
    'callSignature': "0xa513efa4",
    'caller': "0xF9c1fd2f0452FA1c60B15f29cA3250DfcB1081b9",
    'id': "0x8be1198d7f1848ebeddb3f807146ce7d26e63d3b6715f27697428ddb52db9b63"
}

Or as a list when the sequence is important, or more than one event of the same type was fired:

# name of the address
>>> tx.events[1].name
'CountryModified'

# address where the event fired
>>> tx.events[1].address
"0xDd18d6475A7C71Ee33CEBE730a905DbBd89945a1"

>>> tx.events[1]
{
    'country': 1,
    'limits': (0, 0, 0, 0, 0, 0, 0, 0),
    'minrating': 1,
    'permitted': True
}

Internal Transactions and Deployments

TransactionReceipt.internal_transfers provides a list of internal ether transfers that occurred during the transaction.

>>> tx.internal_transfers
[
    {
        "from": "0x79447c97b6543F6eFBC91613C655977806CB18b0",
        "to": "0x21b42413bA931038f35e7A5224FaDb065d297Ba3",
        "value": 100
    }
]

TransactionReceipt.new_contracts provides a list of addresses for any new contracts that were created during a transaction. This is useful when you are using a factory pattern.

>>> deployer
<Deployer Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>

>>> tx = deployer.deployNewContract()
Transaction sent: 0x6c3183e41670101c4ab5d732bfe385844815f67ae26d251c3bd175a28604da92
  Gas price: 0.0 gwei   Gas limit: 79781
  Deployer.deployNewContract confirmed - Block: 4   Gas used: 79489 (99.63%)

>>> tx.new_contracts
["0x1262567B3e2e03f918875370636dE250f01C528c"]

To generate Contract objects from this list, use ContractContainer.at:

>>> tx.new_contracts
["0x1262567B3e2e03f918875370636dE250f01C528c"]
>>> Token.at(tx.new_contracts[0])
<Token Contract object '0x1262567B3e2e03f918875370636dE250f01C528c'>

Debugging Failed Transactions

Note

Debugging functionality relies on the debug_traceTransaction RPC method. If you are using Infura this endpoint is unavailable. Attempts to access this functionality will raise an RPCRequestError.

When a transaction reverts in the console you are still returned a TransactionReceipt, but it will show as reverted. If an error string is given, it will be displayed in brackets and highlighted in red.

>>> tx = Token[0].transfer(accounts[1], 1e18, {'from': accounts[3]})

Transaction sent: 0x5ff198f3a52250856f24792889b5251c120a9ecfb8d224549cb97c465c04262a
Token.transfer confirmed (Insufficient Balance) - block: 2   gas used: 23858 (19.26%)
<Transaction object '0x5ff198f3a52250856f24792889b5251c120a9ecfb8d224549cb97c465c04262a'>

The error string is also available as TransactionReceipt.revert_msg.

>>> tx.revert_msg
'Insufficient Balance'

You can also call TransactionReceipt.traceback to view a python-like traceback for the failing transaction. It shows source highlights at each jump leading up to the revert.

>>> tx.traceback()
Traceback for '0xd31c1c8db46a5bf2d3be822778c767e1b12e0257152fcc14dcf7e4a942793cb4':
Trace step 169, program counter 3659:
    File "contracts/SecurityToken.sol", line 156, in SecurityToken.transfer:
    _transfer(msg.sender, [msg.sender, _to], _value);
Trace step 5070, program counter 5666:
    File "contracts/SecurityToken.sol", lines 230-234, in SecurityToken._transfer:
    _addr = _checkTransfer(
        _authID,
        _id,
        _addr
    );
Trace step 5197, program counter 9719:
    File "contracts/SecurityToken.sol", line 136, in SecurityToken._checkTransfer:
    require(balances[_addr[SENDER]] >= _value, "Insufficient Balance");

Inspecting the Trace

The Trace Object

The best way to understand exactly happened in a transaction is to generate and examine a transaction trace. This is available as a list of dictionaries at TransactionReceipt.trace, with several fields added to make it easier to understand.

Each step in the trace includes the following data:

{
    'address': "",  // address of the contract containing this opcode
    'contractName': "",  // contract name
    'depth': 0,  // the number of external jumps away the initially called contract (starts at 0)
    'error': "",  // occurred error
    'fn': "",  // function name
    'gas': 0,  // remaining gas
    'gasCost': 0,  // cost to execute this opcode
    'jumpDepth': 1,  // number of internal jumps within the active contract (starts at 1)
    'memory': [],  // execution memory
    'op': "",  // opcode
    'pc': 0,  // program counter
    'source': {
        'filename': "path/to/file.sol",  // path to contract source
        'offset': [0, 0]  // start:stop offset associated with this opcode
    },
    'stack': [],  // execution stack
    'storage': {}  // contract storage
}

Call Traces

When dealing with complex transactions the trace can be thousands of steps long - it can be challenging to know where to begin examining it. Brownie provides the TransactionReceipt.call_trace method to view a complete map of every jump that occured in the transaction:

>>> tx.call_trace()
Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
Initial call cost  [21228 gas]
LiquidityGauge.deposit  0:3103  [64010 / 128030 gas]
├── LiquidityGauge._checkpoint  83:1826  [-6420 / 7698 gas]
│   ├── GaugeController.get_period_timestamp  [STATICCALL]  119:384  [2511 gas]
│   ├── ERC20CRV.start_epoch_time_write  [CALL]  411:499  [1832 gas]
│   ├── GaugeController.gauge_relative_weight_write  [CALL]  529:1017  [3178 / 7190 gas]
│   │   └── GaugeController.change_epoch  697:953  [2180 / 4012 gas]
│   │       └── ERC20CRV.start_epoch_time_write  [CALL]  718:806  [1832 gas]
│   └── GaugeController.period  [STATICCALL]  1043:1336  [2585 gas]
├── LiquidityGauge._update_liquidity_limit  1929:2950  [45242 / 54376 gas]
│   ├── VotingEscrow.balanceOf  [STATICCALL]  1957:2154  [2268 gas]
│   └── VotingEscrow.totalSupply  [STATICCALL]  2180:2768  [6029 / 6866 gas]
│       └── VotingEscrow.supply_at  2493:2748  [837 gas]
└── ERC20LP.transferFrom  [CALL]  2985:3098  [1946 gas]

Each line shows the following information:

ContractName.functionName (external call opcode) start:stop [internal / total gas used]

Where start and stop are the indexes of TransactionReceipt.trace where the function was entered and exited. TransactionReceipt.call_trace provides an initial high level overview of the transaction execution path, which helps you to examine the individual trace steps in a more targetted manner and determine where things went wrong in a complex transaction.

Functions that terminated with REVERT or INVALID opcodes are highlighted in red.

For functions with no subcalls, the used gas is shown. Otherwise, the first gas number is the amount of gas used internally by this function and the second number is the total gas used by the function including all sub-calls. Gas refunds from deleting storage or contracts are shown as negative gas used. Note that overwriting an existing zero-value with another zero-value will incorrectly display a gas refund.

Calling TransactionReceipt.call_trace with True as an argument provides an expanded view:

>>> history[-1].call_trace(True)

Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
Initial call cost  [21228 gas]
LiquidityGauge.deposit  0:3103  [64010 / 128030 gas]
├── LiquidityGauge._checkpoint  83:1826  [-6420 / 7698 gas]
│   │
│   ├── GaugeController.get_period_timestamp  [STATICCALL]  119:384  [2511 gas]
│   │       ├── address: 0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e
│   │       ├── input arguments:
│   │       │   └── p: 0
│   │       └── return value: 1594574319
...

The expanded trace includes information about external subcalls, including:

  • the target address

  • the amount of ether transferred

  • input arguments

  • return values

For calls that revert, the revert reason is given in place of the return value:

>>> history[-1].call_trace(True)
...
└── ERC20LP.transferFrom  [CALL]  2985:3098  [1946 gas]
        ├── address: 0xd495633B90a237de510B4375c442C0469D3C161C
        ├── value: 0
        ├── input arguments:
        │   ├── _from: 0x9EC9431CCCCD2C73F0A2F68DC69A4A527AB5D809
        │   ├── _to: 0x5AE569698C5F986665018B6E1D92A71BE71DEF9A
        │   └── _value: 100000
        └── revert reason: Integer underflow

You can also access this information programmatically via the TransactionReceipt.subcalls attribute:

>>> history[-1].subcalls
[
    {
        'from': "0x5AE569698C5F986665018B6e1d92A71be71DEF9a",
        'function': "get_period_timestamp(int128)",
        'inputs': {
            'p': 0
        },
        'op': "STATICCALL",
        'return_value': (1594574319,),
        'to': "0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e"
    },
...