FORENSIAWeb3 forensicsOpen workspace

Checking what a token recipient did next with the same token

An optional check finds a recipient’s later Transfer events for the same token contract and confirms each against its receipt. One 2,000-block window, four checks, two hops.

· Forensia · 7 min read · Tracing, Evidence

Someone receives a token, for example a stablecoin. You want to know whether that address then sent the same token somewhere else. The lookup you ran captured a limited set of records, so the answer may simply not be in it.

Forensia has an optional follow-up check for this. You choose one captured token transfer, and the workspace asks the network source for later transfers of that exact token sent by the recipient, inside one fixed window of blocks. Every event it shows has been matched against the receipt of the transaction that produced it.

The check is bounded and the bounds are part of the result. It covers one window of at most 2,000 later blocks, shows at most 16 events from at most four transactions, and allows four checks and two hops per open result. It is a locally built feature that is still being extended. It is not a token history.

#Plain languageSame token, same recipient, a short window

A token is a small program on the network, and each one has its own address. When tokens move, that program writes a line in the transaction’s log: from this address, to that address, this many units. Those lines are called Transfer events.

Think of a shop’s till roll. If you know a customer was handed a gift card at 2 pm, you can scan the roll for the next hour and list the times that same customer handed a gift card of the same brand to someone else. You would not claim it was the same physical card, and you would not claim the roll covers the whole week.

Forensia treats token events the same way. It matches on the token’s contract address and the network, never on the token’s name or symbol, because anyone can deploy a token with any name. And if the recipient is a known shared service, such as a bridge pool, the workspace says so and stops following automatically, because what a pool sends out cannot be tied to what one user sent in.

#How it worksRunning a token recipient check

  1. On Overview, open Follow captured transfers and choose a Token row under Starting transfer.
  2. Scroll to the panel headed What did this token recipient do next? and press Check recipient’s token events.
  3. If the recipient is a known service, a Shared-service boundary notice appears and the button reads Inspect this service’s token events. You can still run it, as an explicit inspection.
  4. When the check returns, open Read checked events to see each event, and Exact addresses and supporting sources for full identities and source records.
  5. Press Check next token recipient to continue. Open Token recipients and stop reasons to see why each recorded event was or was not followed.
  6. Press Save token trace to keep completed checks in this browser, or Prepare token backup and then Download token trace JSON.

The counter shows how many of 4 token checks are used in this open result, and failures and cancellations count. The starting selector is disabled while a check runs. Saved token traces reopen from Saved EVM traces, under Token traces.

Save token trace is separate from both Save result and Save EVM trace. Once a token trace is saved, each later attempt is recorded before its request is sent and its result is saved automatically. After a reload, a check with no saved result shows One check needs review. Reopening never retries it. Mark check interrupted keeps it counted and claims no result.

#Technical detailFrom eth_getLogs hint to receipt-confirmed event

The browser posts to the existing investigation route with the mode evm-token-branch. The request must name the exact starting transaction hash, log index, token contract, sender, recipient, positive raw amount, block number and a later block boundary. Zero-address and self-transfer starts are refused. The route keeps its usual source-policy, verification, lease and concurrency gates.

The server calls eth_chainId, checks it against the configured chain, and pins the endpoint that answered. Fallback is allowed for that first call only.

Order of calls in acquireTokenBranch
eth_getTransactionReceipt(start)        status 0x1, log at logIndex must match the request
eth_getBlockByNumber(startBlock, false) block hash and transactions[index] must match
eth_getBlockByNumber(upperBound, false) timestamp must be later than start
eth_getLogs({
  address: tokenContract,
  fromBlock: startBlock + 1,
  toBlock: upperBound,
  topics: [TransferTopic, recipientAsTopic]
})
eth_getTransactionReceipt(candidate)    up to four distinct transactions
eth_getBlockByNumber(eventBlock, false) per accepted block
eth_getBlockByNumber(every block read, false)   recheck at the end

The upper bound is the smaller of the original capture boundary and the starting block plus 2,000. The first topic is the ERC-20 Transfer signature hash, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. The second topic is the recipient address left-padded to 32 bytes, which filters on the indexed from field.

Log shape that is accepted

A log is parsed only if removed is false, it has exactly three topics, topic zero is the Transfer signature, both address topics have twelve leading zero bytes, and data is exactly one 32-byte word. That shape excludes ERC-721 Transfer events, which carry the token ID as a fourth topic. Any returned log with a different contract, a different sender or a block outside the window fails the whole check. So does a response with more than 64 logs. No prefix of an oversized response is used.

Duplicates and ordering

Logs are keyed by block number and log index. An exact repeat of a key is counted as a duplicate and dropped. A repeat with different content is a conflict and ends the check. The code also rejects one block position naming two transaction hashes, one transaction appearing at two positions or block hashes, and one block number appearing with two hashes.

Events are then sorted by block number, transaction index and log index. Within a block, log indexes must strictly increase along that order, or the response is rejected as self-contradictory. Zero-value events, burns to the zero address and self-transfers are counted as excluded and not followed.

Receipt corroboration

The filtered logs are treated as hints. For each eligible event, the code fetches the transaction receipt, requires status 0x1, checks that every log in the receipt agrees on transaction hash, block number, block hash and transaction index with strictly increasing log indexes, then finds the log at the same index and requires it to equal the hinted log field for field. The block header must place that transaction hash at the event’s transaction index, and its timestamp must fall inside the window.

Limit in EVM_TOKEN_BRANCH_LIMITSValue
blocks after the start2,000
logs returned by the filter64
distinct candidate receipts4
accepted events16
upstream requests, including fallback attempts24
bytes per upstream response1,048,576 (1 MiB)
acquisition deadline22,000 ms

Eligible events beyond the receipt or event caps are reported as an omitted count. At the end, every block header that was read is fetched again, and a changed hash, timestamp or transaction list invalidates the check.

Queue and stop conditions

EVM_TOKEN_TRACE_LIMITS sets 4 checks, 2 hops and 65 nodes. An event is identified as contract, transaction hash and log index, so several events in one receipt stay distinct. If two branches reach the same event, it is shown once with both parents. A recipient address seen earlier does not suppress a later, distinct event.

Node statusMeaning
queuedRecorded, recipient not checked yet
checkedA check ran, with its accepted and omitted counts
failedThe attempt failed or was cancelled, and was charged
service-boundarySender or recipient is in the known-service registry. Shown, not continued, not attributed
boundary-limitThe event sits at the fixed block horizon
depth-limitTwo hops reached. Event shown, recipient not checked
check-limitFour checks used before this recipient was reached

Before a new page is accepted, all pages are compared together: block hashes and timestamps at every overlapping height, event content at every block and log position, transaction positions, and global log order. A new observation cannot quietly revise an earlier one.

#LimitsWhat a confirmed token event still leaves open

  • A later event from the recipient is a same-token account sequence. It does not show that the same funds moved, and no amounts are allocated.
  • A successful receipt confirms the event was emitted. It does not confirm the contract behaves honestly, that balances changed accordingly, or that the contract conforms to ERC-20.
  • Fee-on-transfer and rebasing behavior are not reconstructed. Onward events are shown in raw units. No decimal scale, price or symbol is inferred.
  • Events sent by a shared service are that service’s activity. They are not attributed to the funds that entered it.
  • The registry of known services is a list. An unlisted service is not detected as one.
  • Same-block activity, internal transfers, mints, burns, NFTs and anything beyond the single window are not covered. There is no pagination.
  • A source may omit logs. An empty result is not inactivity, and a successful query is not proof of completeness.
  • When a check fails, the workspace gives one conservative message. It does not diagnose whether the cause was the provider, an oversized response or a failed verification.
  • Saved traces are unsigned, browser-only metadata, limited to five traces, 10 MiB in total and 3 MiB per backup. Digests show consistency, not authenticity.

The notes record a real check from a transfer in transaction 0x22fc4a1bba0e2a711a5852f5c835eea4cb764f989895b1b48ec73d84dab8ed95 at block 26,126,887. Its fixed window returned four displayed events and 46 further eligible events that were explicitly omitted by the caps. Reading that result correctly means reading both numbers.