The origin tracer is paused. The library is open and still growing.What’s paused, and when it returns

← All articles

Event Logs and How Token Transfers Are Read

Key takeaways

  • A TRC-20 token transfer is not a native transaction. It is a TriggerSmartContract call that runs the token's transfer() and emits a Transfer event — and the standard requires that event to fire.
  • The event is recorded as a log: a contract address, a topics array, and a data field. It is the only durable record that the tokens moved.
  • topics[0] is the event's signature hash; indexed parameters (the from and to) fill the next topics; non-indexed parameters (the amount) go in data.
  • Block explorers reconstruct every "token transfer" you see by decoding these logs. To read token flow on TRON, you read logs — not value movements.

When a block explorer shows “1,000 USDT” moving from one address to another, it is easy to assume a transfer transaction carried that value. There isn’t one. USDT is a contract, a transfer of it is a call to that contract, and the movement of the balance happens inside the contract’s own storage — invisible to anything watching for value edges.

What the movement leaves behind is an event. TRON’s token transfers, approvals, swaps, and almost everything else a contract does are recorded as logs, and the log is the durable, readable trace. An investigator who follows only native transfers sees TRX and TRC-10 move and misses the entire TRC-20 economy.

This article is about that layer: what a log entry is, how a token transfer becomes one, and how the pieces decode. The token standards themselves are covered in token standards on TRON; here the subject is the record they leave.

A token transfer is an event, not a transfer

A native TRX transfer is a TransferContract — a transaction type whose whole purpose is to move value, with sender, receiver, and amount right in the transaction. A TRC-20 transfer is nothing like it.

Moving a TRC-20 token is a TriggerSmartContract transaction: a call to the token contract’s transfer() (or transferFrom()) method. The method updates two balances in the contract’s storage and, as the standard requires, fires an event. The TRC-20 specification is explicit on this. transfer() and transferFrom() “MUST fire the Transfer event,” and even “transfers of 0 values MUST be treated as normal transfers and fire the Transfer event.”

NATIVE TRX — VALUE IS ON THE TRANSACTION TransferContract SENDER · TO · AMOUNT [ MOVES VALUE ] Recipient balance TRX ARRIVES DIRECTLY TRC-20 — VALUE IS ONLY IN THE EMITTED LOG TriggerSmartContract RUNS token.transfer() NO AMOUNT IN THE TX BODY [ EMITS ] Transfer event EMITTED TO THE TX LOG THE ONLY RECORD OF THE MOVE KEY Same transaction type for any amount — from, to, and value live in the event
Native value moves in the transaction; token value moves in the contract, and only the event records it.

So the transaction type for a billion-dollar USDT movement and for a tiny one is the same — TriggerSmartContract — and neither carries the amount in the transaction body. The amount, the sender, and the recipient are in the event. This is why internal transactions and event logs are different things: an internal transaction is value a contract moves during execution; an event log is a record a contract emits. A token transfer is the second kind.

Anatomy of a log entry

Each event a transaction emits becomes one entry in the log field of that transaction’s TransactionInfo. The TVM writes it with the LOG instruction, and it has three parts:

PartWhat it holds
addressThe contract that emitted the event — for a token transfer, the token contract itself. This is what identifies which token moved.
topicsAn array of 32-byte values. topics[0] is the event signature; each subsequent entry is one indexed parameter.
dataThe non-indexed parameters, concatenated.

topics[0] is the fingerprint of the event type: the keccak-256 hash of the event’s signature (its name and parameter types). For a token transfer that signature is Transfer(address,address,uint256), and its hash is always ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. Every TRC-20 Transfer event on the chain, from any token, carries that same first topic.

Log entry ONE PER EMITTED EVENT IN TransactionInfo.log · WRITTEN BY THE TVM LOG INSTRUCTION [ THREE PARTS ] address THE TOKEN CONTRACT — WHICH TOKEN MOVED topics[ ] 32-BYTE VALUES [0] ddf252ad…b3ef = keccak256( Transfer(address,address,uint256) ) THE EVENT'S FINGERPRINT · SAME FOR EVERY TRC-20 TRANSFER, ANY TOKEN KEY [1] from — INDEXED · SEARCHABLE [2] to — INDEXED · SEARCHABLE data value — uint256 · NON-INDEXED FILTER BY topics[0] → EVERY TOKEN TRANSFER ON THE CHAIN
The signature hash names the event, the topics carry the indexed parties, and the amount sits in data.

Which parameters land where is set by the event definition. The TRC-20 Transfer event marks its from and to as indexed, so they become topics[1] and topics[2], where they can be filtered and searched on directly. The value is not indexed, so it sits in data. The Approval event that an approval fires works the same way: owner and spender in the topics, allowance amount in the data.

Reading transfers from logs

This is what a block explorer is doing when it shows you a token transfer. It scans transactions’ logs, matches topics[0] against known event signatures, reads the token contract from the log’s address, pulls from and to out of the topics and the amount out of the data, and renders a human-readable line. That is the whole pipeline.

That decoding is a mechanism a reader can run themselves, and it changes what is searchable. Because the signature hash is constant, filtering transactions’ logs by topics[0] = the Transfer hash finds every token transfer on the chain regardless of which token. Adding the log address narrows to one token. Adding an address in topics[1] or topics[2] narrows to transfers a specific wallet sent or received.

It also protects against a common misreading. A transfer buried inside a complex contract call — a swap, a batched payout, a contract paying out to many addresses — still emits its Transfer log, even though the top-level transaction looks like a single call to some router. The value moved is sitting in the logs whether or not the transaction’s surface gives any sign of it.

What this means for a reader

The working rule for tracing on TRON is that token flow lives in the logs, and each part of a log answers a different question.

The address field settles which asset moved. It is the token contract itself, so it tells you which token you are looking at before you read anything else — a transfer of USDT and a transfer of a scam token are distinguished by that field. The topics settle the rest: topics[0] says what kind of event fired, and topics[1] and topics[2] hold the from and to. A wallet’s complete TRC-20 history is the set of logs where its address appears in those two positions, and it can be reconstructed without trusting an explorer’s rendering.

A missing value edge is not a missing transfer. Because a token transfer moves nothing at the native level, it will never appear in a view built around TRX movement. Look for the Transfer log instead.

A transaction on its own records only that a contract was called. What the call did — which token, from whom, to whom, how much — is written in the logs, and nowhere else on the chain.

Sources