Abstract
This EIP adds the CALLSTATUS instruction. It pushes the completion status of the most recent CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE or CREATE2 executed in the current frame, as three bits: whether the callee reverted, whether it halted exceptionally or failed consuming its gas, and whether such a failure occurred in a frame below the callee. The values are 0 (success), 1 (reverted or not started), 2 (halted exceptionally, or collided), 4 (succeeded after a halt below) and 5 (reverted after a halt below). A value of 2 or more means that gas forwarded somewhere in the line of the call, if any, was consumed by an exceptional halt or an address collision. The instruction reads a per-frame register, pops nothing, and costs 2 gas.
Motivation
A contract that continues after a failed call needs to know why the call failed. A failure that the callee signalled with REVERT can be recorded as final. A failure caused by too little gas must not be, because the transaction sender controls the gas and can use an underfunded call to make the contract record a false failure, for example to burn a retry or to mark a payment as rejected.
Today the EVM gives the caller no direct evidence of the cause. The caller sees a success flag of 0, the return data, and the gas it has left. Contracts therefore infer the cause from gas arithmetic: they compare GAS after the call with a threshold derived from the gas they forwarded, or they rely on the failure handler itself running out of gas. Such checks hold only under assumptions about the gas schedule: that an exceptional halt consumes all forwarded gas, that no gas returns to the caller before the check, and that the costs of the operations in the handler do not change. Each repricing fork has broken thresholds of this kind, and the separate metering of state creation, with gas that a frame can draw from a transaction-wide reservoir, weakens GAS further as evidence of what happened in the callee. Return data does not help either: a REVERT with no data and an exceptional halt both leave empty return data, and a wrapper that reverts with a reason after a failed call hides the halt behind the reason.
The EVM knows at every frame boundary whether the frame halted, and it can carry that fact up through the frames above, whether they revert, return, or handle the failure and go on. CALLSTATUS exposes it, so that a contract can treat a status of 2 or more as "not final" and stop depending on the gas schedule.
Specification
Parameters
| Constant | Value |
|---|---|
CALLSTATUS_OPCODE | 0xA5 |
CALLSTATUS_GAS | 2 |
Status values
A status is the sum of up to three bits:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 1 | the operation reverted, or was not started and returned its gas |
| 1 | 2 | the operation halted exceptionally, or failed in a way that consumed the gas it would have forwarded |
| 2 | 4 | an operation in a frame below halted exceptionally or had an address collision |
The values that occur are 0, 1, 2, 4 and 5. Bit 1 is never combined with another bit.
Per-frame registers
Every frame of execution has a register call_status, initialised to 0, and a flag halt_seen, initialised to false, when the frame starts. DELEGATECALL and CALLCODE start a frame of their own, with its own registers, like CALL, STATICCALL, CREATE and CREATE2. The registers of a frame are not visible to, and not changed by, the execution of any other frame, except as stated below for the completion of a child frame.
At the completion of each CALL (0xF1), CALLCODE (0xF2), DELEGATECALL (0xF4), STATICCALL (0xFA), CREATE (0xF0) and CREATE2 (0xF5) instruction, the executing frame sets call_status to:
1if no child frame was started because the operation failed before it, and the gas that would have been forwarded is returned: insufficient balance for the value transfer or the endowment, the call depth limit, or, forCREATEandCREATE2, a nonce overflow of the creating account.2if, forCREATEandCREATE2, no child frame was started because of an address collision, which is checked only after the conditions above. The gas that would have been forwarded is consumed.2if a child frame was started and halted exceptionally, whatever the cause: out of gas, an invalid instruction, an invalid jump destination, a stack underflow or overflow, a write in a static context, an out-of-bounds return data access, a precompiled contract that fails, and, forCREATEandCREATE2, a failed code deposit. The gas forwarded to the child frame is consumed.1if a child frame was started and completed withREVERT, and the child'shalt_seenisfalse.5if a child frame was started and completed withREVERT, and the child'shalt_seenistrue.0if a child frame was started and completed successfully, and the child'shalt_seenisfalse. Successful completion isSTOP,RETURNorSELFDESTRUCT, running past the end of the code, executing no code because the target has none or is delegated to a precompiled contract, or, for a call to a precompiled contract, completing without error. ForCREATEandCREATE2, success includes the code deposit.4if a child frame was started and completed successfully, and the child'shalt_seenistrue.
After setting call_status, the executing frame sets halt_seen to true if call_status is 2 or more. halt_seen is never set back to false.
A child that halted exceptionally reports 2 whatever its own registers hold. A precompiled contract runs no calls, so its halt_seen is false.
If the instruction halts the executing frame itself, for example a CALL with value in a static context, insufficient gas for the memory expansion, or initcode longer than the limit, the frame ends and its registers have no further effect.
No other instruction changes the registers, and CALLSTATUS does not reset them.
The CALLSTATUS instruction
CALLSTATUS (0xA5) pops no value from the stack and pushes the value of the call_status register of the executing frame. It charges CALLSTATUS_GAS. It is allowed in a static context. It fails with a stack overflow in the usual way.
Relation to the success flag and the return data
The status values are consistent with what the instructions push: the success flag of CALL, CALLCODE, DELEGATECALL and STATICCALL is 1 if and only if bits 0 and 1 of the status are clear, that is, the status is 0 or 4, and the address that CREATE and CREATE2 push is non-zero if and only if the status is 0 or 4. The return data buffer is set by the same instructions as before, and CALLSTATUS does not change it.
A status of 2 or more means that the gas forwarded to an operation in the line of the call, in the callee or in any frame below it, if any, was consumed by an exceptional halt or by an address collision. A status of 1 is the only code of a failure in whose line no such thing happened. A later proposal that defines further values keeps these two properties, so contracts test CALLSTATUS >= 2 and not for single values.
Rationale
A separate instruction instead of a changed success flag
The success flag of the call instructions cannot carry more than one bit. Deployed code tests it as a boolean, in both directions, so a status code in its place would change the behaviour of existing contracts. EIP-7069 replaced the flag with status codes in a new set of call instructions. This EIP keeps the legacy instructions unchanged and adds one instruction that reads a register, in the way that RETURNDATASIZE exposes the return data of the last call.
The codes
0, 1 and 2 are the codes of EIP-7069: success, revert, failure. The distinction between 1 and 2 is whether the forwarded gas was consumed. A REVERT and an operation that was not started both return the unused gas, so both are 1. An exceptional halt of any kind, an address collision and a failed code deposit all consume the forwarded gas, so all are 2. The cause of an exceptional halt is not exposed: the caller cannot verify it, and the response that protects the caller is the same for every cause.
The halt-below bit
Most contracts are reached through intermediaries: a proxy that delegates to an implementation, a router, a wrapper that calls and checks, a signature checker that returns false on any failure of the call it makes, a batch that lets some calls fail. An exceptional halt two or more frames down reaches the caller in whatever form the intermediaries give it: a revert with the empty return data of the halt, a revert with a reason of their own, a revert after a further call, or a successful return that carries false. If the status reported only the direct callee's own ending, every one of these shapes would read as a deliberate failure, and the caller would be back to gas arithmetic for the most common call shapes on the network.
The halt_seen flag therefore records whether any operation of a frame, or of any frame below it, halted exceptionally or consumed its forwarded gas, and the flag travels up through every frame boundary, whether the frame reverts or returns. A frame that reverts after such a halt reports 5, and a frame that returns after one reports 4. The flag is set by a status of 2 or more, so a call to a precompiled contract between the halt and the revert, which compilers and libraries emit for hashing or signature recovery, does not break the chain.
The price is that some deliberate failures carry the bit too: a frame that makes a low-gas probe call that halts, for example to test whether an address has code, and later reverts for a reason of its own, reports 5. The caller then treats the failure as not final, which is the safe direction, and still has the revert data. A frame that has handled a halt and returns a result reports 4 to its caller; whether the caller finalises on that result is the caller's decision, based on how far it trusts the frame's own handling.
Why a success with a halt below is visible
A status of 4 tells the caller that the callee reported success although something in its line halted, for example a check that returned false because the call it made ran out of gas. A caller that depends on the result can then verify it or treat the operation as not final, instead of taking the false as final. Reporting it also keeps the rule simple: the flag travels through every frame boundary in the same way.
A register rather than a stack value
A register costs nothing for calls whose status is never read, keeps the stack effects of the call instructions unchanged, and gives the value to compilers through one instruction with no arguments. The register starts at 0 so that it has a defined value before the first call of a frame; a contract reads it after a call.
Opcode number
0xA5 is the first unused number after the LOG instructions, a range that other new instructions also use. The numbers next to the environment instructions, 0x4B to 0x4F, all have open proposals. The number is tentative and may change to avoid a collision.
Interaction with multidimensional gas
With state creation metered separately from execution, a frame may draw part of the cost of a new storage slot or account from a reservoir that GAS does not show, and gas may return to a reservoir when state is cleared or a frame reverts. Checks that infer the cause of a failure from GAS must then account for the reservoir, and a fixed gas budget that was sufficient before a repricing may no longer be. CALLSTATUS makes none of this matter to the inference: whatever the schedule, a status of 2 or more means that gas was consumed by a halt in the line of the call. Transaction types that execute several frames may expose the same codes for completed frames; that is left to their specifications.
Backwards Compatibility
0xA5 is currently an undefined instruction, so any deployed code that executes it halts exceptionally. No deployed contract can depend on that outcome in a way this EIP changes, as with every instruction added at an undefined opcode. The instruction is not a PUSH, so jump destination analysis does not change. The behaviour of the call instructions, their gas costs and the return data buffer are unchanged. The registers are frame-local and need no journaling: a child's registers are read by the parent at the child's completion, and end with the child.
Test Cases
All cases execute CALLSTATUS in the calling frame right after the call and compare the pushed value. Unless stated, calls forward all available gas and transfer no value. "Bubbles" means that the frame reverts with the return data of its last call. "Halting callee" means a contract that executes INVALID.
| # | Call | Callee or initcode | Expected CALLSTATUS |
|---|---|---|---|
| 1 | none yet | - | 0 |
| 2 | CALL | STOP | 0 |
| 3 | CALL | RETURN with 32 bytes | 0 |
| 4 | CALL | code that ends without STOP | 0 |
| 5 | CALL | SELFDESTRUCT | 0 |
| 6 | CALL to an account with no code, with value | - | 0 |
| 7 | CALL to an account delegated to a precompiled contract | - | 0 |
| 8 | CALL to an account delegated to a contract | REVERT in the delegated code | 1 |
| 9 | CALL | REVERT with no data | 1 |
| 10 | CALL | REVERT with data | 1 |
| 11 | CALL with gas 0 | STOP | 0 |
| 12 | CALL with gas 0 | PUSH1 0 | 2 |
| 13 | CALL with gas 1000 | loop until out of gas | 2 |
| 14 | CALL with value and gas 0 | SSTORE within the stipend | 2 |
| 15 | CALL | INVALID (0xFE) | 2 |
| 16 | CALL | JUMP to a non-JUMPDEST | 2 |
| 17 | CALL | POP on an empty stack | 2 |
| 18 | CALL | RETURNDATACOPY past the end of the return data | 2 |
| 19 | CALL with value above the caller's balance | - | 1 |
| 20 | CALLCODE with value above the caller's balance | - | 1 |
| 21 | CALL at depth 1024 | - | 1 |
| 22 | DELEGATECALL at depth 1024 | - | 1 |
| 23 | STATICCALL | SSTORE | 2 |
| 24 | STATICCALL | CALL with value | 2 |
| 25 | DELEGATECALL | REVERT | 1 |
| 26 | CALLCODE | STOP | 0 |
| 27 | CALL to the identity precompile (0x04) | - | 0 |
| 28 | CALL to ecrecover (0x01) with input that recovers nothing | - | 0 |
| 29 | CALL to the modexp precompile (0x05) with less gas than its cost | - | 2 |
| 30 | CREATE | initcode REVERT | 1 |
| 31 | CREATE | initcode returns MAX_CODE_SIZE + 1 bytes of code | 2 |
| 32 | CREATE | initcode returns code starting with 0xEF | 2 |
| 33 | CREATE | initcode returns code whose deposit cost exceeds the gas left | 2 |
| 34 | CREATE to an address that already has a nonce | - | 2 |
| 35 | CREATE2 to an address that already has code | - | 2 |
| 36 | a CALL to STOP, then CREATE2 to an address that already has code | - | 2 |
| 37 | CREATE from an account with nonce 2**64 - 1 | - | 1 |
| 38 | CREATE with endowment above the creator's balance | - | 1 |
| 39 | CREATE | initcode STOP | 0 |
| 40 | CREATE with initcode longer than MAX_INITCODE_SIZE | - | the calling frame halts |
| 41 | CREATE in a static context | - | the calling frame halts |
| 42 | CALL with value, in a static context | - | the calling frame halts |
| 43 | two calls: halting callee, then STOP | - | 0 |
| 44 | two calls: STOP, then REVERT | - | 1 |
| 45 | CALL | callee calls a halting callee, then bubbles | 5 |
| 46 | CALL | callee calls a halting callee, then reverts with a reason string | 5 |
| 47 | CALL | callee calls a contract that reverts with data, then bubbles | 1 |
| 48 | CALL | callee calls a halting callee, then calls sha256 (0x02), then reverts | 5 |
| 49 | CALL | callee calls a halting callee, then calls a contract that STOPs, then reverts | 5 |
| 50 | CALL | callee calls a halting callee, then returns false | 4 |
| 51 | CALL | callee calls the contract of case 50, reads false, and reverts | 5 |
| 52 | CALL | callee calls a halting callee, then executes INVALID itself | 2 |
| 53 | CALL to a proxy | proxy delegates to code that calls a contract which runs out of gas; the implementation bubbles; the proxy bubbles | 5 |
| 54 | CALL to a proxy behind a proxy | as 53, one level deeper | 5 |
| 55 | CREATE | initcode calls a halting callee, then bubbles | 5 |
| 56 | CREATE | initcode calls a halting callee, then returns code | 4 |
| 57 | CALLSTATUS in a static context | - | the register value |
| 58 | CALLSTATUS on a full stack | - | exceptional halt |
In case 45 the callee observes 2 before it reverts. In case 51 the callee observes 4 before it reverts. In cases 53 and 54 each delegated frame observes 2 or 5 before it reverts. Cases 43 and 48 show that the register reports the last call, while the flag keeps the halt.
Reference Implementation
Pseudocode in the style of the execution specifications. The call and create logic already distinguishes the outcomes; the change records them in the registers of the calling frame.
CALLSTATUS_GAS = 2
class Evm:
...
call_status: int = 0 # per frame, initialised at frame start
halt_seen: bool = False # per frame, initialised at frame start
def call_status(evm: Evm) -> None:
charge_gas(evm, CALLSTATUS_GAS)
push(evm.stack, U256(evm.call_status))
evm.pc += 1
def set_call_status(evm: Evm, status: int) -> None:
evm.call_status = status
if status >= 2:
evm.halt_seen = True
# In the shared call and create logic, where the operation fails before a
# child frame is started and returns its gas (insufficient balance, depth
# limit, nonce overflow):
# evm.gas_left += forwarded_gas
# set_call_status(evm, 1)
# push(evm.stack, U256(0))
#
# In the create logic, on an address collision (no child frame, gas consumed):
# increment_nonce(...)
# set_call_status(evm, 2)
# push(evm.stack, U256(0))
#
# Where a child frame has completed:
def record_child_outcome(evm: Evm, child: Evm) -> None:
if child.error is None:
set_call_status(evm, 4 if child.halt_seen else 0)
elif isinstance(child.error, Revert):
set_call_status(evm, 5 if child.halt_seen else 1)
else:
set_call_status(evm, 2)A failed code deposit is an exceptional halt of the create frame in the execution specifications, so it sets 2 through record_child_outcome. A precompiled contract is a child with halt_seen equal to false, so it sets 0 or 2.
Security Considerations
What the status tells, and what it does not
The status reports outcomes, not causes. A status of 2 or more means that the gas forwarded somewhere in the line of the call, if any, was consumed by an exceptional halt or an address collision. It does not say whether the halt was caused by the gas the transaction sender supplied, by a gas budget that the caller chose, or by the callee on purpose.
With CALLSTATUS, a transaction sender, who controls only the gas, cannot make a caller see 1 for a failure whose line contains a halt, whether the halted frame is the caller's direct callee or a frame behind proxies, wrappers, handlers or checks that swallow failures. Two gas-dependent outcomes stay under the sender's control, because no halt occurs in them: a callee whose revert depends on the gas it had, for example because it read GAS and reverted on purpose, or because an arithmetic check on GAS failed, and a callee that returns a result that depends on the gas it had. A gas guard that wants its outcome reported as a halt should halt, with INVALID, rather than revert. A caller that depends on a returned value should treat a status of 4 as a reason to verify the value or to treat the operation as not final.
What the status does not change for a caller with a fixed budget
A caller that forwards a fixed gas budget and observes 2 or more learns that the budget did not suffice for the work, or that the callee halted on purpose. It does not learn whether the budget was adequate. A fixed budget that was sufficient under a previous gas schedule may be insufficient under the current one. Such a caller should check before the call that it can forward the full budget, and should decide what a halt means under the current schedule. A caller that forwards all available gas and observes 2 or more knows that the gas it had was consumed, and that the sender could have supplied more; treating the failure as not final is the safe response.
Finality and liveness
A callee can produce 2 on purpose with INVALID, and a frame below it can produce 4 or 5 at the caller with one cheap halting call. The gas consumed is paid by the transaction sender, so a callee could already make a failure look like out of gas at no cost to itself; the instruction gives a callee no new ability. It does make "not final" cheap to signal. Contracts should therefore decide finality so that neither party can keep an item pending for ever or burn its retries:
- A batch that skips failed items should not revert the whole batch on a status of
2or more; it should mark the item as retryable. - A bound on retries should be a deadline, in time or blocks, after which the item is finalised as failed, not a counter of attempts, because a sender can produce any number of attempts that end in
2or more by choosing the gas. - A failure whose line contains no halt, status
1, can be finalised at once.
Intermediaries and probes
For a chain of calls A -> B -> C where C halts exceptionally, B observes 2. If B reverts at any later point without halting, A observes 5. If B returns, A observes 4. If B halts exceptionally itself, A observes 2. B cannot report 1 or 0 to A after C's halt, whatever it does in between. A B that has handled the halt returns a result, and A sees 4; A finalises on that result only as far as it trusts B's handling.
A frame that makes a deliberate low-gas probe call that halts, and later reverts for a reason of its own, reports 5 to its caller. The caller then treats the failure as not final, which is the safe direction, and still has the revert data. Contracts that make such probes should prefer forms that do not halt, such as EXTCODESIZE, or a call with enough gas for the target to complete or to revert cleanly, when the information they need allows it.
Gas and consensus
The instruction does not change how much gas a call needs. A caller that forwards too little gas still fails and observes 2; the status only tells it so. The registers add one word and one bit per frame, no persistent state, and no journaling, because they end with the frame that holds them. Every client sets them at the same points, including the operations that start no child frame: the failures that return the gas and the address collision of CREATE and CREATE2. A client that set the register only at the completion of a child frame would leave a stale value in those cases and diverge.
Copyright
Copyright and related rights waived via CC0.
