Skip to content

Research notes

Event Logs vs Internal Transactions on Ethereum

Follow one payout through a successful payment, a reverted subcall and a refund to reconcile trace attempts, surviving logs and net ETH balances.

Ivory paper channels branch toward three ceramic bowls, with a curled path and lime bead on a dark surface.
Illustration for research notes; examples in this article are educational.

Ethereum event logs are contract-emitted records kept in transaction receipts. “Internal transactions” are explorer labels for selected contract calls or ETH movements reconstructed from execution traces. A trace can include failed calls whose value transfers and logs were rolled back, so neither a displayed call nor a successful outer receipt proves the recipient received funds.

One wallet action can produce many calls and several event logs. These records describe different aspects of the same execution. Keeping them together helps explain where ETH went without treating every displayed row as another payment.

One execution, several records

Start with the question you need to answer.

RecordQuestion it helps answerImportant boundary
Transaction and receiptWhich submitted transaction executed, and did its top-level call succeed?Success does not establish every nested outcome
Event logWhat event data survived execution at a particular emitting address?An event is not a complete list of calls or balance changes
Execution traceWhich calls or instructions ran, including failed branches?Attempted operations can have no surviving state effect
Explorer internal-transfer rowWhich execution movements did this explorer select and display?Its label and selection rules belong to that explorer

The ordinary transaction has a sender authorization, nonce and hash. Contract calls inside its execution do not each become another independently signed transaction. Ethereum’s transaction documentation defines those transaction fields.

A receipt supplies the transaction’s retained log list. The JSON-RPC receipt and log schemas expose the emitting address, topics, data and block context. The receipt’s status reports the top-level outcome, as specified by EIP-658.

A trace provides another view. Geth reconstructs execution by rerunning the transaction locally. Its tracing introduction explains why execution details require this additional work. The trace response is not another list of transactions submitted to the network.

Etherscan specifically describes an internal transaction as an ETH transfer through a smart contract intermediary in its transaction terminology guide. Treat that as a description of its display category. Another explorer or data provider may expose a broader call tree, exclude unsuccessful calls, or arrange rows differently.

Follow a payout with one rejected branch

Consider a hypothetical Ethereum layer 1 transaction from an ordinary account called User to a contract called Payout. Every amount and contract behavior here is invented. This is a state-transition illustration, not an observed transaction.

User begins with 2.000 ETH. Payout, Merchant and Rejector each begin with zero ETH. User sends 1.000 ETH to Payout and separately pays the network fee. Payout is designed to handle a rejected payment and refund all unused ETH.

Assume sufficient gas, no other relevant movements, and no events beyond those listed. Execute these operations in order:

StepOperationResultSurviving effect
1User calls Payout with 1.000 ETHTop-level execution beginsPayout receives the funding if execution ultimately succeeds
2Payout emits StartedContinuesStarted remains in the final receipt
3Payout calls Merchant with 0.600 ETH, then emits PaidCall succeedsMerchant gains 0.600 ETH; Paid remains
4Payout calls Rejector with 0.300 ETHRejector emits Attempt, then revertsRejector gains zero ETH; Attempt is discarded
5Payout handles the failed call and refunds User 0.400 ETHRefund succeedsUser receives 0.400 ETH
6Payout emits Completed and returns successfullyReceipt status is 1Completed remains; the transaction commits

The failed branch is possible because Payout deliberately checks the result of a low-level call and continues. The Solidity 0.8.30 error-handling documentation explains that low-level calls report failure rather than automatically propagating an exception into their caller. This is an explicit behavior of the hypothetical contract, not an assumption about every payout contract.

Rejector temporarily executes with the proposed value, emits its event and then reverts. That execution can be visible in a sufficiently detailed trace. Its state changes and logs do not survive. EIP-140 defines the rollback behavior of REVERT, including the loss of logs from the reverted execution.

The final receipt therefore has three events: Started, Paid and Completed. It has no surviving Attempt event. A call trace still has a failed branch to Rejector. An explorer view that displays only successful internal ETH movements would show the Merchant payment and User refund. That last description is a hypothetical display filter, not a promise about a particular explorer’s current output.

Reconcile value before counting activity

The internal calls attempted 0.600 ETH to Merchant, 0.300 ETH to Rejector and 0.400 ETH back to User. Adding their proposed values gives 1.300 ETH. Filtering for committed value movements leaves 1.000 ETH.

Neither figure is the sender’s net payment to Merchant.

MeasureAmountMeaning
Outer transaction value1.000 ETHFunding delivered to Payout
Attempted internal call values1.300 ETHIncludes the rejected 0.300 ETH attempt
Committed internal value movements1.000 ETHMerchant payment plus refund
Merchant’s net receipt0.600 ETHSuccessful payment to the merchant
User’s net value outflow before fees0.600 ETHFunding minus refund

Adding outer value and committed internal movements produces 2.000 ETH of gross movement along the path. That does not establish 2.000 ETH of economic payment. The same funding entered Payout and then left it. Choose whether the report measures path activity, a recipient’s receipt, or a payer’s net outflow before aggregating.

For this example, execution gas is the only network fee component. Assume the transaction’s receipt records 100,000 gas used at an effective price of 20 gwei. The hypothetical execution fee is 0.002 ETH. Funding and refund leave User with 1.400 ETH before accounting for that fee, or 1.398 ETH after it.

The final balances reconcile as follows: User 1.398 ETH, Merchant 0.600 ETH, Rejector zero and Payout zero. Together these accounts hold 1.998 ETH, which is the initial 2.000 ETH less the network charge. The fee’s distribution or burn lies outside this four-account ledger.

Record the outer receipt’s actual charge once. Internal calls consume work within that execution; they are not separate network submissions with separate transaction fees to add again. The failed-swap gas worksheet explains receipt-based fee accounting across genuinely separate attempts.

A local success can still be rolled back

The Merchant branch above survives because all its enclosing execution completes successfully. Change the scenario so Payout later reverts, and the Merchant payment rolls back too. Looking only at Merchant’s locally successful call would produce the wrong result.

For each proposed movement, check the frame and its ancestors. A failure higher in that branch can erase the state effects of a child that returned successfully. A top-level failure rolls back the transaction’s execution state changes and logs; the included transaction can still incur a network fee.

A trace also contains operations that do not represent native-ETH payments. A zero-value call may execute substantial token or application logic. A DELEGATECALL executes another address’s code in the caller’s context while preserving the calling context’s value. The Solidity 0.8.30 explanation of delegatecall describes this distinction. Do not translate every displayed value field into another ETH transfer.

These checks prevent two different mistakes: counting a reverted payment as delivered, and interpreting a code-execution relationship as a value movement.

Read events as contract statements

An event is emitted because code executed a logging operation. Native ETH transfers do not automatically produce an ERC-20 Transfer event. In the illustration, Payout chooses to emit Paid after Merchant’s payment; a different contract could make the same ETH movement without that custom event.

Solidity’s event documentation, version 0.8.30, explains how events use EVM logs and how topics and data are interpreted. Preserve the raw emitter address and bytes before relying on a decoded event name.

A recognizable event signature also does not establish that the emitting contract is the asset you intended to inspect. Check the chain and contract address, then use the correct event definition. A custom event named Paid is a statement from that implementation, whose logic still needs interpretation.

Token accounting introduces another boundary. Token balances live in contract state; a native-value call tree cannot substitute for token transfer evidence. For that investigation, the guide to receipt success versus token transfer evidence separates execution status, logs and recipient balance changes.

Tracing depends on the endpoint and configuration

Geth’s documented callTracer returns nested frames with call type, addresses, value, errors and child calls. Its onlyTopCall setting deliberately omits subcalls; withLog requests additional per-call log collection. A response configured to omit subcalls cannot support a conclusion that no internal calls occurred.

The debug_traceTransaction documentation describes replaying the original transaction context. Whether your endpoint exposes that method, retains the required state and completes the request is a separate operational question. Record a method error or unavailable history as missing trace evidence, not as an empty execution tree.

Also distinguish a tracer’s diagnostic “logs” from the final receipt log list. Depending on the tracer and options, “logs” may mean instruction records or collected event data. Use the receipt to determine which event records survived, and the trace to investigate how execution reached that result.

Save evidence at the level of the claim

Use this worksheet when an explorer’s activity rows and a wallet balance seem inconsistent.

Claim to verifyEvidence to saveValidation
This transaction executedChain, hash, receipt and inclusion block hashSame transaction and canonical inclusion
This event survivedRaw receipt log, emitter, topics, data and log indexPresent in that receipt; correct decoding
This call was attemptedFull trace, frame path, type and proposed valueTrace configuration includes relevant subcalls
This ETH payment survivedFrame result and enclosing outcomesNo enclosing rollback; amount reconciles
This account gained valueCorrectly scoped balance evidence and fee treatmentIsolate this transaction’s effects
This explorer omitted a rowProvider, query, filters and response timeUnderstand selection and coverage before concluding absence

For balance comparisons, record the exact state being read. Two calls labeled latest can observe changing state; the RPC batch versus block snapshot guide explains how to make multi-read comparisons consistent. Even pinned whole-block balances can include other transactions in that block, so they do not automatically isolate one transaction’s effect.

Finally, keep execution evidence separate from settlement status. The confirmations versus finality guide covers whether the inclusion remains accepted and meets the relevant settlement condition.

Close the record with a precise result: the call was attempted, the event survived, or the recipient gained the reconciled amount. In the payout example, the useful conclusion is that Merchant received 0.600 ETH, Rejector received zero, and User recovered 0.400 ETH while paying one transaction fee.

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.