FORENSIAWeb3 forensicsOpen workspace

What ran inside a transaction

How a transaction's internal calls are rebuilt into an ordered list, and how to read the values on them.

3 min read · Transaction analysis

#Plain languageIn plain terms

A transaction is one instruction from one account. What that instruction sets off can be large. A contract can call another contract, which calls a third, and so on. One swap on an exchange can involve dozens of these internal steps.

None of that is visible in the transaction itself. To see it you ask a node to replay the transaction and report each step. That report is a call trace. Forensia fetches one where the source allows it and lays the steps out in order.

#How it worksHow it works

  1. Ask the node for a call trace of the transaction.
  2. Check the top of the trace matches the transaction: same sender, recipient, input and value.
  3. Walk the nested calls from the top, recording each one with its position in the tree.
  4. Stop at fixed depth and size limits.
  5. Mark each call as succeeded, failed or unknown.

#Technical detailThe exact rules

The request is debug_traceTransaction with the callTracer, a 5 second limit at the node and 7 seconds at the client. Tracing is optional: many public sources do not offer it, and the result then falls back to the receipt alone. A trace is also set aside when its top call disagrees with the receipt about whether the transaction failed.

LimitValue
Depth read6 levels
Calls kept64
Frames inspected at most257
Error text kept300 characters

The walk is depth-first, visiting a call and then its children in order. Each kept call records its path in the tree, for example [0, 2] for the third child of the first child. Parentage is rebuilt only from that path, never from depth, address or amount.

Call typeWhat it means for value
CALLRuns the target's code. Can send value to the target.
STATICCALLRead-only. Cannot send value or change state.
DELEGATECALLRuns the target's code inside the caller's own account. No value moves to the target.
CALLCODEAn older form of the same idea.
CREATE, CREATE2Deploys a new contract.
SELFDESTRUCTKept as a step when the tracer reports it. No account context is worked out for it.

Reading a call in the interface

On the Overview view the steps sit under "Inspect transaction steps". "Which calls happened inside other calls?" shows the nesting, and each call has "Whose account does this call use?", which lists the account context, the requested target, the sender seen by the call and the native call value.

The amount on a DELEGATECALL, CALLCODE or STATICCALL is labelled "Call-context value". Other successful calls are labelled "Trace-reported value". In Activity, a delegated call is titled "Delegated internal call" and its destination is labelled "Requested code target": the call requests code from the displayed target while keeping the parent's account. The exact code bytes are not resolved. A read-only call is titled "Read-only internal call" with a "Read-only target".

A DELEGATECALL shows a call value only when the parent and the frame both retain the same valid value. Otherwise it is shown as unavailable, not as zero. Account context is worked out only for Ethereum mainnet transactions of type 0 to 4 whose block matches the pinned fork schedule, and only from a consistent recorded path. Elsewhere the calls are still listed but the context is withheld.

A branch with malformed data is skipped whole. Its children are not kept, because their context cannot be trusted once the parent is broken. Skipped branches are counted and reported.

The result is the finding finding:execution-trace. It is class observed when a trace was captured with nothing skipped, and class limitation otherwise.

#LimitsWhat it does not tell you

  • For DELEGATECALL, CALLCODE and STATICCALL the displayed value is context, not a transfer to the code address.
  • Missing call details are never assumed to be zero-value successful calls.
  • The trace comes back without a transaction hash, so its link to the transaction is a consistency check, not a proof.
  • Calls beyond depth 6 or past 64 frames are not shown.
  • Events are not tied to the call that emitted them.
  • A call's return data is kept only when it is 64 bytes or less.