FORENSIAWeb3 forensicsOpen workspace

Code at the time of a transaction

How Forensia records what code a contract held when a transaction ran, and why the timing matters.

3 min read · Contract analysis

#Plain languageIn plain terms

Contracts can change. An upgradeable contract may run different code today from what it ran last year. So when you study an old transaction, the contract's current code can mislead you.

For the contracts a transaction called, Forensia asks what code they held at the end of the block that contained it. That is close to the moment in question, and the page is careful to say it is not the exact moment.

#How it worksHow it works

  1. From the transaction's top-level call and its call trace, collect the addresses that were called.
  2. For the first few, ask the node for the code at the exact block the transaction was in, identified by that block's hash.
  3. Store each answer with its evidence record.
  4. Where a state diff was also captured, compare code at the start of the transaction, the end of the transaction, and the end of the block.

#Technical detailThe exact rules

Targets are the destinations of CALL, STATICCALL, DELEGATECALL and CALLCODE frames. Creation frames and log emitters are excluded. The transaction's own top-level call counts, so its recipient is read even when no trace was available. The request is eth_getCode with a block-hash parameter and requireCanonical: true, so a block that has been reorganised away is refused.

LimitValue
Targets read per transaction4
Code size accepted49,152 bytes
Per request2.5 seconds
Saved response size re-checked locally128 KiB

Each observation is described as one of: unavailable, no code returned, a delegation pointer, or code returned with its size and Keccak-256 hash. When two or more boundaries are known, the comparison reports same-observed-bytes or different-boundary-bytes by comparing hashes. With fewer than two it reports not-comparable.

The comparison is the section "Compare code observations" in the transaction steps, with the boundaries "Transaction start", "Transaction end" and "Containing-block end". The block-end reads on their own sit under "Original block-end code observations". The state diff is requested on Ethereum mainnet only, so on other networks the first two boundaries read "Not available". When the diff does not carry a target's starting code, one further optional trace request asks for it. When an account appears on both sides of the diff with no ending code field, the ending code is taken to be the starting code, and the row says so. The comparison covers up to 4 call targets whose account context could be worked out, and opening it makes no new requests.

A saved result can be re-checked offline. The check re-reads the stored response, confirms the request asked for the same address and block, and confirms the code shown equals the code returned. It makes no network request. Its outcomes are matched, conflict, unavailable and check-limit.

#LimitsWhat it does not tell you

  • Other transactions in the same block can change a contract's code before or after the one you are studying.
  • Matching hashes at two boundaries do not prove the code was unchanged in between.
  • An empty answer does not rule out a precompile or a contract destroyed earlier in the block.
  • The offline re-check confirms the page matches what the provider sent. It does not confirm the provider was right.
  • This runs for transaction lookups. An address lookup reads code at the latest block only.