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

feat(contracts): native ETH value support for ovmCALL #8

Open
wants to merge 14 commits into
base: regenesis/0.4.0
Choose a base branch
from

Conversation

ben-chain
Copy link
Owner

@ben-chain ben-chain commented May 27, 2021

Lots of good progress. Includes:

  • experimental compiler build working smoothly
  • all opcodes implemented
  • contract unit tests for most cases
  • basic integration test cases

Still missing:

  • change to OVM_ECDSAContractAccount
    • dependency: fix smock
  • fix gas check
  • addition of value to simulateMessage/eth_call
  • ovmCREATE types with value
  • sad path testing for INVALID_STATE_ACCESS during fraud proof (low priority; this only JUMPs to existing code paths

@@ -13,6 +13,7 @@ import { Lib_ErrorUtils } from "../../libraries/utils/Lib_ErrorUtils.sol";
import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManager.sol";
import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol";
import { iOVM_SafetyChecker } from "../../iOVM/execution/iOVM_SafetyChecker.sol";
import { IUniswapV2ERC20 } from "../../libraries/standards/IUniswapV2ERC20.sol";
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to import OVM_ETH here instead, but could not access OVM_ETH.balanceOf.selector for whatever reason...

@@ -830,6 +932,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(bool success, bytes memory data) = ovmCALL(
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noticed: this could be a staticcall


// Since we transferred it in above and the call reverted, the transfer back should always pass.
// If it did not, this is an OOG, and we have to make the parent out-of-gas as well.
// TODO: should we also enforce there is always enough extra gas to make it past this step? This would mean this condition is only triggered due to some critical bug in the ERC20 implementation.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking this through with @maurelian this definitely needs to be fixed. We need to do some sort of enforcement of gasleft beforehand (perhaps line 1112 above becomes (success, returndata) = _contract.call{gas: min(_gasLimit, gasleft() - MIN_GAS_FOR_RETURN_VALUE_BACK) }(_data);

Otherwise, this code path here potentially allows a malicious contract to out-of-gas anyone who calls it with nonzero value, even if they only give the malicious contract a small portion of their own gas.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to ^^^ I'm going to give my understanding of the problem we're facing here and the proposed solution because I found it quite difficult to wrap my head around. This is based on conversation with @ben-chain (please correct me if you see any mistakes).

What we're trying to achieve here is to duplicate the behavior of EVM calls that come with attached value. Specifically, this behavior is as follows:

  1. If making a call with a value > 0 and the call fails, then the value IS NOT transferred to the recipient.
  2. If making a call with a value > 0 and the call succeeds, then the value IS transferred to the recipient.
  3. If making a call with a value > 0 and the user is unable to transfer funds (e.g., because they don't have enough balance OR because they don't have enough gas to execute the payment), then the value IS NOT transferred to the recipient AND the call fails with (success=false, returndata=0x). This IS how the EVM behaves. See geth: https://github.com/ethereum/go-ethereum/blob/2dee31930c9977af2a9fcb518fb9838aa609a7cf/core/vm/evm.go#L298

Case (2) is the straightforward happy path case.
Case (3) is also pretty easy to handle by checking for failure of the initial transfer.

Case (1) is slightly harder -- you want to revert the payment if the call fails. You can either do that by reverting the call frame in which the payment was transferred or by transferring the payment back to the original sender. Reverting the call frame here IS technically an option for cases where the transfer fails for some unexpected reason but we can avoid this because OVM_ETH is trusted. The only case we really need to be careful of is if we were to run out of gas when trying to transfer the ETH back. We could get around this by adding an extra call frame (expensive) or by making sure that the user always provides enough gas to execute both transfer steps when the value is greater than 0.

@@ -1908,6 +2085,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(bool success, bytes memory returndata) = ovmCALL(
_transaction.gasLimit,
_transaction.entrypoint,
0,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can also remove this 0

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on second thought: gonna leave these. Slightly more efficient and minimizes usage of an effectively deprecated code path

@ben-chain ben-chain force-pushed the feat/OP759/call-with-value branch from 4572dfd to ea5c006 Compare June 1, 2021 22:01
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address;
nextMessageContext.isStatic = true;
nextMessageContext.ovmCALLVALUE = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed with @ben-chain, we can either attach the call value to the message context or pass it as a parameter to _callContract. Attaching the call value to the message context is probably best because it effectively is a message-specific parameter and it must be accessible for the sake of msg.value.


// Since we transferred it in above and the call reverted, the transfer back should always pass.
// If it did not, this is an OOG, and we have to make the parent out-of-gas as well.
// TODO: should we also enforce there is always enough extra gas to make it past this step? This would mean this condition is only triggered due to some critical bug in the ERC20 implementation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to ^^^ I'm going to give my understanding of the problem we're facing here and the proposed solution because I found it quite difficult to wrap my head around. This is based on conversation with @ben-chain (please correct me if you see any mistakes).

What we're trying to achieve here is to duplicate the behavior of EVM calls that come with attached value. Specifically, this behavior is as follows:

  1. If making a call with a value > 0 and the call fails, then the value IS NOT transferred to the recipient.
  2. If making a call with a value > 0 and the call succeeds, then the value IS transferred to the recipient.
  3. If making a call with a value > 0 and the user is unable to transfer funds (e.g., because they don't have enough balance OR because they don't have enough gas to execute the payment), then the value IS NOT transferred to the recipient AND the call fails with (success=false, returndata=0x). This IS how the EVM behaves. See geth: https://github.com/ethereum/go-ethereum/blob/2dee31930c9977af2a9fcb518fb9838aa609a7cf/core/vm/evm.go#L298

Case (2) is the straightforward happy path case.
Case (3) is also pretty easy to handle by checking for failure of the initial transfer.

Case (1) is slightly harder -- you want to revert the payment if the call fails. You can either do that by reverting the call frame in which the payment was transferred or by transferring the payment back to the original sender. Reverting the call frame here IS technically an option for cases where the transfer fails for some unexpected reason but we can avoid this because OVM_ETH is trusted. The only case we really need to be careful of is if we were to run out of gas when trying to transfer the ETH back. We could get around this by adding an extra call frame (expensive) or by making sure that the user always provides enough gas to execute both transfer steps when the value is greater than 0.

@ben-chain
Copy link
Owner Author

ben-chain commented Jun 2, 2021

@smartcontracts that is a great writeup, right on the money. See here for proposed fix.

@ben-chain ben-chain force-pushed the feat/OP759/call-with-value branch from 34b479f to 7a59b2b Compare June 2, 2021 00:29
ben-chain added 3 commits June 8, 2021 13:40
* add ovmCALLVALUE context

* add ovmBALANCE

* test success and revert cases

* test empty contract case

* chore: lint
* fix ovmDELEGATECALL type, update tests

* add ovmSELFBALANCE

* fix ovmDELEGATECALL jumping to CALL

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

Successfully merging this pull request may close these issues.

3 participants