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.
#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
- Take the event logs from a receipt that has been bound to its transaction and block.
- Check every log really belongs to that transaction.
- Pick out logs with one of three standard transfer signatures.
- Decode each strictly. A log that does not fit the standard layout exactly is excluded, not guessed at.
- Show amounts in raw units unless the token's decimal scale is known.
#Technical detailThe exact rules
| Event | First 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:
| Layout | Standard | Amount |
|---|---|---|
| 3 topics, data is one 32-byte word | ERC-20 | The data word, in raw units |
| 4 topics, data empty | ERC-721 | One token. The fourth topic is the token ID. |
TransferSingle, 4 topics, data is two words | ERC-1155 | Token ID, then quantity |
TransferBatch, canonical array layout | ERC-1155 | One 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
Transferevent. 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.