Orca Crypto
Menu
Start Here
Learn
Chains
Exchanges
Markets
Tools
Safety
Glossary
More
Our tokens Book a session
Advanced · Onchain analysis

Reading a transaction like an analyst

An explorer page is a summary. Underneath it are four bytes, a few 32 byte words and a handful of logs, and you can read every one.

Loading live prices
By the Orca Crypto teamUpdated 2026-09-28How we check this15 min readAdvanced
The short answer

An EVM transaction is a signed request: a target, an ETH value, gas limits and input data whose first four bytes choose the function and whose remaining 32 byte words carry the arguments. What actually happened lives in the receipt, as event logs whose first topic identifies the event, and in the trace, which records every internal call. Read all three layers and no explorer summary can mislead you.

Three layers: request, receipt and trace

A block explorer blends three different objects into one page. Pulling them apart is the first analyst habit, because each answers a different question and each can hide something the others reveal.

LayerWhat it isWhat it answersWhat it misses
TransactionThe signed request: nonce, gas fields, target, ETH value, input dataWhat the sender asked forWhether it worked, and what it touched
ReceiptProduced by execution: status, gas used, event logsDid it succeed, and what did the contracts announceAnything a contract did without emitting an event, including plain ETH movements
TraceA replay of every call inside the transaction, with values and inputsThe full call tree, internal ETH transfers, the exact frame that revertedLittle; the catch is that only archive or tracing nodes can produce one

The rest of this guide takes one real transaction apart: a sale of 20 USDC for ETH through the Uniswap V2 router, mined in late September 2026, small enough to check by hand. The trader’s address has been swapped for an obvious placeholder and the hash shortened, because the lesson is the structure, not the person who sent it. If you have read the beginner guide to block explorers, this is the same page with the lid off.

The envelope: nonce, gas and value

text
hash                  0x70b8...ba3f   (shortened, see below)
block                 a mainnet block in late September 2026
type                  2            (EIP-1559)
nonce                 (the sender's transaction count)
to                    0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D   (Uniswap V2 Router 02)
value                 0
gas limit             250,000
maxFeePerGas          9.296196401 gwei
maxPriorityFeePerGas  1 wei
input                 260 bytes, starting 0x18cbafe5

receipt:
status                1            (success)
gasUsed               114,443
block base fee        4.594749928 gwei
effectiveGasPrice     4.594749929 gwei
logs                  5

Three fields repay close attention. The nonce is the sender’s sequence number; a pending transaction with a lower nonce blocks this one, and resending the same nonce with a higher fee is how wallets speed up or cancel. Value is ETH only, so it is zero here even though 20 USDC moved: the token amounts travel in the input data. And the signed payload has no from field at all. The sender is recovered from the signature, which is why nobody can forge a transaction for an address whose key they do not hold.

The fee follows the EIP-1559 rule:

effective gas price = base fee + min( max priority fee, max fee − base fee )The base fee part is burned. Only the priority part goes to the block producer.

The sender offered a priority fee of 1 wei, so the effective price was the base fee plus 1 wei. The total was 114,443 gas × 4.594749929 gwei = 0.000525837 ETH, about 1.42 dollars at the pool’s own price, or roughly 7% of this small trade. All of it was burned except 114,443 wei, the tip. The 9.30 gwei cap was headroom in case the base fee rose; unused headroom is never charged. The type field says which rules apply:

TypeIntroduced byWhat is different
0The original formatA single gasPrice
1EIP-2930Adds an access list of addresses and storage slots
2EIP-1559A burned base fee plus a priority fee, with maxFeePerGas as the cap
3EIP-4844 (Dencun, March 2024)Carries blobs; this is how rollups post data
4EIP-7702 (Pectra, May 2025)An authorization list that points an ordinary address at contract code

Function selectors: the first four bytes

The input data opens with four bytes that choose the function. They are the first four bytes of the Keccak-256 hash of the function’s canonical signature: its name and parameter types, with no spaces, no parameter names, and aliases written out in full, so uint becomes uint256.

keccak256("swapExactTokensForETH(uint256,uint256,address[],address,uint256)")[0:4] = 0x18cbafe5

Some selectors are worth recognizing on sight:

SelectorSignatureWhat it usually means
0xa9059cbbtransfer(address,uint256)An ERC-20 transfer from the sender
0x095ea7b3approve(address,uint256)Sets an allowance; read it before you sign it
0x23b872ddtransferFrom(address,address,uint256)A spender using an allowance
0xa22cb465setApprovalForAll(address,bool)An operator approved for every NFT in a collection
0xd505accfpermit(...) (ERC-2612)A signed approval being submitted onchain
0x3593564cexecute(bytes,bytes[],uint256)Uniswap Universal Router, and other contracts that copied its interface
0x6a761202execTransaction(...)A Safe multisig executing a transaction its owners signed
A selector is a hint, not a proof
Four bytes allow only about 4.3 billion values, and selectors collide. The public database at 4byte.directory lists six signatures for 0xa9059cbb, including workMyDirefulOwner(uint256,uint256) and many_msg_babbage(bytes1), and every one of them hashes to the same four bytes as transfer(address,uint256). A signature database gives you candidates. Only the verified source or ABI of the contract in front of you tells you which function actually runs, and a hostile contract is free to put dangerous code behind a harmless looking name.
Decode the arguments, not just the name
The Bybit theft of February 2025 turned on this layer. The signers approved a Safe execTransaction whose operation argument was 1, a delegatecall, instead of 0, a plain call. The contract it called overwrote storage slot 0 of the wallet proxy, where the Safe keeps the address of its logic contract. The function name looked routine; the arguments were the attack. The full story is in what went wrong at Bybit.

ABI decoding by hand

After the selector, arguments are laid out in 32 byte words. Static types such as uint256, address and bool sit in place, left padded with zeros, so an address is 12 zero bytes followed by its 20 bytes. Dynamic types such as arrays, bytes and string put an offset in the head that points to a tail section, where the length comes first and the contents follow. Offsets count from the start of the arguments, after the selector. Here is the whole input, one word per line:

text
0x18cbafe5                                                        selector
0x00  0000000000000000000000000000000000000000000000000000000001312d00  amountIn      20,000,000 = 20 USDC (6 decimals)
0x20  0000000000000000000000000000000000000000000000000018fc1e39b55b88  amountOutMin  7,032,606,188,460,936 wei
0x40  00000000000000000000000000000000000000000000000000000000000000a0  path          offset: the array starts at 0xa0
0x60  000000000000000000000000a11ce000000000000000000000000000000a11ce  to            the recipient
0x80  000000000000000000000000000000000000000000000000000000006ab93ca0  deadline      1,790,524,576 (Unix time)
0xa0  0000000000000000000000000000000000000000000000000000000000000002  path.length   2
0xc0  000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48  path[0]       USDC
0xe0  000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2  path[1]       WETH

Word 0x40 holds 0xa0, 160: the path array starts 160 bytes in, at the word that holds its length, 2, followed by USDC and WETH. The deadline is a Unix timestamp for 15:56:16 UTC, 19 minutes and 41 seconds after the block. The minimum output was 0.0070326 ETH and the actual output was 0.0074031 ETH, so the sender accepted a result 5.0% worse than they got: a 5% slippage setting, which the guide to getting a good fill explains how to choose.

python
# Decode the calldata above with the standard library alone.
data = bytes.fromhex(calldata[2:])         # calldata: the full input as hex
args = data[4:]                            # offsets count from after the selector

def uint(at):
    return int.from_bytes(args[at:at + 32], 'big')

def address(at):                           # the last 20 bytes of the word
    return '0x' + args[at + 12:at + 32].hex()

amount_in, min_out = uint(0x00), uint(0x20)
path_at   = uint(0x40)                     # 160, a pointer into the tail
recipient = address(0x60)
deadline  = uint(0x80)
path = [address(path_at + 32 * (i + 1)) for i in range(uint(path_at))]
bash
cast 4byte 0x18cbafe5                  # candidate signatures from a public database
cast 4byte-calldata 0x18cbafe5000...   # decode calldata using that database
cast decode-calldata "swapExactTokensForETH(uint256,uint256,address[],address,uint256)" 0x18cbafe5000...
cast run <tx hash> --rpc-url $ETH_RPC_URL   # replay with a call trace

Event logs: what the contracts announced

A log has three parts: the address of the contract that emitted it, up to four 32 byte topics, and a data blob. For an ordinary event, topic0 is the full Keccak-256 hash of the event signature (all 32 bytes, not four). Parameters marked indexed fill topics 1 to 3, so an event can index at most three. Everything else is ABI encoded into the data. An indexed string, array or struct is stored as its hash, which lets you filter on it but never read it back. These are the topics you will meet most:

text
Transfer(address,address,uint256)
  0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef   ERC-20 and ERC-721 transfers
Approval(address,address,uint256)
  0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925   allowances
Swap(address,uint256,uint256,uint256,uint256,address)
  0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822   Uniswap V2 style pairs
Sync(uint112,uint112)
  0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1   V2 reserves after every change
Swap(address,address,int256,int256,uint160,uint128,int24)
  0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67   Uniswap V3 pools
Withdrawal(address,uint256)
  0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65   WETH unwrapped to ETH

Our swap produced five logs, decoded here:

text
#0  USDC   Transfer    from sender  to pair    value 20000000            (20 USDC)
#1  WETH   Transfer    from pair    to router  value 7403119027386028    (0.007403119 WETH)
#2  pair   Sync        reserve0 10493604956746          (10,493,604.96 USDC)
                       reserve1 3895950775264699295616  (3,895.95 WETH)
#3  pair   Swap        sender router, to router
                       amount0In 20000000, amount1In 0, amount0Out 0, amount1Out 7403119027386028
#4  WETH   Withdrawal  src router, wad 7403119027386028

In order, they tell the story. The sender’s USDC went to the pair (log 0). The pair sent WETH to the router, not to the sender (log 1). Sync reports the reserves after the trade, which puts the pool’s price at 10,493,604.96 ÷ 3,895.95 = 2,693.46 USDC per ETH. Swap gives the same amounts from the pair’s point of view. USDC is token0 because V2 pairs order their tokens by address, and 0xa0b8 sorts before 0xc02a. Finally the router unwrapped the WETH (log 4).

The sender paid 20 ÷ 0.0074031 = 2,701.56 USDC per ETH, 0.30% above the pool price: almost exactly the pool’s fee, because 20 USDC barely moves a pool holding 10.5 million. Put the reserves before the trade into the constant product formula from AMM math and you reproduce the output to the last wei.

Two details catch people out. The USDC log is emitted from the USDC proxy address, even though the code that ran lives in its implementation, because a delegatecall executes in the caller’s context. And ERC-721 NFTs use the very same Transfer topic, but index the token ID as well, so an NFT transfer has four topics and empty data while an ERC-20 transfer has three topics and the amount in data.

Internal calls and traces

Look again at the logs: nothing says the sender received any ETH. Plain ETH transfers emit no event. The router unwrapped WETH and then sent ETH onward with a call that carried value, and explorers display that as an "internal transaction". It is not a transaction at all, just a message call inside this one, and it can only be found by re-executing the transaction with a tracer (debug_traceTransaction with a call tracer, or trace_transaction on clients that support it). Simplified, the call tree looks like this:

text
CALL          sender -> Router.swapExactTokensForETH(...)
  STATICCALL  Router -> Pair.getReserves()                       quote the output
  CALL        Router -> USDC.transferFrom(sender, Pair, 20 USDC)
    DELEGATECALL  USDC proxy -> USDC implementation             log #0 is emitted here
  CALL        Router -> Pair.swap(0, 0.007403119 WETH, Router, "")
    CALL        Pair -> WETH.transfer(Router, 0.007403119)       log #1
    STATICCALL  Pair -> USDC.balanceOf(Pair)
    STATICCALL  Pair -> WETH.balanceOf(Pair)                     logs #2 and #3 follow
  CALL        Router -> WETH.withdraw(0.007403119)               log #4
    CALL        WETH -> Router   value 0.007403119 ETH
  CALL        Router -> sender   value 0.007403119 ETH           the "internal transaction"

Only a trace shows ETH moving between contracts, calls that reverted inside a try block while the transaction succeeded, delegatecall hops through proxies, and the frame where a failure began. Tracing needs an archive or tracing node, which is why explorers and data platforms precompute traces. What each of those calls costs is the subject of the EVM internals guide.

Reading a failed transaction

A failed transaction has status 0. Every state change is rolled back and every log discarded, but the gas used is still paid and the nonce is still consumed, because the network did the work of running it. The reason is in the revert data, which follows one of four patterns:

Revert data starts withMeaningHow to read it
0x08c379a0Error(string) from require or revert with a messageABI decode one string
0x4e487b71Panic(uint256) from a compiler check0x01 failed assert, 0x11 overflow or underflow, 0x12 division by zero, 0x32 array index out of bounds
Any other 4 bytesA custom error (Solidity 0.8.4 and later)The selector of the error signature; decode against the ABI or with cast decode-error
Nothing at allA bare require, or the transaction ran out of gasIf gas used equals the gas limit, it was out of gas

Had the price moved more than 5% before our swap was included, the router would have reverted with the message below. It is ordinary ABI encoding: a selector, an offset, a length and the padded bytes of the string.

text
Error(string) revert data for 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'

08c379a0                                                          selector of Error(string)
0000000000000000000000000000000000000000000000000000000000000020  offset of the string: 32
000000000000000000000000000000000000000000000000000000000000002b  length: 43 bytes
556e69737761705632526f757465723a20494e53554646494349454e545f4f55  "UniswapV2Router: INSUFFICIENT_OU"
545055545f414d4f554e54000000000000000000000000000000000000000000  "TPUT_AMOUNT", padded with zeros

In a deep call tree, ask where the revert started. Outer contracts often pass the inner error up unchanged but some replace it, so the trace, not the top level message, tells you which contract refused.

The same questions on Solana

A Solana transaction is a list of instructions, each naming a program, every account it will read or write, and an opaque data field. All instructions succeed or none do. There is no calldata standard like the EVM ABI and no topics: programs write log lines, and when one program calls another (a cross program invocation) the call is recorded as an inner instruction. Here are the logs of a real Orca Whirlpool swap from September 2026, trimmed:

text
Program ComputeBudget111111111111111111111111111111 invoke [1]
Program ComputeBudget111111111111111111111111111111 success
Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [1]
Program log: Instruction: SwapV2
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 105 of 13472 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 112 of 10675 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program data: 4cpJr5MroJazrJmfc0yJa40v9BRk1Et7LAO/DzpiUkK2Wtn0Pd1oxgBCsIATwyy2tgAA...
Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc consumed 31093 of 38809 compute units
Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc success

The number in brackets is the call depth: [1] for top level instructions, [2] for the two Token program transfers the Whirlpool program invoked, one from the user into the pool and one back out. Programs built with the Anchor framework start their instruction data with an 8 byte discriminator, the first 8 bytes of sha256("global:<instruction name>"); for this swap that is 2b04ed0b1ac91e62, sha256("global:swap_v2"). Anchor events arrive as base64 Program data: lines with the same trick, and this one decodes to e1ca49af932ba096, sha256("event:Traded"), followed by the trade details. Non-Anchor programs pick their own layouts: the Compute Budget program uses a single leading byte, 0x02 to set a compute unit limit and 0x03 to set a price.

The fee is 5,000 lamports per signature plus an optional priority fee: the compute unit limit times a price in micro-lamports per unit. This swap set its price to zero and paid exactly 5,000 lamports. For "who received what", read the pre and post token balances in the transaction metadata, which is where explorers such as Solscan and Solana Explorer build their balance change tables. Versioned transactions can also load accounts from address lookup tables, so an explorer may list more accounts than the signed message does.

Tools, and a routine that works on anything

Explorers

Etherscan style explorers decode input for verified contracts, list logs and internal transactions, and label known addresses. Every panel is their interpretation of the raw data.

Debuggers and simulators

Tenderly replays a transaction step by step and lets you re-simulate it with changed inputs. BlockSec Phalcon Explorer shows the call tree beside a fund flow view built for incident analysis.

Signature databases

4byte.directory and Sourcify’s 4byte.sourcify.dev, which took over the openchain.xyz database, map selectors and topics back to text signatures. Treat every answer as a candidate.

Your own tools

Foundry’s cast decodes calldata, looks up selectors and replays a transaction with a full trace. A few lines of Python cover the rest.

  1. Status and fee first

    Did it succeed, how much gas did it use against its limit, and what was the effective gas price? A failure that used every unit of gas is a different problem from one that stopped early.

  2. Identify the target

    Is to a contract, and is it a proxy? If it is, the logic lives at the implementation address, and that is the code you need to read. The guide to admin keys and upgradeability shows how to find it.

  3. Decode the input against the verified ABI

    Use the contract’s own ABI, not a database guess. Check every argument that can move value: amounts, recipients, deadlines, and any nested call data.

  4. Read the logs in order

    Follow the Transfer events to see who ended up with what, and check that they agree with the Swap or protocol events around them.

  5. Check the trace for what the logs leave out

    ETH movements, calls that failed inside a try, and the exact frame where a revert began.

To find the right explorer for any address or hash, the explorer router sorts it by format. The specifications are short and worth one careful read: the Solidity ABI specification, EIP-1559 for the fee fields, and Solana’s transaction documentation.

Common questions

What is the difference between a transaction and an internal transaction?

A transaction is signed by an externally owned account and included in a block. An internal transaction is a label explorers use for a message call made by a contract during that transaction, usually one that moved ETH. It has no signature, no nonce and no hash of its own, and you only see it in a trace.

Why does a failed transaction still cost gas?

Every node had to execute it to discover that it failed, and that work is what gas pays for. The state changes and logs are discarded, but the gas used is still charged and the nonce is still consumed. Simulating before you send is the cheap way to find a revert.

Can I trust the decoded input an explorer shows me?

For a verified contract, the explorer decodes with the contract's own ABI, which is reliable. For an unverified contract it may guess from a signature database, and selectors collide. Check that the target is verified and, for a proxy, that you are reading the implementation's ABI.

Do layer 2 transactions read the same way?

On EVM rollups the transaction format, the ABI, logs and traces work as they do on Ethereum. The fee differs: it adds a charge for publishing the data to Ethereum, which most layer 2 explorers show separately.

Kept on this device only. Nothing is sent anywhere.

Where to go next

Next: Following the money: flows and clustering

Guide 14 of 16 in the Advanced level. Your place is kept on this device, so you can pick up exactly here next time.

Share this page

Was this page useful?