Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-1930: CALLs with strict gas semantic. Revert if not enough gas available.

Authors Ronan Sandford (@wighawag)
Created 2019-04-10
Discussion Link https://github.com/ethereum/EIPs/issues/1930

Simple Summary

Add the ability for smart contract to execute calls with a specific amount of gas. If this is not possible the execution should revert.

Abstract

The current CALL, DELEGATE_CALL, STATIC_CALL opcode do not enforce the gas being sent, they simply consider the gas value as a maximum. This pose serious problem for applications that require the call to be executed with a precise amount of gas.

This is for example the case for meta-transaction where the contract needs to ensure the call is executed exactly as the signing user intended.

But this is also the case for common use cases, like checking “on-chain” if a smart contract support a specific interface (via EIP-165 for example).

The solution presented here is to add new call semantic that enforce the amount of gas specified : the call either proceed with the exact amount of gas or do not get executed and the current call revert.

Specification

There are 2 possibilities

a) one is to add opcode variant that have a stricter gas semantic

b) The other is to consider a specific gas value range (one that have never been used before) to have strict gas semantic, while leaving other values as before

Here are the details description

option a)

  • add a new variant of the CALL opcode where the gas specified is enforced so that if the gas left at the point of call is not enough to give the specified gas to the destination, the current call revert
  • add a new variant of the DELEGATE_CALL opcode where the gas specified is enforced so that if the gas left at the point of call is not enough to give the specified gas to the destination, the current call revert
  • add a new variant of the STATIC_CALL opcode where the gas specified is enforced so that if the gas left at the point of call is not enough to give the specified gas to the destination, the current call revert
Rational for a)

This solution has the merit to avoid any possibility of old contract be affected by the change. On the other hand it introduce 3 new opcodes. With EIP-1702, we could render the old opcode obsolete though.

option b)

For all opcode that allow to pass gas to another contract, do the following:

  • If the most significant bit is one, consider the 31 less significant bit as the amount of gas to be given to the receiving contract in the strict sense. SO like a) if the gas left at the point of call is not enough to give the specified gas to the destination, the current call revert.
  • If the 2nd most significant bit is zero, consider the whole value to behave like before, that is, it act as a maximum value, and even if not enough gas is present, the gas that can be given is given to the receiving contract
Rational for b)

This solution relies on the fact that no contract would have given any value bigger or equal to 0x8000000000000000000000000000000000000000000000000000000000000000

Note that solidity for example do not use value like 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF as it is more expensive than passing the gasLeft.

Its main benefit though is that it does not require extra opcodes.

strict gas semantic

To be precise, regarding the strict gas semantic, based on EIP-150, the current call must revert unless G >= I x 64/63 where G is gas left at the point of call (after deducing the cost of the call itself) and I is the gas specified.

So instead of

availableGas = availableGas - base
gas := availableGas - availableGas/64
...
if !callCost.IsUint64() || gas < callCost.Uint64() {
    return gas, nil
}

see https://github.com/ethereum/go-ethereum/blob/7504dbd6eb3f62371f86b06b03ffd665690951f2/core/vm/gas.go#L41-L48

we would have

availableGas = availableGas - base
gas := availableGas - availableGas/64
if !callCost.IsUint64() || gas < callCost.Uint64() {
    return 0, errNotEnoughGas
}

Rationale

Currently the gas specified as part of these opcodes is simply a maximum value. And due to the behavior of EIP-150 it is possible for an external call to be given less gas than intended (less than the gas specified as part of the CALL) while the rest of the current call is given enough to continue and succeed. Indeed since with EIP-150, the external call is given at max G - Math.floor(G/64) where G is the gasleft() at the point of the CALL, the rest of the current call is given Math.floor(G/64) which can be plenty enough for the transaction to succeed. For example, when G = 6,400,000 the rest of the transaction will be given 100,000 gas plenty enough in many case to succeed.

This is an issue for contracts that require external call to only fails if they would fails with enough gas. This requirement is present in smart contract wallet and meta transaction in general, where the one executing the transaction is not the signer of the execution data. Because in such case, the contract needs to ensure the call is executed exactly as the signing user intended.

But this is also true for simple use case, like checking if a contract implement an interface via EIP-165. Indeed as specified by such EIP, the supporstInterface method is bounded to use 30,000 gas so that it is theoretically possible to ensure that the throw is not a result of a lack of gas. Unfortunately due to how the different CALL opcodes behave contracts can’t simply rely on the gas value specified. They have to ensure by other means that there is enough gas for the call.

Indeed, if the caller do not ensure that 30,000 gas or more is provided to the callee, the callee might throw because of a lack of gas (and not because it does not support the interface), and the parent call will be given up to 476 gas to continue. This would result in the caller interpreting wrongly that the callee is not implementing the interface in question.

While such requirement can be enforced by checking the gas left according to EIP-150 and the precise gas required before the call (see solution presented in that bug report or after the call (see the native meta transaction implementation here, it would be much better if the EVM allowed us to strictly specify how much gas is to be given to the CALL so contract implementations do not need to follow EIP-150 behavior and the current gas pricing so closely.

This would also allow the behaviour of EIP-150 to be changed without having to affect contract that require this strict gas behaviour.

As mentioned, such strict gas behaviour is important for smart contract wallet and meta transaction in general. The issue is actually already a problem in the wild as can be seen in the case of Gnosis safe which did not consider the behavior of EIP-150 and thus fails to check the gas properly, requiring the safe owners to add otherwise unnecessary extra gas to their signed message to avoid the possibility of losing funds. See https://github.com/gnosis/safe-contracts/issues/100

As for EIP-165, the issue already exists in the example implementation presented in the EIP. Please see the details of the issue here

The same issue exists also on OpenZeppelin implementation, a library used by many. It does not for perform any check on gas before calling supportsInterface with 30,000 gas (see here and is thus vulnerable to the issue mentioned.

While such issue can be prevented today by checking the gas with EIP-150 in mind, a solution at the opcode level is more elegant.

Indeed, the two possible ways to currently enforce that the correct amount of gas is sent are as follow :

1) check done before the call

uint256 gasAvailable = gasleft() - E;
require(gasAvailable - gasAvailable / 64  >= `txGas`, "not enough gas provided")
to.call.gas(txGas)(data); // CALL

where E is the gas required for the operation between the call to gasleft() and the actual call PLUS the gas cost of the call itself. While it is possible to simply over estimate E to prevent call to be executed if not enough gas is provided to the current call it would be better to have the EVM do the precise work itself. As gas pricing continue to evolve, this is important to have a mechanism to ensure a specific amount of gas is passed to the call so such mechanism can be used without having to relies on a specific gas pricing.

2) check done after the call:

to.call.gas(txGas)(data); // CALL
require(gasleft() > txGas / 63, "not enough gas left");

This solution does not require to compute a E value and thus do not relies on a specific gas pricing (except for the behaviour of EIP-150) since if the call is given not enough gas and fails for that reason, the condition above will always fail, ensuring the current call will revert. But this check still pass if the gas given was less AND the external call reverted or succeeded EARLY (so that the gas left after the call > txGas / 63). This can be an issue if the code executed as part of the CALL is reverting as a result of a check against the gas provided. Like a meta transaction in a meta transaction.

Similarly to the previous solution, an EVM mechanism would be much better.

Backwards Compatibility

for specification a) : Backwards compatible as it introduce new opcodes.

for specification b) : Backwards compatible as it use value range outside of what is used by existing contract (to be verified)

Test Cases

Implementation

None fully implemented yet. But see Specifications for an example in geth.

References

  1. EIP-150, ./eip-150.md

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Ronan Sandford (@wighawag), "EIP-1930: CALLs with strict gas semantic. Revert if not enough gas available. [DRAFT]," Ethereum Improvement Proposals, no. 1930, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1930.