EIP-7069: Revamped CALL instructions

Discussion topic for

Reading the EIP, it’s unclear how “failure” of a call is defined. I assume “failure” is when the call can’t be initiated due to the contract’s balance < value or there being insufficient remaining gas? Are there any other constellations that would constitute a “failure” pushing the status code 2 onto the stack? An exceptional revert in the call context would still be considered a “revert” and not a “failure”, right?

revert is only returned if the callee uses the REVERT instruction. Should clarify it.

Historical context: these reworked CALL instructions were discussed starting late December when certain unobservability properties were required from EOF. The basic specification was discussed in January and until now kept in the “EOF mega spec”:

Creating the EIP is the next step, especially as it is not strictly dependant on EOF.

Note: Unlike CALL there is no extra charge for value bearing calls.

What is the justification for this?

Currently the cheapest way to change a value in a MPT is by modifying a storage key. This costs 5000 gas, leading to a theoretical limit 6000 MPT writes per block under current gas limits. Under this EIP, using the CALL opcode you can modify the balance of an account for only 2600 gas, raising the theoretical limit to 11538 MPT writes per block.

MPT writes are likely to be expensive. Each MPT write incurs multiple database writes (due to a trie node updates) compared to a singe database read for an MPT read. Charging the same for a read and a write seems questionable.

I agree with @Philogy that non-malicious code overpays for value carrying calls in practice, but I’m concerned this might be DOS vector.

1 Like

So the “revert state” does not include e.g. INVALID (0xfe) and exceptional reverts due to e.g. missing jumpdest, insufficient stack args, out-of-bounds returndatacopy?

Correct, all these cases result with status code 2 on stack.

1 Like

HELL YES FINALLY

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.

Because the proposed instructions remove the output buffer, I think this proposal needs to include a RETURNDATALOAD instruction to be complete.

Here’s why: to replicate current CALL semantics, you need to add extra instructions to copy from returndata into an output buffer (which, I think the best you could do is returndatacopy output_buffer 0 (min returndatasize buf_size) – the best implementation I have for min here is something like push2<buf_size> returndatasize dup2 xor push2 <buf_size> returndatasize lt push2<buf_size> mul xor). Currently, copying into memory is important because it improves the performance of ABI decoding.

Introducing RETURNDATALOAD allows for (efficient) ABI decoding directly from the returned data and would solve the above concerns because we can skip copying to memory, also allowing us to skip returndatasize checks on account of the OOB semantics of RETURNDATACOPY/RETURNDATALOAD.

1 Like

This EIP is related to the project of removing gas observability. I want to raise that the ability to observe “out of gas” errors might be necessary, that it’s not really possible with the current instruction set, and that a solution to this problem might be a good fit for this EIP.

The issue is that whenever a contract has logic such as “try this call, and if it reverts do this other thing”, due to EIP-150 there is a chance that the transaction originator can force a contract to follow the “catch” path by triggering the subcall to run out of gas while providing enough gas for the rest of execution to continue (perhaps more so with the introduction of MIN_RETAINED_GAS?). An example where this pattern could be used is to call a getter and have a fallback value if the getter is not implemented. Ideally, the contract would be able to specify that if the subcall reverts out of gas it should not continue execution and should revert. With the revamped CALL instructions this is semi-possible, because through the status code you can distinguish explicit revert from out of gas failure. However, the out of gas error can be triggered in a more deeply nested call, and in this case the “failure” information is lost in the outer scopes.

My proposal is to encode in the status code whether the call or any nested subcall ran out of gas. This would allow detecting that the code is operating without sufficient gas available.

We have added a charge for calls with value, thank you for bringing this up Update EIP-7069: Add charge for value-bearing calls by gumb0 · Pull Request #7220 · ethereum/EIPs · GitHub

@charles-cooper @ekpyron There’s a topic we’d like some feedback on.

The revamped calls change the stack return values from

0-failure, 1-success - for original CALLs

to:

0-success, 1-revert, 2-failure (and possibly more failure codes c.f. this PR thread

Note that success and failure are flipped. Would this be problematic? In particular (from @shemnon ):

I have concerns about flipping the outputs of calls. e.g. 1 is success vs 0 is success. Specifically for how it will impact the low-level call functions - Units and Globally Available Variables — Solidity 0.8.26 documentation (call, delegatecall, staticcall) as well as inline assembly. Have solidity and vyper chimed in on it on how they would handle it? i.e. would there be compatibility for call and friends and a new variant that will only work when the EOF mode is flipped? and inline assembly would fail if compiled with the wrong mode?

Flipping would not be problematic – in fact it’s a slight improvement because you can use JUMPI to the shared revert block instead of ISZERO … JUMPI.