CALL executes the target’s code using the target’s storage, balance and address. DELEGATECALL executes the target’s code using the caller’s storage, balance and address, while keeping the parent frame’s msg.sender and msg.value. Delegation itself does not send ETH to the code address.
Separate the code address from the state address
An Ethereum call needs instructions to execute and an account whose state those instructions use. An ordinary CALL to a deployed contract uses the target for both. With DELEGATECALL, another address supplies the instructions while the current account supplies the storage and funds. The EVMC message reference makes this distinction explicit through its separate code-address and recipient fields.
The comparison below uses Solidity’s field names, following its version 0.8.30 documentation. A frame is one execution within the nested call tree. Assume A is executing and selects deployed contract B as its target.
| Property inside B’s code | CALL from A to B | DELEGATECALL from A to B |
|---|---|---|
| Code address | B | B |
| Account whose storage is used | B | A |
address(this) | B | A |
msg.sender | A | A’s current msg.sender |
msg.value | Value explicitly supplied by A | A’s current msg.value |
| ETH sent to B by this operation | The supplied value | None |
EIP-7 specifies the inherited sender and value. Solidity’s address-member documentation also distinguishes the interfaces: only call accepts a value option.
“Preserved sender” means the sender visible in the frame doing the delegation. It need not be the wallet that submitted the transaction. Solidity distinguishes the current message’s msg.sender from the transaction’s tx.origin in its global-variable reference.
Delegated execution still gets its own call memory. Its payload arrives as calldata, and results return through returndata. See Solidity’s message-call and delegation explanation.
Trace a router, a proxy and one outgoing payment
Consider a hypothetical execution with five addresses: wallet U, router R, proxy-like contract P, logic contract L and recipient contract M. These are teaching labels, not deployment addresses. All entry points accept the specified ETH, gas is sufficient, every call succeeds and there is no reentrant execution. The balance ledger excludes transaction fees.
Before execution, U holds 5 ETH, P holds 1 ETH and L holds 2 ETH. R and M each hold zero ETH.
P always delegates to a fixed L address embedded in its code. Its application data uses three deliberately compatible slots: slot 0 holds a tally, slot 1 holds a saved call value and slot 2 holds a saved sender. L interprets those same slots identically. Initially, P’s tally is 10 and L’s tally is 100; both saved-value fields are zero and both saved-sender fields are unset.
L’s routine adds 7 to the tally, saves msg.value and msg.sender, then calls M with 0.10 ETH. U starts the transaction by sending R 0.60 ETH. R forwards 0.25 ETH to P, which delegates execution to L.
| Execution frame | Code address | Storage address | msg.sender | msg.value, ETH |
|---|---|---|---|---|
| U enters R | R | R | U | 0.60 |
| R calls P | P | P | R | 0.25 |
| P delegates to L | L | P | R | 0.25 |
| Delegated routine calls M | M | M | P | 0.10 |
In the delegated frame, L’s instructions read P’s tally of 10, add 7 and write 17 into P’s slot 0. P’s saved value becomes 250,000,000,000,000,000 wei, or 0.25 ETH, and its saved sender becomes R. L’s own tally remains 100 and its other stored fields remain untouched.
Two details matter. First, the delegated routine sees R as its sender, because R called P. The earlier wallet U does not reappear merely because P delegates. Second, when that routine makes an ordinary CALL to M, it acts from P’s context. M therefore sees P as its sender, and P’s balance funds the 0.10 ETH payment.
The value visible to the delegated routine is 0.25 ETH even though U supplied R with 0.60 ETH. It also differs from P’s total balance: with an initial 1 ETH, P holds 1.25 ETH immediately before the payment. msg.value, account balance and total transaction funding answer different questions.
Replace delegation with CALL and reset the state
Run a second hypothetical execution from the same starting balances and storage. Change P’s routing operation to an ordinary CALL to L with an explicit value of 0.25 ETH. This is a fresh run, not a continuation of the first one.
Now L executes in L’s own account context. Its tally changes from 100 to 107; its saved sender becomes P and its saved value becomes 0.25 ETH. P’s tally remains 10, with its saved fields unchanged. The subsequent payment to M comes from L, so M sees L as its sender.
The balances locate the money left after that payment:
| Account | Initial ETH | Delegated run, ETH | Fresh CALL run, ETH |
|---|---|---|---|
| U | 5.00 | 4.40 | 4.40 |
| R | 0.00 | 0.35 | 0.35 |
| P | 1.00 | 1.15 | 1.00 |
| L | 2.00 | 2.00 | 2.15 |
| M | 0.00 | 0.10 | 0.10 |
| Total before fees | 8.00 | 8.00 | 8.00 |
Under delegation, P retains 0.25 − 0.10 = 0.15 ETH above its initial balance. Under CALL, that same remainder sits at L. Both totals reconcile to 8 ETH because the model only moves value among these accounts. An actual transaction also charges fees, which require a separate ledger entry.
The delegated frame’s visible 0.25 ETH is inherited call information, not another transfer to L. The EVMC value definition distinguishes transferred CALL value from apparent DELEGATECALL value. A trace value field must be interpreted alongside the call type.
Keep token quantities separate too. msg.value is denominated in wei; it does not decode a token amount carried in calldata. When reconciling a token operation beside an ETH call, the ERC-20 decimals versus amount guide helps convert that separate quantity.
A matching function signature does not protect storage
Our example assigns compatible slots on purpose. In a real system, delegated instructions use the current account’s slots according to the logic’s expected layout. If logic treats slot 0 as a tally while the calling account uses it for an owner, a write targets that same slot regardless of the source-code variable names.
Types, packing and inheritance affect where Solidity places fields. Compare the storage-layout artifacts for the actual versions involved, including any explicit layout choices. The Solidity storage-layout specification documents these rules. Matching an ABI or renaming a variable does not establish layout compatibility.
ERC-1967 standardizes slots for proxy information, such as the implementation address, to avoid collisions with compiler-allocated application storage. Using those metadata slots does not validate changes to the application’s layout or establish that the selected logic is trustworthy.
DELEGATECALL also does not, by itself, establish an upgrade mechanism. P’s target is fixed in this example. For an upgradeable deployment, separately follow who can replace the implementation using the contract admin-risk worksheet. The context mechanism and the authority to change its target are separate inspection tasks.
Check failure and identity before accepting a result
Suppose L’s routine writes the tally and then reverts. That delegated execution’s state changes are rolled back. P’s low-level delegatecall reports failure; P’s own logic decides whether to propagate that failure or handle it. A successful outer transaction alone cannot establish that the tally changed. Solidity documents these low-level exception rules, while EIP-140 specifies REVERT’s rollback behavior.
In a variant of the delegated run, let L revert after paying M, and let P handle the failure while P and R return successfully. The payment and delegated writes roll back: M keeps zero ETH and P’s tally stays 10. P retains R’s earlier 0.25 ETH, leaving 1.25 ETH. If P or R instead reverts, that enclosing rollback also erases state changes from successful descendants. Check the frame and every ancestor before treating its effects as final. REVERT state rollback.
A successful low-level result also needs a postcondition: an observable result the function was meant to produce. Solidity documents that calling a non-existent account can report success. An empty return is therefore insufficient evidence that the intended function ran. Verify the resolved target, expected returned data where applicable and the intended state change.
Choosing a delegate target is consequential because its instructions can write the current account’s storage and initiate spending from its balance. Even a correct context table cannot establish whether those instructions are safe. Inspect the target-selection rules and the code that executes with that authority.
Do not substitute tx.origin when an intermediary changes msg.sender. In our routed example, U and R identify different roles in the execution. Solidity’s security guidance explicitly warns against using transaction origin for authorization.
Build a frame record from a real trace
Start with the chain, transaction hash and inclusion block hash, then resolve the deployed proxy and logic at that execution point. If reproducing their deployment, the CREATE versus CREATE2 guide explains address derivation. A deployment address alone does not tell you whose storage a later delegated call uses.
Geth’s documented callTracer returns nested frames. Its DELEGATECALL to field identifies the code being executed. Derive the state address and visible sender by walking the parent frames; do not automatically equate a tracer’s initiating address with Solidity’s msg.sender.
Use this worksheet for each relevant frame:
| Record | What to retain |
|---|---|
| Frame identity | Parent path and CALL or DELEGATECALL type |
| Code identity | Resolved code address and matching deployed code |
| State identity | Account whose storage and balance the frame uses |
| Message fields | Visible sender and value in wei |
| Outcome | Frame error, enclosing outcomes and expected post-state |
Record the tracer and configuration as well. Geth’s onlyTopCall option omits subcalls; an incomplete tree cannot establish that delegation was absent. Where available, its prestateTracer in diff mode helps verify changed storage. Confirm that storage capture was not disabled. Provider support and historical-state availability still need checking.
The event logs versus internal transactions guide explains how to reconcile that execution evidence with retained receipt records. For this example, the completed record is specific: L supplied the instructions, P’s tally became 17, the routine saw sender R and value 0.25 ETH, and M received 0.10 ETH from P.
