Inspecting and Debugging Transactions

Each time you perform a transaction you are returned a TransactionReceipt. This object contains all relevant information about the 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.

Note

Event data is still available when a transaction reverts.

>>> 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:

>>> tx.events[1].name
'CountryModified'
>>> 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 may thousands of steps long - it can be challenging to know where to begin when examining it. Brownie provides the TransactionReceipt.call_trace method to view a complete map of every jump that occured in the transaction, along with associated trace indexes and gas usage:

>>> tx.call_trace()
Call trace for '0x7f618202ef31ab6927824caa3d338abe192fc6eb062dac1ee195d186a8a188f0':
Initial call cost  [21368 gas]
LgtHelper.burnAndFree 0:4126  [1263 / 148049 gas]  (0xe7CB1c67752cBb975a56815Af242ce2Ce63d3113)
├─LgtHelper.burnGas 68:3083  [200081 gas]
└─LiquidGasToken.freeFrom 3137:4080  [2312 / -53295 gas]  (0x00000000007475142d6329FC42Dc9684c9bE6cD0)
  ├─ERC20PointerSupply.allowance 3228:3264  [986 gas]
  ├─ERC20PointerSupply.balanceOf 3275:3296  [902 gas]
  ├─LiquidGasToken._destroyContracts 3307:3728  [687 / -52606 gas]
  │ ├─ERC20PointerSupply.totalBurned 3312:3317  [815 gas]
  │ ├─LiquidGasToken.computeAddress2 3352:3413  [245 gas]
  │ ├─<UnknownContract>.0x00000000 3434:3441  [-18278 gas]  (0x5E77b3934E758eDfC7baCAbD84c6c91295d5eF15)
  │ ├─LiquidGasToken.computeAddress2 3477:3538  [242 gas]
  │ ├─<UnknownContract>.0x00000000 3559:3566  [-18278 gas]  (0x88d97e2fD96a170F57c5c5AD636Ab8a8de3Ec776)
  │ ├─LiquidGasToken.computeAddress2 3602:3663  [239 gas]
  │ └─<UnknownContract>.0x00 3684:3691  [-18278 gas]  (0xb1fC7df83B2b966a3E988679e1504036B18A7f42)
  ├─ERC20PointerSupply._burnFrom 3734:3892  [-4148 / -12023 gas]
  │ └─ERC20PointerSupply._unassign 3747:3886  [-8067 / -7875 gas]
  │   ├─SafeMath.sub 3792:3809  [56 gas]
  │   └─SafeMath.sub 3838:3879  [136 gas]
  ├─SafeMath.sub 3950:3967  [56 gas]
  └─ERC20PointerSupply._approve 3970:4049  [7078 gas]

Each line shows the following information:

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

Where start and stop are the indexes of TransactionReceipt.trace where the function was entered and exited.

For functions with no further calls, 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 is shown as negative gas used. Note that overwriting an existing zero-value with another zero-value will incorrectly display a gas refund.

If an address is also shown, it means the function was entered via an external jump. Functions that terminated with REVERT or INVALID opcodes are highlighted in red.

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 can help profile a transaction for gas usage.

Accessing Transaction History

The TxHistory container, available as history, holds all the transactions that have been broadcasted. You can use it to access TransactionReceipt objects if you did not assign them a unique name when making the call.

>>> history
[<Transaction object '0xe803698b0ade1598c594b2c73ad6a656560a4a4292cc7211b53ffda4a1dbfbe8'>, <Transaction object '0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b'>]