Skip to content

Learn

Crypto Spot Research

Native asset flows: reconcile call traces with net balances

An execution trace can contain several movements of the same native asset as a transaction passes through a router. Adding every displayed value does not measure the user’s net outflow or a protocol’s new deposits. Reconcile committed transfers as a directed ledger, preserving call types and rollback boundaries.

The task is to explain address-level balance changes for one transaction. It is different from decoding a function label, counting internal-call rows or treating every successful outer transaction as a completed economic action at every level.

Preserve the call tree before flattening it

Geth’s built-in tracer documentation, callTracer, describes nested call frames with type, from, to, value, error and child calls. Its field explanations also distinguish a DELEGATECALL code target from an ordinary receiving address. Preserve the client version and tracer configuration because other providers can format these objects differently.

Assign each frame a transaction hash and structural path, such as root, child 0 and child 1. Keep the parent relationship after exporting rows. Without it, a child that succeeded locally may be counted even when a later failure in its parent rolled the whole subtree back.

Do not deduplicate by value or address pair alone. Two real calls can send identical amounts between the same addresses, while a provider can also display one call in both an outer summary and an internal-transfer table. Identify whether rows represent the same execution frame before counting them.

Define the transfer rule explicitly

For this worksheet, create a value-transfer edge only for a frame whose semantics actually move the native asset and whose effects survive all relevant ancestor rollbacks. A DELEGATECALL’s inherited context value is not automatically another payment to the code address.

Keep attempted edges separately. They can explain behavior and failure cost, but they do not become committed asset movements merely because their requested values appear in the trace. A top-level success flag does not remove the need to inspect nested errors.

Then calculate each address’s flow contribution as committed inflows minus committed outflows. The sum across the fully covered transfer graph should be zero before execution fees and other separately identified balance mechanisms. This is an accounting check, not a guarantee that the trace covers every possible state change.

A hypothetical router transaction

Assume fictional accounts A, R, B and C, and native-asset units large enough to avoid rounding. A sends 100 units to router R. R uses a delegated code call whose displayed context value is also 100, sends 70 to B, attempts 10 to C in a caught reverting call, and refunds 30 to A.

The outer transaction succeeds. The R-to-C transfer does not survive its call failure. The delegated code address is not a recipient of a second hundred-unit transfer under these stipulated call semantics.

FrameDisplayed valueClassificationCommitted ledger edge
Root A→R100Ordinary successful value callA→R 100
Delegated R context→code100Context/code executionNone from this displayed value
R→B70Successful value callR→B 70
R→C10Reverted and caughtNone
R→A30Successful refundR→A 30

Summing every displayed value gives 310. Summing only committed transfer edges gives gross transferred value 200. Neither number is A’s net economic outflow. For A, 30 − 100 = −70; for R, 100 − 70 − 30 = 0; for B, the contribution is +70; C has zero contribution.

The contributions sum to −70 + 0 + 70 + 0 = 0. This identity exposes a missing refund or duplicate delegated-value edge. It does not label B as a merchant or interpret the transfer as a sale; those claims need separate asset and counterparty evidence.

Reconcile the balance difference and fees

Suppose A starts with 150 units and pays an independently established transaction fee of 0.02 units. Under the example’s complete set of assumptions, A ends with 150 − 70 − 0.02 = 79.98. Calling the full 70.02 a payment to B would misallocate the fee.

If B starts with 10, it ends with 80 when there are no other changes. R’s balance remains unchanged under the stated flows. Record whether the fee is paid by A or another sender; a sponsored or otherwise different arrangement changes that reconciliation.

Do not assume every fee unit reaches one visible recipient. Fee accounting and protocol rules can split or otherwise treat components differently. For the narrow transfer question, retain the established total debit as a separate fee item and avoid inventing recipients for any unexplained residual.

Use actual pre- and post-state observations appropriate to the transaction boundary. Two whole-block balance snapshots can include other transactions and system operations. Their difference cannot be attributed to this transaction without accounting for those additional changes.

Different boundary: a child succeeds, then its parent fails

Consider an alternative history. R calls B with 70; inside B, a call sends 20 to D and returns successfully. B subsequently reverts, and R catches that failure. The twenty-unit child’s local success does not preserve its transfer after its parent subtree is rolled back.

In this alternative, assume R then refunds the entire 100 to A. The committed edges are A→R 100 and R→A 100. B and D have zero net contribution, while A’s transfer contribution is zero before the separately accounted transaction fee.

This is not the earlier 70-unit-payment history with an extra twenty added. It is a different execution path. A flat table retaining the child’s success but dropping the parent error would manufacture a payment that never became state.

Do not make CALL coverage universal

Contract creation, self-destruction-related balance effects under applicable rules, validator withdrawals and other system-level mechanisms may need separate treatment. A dataset containing only ordinary CALL entries is not a completeness certificate for every native balance change.

Identify the exact mechanism when a residual appears. The Geth page also describes state-oriented tracers that can help compare changed accounts, but their output still needs an appropriate historical context and interpretation. A state difference verifies the balance change; it does not automatically explain the economic purpose.

Do not silently allocate a discrepancy to miscellaneous internal transfers. Preserve its amount, affected addresses and candidate causes, then retrieve the missing frame, system record or state boundary. A documented unresolved residual is preferable to a fabricated balancing edge.

Aggregate addresses only after fixing the entity boundary

If independently verified evidence places both R and B inside one entity, the main example’s crossing inflow is 100 and crossing refund is 30, so entity net inflow is 70. R→B is internal to that stated set. If the study covers only R, incoming 100 is offset by outgoing 70 plus 30, and its net is zero. Both calculations can be correct for different boundaries.

Retain address-level nets before aggregation and document uncertain membership. A router brand does not make every called address part of its entity. In the alternative full-refund history, an A balance starting at 150 would end at 149.98 if A still paid the assumed 0.02 fee. The transfer contribution is zero while the fee debit remains; those facts should not be collapsed into a fictitious payment.

A zero sum alone cannot certify completeness. Omitting the whole R→B edge from the main example would leave A at −70, R at +70 and B at zero, still summing to zero. The actual unchanged R balance and B’s increase would expose the missing edge. Use graph conservation and observed account reconciliation together.

An executable reconciliation checklist

ActionRequired recordError caught
Fix execution identityNetwork, transaction hash, block and clientMixing chains, branches or formats
Preserve hierarchyFrame paths and parentsCounting a rolled-back descendant
Classify semanticsCALL type and actual value effectCharging inherited delegate context as another transfer
Record survivalFrame error and ancestor outcomesTreating a caught failure as committed movement
Calculate address netsCommitted incoming/outgoing edge listConfusing gross hops with user outflow
Reconcile balancesCorrect state boundary, fees and other mechanismsAssigning all balance difference to one payment

Reopen the packet after a tracer correction, recovered parent error, changed chain status or newly identified system operation. Release gross committed hops, address-level net contributions and fees as separate quantities. The note can explain how value moved through execution without turning a long call tree into exaggerated deposits or user demand.

FAQ

Can I add every internal value field to measure native inflows?

No. Values can describe successive hops, reverted attempts or delegated context. Classify the frames and compute the required net flow.

Does a successful child call survive a later parent revert?

Not when the parent rollback removes that child’s effects. Preserve ancestry before deciding which edges committed.