Skip to content

Research notes

Receipt Success vs Token Transfer Evidence

A successful receipt can accompany a failed token operation. Follow five checked outcomes and separate one transfer's credit from a whole-block balance delta.

Ivory paper with a blank embossed seal curves beside an empty ceramic tray and a lime capsule.
Illustration for research notes; examples in this article are educational.

A successful Ethereum receipt means the transaction's top-level execution succeeded; it does not prove the intended token amount reached its recipient. An ERC-20 call can return false without reverting, and logs reflect the emitting contract's behavior. Verify the token contract, decoded transfer evidence and attributable recipient balance change before marking delivery complete.

What the receipt actually reports

On Ethereum after Byzantium, receipt status answers whether the top-level call succeeded or failed. The success value is conventionally displayed as 0x1; failure is 0x0. This is the scope defined by EIP-658. A wallet's green success label is a presentation of that execution result, not an independent check of the payment instruction.

The official receipt API returns transaction and block identities, status, logs and gas information. Its receipt schema does not include an arbitrary token function's returned bytes. Consequently, reading status does not reveal the boolean returned by a token's transfer function.

An approval transaction can also succeed while moving no tokens because permission was the requested action. The token allowance guide explains that boundary. Start a transfer investigation by decoding what the transaction actually asked the contracts to do, including the token recipient inside the call. The transaction destination might be a token, router or another contract rather than the intended recipient.

Why returning false can leave a successful receipt

The ERC-20 specification requires callers to handle false boolean returns. Consider a token that rejects an operation by returning false without reverting or changing balances. A transaction directly calling that function can finish normally. Another contract calling the token can also finish normally if it ignores the false result. Execution completed; the requested token operation did not.

For a low-level call, keep its completion flag separate from the bytes it returns. Solidity 0.8.30 documents both results of .call. A true call-success flag can accompany bytes that decode to the token's false return. Checking only that flag misses the failed token operation.

This possibility does not mean every token fails this way. OpenZeppelin Contracts 5.x documents that its base ERC20 implementation reverts on failure. Its SafeERC20 library provides safeTransfer, which rejects a false return and supports tokens that return no value by accepting a non-reverting call. These are specific implementation behaviors.

The exact function matters. In the same current documentation, trySafeTransfer returns a boolean instead of reverting on an unsuccessful operation. A caller still needs to decide what that boolean means for the surrounding workflow. A library name alone does not establish that the whole transaction will fail when one payment fails.

If a caller's failure propagates to the top level, the receipt reports failure. EIP-140 specifies that reverted execution loses its state changes and logs. If an outer contract catches a failed subcall and continues, inspect the surviving actions separately. The event logs versus internal transactions comparison explains why a call trace and a receipt log answer different questions.

Five outcomes for the same 100-token instruction

This hypothetical ledger uses five independent scenarios. Reset the sender to 1,000 token units and the recipient to 40 before each case. Each token has six decimals, the requested amount is 100, and no other activity occurs. A separate fee collector starts at zero. Gas is excluded from these token balances.

The custom fee and dishonest cases deliberately have different token behavior from the conventional case. They are invented examples, not descriptions of a named asset or assertions that those behaviors satisfy every ERC-20 requirement.

Independent caseReceipt statusSurviving log to recipientRecipient afterCredited units
Conventional transferSuccessTransfer of 100140100
Token returns false; caller does not reject itSuccessNone400
Caller rejects false; revert reaches top levelFailureNone400
Custom token deducts a 2% transfer feeSuccessTransfer of 9813898
Dishonest contract emits a log without moving balancesSuccessApparent Transfer of 100400

In the first case, the sender ends with 900 and the recipient with 140. Both false-return cases and the dishonest log-only case leave the sender at 1,000. In the fee case, the sender ends with 900, the recipient with 138 and the collector with 2. The fixture also emits a separate transfer log for the collector's 2 units.

Check the fee case in raw integers. The request is 100 × 1,000,000 = 100,000,000 base units. The fee is 100,000,000 × 2 / 100 = 2,000,000. The recipient receives 98,000,000, or 98 displayed units. The recipient balance becomes 40 + 98 = 138. Conservation holds: the closing 900 + 138 + 2 = 1,040 equals the opening 1,000 + 40.

The dishonest fixture emits the expected-looking event and returns normally but changes no balances. Its reported event does not establish delivery. This is a limitation of trusting arbitrary contract behavior, not an ordinary ERC-20 transfer pattern.

The table separates three questions that a single success badge collapses: did execution complete, what did the contract record, and what changed in the recipient's holding? A successful wrapper call can improve failure handling without proving that a custom token delivered the entire requested amount. For the broader inventory effects of custom mechanics, see fee-on-transfer and rebasing token accounting.

Decode the event against the intended asset

For the standard ERC-20 Transfer event, the sender and recipient are indexed fields and the amount is the non-indexed value. The standard also requires an event for zero-value transfers, so a matching event's existence alone says nothing about a positive amount. Verify the amount and units against the ERC-20 interface.

Before counting a log, match its emitting address to the intended token on the intended chain. Then use that contract's expected event layout. Solidity's event documentation explains that indexed fields become topics, other fields become encoded data, and correct interpretation requires knowing the event type and layout. A familiar event name or topic does not authenticate the emitting asset.

Check the decoded recipient, sender and integer amount rather than an explorer's symbol alone. Verify the token's unit convention; six decimals are an assumption of the example. In a routed operation, the first transfer could credit an intermediate contract. Several later transfers may be needed before the intended recipient receives anything. Preserve individual records until that route is reconciled.

Even a log from the intended address establishes what its code emitted, not trustworthy economics. A malicious implementation can report misleading events or balances. Confirming on-chain state does not establish market value, redeemability or the ability to make a future transfer.

Keep the block identity and event position with each record. The transaction hash versus log index guide covers identifying an individual event and handling changed block placement. Adding every repeated provider response to a payment total would count one event more than once.

Attribute a balance change before calling it delivery

For a known token implementation, calling balanceOf(recipient) on the token contract is a useful cross-check. Read the intended account at recorded block states, preserving raw units. The eth_call API supports a block parameter and returns the call's bytes without creating a chain transaction. Historical state availability still needs checking with the provider.

A block-end comparison combines all changes between its two endpoints. Extend the hypothetical fee case: the recipient begins with 40, receives the target transfer's 98, and receives another unrelated deposit of 20 later in the same block. Its closing balance is 40 + 98 + 20 = 158. The observed increase is 158 − 40 = 118, while the target transfer contributed 98.

Attributing all 118 units to the target would overstate its credit by 20. Identified transfer evidence explains that difference in this fixture. With a rebase, a transfer out, custom accounting or unknown activity, the reconciliation needs the corresponding explanation. A balance delta cannot supply that attribution by itself.

Reading the previous block and the inclusion block also does not provide the precise state immediately before and after one transaction when several transactions touch the account. Where isolation matters, obtain suitable transaction-level execution evidence or reproduce execution against the correct pre-state. Label a block-level observation honestly if those narrower observations are unavailable.

A fresh simulation after inclusion is another call against its selected state. It does not recover the original token return value merely because it uses the same input. Keep simulation results distinct from execution evidence.

Use a completion record with explicit evidence

Store these fields together for each intended payment. The worksheet works for an investigation or a transfer-processing integration without relying on one explorer's screen labels.

RecordWhat to retainCompletion check
InstructionChain, token, recipient and raw intended amountCorrect asset and destination
ExecutionTransaction hash, block hash and receipt statusTop-level outcome established
Contract pathCalled function and handling of false or missing returnsFailure semantics understood
EventsEmitter, decoded parties, amount and event positionRelevant surviving records identified
StateRecipient balance observations and their block referencesUnits reconciled within the observed scope
AttributionOther transfers, fees and balance adjustmentsTarget credit separated from unrelated changes
AcceptanceRequired credit and confirmation policyRecorded evidence satisfies the actual instruction

Define the acceptance rule before sending. If the instruction requires the recipient to obtain 100 units, the hypothetical 98-unit credit is a shortfall even though the receipt succeeds. If a fee-aware instruction explicitly permits 98, record that different requirement. A status label cannot choose the requirement afterward.

For an unresolved successful receipt, investigate before issuing another transfer. The original operation may already have delivered some or all of the amount. Repeating it can create a second payment while leaving the first unexplained. For a failed receipt, review the failure and current state before a new attempt; earlier transactions remain separate records.

Close a payment with a concrete statement: the identified token credited this recipient with this attributable amount in this transaction, at this block, under the recorded acceptance rule. If the evidence supports only execution success, keep delivery unresolved until the missing evidence is obtained.

Keep the context with the example.

Each worked example is illustrative, with its sources and assumptions in the article. Read our editorial standards for authorship, verification and corrections.