EIP 1930: CALLs with strict gas semantic. Revert if not enough gas available.
Author | Ronan Sandford |
---|---|
Discussions-To | https://github.com/ethereum/EIPs/issues/1930 |
Status | Draft |
Type | Standards Track |
Category | Core |
Created | 2019-04-10 |
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 opcodes 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
- 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
In other words, 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 theorically 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 interepreting 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 the previous solution, an EVM mechanism would be much better.
Backwards Compatibility
Backwards compatible as it introduce new opcodes.
Test Cases
Implementation
None fully implemented yet. But see Specifications for an example in geth.
References
- EIP-150, http://eips.ethereum.org/EIPS/eip-150
Copyright
Copyright and related rights waived via CC0.