Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-1930: call variant with strict gas to ensure a specific amount is sent #1930

Closed
Tracked by #5111
wighawag opened this issue Apr 10, 2019 · 25 comments
Closed
Tracked by #5111
Labels

Comments

@wighawag
Copy link
Contributor

wighawag commented Apr 10, 2019

EIP : https://eips.ethereum.org/EIPS/eip-1930


eip: 1930
title: CALLs with strict gas semantic. Revert if not enough gas available.
author: Ronan Sandford (@wighawag)
type: Standards Track
discussions-to: #1930
category: Core
status: Draft
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 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 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 safe-global/safe-smart-account#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.

  1. 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

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)

@wighawag
Copy link
Contributor Author

wighawag commented Apr 28, 2019

How do we get that (and in general any EIP) merged in?
Should I call out maintainer like @Arachnid ?

@wighawag
Copy link
Contributor Author

wighawag commented May 1, 2019

Could you merge the PR @Arachnid @nicksavers @gcolvin @Souptacular?
Sorry if I ping the wrong person. Let me know though as I have few more PR to merge.
And if there are any issues let me know
Thanks

@veox
Copy link
Contributor

veox commented May 13, 2019

As I've mentioned in #881 (comment), there is no need to implement this at the EVM level.

This can be done at language level - either in the compiler, introducing new keywords for "call with extra checks"; or in the program, using macros or function modifiers or decorators or whatever-is-available in the language of choice.

Otherwise, we'll run out of unassigned opcodes by the end of the year.

@wighawag
Copy link
Contributor Author

As mentioned in the proposal as well as in my reply :

These solution are not perfect (either forbid certain behavior or are susceptible to gas pricing change). They also waste gas for computing what is already computed by the evm.

Ideally the version of the opcode proposed here should replace the old one.

Then if you worry about opcode space, we can version the old one out when times come.

@Arachnid
Copy link
Contributor

I agree with @veox - this is a niche requirement that can be satisfied with current opcodes. I think it's a bad idea to use up two new opcodes, and add client complexity, for it.

@wighawag
Copy link
Contributor Author

wighawag commented May 14, 2019

@Arachnid

this is a niche requirement ...

Even if it was, I don't think this is a valid argument.

But as mentioned in the proposal this is already an issue for meta-transactions and introspection (like #165), two use-cases far from being niche.

Furthermore, it is apparent that everybody assumes that the behaviour of the current CALL opcodes is to enforce the gas somehow (maybe because forgetting about EIP-150, they wrongly think there would be not enough gas to complete the transaction if the call throw because it did not receive the amount of gas specified)
see Gnosis Safe issue for a meta-tx example and Openzeppelin issue or EIP-165 example implementation for introspection.

... that can be satisfied with current opcodes

As mentioned in the proposal, this is not trivial and it depends on gas pricing.

This is a concern shared by @tschubotz and the rest of GnosisSafe team that decided because of this to not fix the issue on their smart contract but to do it at the interface level, leaving unaware client on their own and at risk of making their users loose funds.

But this also shared by @chriseth in a different context when proposing #90 (which resulted in #150) where he states:

At he same time, this mechanism relies on actual gas costs inside the EVM and is very fragile:
The amount of gas left can be retrieved via the gas instruction - the problem is that some gas is already spent again between this instruction and the actual call, most notably the gas for the call instruction itself (which can even vary depending on its arguments).
The usual way to cope with this is to subtract a certain constant from the value of the gas instruction (this constant includes the costs for performing this subtraction...).
This is of course all very fragile and means that if we ever increase the gas costs of the instructions call, gas, sub or of pushing constants, we will break existing contracts.
Furthermore, sometimes some gas has to be retained by the caller because it cannot efficiently estimate how costly the call opcode itself will be.

Ultimately we should replace the old CALLs opcode with the ones proposed here. If space is lacking we can version smart contracts and remove old opcodes. To allow for limitless gas call (not available in the current draft) we could give the value 0 a special meaning : "give all gas available". This would allow to replicate the current use cases, making old opcodes obsolete.

The fear of lack of space is not warranted. It is very likely that more and more opcode will become obsolete as we discover better ways to do things. Let's accept it. When we run out of opcode space, we can start versioning smart contract and remove obsolete opcodes.

For now though, as we still have some space left, we should not block ourselves from fixing issues affecting valid use cases.

@Arachnid
Copy link
Contributor

As mentioned in the proposal, this is not trivial and it depends on gas pricing.

It's not terribly difficult. Increasing opcode gas costs is never likely to happen, precisely because people already have this kind of dependency on it. See for instance this recent Twitter thread between Vitalik and I: https://twitter.com/VitalikButerin/status/1128076287678013442

@wighawag
Copy link
Contributor Author

Increasing opcode gas costs is never likely to happen, precisely because people already have this kind of dependency on it.

This is a possible take on it. But that does not remove the risk completely. That depends on the community.

It is worth nothing that the argument also include:

"Eth1 is not forever;"

If so, we could argue that the opcode space is not that much a concern. We have still more than 100 opcodes left

But regardless, since replacing the old opcode with these new one would be objectively better, I still vote for the proposal.

Let's improve the evm when we can.

@jochem-brouwer
Copy link
Member

jochem-brouwer commented May 19, 2019

This would require - per the EIP - 3 new opcodes. For some reason CALLCODE is not considered (why would this not have the same behavior?).

As mentioned by @Arachnid this can be implemented in EVM directly with minimal extra gas costs: (solidity code)

require(gasleft() >= MIN_GAS);
address(target).call.value(x).gas(MIN_GAS)(data);

The fact that this requires 3 (4?) new opcodes plus the fact that this can already be implemented in EVM are both arguments for me to not implement this as this can be (easily) implemented using EVM already.

Increasing opcode gas costs is never likely to happen, precisely because people already have this kind of dependency on it.

@Arachnid Huh? We have had several times in the past hard forks that opcode gas costs are actually increased, because of possible DoS attacks or simply that execution costs of certain operations rise (e.g. Istanbul proposed EIP 1884 or in the past for example EIP 150.

I know that gas costs increases in general are not wanted because there are hardcoded gas limits in some contracts. However, if this leads to DoS attacks (see EIP 150) then to protect the network these costs should be raised anyways. In general, contract developers should be made aware that they cannot rely on gas costs of operations being constant.

@wighawag
Copy link
Contributor Author

@jochem-brouwer

This would require - per the EIP - 3 new opcodes. For some reason STATICCALL is not considered (why would this not have the same behavior?).

STATIC_CALL is mentioned

require(gasleft() >= MIN_GAS);
address(target).call.value(x).gas(MIN_GAS)(data);

This naive implementation is not sufficient as explained in the proposal. Please read the proposal and the materials linked (EIP-150 in particular).

As @Arachnid say, it is technically possible to do it via assembly and we can imagine solidity adding a parameter or a variant to the various "calls" for that but this would be dependent on gas opcode pricing. This is also a waste of gas for something that evm already compute.

In general, contract developers should be made aware that they cannot rely on gas costs of operations being constant.

I agree and the current *CALL opcodes do not allow this if the contract want to ensure the call will get a specific amount of gas.

@jochem-brouwer
Copy link
Member

@wighawag Woops - I meant CALLCODE instead of STATICCALL

@wighawag
Copy link
Contributor Author

CALLCODE is kind of deprecated as you can achieve the same with DELEGATECALL while preserving the caller context. So I don't think we need to keep supporting it.

@wighawag
Copy link
Contributor Author

By the way, we have this proposal (https://eips.ethereum.org/EIPS/eip-1702) currently considered to add versioning to smart contract so we could remove obsolete opcodes when need arise

@axic axic changed the title call variant with strict gas to ensure a specific amount is sent EIP-1930: call variant with strict gas to ensure a specific amount is sent May 23, 2019
@wighawag
Copy link
Contributor Author

I added a variant spec that do not require new opcodes but relies on existing contract not using some specific range of values

@0xalizk
Copy link
Contributor

0xalizk commented Feb 28, 2020

For the sake of concision, option a) could be compressed:

add new variants of the CALL, DELEGATE_CALL, STATIC_CALL opcodes 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

The following is not clear, I don't understand it:

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

This new EIP seems related (eip-2542)

@livingrock7
Copy link

Can anyone guide me with numbers of gas consumed for gasleft() and STATICCALL
Any help is appreciated! Thanks

@livingrock7
Copy link

@DGKSK8LIFE Thanks! what about cost of using gasleft() itself and hardhat's console.log? anyone?

@DGKSK8LIFE
Copy link

the first link I sent references the gas cost of a static call of a pure function; might be related to gasleft() @livingrock7

@github-actions
Copy link

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@github-actions github-actions bot added the stale label Nov 20, 2021
@wighawag
Copy link
Contributor Author

I would like to move this EIP forward, since there is 2 potential spec, I guess I might as well split into 2 proposals. but would like feedback on that:

Do we add new opcodes or do we handle the new semantic with special gas value (first bit being considered as a flag)

@MicahZoltu
Copy link
Contributor

Generally speaking, it is always preferable to have multiple smaller specifications rather than fewer larger specifications. If it is possible to split this up into multiple smaller specifications then you are strongly encouraged to do so.

Note about this EIP idea in general: Contracts should never have any hard coded values related to gas in them because gas costs can and do change as hardware and technology changes. While 300,000 gas may work today, it may not work tomorrow and vice versa.

@github-actions github-actions bot removed the stale label Nov 21, 2021
@wighawag
Copy link
Contributor Author

wighawag commented Nov 21, 2021

@MicahZoltu ok, thanks for the input, I guess what I would like to see is whether option A or B is the way to go and focus on one. but if it undecided I might as well create 2 spec.

re gas, this is not important for the rationale mentioned here, the gas can be provided by the user (meta-transaction for example)

Actually the whole point of this EIP is to avoid the use of hard-coded gas value

@github-actions
Copy link

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@MicahZoltu
Copy link
Contributor

Closing this for housekeeping purposes. Feel free to continue using this issue for discussion about EIP-1930.

Note: If 1930 is pulled out of stagnant, the discussions-to link should be moved to a thread on Ethereum Magicians.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants