Alert Source Discuss
⚠️ Review Standards Track: Core

EIP-7069: Revamped CALL instructions

Introduce EXTCALL, EXTDELEGATECALL and EXTSTATICCALL with simplified semantics

Authors Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Danno Ferrin (@shemnon), Andrei Maiboroda (@gumb0), Charles Cooper (@charles-cooper)
Created 2023-05-05
Requires EIP-150, EIP-211, EIP-214, EIP-2929, EIP-3540

Abstract

Introduce three new call instructions, EXTCALL, EXTDELEGATECALL and EXTSTATICCALL, with simplified semantics. Introduce another instruction, RETURNDATALOAD for loading a word from return data into stack. Modify the behavior of RETURNDATACOPY instruction executed within EOF formatted code (as defined by EIP-3540). The existing *CALL instructions remain unchanged.

The new instructions do not allow specifying a gas limit, but rather rely on the “63/64th rule” (EIP-150) to limit gas. An important improvement is the rules around the “stipend” are simplified, and callers do not need to perform special calculation whether the value is sent or not.

Furthermore, the obsolete functionality of specifying output buffer address is removed in favor of using RETURNDATACOPY instead. For cases which would previously *CALL output into a buffer and then MLOAD from the buffer, RETURNDATALOAD is provided instead.

Lastly, instead of returning a boolean for execution status, an extensible list of status codes is returned: 0 for success, 1 for revert, 2 for failure.

We expect most new contracts to rely on the new instructions (for simplicity and in order to save gas), and some specific contracts where gas limiting is required to keep using the old instructions (e.g. ERC-4337).

Motivation

Observability of gas has been a problem for very long. The system of gas has been (and likely must be) flexible in adapting to changes to both how Ethereum is used as well as changes in underlying hardware.

Unfortunately, in many cases compromises or workarounds had to be made to avoid affecting call instructions negatively, mostly due to the complex semantics and expectations of them.

This change aims to remove gas observability from the new instructions and opening the door for new classes of contracts that are not affected by repricings. Furthermore, once the EVM Object Format (EOF) is introduced, the legacy call instructions can be rejected within EOF contracts, making sure they are mostly unaffected by changes in gas fees. Because these operations are required for removing gas observability they will be required for EOF in lieu of the existing instructions.

It is important to note that starting Solidity 0.4.21, the compiler already passes all remaining gas to calls (using call(gas(), ...), unless the developer uses the explicit override ({gas: ...}) in the language. This suggests most contracts don’t rely on controlling gas.

Besides the above, this change introduces a convenience feature of returning more detailed status codes: success (0), revert (1), failure (2). This moves from the boolean option to codes, which are extensible in the future.

Lastly, the introduction of the RETURNDATA* instructions (EIP-211) has obsoleted the output parameters of calls, in a large number of cases rendering them unused. Using the output buffers have caused “bugs” in the past: in the case of ERC-20, conflicting implementations caused a lot of trouble, where some would return something, while others would not. With relying on RETURNDATA* instructions this is implicitly clarified. This proposal also adds the “missing” RETURNDATALOAD instruction to round out returndata buffer access instructions.

Specification

Name Value Comment
WARM_STORAGE_READ_COST 100 From EIP-2929
COLD_ACCOUNT_ACCESS 2600 From EIP-2929
CALL_VALUE_COST 9000  
ACCOUNT_CREATION_COST 25000  
MIN_RETAINED_GAS 5000  
MIN_CALLEE_GAS 2300  

We introduce four new instructions:

  • EXTCALL (0xf8) with arguments (target_address, input_offset, input_size, value)
  • EXTDELEGATECALL (0xf9) with arguments (target_address, input_offset, input_size)
  • EXTSTATICCALL (0xfb) with arguments (target_address, input_offset, input_size)
  • RETURNDATALOAD (0xf7) with argument offset

In case this EIP is included as part of the greater EOF upgrade, these four new instructions are undefined in legacy code and only available in EOF code.

Execution semantics of EXT*CALL:

  1. Charge WARM_STORAGE_READ_COST (100) gas.
  2. Pop required arguments from stack, halt with exceptional failure on stack underflow.
    • NOTE: When implemented in EOF, stack underflow check is done during stack validation and runtime check is omitted.
  3. If value is non-zero:
    • Halt with exceptional failure if the current frame is in static-mode.
    • Charge CALL_VALUE_COST gas.
  4. If target_address has any of the high 12 bytes set to a non-zero value(i.e. it does not contain a 20-byte address) then halt with an exceptional failure.
  5. Perform (and charge for) memory expansion using [input_offset, input_size].
  6. If target_address is not in the warm_account_list, charge COLD_ACCOUNT_ACCESS - WARM_STORAGE_READ_COST (2500) gas.
  7. If target_address is not in the state and the call configuration would result in account creation, charge ACCOUNT_CREATION_COST (25000) gas.
    • The only such case in this EIP is if value is non-zero.
  8. Calculate the gas available to callee as caller’s remaining gas reduced by max(floor(gas/64), MIN_RETAINED_GAS).
  9. Clear the returndata buffer.
  10. Fail with status code 1 returned on stack if any of the following is true (only gas charged until this point is consumed):
    • Gas available to callee at this point is less than MIN_CALLEE_GAS.
    • Balance of the current account is less than value.
    • Current call stack depth equals 1024.
  11. Perform the call with the available gas and configuration.
  12. Push a status code on the stack:
    • 0 if the call was successful.
    • 1 if the call has reverted (also can be pushed earlier in a light failure scenario).
    • 2 if the call has failed.
  13. Gas not used by the callee is returned to the caller.

Execution semantics of RETURNDATALOAD:

  1. Charge G_verylow (3) gas
  2. Pop 1 item from the stack, to be referred to as offset
  3. Push 1 item onto the stack, the 32-byte word read from the returndata buffer starting at offset.
  4. If offset + 32 > len(returndata buffer), the result is zero-padded.

In case this EIP is included as part of the greater EOF upgrade, execution semantics of RETURNDATACOPY in EOF formatted code (EIP-3540) is modified as follows:

  1. Assume the 3 arguments popped from stack are destOffset, offset and size.
  2. If offset + size > len(returndata buffer) do not halt with exceptional failure, but instead set the offset + size - len(returndata buffer) memory bytes after the copied ones to zero.
  3. Gas charged for memory copying remains 3 * num_words(size), regardless of the number of bytes actually copied or set to zero.

Execution of RETURNDATACOPY which is not in EOF formatted code (i.e. is in legacy code) is not changed.

TODO: Clarify which side (caller/callee) is gas deducted from and where an error originates from.

TODO: Mention gas refunds?

TODO: Consider option where non-calldata value transfer is not allowed, but there’s a specific TRANSFER/PAY function for that. Would simplify the logic greatly.

Rationale

Removing gas selectability

One major change from the original CALL series of instructions is that the caller has no control over the amount of gas passed in as part of the call. The number of cases where such a feature is essential are probably better served by direct protocol integration.

Removing gas selectability also introduces a valuable property that future revisions to the gas schedule will benefit from: you can always overcome Out of Gas (OOG) errors by sending more gas as part of the transaction (subject to the block gas limit). Previously when raising storage costs (EIP-1884) some contracts that sent only a limited amount of gas to their calls were broken by the new costing.

Hence some contracts had a gas ceiling they were sending to their next call, permanently limiting the amount of gas they could spend. No amount of extra gas could fix the issue as the call would limit the amount sent. The notion of a stipend floor is retained in this spec. This floor can be changed independent of the smart contracts and still preserve the feature that OOG halts can be fixed by sending more gas as part of the transaction.

Stipend and 63/64th rule

The purpose of the stipend is to have enough gas to emit logs (i.e. perform non-state-changing operations) when a “contract wallet” is called. The stipend is only added when the CALL instruction is used and the value is non-zero.

The 63/64th rule has multiple purposes:

a. to limit call depth, b. to ensure the caller has gas left to make state changes after a callee returns.

Additionally, there is a call depth counter, and calls fail if the depth would exceed 1024.

Before the 63/64th rule was introduced, it was required to calculate available gas semi-accurately on caller side. Solidity has a complicated ruleset where it tries to estimate how much it will cost on the caller side to perform the call itself, in order to set a reasonable gas value.

We have changed the ruleset:

The 63/64th rule is still applied, but - At least MIN_RETAINED_GAS gas is retained prior to executing the callee, - At least MIN_CALLEE_GAS gas is available to the callee.

The MIN_CALLEE_GAS rule is a replacement for stipend: it simplifies the reasoning about the gas costs and is applied uniformly for all introduced EXT*CALL instructions. The following table visualizes the differences (note the discrepancy between caller required gas and caller cost for CALL).

  Caller required gas Caller cost (burned gas) Caller min retained gas Callee min gas
CALL V=0 100 100 0 0
CALL V≠0 100+9000 100+6700 0 2300
DELEGATECALL 100 100 0 0
STATICCALL 100 100 0 0
EXTCALL V=0 100 100 5000 2300
EXTCALL V≠0 100+9000 100+9000 5000 2300
EXTDELEGATECALL 100 100 5000 2300
EXTSTATICCALL 100 100 5000 2300
  • Caller required gas: the minimum amount of gas a caller is required to have to execute a call instruction, lower value causes caller’s OOG,
  • Caller cost (burned gas): the amount of gas deducted from the caller to execute the instruction, this amount is not available to the callee,
  • Caller min retained gas: the minimum amount of gas the caller is guaranteed to have after the call, if this cannot be guaranteed the call fails without even reaching the callee,
  • Callee min gas: the minimum gas limit for the callee’s execution.

Removing the call stack depth check was initially considered, but this would be incompatible with the original *CALL instructions, as well as CREATE* instructions, which can be intertwined with the new EXT*CALL instructions in the call stack. As such, keeping the call stack depth check involves no change affecting legacy code.

Also, we find the simple (as opposed to the complex 63/64th rule) hard cap reassuring, that the call stack depth is limited, in case the gas rules can be bypassed. Lastly, the amount of gas to reach depth of 1024 is huge, but not absurdly huge, and we want to avoid constraining ourselves by dependency of this check on current gas limits.

Output buffers

The functionality of specifying output buffer address is removed, because it is added complexity and in a large number of cases implementers prefer to use RETURNDATACOPY instead. Even if they rely on the output buffer (like in the case of Vyper), they would still check the length with RETURNDATASIZE. In Solidity one exception is the case when the expected return size is known (i.e. non-dynamic return values), in this case Solidity still uses the output buffer. For these cases, RETURNDATALOAD is introduced, which simplifies the workflow of copying returndata into a (known) output buffer and using MLOAD from there; instead, RETURNDATALOAD can be used directly.

Status codes

Current call instructions return a boolean value to signal success: 0 means failure, 1 means success. The Solidity compiler assumed this value is a boolean and thus uses the value as branch condition to status (if iszero(status) { /* failure */ }). This prevents us from introducing new status codes without breaking existing contracts. At the time of the design of EIP-211 the idea of return a specific code for revert was discussed, but ultimately abandoned for the above reason.

We change the value from boolean to a status code, where 0 signals success and thus it will be possible to introduce more non-success codes in the future, if desired.

Status code 1 is used for both reverts coming from the callee frame and light failures encountered in the execution of the instructions. The reason for combining them is keeping the semantics similar to the original CALLs - both scenarios preserve unused gas and continue being indistinguishable to the caller.

Parameter order

The order of parameters has been changed to move the value field to be the last. This allows the instructions to have identical encoding with the exception of the last parameter, and simplifies EVM and compiler implementations slightly.

Opcode encoding

Instead of introducing three new EXT*CALL opcodes we have discussed a version with an immediate configuration byte (flags). There are two main disadvantages to this:

  1. Some combination of flags may not be useful/be invalid, and this increases the testing/implementation surface.
  2. The instruction could take variable number of stack items (i.e. value for EXTCALL) would be a brand new concept no other instruction has.

It is also useful to have these as new opcodes instead of modifying the existing CALL series inside of EOF. This creates an “escape hatch” in case gas observability needs to be restored to EOF contracts. This is done by adding the GAS and original CALL series opcodes to the valid EOF opcode list.

CALLCODE

Since CALLCODE is deprecated, we do not introduce a counterpart here.

Halting when target_address is not a 20-byte ethereum addresses

When existing CALL series operations encounter an address that does not fit into 20 bytes the current behavior is to mask the address so that it fits into 20 bytes, ignoring all high bytes. For the EXT*CALL operations a halt was chosen over treating the contract as empty for two reasons. First, it handles the case of sending value to an address that doesn’t exist without having to create a special case. Second, it keeps the warm_access_list from needing to track anything that is not a 20-byte ethereum address.

Smart contract developers should not rely on the operation reverting when such addresses are passed in. When a suitable proposal for the use of Address Space Extension is adopted it is expected that the EXT*CALL series of operations will adopt those changes.

New instructions undefined in legacy (only if this EIP is part of EOF)

There is an alternative scenario where, in case this EIP is included as part of the greater EOF upgrade, the four new instructions are additionally available in legacy EVM. There is, however, a preference to limit changes to legacy EVM in the fork where EOF is included as well as in subsequent ones.

RETURNDATALOAD and RETURNDATACOPY padding behavior

This EIP initially proposed keeping the halt-on-OOB behavior of legacy RETURNDATACOPY. This makes compilers optimizations harder, because unnecessary RETURNDATA* instructions cannot be optimized out without change to code semantics.

It could be that only RETURNDATALOAD is given the padding behavior, but that would make it confusingly inconsistent with the closely related RETURNDATACOPY instruction.

There also was the alternative to have RETURNDATACOPY2 introduced with the padding behavior, available in EOF only, at the same time banning RETURNDATACOPY in EOF. This has been rejected in order to avoid multiplying opcodes, and also as suboptimal from the point of view of compiler implementation.

Backwards Compatibility

No existing instructions are changed and so we do not think any backwards compatibility issues can occur.

Security Considerations

It is expected that the attack surface will not grow. All of these operations can be modeled by existing operations with fixed gas (all available) and output range (zero length at zero memory).

When implemented in EOF (where the GAS opcode and the original CALL operations are removed) existing out of gas attacks will be slightly more difficult, but not entirely prevented. Transactions can still pass in arbitrary gas values and clever contract construction can still result in specific gas values being passed to specific calls. It is expected the same surface will remain in EOF, but the ease of explotation will be reduced.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Danno Ferrin (@shemnon), Andrei Maiboroda (@gumb0), Charles Cooper (@charles-cooper), "EIP-7069: Revamped CALL instructions [DRAFT]," Ethereum Improvement Proposals, no. 7069, May 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7069.