Revamped CALL instructions
相关视频
正文
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 argumentoffset
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
:
- Charge
WARM_STORAGE_READ_COST
gas. - 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.
- If
value
is non-zero (onlyEXTCALL
):- Halt with exceptional failure if the current frame is in
static-mode
. - Charge
CALL_VALUE_COST
gas.
- Halt with exceptional failure if the current frame is in
- 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. - Perform (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
gas. - If
target_address
is not in the state and the call configuration would result in account creation, chargeACCOUNT_CREATION_COST
gas.- The only such case in this EIP is if
value
is non-zero (onlyEXTCALL
).
- The only such case in this EIP is if
- Calculate the gas available to callee as caller's remaining gas reduced by
max(floor(gas/64), MIN_RETAINED_GAS)
. - Clear the returndata buffer.
- 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
(onlyEXTCALL
). - Current call stack depth equals
1024
.
- Gas available to callee at this point is less than
- Perform the call with the available gas and configuration.
- 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.
- Gas not used by the callee is returned to the caller.
Execution semantics of RETURNDATALOAD
:
- Charge
G_verylow
(3) gas - Pop 1 item from the stack, to be referred to as
offset
- Push 1 item onto the stack, the 32-byte word read from the returndata buffer starting at
offset
. - 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:
- Assume the 3 arguments popped from stack are
destOffset
,offset
andsize
. - If
offset + size > len(returndata buffer)
do not halt with exceptional failure, but instead set theoffset + size - len(returndata buffer)
memory bytes after the copied ones to zero. - 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.
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:
- 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
forEXTCALL
) 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
Copyright and related rights waived via CC0.