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

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

Open
wighawag opened this Issue Apr 10, 2019 · 0 comments

Comments

Projects
None yet
1 participant
@wighawag
Copy link
Contributor

wighawag commented Apr 10, 2019

Currently the gas specified by the CALL opcode is not enforced but simply act as a maximum value. So if the the gas left in the current call is not enough to pass the gas specified, the receiver will get less than intended.

In some application, like meta transaction this can be a problem since in such case we want to be sure the call will receive exactly the amount specified and revert otherwise.

I thus propose to add a variant to CALL and other relevant call mechanism that will offer meta transaction the possibility to enforce such gas without relying on ad-hoc gas cost computation.

Here is the EIP specification and rationales copied from the EIP draft here : https://github.com/wighawag/EIPs/blob/call_strict_gas/EIPS/eip-1930.md

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

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

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

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 : https://web.solidified.io/contract/5b4769b1e6c0d80014f3ea4e/bug/5c83d86ac2dd6600116381f9) or after the call (see the native meta transaction implementation here https://github.com/pixowl/thesandbox-contracts/blob/623f4d4ca10644dcee145bcbd9296579a1543d3d/src/Sand/erc20/ERC20MetaTxExtension.sol#L176), 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 unecessary extra gas to their signed message to avoid the possibility of losing funds. See gnosis/safe-contracts#100

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

References

  1. EIP-150, https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.