FORENSIAWeb3 forensicsOpen workspace

Reading token transfers

How token movements are decoded from a transaction's event logs, and why an event is not the same as a balance change.

2 min read · Transaction analysis

#Plain languageIn plain terms

Sending a token does not move anything the way sending ETH does. A token is a contract that keeps a list of who holds how much. A transfer is the contract updating its list and then announcing it with a public notice called an event.

Forensia reads those notices. It is strict about their format and it is careful with its words: an event is what a contract said happened. A badly written or dishonest contract can announce a transfer that never changed anything.

#How it worksHow it works

  1. Take the event logs from a receipt that has been bound to its transaction and block.
  2. Check every log really belongs to that transaction.
  3. Pick out logs with one of three standard transfer signatures.
  4. Decode each strictly. A log that does not fit the standard layout exactly is excluded, not guessed at.
  5. Show amounts in raw units unless the token's decimal scale is known.

#Technical detailThe exact rules

EventFirst topic
Transfer(address,address,uint256)0xddf252ad…f523b3ef
TransferSingle(address,address,address,uint256,uint256)0xc3d58168…aa2d0f62
TransferBatch(address,address,address,uint256[],uint256[])0x4a39dc06…26c8f7fb

Fungible tokens (ERC-20) and single NFTs (ERC-721) share the same Transfer signature. They are told apart by layout:

LayoutStandardAmount
3 topics, data is one 32-byte wordERC-20The data word, in raw units
4 topics, data emptyERC-721One token. The fourth topic is the token ID.
TransferSingle, 4 topics, data is two wordsERC-1155Token ID, then quantity
TransferBatch, canonical array layoutERC-1155One item per ID and quantity pair

Address topics must have 12 zero bytes of padding in front of the 20-byte address. A topic that does not is excluded. A supported batch may encode up to 4,096 pairs; a transaction lookup retains up to 256 pairs per batch and 2,048 transfer items across the receipt. Any omitted items remain explicit.

Before decoding, every log is bound to the transaction: its transaction hash, block hash and block number must match, it must not be flagged as removed, and log positions must be unique and strictly increasing.

Decimals

The decoder never assumes a decimal scale. It asks the token contract for decimals() for the first 4 contracts seen and accepts values from 0 to 255. Without an answer, amounts are shown as raw units. If two sources report different scales for one token, the row is flagged as conflicting.

A transaction keeps at most 2,048 token items, separate from up to 64 nested call frames. Activity pages do not reduce that captured record. Findings: finding:decoded-surface, class observed, and finding:token-log-incomplete, class limitation, when a log was excluded or items were omitted. Unsupported events and incomplete traces are not called complete history.

#LimitsWhat it does not tell you

  • Any contract can emit a Transfer event. The event does not prove the token is real, valuable or the one its name suggests.
  • Tokens that take a fee on transfer, or that rebase, change balances in ways events do not describe.
  • An excluded log is not a sign of wrongdoing. It only means the layout was not the strict standard one.
  • Name and symbol come from the token contract and can be set to anything.