Appearance
Abstract
Introduce three new call instructions, CALL2
, DELEGATECALL2
and STATICCALL2
, with simplified semantics. 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.
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.
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 three new instructions:
CALL2
(0xf8
) with arguments(target_address, input_offset, input_size, value)
DELEGATECALL2
(0xf9
) with arguments(target_address, input_offset, input_size)
STATICCALL2
(0xfb
) with arguments(target_address, input_offset, input_size)
Execution semantics:
- Charge
WARM_STORAGE_READ_COST
(100) gas. - Pop required arguments from stack, fail with error on stack underflow.
- If
value
is non-zero: 3a. Fail with error if the current frame is instatic-mode
. 3b. Fail with error if the balance of the current account is less thanvalue
. 3c. ChargeCALL_VALUE_COST
gas. - Peform (and charge for) memory expansion using
[input_offset, input_size]
. - If
target_address
is not in thewarm_account_list
, chargeCOLD_ACCOUNT_ACCESS - WARM_STORAGE_READ_COST
(2500) gas. - If
target_address
is not in the state and the call configuration would result in account creation, chargeACCOUNT_CREATION_COST
(25000) gas.- The only such case in this EIP is if
value
is non-zero.
- The only such case in this EIP is if
- Calculate the gas available to callee as caller's remaining gas reduced by
max(ceil(gas/64), MIN_RETAINED_GAS)
(MIN_RETAINED_GAS
is 5000). - Fail with error if the gas available to callee at this point is less than
MIN_CALLEE_GAS
(2300). - Perform the call with the available gas and configuration.
- Push a status code on the stack: 11a.
0
if the call was successful. 11b.1
if the call has reverted. 11c.2
if the call has failed. - Gas not used by the callee is returned to the caller.
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 target has code and the call 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:
- Removed the call depth check.
- Use the 63/64th rule, but 2a. ensure that at least 5000 gas is retained prior to executing the callee, 2b. ensure that at least 2300 gas is available to the callee.
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.
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.
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 opcodes we have discussed a version with an immediate configuration byte (flags). There are two main disadvantages to this:
- Some combination of flags may not be useful/be invalid, and this increases the testing/implementation surface.
- The instruction could take variable number of stack items (i.e.
value
forCALL2
) would be a brand new concept no other instruction has.
It is also useful to have these as new opcodes instead of modifying the exiting 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.
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
Copyright and related rights waived via CC0.