Commit e04de624 authored by ben-chain's avatar ben-chain Committed by GitHub

feat(contracts, l2geth): native ETH value support for ovmCALL (#1038)

* feat(contracts): add ovmCALL-types with native value

* add ovmCALLVALUE context

* add ovmBALANCE

* test success and revert cases

* test empty contract case

* chore: lint

* test(integration-tests): ovmCALL-types with value (compiler and wrapper)

* fix ovmDELEGATECALL type, update tests

* add ovmSELFBALANCE

* fix ovmDELEGATECALL jumping to CALL

* chore: lint

* fix(contracts): account for intrinsic gas of OVM_ETH sends

* fix(contracts): merge conflict bug

* fix(contracts): update gas benchmark

* feat(contracts, integration-tests): use new value-compatible compiler

* feat(contracts,l2geth): support value calls in OVM_ECDSAContractAccount

* fix(contracts): ovmDELEGATECALL does not change message context

* feat(contracts): sending value between EOAs

* test(integration-tests): ovmDELEGATECALL preserves ovmCALLVALUE

* test(integration-tests): assert ovmSELFBALANCEs correct

* test(integration-tests): intrinsic gas for eth value calls

* test(integration-tests): update gas values

* chore(contracts): lint

* feat(contracts, l2geth): eth_calls with nonzero value

* chore: minor fixups and comments based on PR feedback

* test(integration-tests): add requested tests from PR reviews

* test(integration-tests): ovmSELFBALANCE is preserved in ovmDELEGATECALLs

* fix(contracts): fix bug where ovmDELEGATECALL could fail if balance was lower than the ovmCALLVALUE

* chore: add changeset

* fix(contracts): update intrinsic gas for worst-case value sends

* chore: address final PR nits/improvements
Co-authored-by: default avatarKelvin Fichter <kelvinfichter@gmail.com>
parent c2a04893
---
'@eth-optimism/integration-tests': minor
'@eth-optimism/l2geth': minor
'@eth-optimism/contracts': minor
---
Add support for ovmCALL with nonzero ETH value
......@@ -2,10 +2,16 @@
pragma solidity >=0.7.0;
contract TestOOG {
constructor() {
function runOutOfGas() public {
bytes32 h;
for (uint256 i = 0; i < 100000; i++) {
h = keccak256(abi.encodePacked(h));
}
}
}
contract TestOOGInConstructor is TestOOG {
constructor() {
runOutOfGas();
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
contract ValueContext {
function getSelfBalance() external payable returns(uint256) {
uint selfBalance;
assembly {
selfBalance := selfbalance()
}
return selfBalance;
}
function getAddressThisBalance() external view returns(uint256) {
return address(this).balance;
}
function getBalance(
address _address
) external payable returns(uint256) {
return _address.balance;
}
function getCallValue() public payable returns(uint256) {
return msg.value;
}
}
contract ValueCalls is ValueContext {
receive() external payable {}
function nonPayable() external {}
function simpleSend(
address _address,
uint _value
) external payable returns (bool, bytes memory) {
return sendWithData(_address, _value, hex"");
}
function sendWithDataAndGas(
address _address,
uint _value,
uint _gasLimit,
bytes memory _calldata
) public returns (bool, bytes memory) {
return _address.call{value: _value, gas: _gasLimit}(_calldata);
}
function sendWithData(
address _address,
uint _value,
bytes memory _calldata
) public returns (bool, bytes memory) {
return _address.call{value: _value}(_calldata);
}
function verifyCallValueAndRevert(
uint256 _expectedValue
) external payable {
bool correct = _checkCallValue(_expectedValue);
// do the opposite of expected if the value is wrong.
if (correct) {
revert("expected revert");
} else {
return;
}
}
function verifyCallValueAndReturn(
uint256 _expectedValue
) external payable {
bool correct = _checkCallValue(_expectedValue);
// do the opposite of expected if the value is wrong.
if (correct) {
return;
} else {
revert("unexpected revert");
}
}
function delegateCallToCallValue(
address _valueContext
) public payable returns(bool, bytes memory) {
bytes memory data = abi.encodeWithSelector(ValueContext.getCallValue.selector);
return _valueContext.delegatecall(data);
}
function delegateCallToAddressThisBalance(
address _valueContext
) public payable returns(bool, bytes memory) {
bytes memory data = abi.encodeWithSelector(ValueContext.getAddressThisBalance.selector);
return _valueContext.delegatecall(data);
}
function _checkCallValue(
uint256 _expectedValue
) internal returns(bool) {
return getCallValue() == _expectedValue;
}
}
contract ValueGasMeasurer {
function measureGasOfTransferingEthViaCall(
address target,
uint256 value,
uint256 gasLimit
) public returns(uint256) {
uint256 gasBefore = gasleft();
assembly {
pop(call(gasLimit, target, value, 0, 0, 0, 0))
}
return gasBefore - gasleft();
}
}
contract PayableConstant {
function returnValue() external payable returns(uint256) {
return 42;
}
}
contract SendETHAwayAndDelegateCall {
function emptySelfAndDelegateCall(
address _delegateTo,
bytes memory _data
) public payable returns (bool, bytes memory) {
address(0).call{value: address(this).balance}(_data);
return _delegateTo.delegatecall(_data);
}
}
......@@ -20,7 +20,7 @@ const config: HardhatUserConfig = {
},
solidity: '0.7.6',
ovm: {
solcVersion: '0.7.6',
solcVersion: '0.7.6+commit.3b061308',
},
gasReporter: {
enabled: enableGasReport,
......
import { BigNumber, Contract, ContractFactory, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import chai, { expect } from 'chai'
import { GWEI, fundUser, encodeSolidityRevertMessage } from './shared/utils'
import { OptimismEnv } from './shared/env'
import { solidity } from 'ethereum-waffle'
import { sleep } from '../../packages/core-utils/dist'
import {
getContractFactory,
getContractInterface,
} from '../../packages/contracts/dist'
import { Interface } from 'ethers/lib/utils'
chai.use(solidity)
describe('Native ETH value integration tests', () => {
let env: OptimismEnv
let wallet: Wallet
let other: Wallet
before(async () => {
env = await OptimismEnv.new()
wallet = env.l2Wallet
other = Wallet.createRandom().connect(wallet.provider)
})
it('should allow an L2 EOA to send to a new account and back again', async () => {
const getBalances = async (): Promise<BigNumber[]> => {
return [
await wallet.provider.getBalance(wallet.address),
await wallet.provider.getBalance(other.address),
]
}
const checkBalances = async (
expectedBalances: BigNumber[]
): Promise<void> => {
const realBalances = await getBalances()
expect(realBalances[0]).to.deep.eq(expectedBalances[0])
expect(realBalances[1]).to.deep.eq(expectedBalances[1])
}
const value = 10
await fundUser(env.watcher, env.gateway, value, wallet.address)
const initialBalances = await getBalances()
const there = await wallet.sendTransaction({
to: other.address,
value,
gasPrice: 0,
})
await there.wait()
await checkBalances([
initialBalances[0].sub(value),
initialBalances[1].add(value),
])
const backAgain = await other.sendTransaction({
to: wallet.address,
value,
gasPrice: 0,
})
await backAgain.wait()
await checkBalances(initialBalances)
})
describe(`calls between OVM contracts with native ETH value and relevant opcodes`, async () => {
const initialBalance0 = 42000
let Factory__ValueCalls: ContractFactory
let ValueCalls0: Contract
let ValueCalls1: Contract
const checkBalances = async (expectedBalances: number[]) => {
// query geth as one check
const balance0 = await wallet.provider.getBalance(ValueCalls0.address)
const balance1 = await wallet.provider.getBalance(ValueCalls1.address)
expect(balance0).to.deep.eq(BigNumber.from(expectedBalances[0]))
expect(balance1).to.deep.eq(BigNumber.from(expectedBalances[1]))
// query ovmBALANCE() opcode via eth_call as another check
const ovmBALANCE0 = await ValueCalls0.callStatic.getBalance(
ValueCalls0.address
)
const ovmBALANCE1 = await ValueCalls0.callStatic.getBalance(
ValueCalls1.address
)
expect(ovmBALANCE0).to.deep.eq(
BigNumber.from(expectedBalances[0]),
'geth RPC does not match ovmBALANCE'
)
expect(ovmBALANCE1).to.deep.eq(
BigNumber.from(expectedBalances[1]),
'geth RPC does not match ovmBALANCE'
)
// query ovmSELFBALANCE() opcode via eth_call as another check
const ovmSELFBALANCE0 = await ValueCalls0.callStatic.getSelfBalance()
const ovmSELFBALANCE1 = await ValueCalls1.callStatic.getSelfBalance()
expect(ovmSELFBALANCE0).to.deep.eq(
BigNumber.from(expectedBalances[0]),
'geth RPC does not match ovmSELFBALANCE'
)
expect(ovmSELFBALANCE1).to.deep.eq(
BigNumber.from(expectedBalances[1]),
'geth RPC does not match ovmSELFBALANCE'
)
// query ovmSELFBALANCE() opcode via eth_call as another check
const ovmEthBalanceOf0 = await env.ovmEth.balanceOf(ValueCalls0.address)
const ovmEthBalanceOf1 = await env.ovmEth.balanceOf(ValueCalls1.address)
expect(ovmEthBalanceOf0).to.deep.eq(
BigNumber.from(expectedBalances[0]),
'geth RPC does not match OVM_ETH.balanceOf'
)
expect(ovmEthBalanceOf1).to.deep.eq(
BigNumber.from(expectedBalances[1]),
'geth RPC does not match OVM_ETH.balanceOf'
)
// query address(this).balance solidity via eth_call as final check
const ovmAddressThisBalance0 = await ValueCalls0.callStatic.getAddressThisBalance()
const ovmAddressThisBalance01 = await ValueCalls1.callStatic.getAddressThisBalance()
expect(ovmAddressThisBalance0).to.deep.eq(
BigNumber.from(expectedBalances[0]),
'geth RPC does not match address(this).balance'
)
expect(ovmAddressThisBalance01).to.deep.eq(
BigNumber.from(expectedBalances[1]),
'geth RPC does not match address(this).balance'
)
}
before(async () => {
Factory__ValueCalls = await ethers.getContractFactory(
'ValueCalls',
wallet
)
})
beforeEach(async () => {
ValueCalls0 = await Factory__ValueCalls.deploy()
ValueCalls1 = await Factory__ValueCalls.deploy()
await fundUser(
env.watcher,
env.gateway,
initialBalance0,
ValueCalls0.address
)
// These tests ass assume ValueCalls0 starts with a balance, but ValueCalls1 does not.
await checkBalances([initialBalance0, 0])
})
it('should allow ETH to be sent', async () => {
const sendAmount = 15
const tx = await ValueCalls0.simpleSend(ValueCalls1.address, sendAmount, {
gasPrice: 0,
})
await tx.wait()
await checkBalances([initialBalance0 - sendAmount, sendAmount])
})
it('should revert if a function is nonpayable', async () => {
const sendAmount = 15
const [success, returndata] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
ValueCalls1.interface.encodeFunctionData('nonPayable')
)
expect(success).to.be.false
expect(returndata).to.eq('0x')
})
it('should allow ETH to be sent and have the correct ovmCALLVALUE', async () => {
const sendAmount = 15
const [success, returndata] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
ValueCalls1.interface.encodeFunctionData('getCallValue')
)
expect(success).to.be.true
expect(BigNumber.from(returndata)).to.deep.eq(BigNumber.from(sendAmount))
})
it('should have the correct ovmSELFBALANCE which includes the msg.value', async () => {
// give an initial balance which the ovmCALLVALUE should be added to when calculating ovmSELFBALANCE
const initialBalance = 10
await fundUser(
env.watcher,
env.gateway,
initialBalance,
ValueCalls1.address
)
const sendAmount = 15
const [success, returndata] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
ValueCalls1.interface.encodeFunctionData('getSelfBalance')
)
expect(success).to.be.true
expect(BigNumber.from(returndata)).to.deep.eq(
BigNumber.from(initialBalance + sendAmount)
)
})
it('should have the correct callvalue but not persist the transfer if the target reverts', async () => {
const sendAmount = 15
const internalCalldata = ValueCalls1.interface.encodeFunctionData(
'verifyCallValueAndRevert',
[sendAmount]
)
const [success, returndata] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
internalCalldata
)
expect(success).to.be.false
expect(returndata).to.eq(encodeSolidityRevertMessage('expected revert'))
await checkBalances([initialBalance0, 0])
})
it('should look like the subcall reverts with no data if value exceeds balance', async () => {
const sendAmount = initialBalance0 + 1
const internalCalldata = ValueCalls1.interface.encodeFunctionData(
'verifyCallValueAndReturn',
[sendAmount] // this would be correct and return successfuly, IF it could get here
)
const [success, returndata] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
internalCalldata
)
expect(success).to.be.false
expect(returndata).to.eq('0x')
})
it('should preserve msg.value through ovmDELEGATECALLs', async () => {
const Factory__ValueContext = await ethers.getContractFactory(
'ValueContext',
wallet
)
const ValueContext = await Factory__ValueContext.deploy()
await ValueContext.deployTransaction.wait()
const sendAmount = 10
const [
outerSuccess,
outerReturndata,
] = await ValueCalls0.callStatic.sendWithData(
ValueCalls1.address,
sendAmount,
ValueCalls1.interface.encodeFunctionData('delegateCallToCallValue', [
ValueContext.address,
])
)
const [
innerSuccess,
innerReturndata,
] = ValueCalls1.interface.decodeFunctionResult(
'delegateCallToCallValue',
outerReturndata
)
const delegatedOvmCALLVALUE = ValueContext.interface.decodeFunctionResult(
'getCallValue',
innerReturndata
)[0]
expect(outerSuccess).to.be.true
expect(innerSuccess).to.be.true
expect(delegatedOvmCALLVALUE).to.deep.eq(BigNumber.from(sendAmount))
})
it('should have correct address(this).balance through ovmDELEGATECALLs to another account', async () => {
const Factory__ValueContext = await ethers.getContractFactory(
'ValueContext',
wallet
)
const ValueContext = await Factory__ValueContext.deploy()
await ValueContext.deployTransaction.wait()
const [
delegatedSuccess,
delegatedReturndata,
] = await ValueCalls0.callStatic.delegateCallToAddressThisBalance(
ValueContext.address
)
expect(delegatedSuccess).to.be.true
expect(delegatedReturndata).to.deep.eq(BigNumber.from(initialBalance0))
})
it('should have correct address(this).balance through ovmDELEGATECALLs to same account', async () => {
const [
delegatedSuccess,
delegatedReturndata,
] = await ValueCalls0.callStatic.delegateCallToAddressThisBalance(
ValueCalls0.address
)
expect(delegatedSuccess).to.be.true
expect(delegatedReturndata).to.deep.eq(BigNumber.from(initialBalance0))
})
it('should allow delegate calls which preserve msg.value even with no balance going into the inner call', async () => {
const Factory__SendETHAwayAndDelegateCall: ContractFactory = await ethers.getContractFactory(
'SendETHAwayAndDelegateCall',
wallet
)
const SendETHAwayAndDelegateCall: Contract = await Factory__SendETHAwayAndDelegateCall.deploy()
await SendETHAwayAndDelegateCall.deployTransaction.wait()
const value = 17
const [
delegatedSuccess,
delegatedReturndata,
] = await SendETHAwayAndDelegateCall.callStatic.emptySelfAndDelegateCall(
ValueCalls0.address,
ValueCalls0.interface.encodeFunctionData('getCallValue'),
{
value,
}
)
expect(delegatedSuccess).to.be.true
expect(delegatedReturndata).to.deep.eq(BigNumber.from(value))
})
describe('Intrinsic gas for ovmCALL types', async () => {
let CALL_WITH_VALUE_INTRINSIC_GAS
let ValueGasMeasurer: Contract
before(async () => {
// Grab public variable from the EM
const OVM_ExecutionManager = new Contract(
await env.addressManager.getAddress('OVM_ExecutionManager'),
getContractInterface('OVM_ExecutionManager', false),
env.l1Wallet.provider
)
const CALL_WITH_VALUE_INTRINSIC_GAS_BIGNUM = await OVM_ExecutionManager.CALL_WITH_VALUE_INTRINSIC_GAS()
CALL_WITH_VALUE_INTRINSIC_GAS = CALL_WITH_VALUE_INTRINSIC_GAS_BIGNUM.toNumber()
const Factory__ValueGasMeasurer = await ethers.getContractFactory(
'ValueGasMeasurer',
wallet
)
ValueGasMeasurer = await Factory__ValueGasMeasurer.deploy()
await ValueGasMeasurer.deployTransaction.wait()
})
it('a call with value to an empty account consumes <= the intrinsic gas including a buffer', async () => {
const value = 1
const gasLimit = 1_000_000
const minimalSendGas = await ValueGasMeasurer.callStatic.measureGasOfTransferingEthViaCall(
ethers.constants.AddressZero,
value,
gasLimit,
{
gasLimit: 2_000_000,
}
)
const buffer = 1.2
expect(minimalSendGas * buffer).to.be.lte(CALL_WITH_VALUE_INTRINSIC_GAS)
})
it('a call with value to an reverting account consumes <= the intrinsic gas including a buffer', async () => {
// [magic deploy prefix] . [MSTORE] (will throw exception from no stack args)
const AutoRevertInitcode = '0x600D380380600D6000396000f3' + '52'
const Factory__AutoRevert = new ContractFactory(
new Interface([]),
AutoRevertInitcode,
wallet
)
const AutoRevert: Contract = await Factory__AutoRevert.deploy()
await AutoRevert.deployTransaction.wait()
const value = 1
const gasLimit = 1_000_000
// A revert, causing the ETH to be sent back, should consume the minimal possible gas for a nonzero ETH send
const minimalSendGas = await ValueGasMeasurer.callStatic.measureGasOfTransferingEthViaCall(
AutoRevert.address,
value,
gasLimit,
{
gasLimit: 2_000_000,
}
)
const buffer = 1.2
expect(minimalSendGas * buffer).to.be.lte(CALL_WITH_VALUE_INTRINSIC_GAS)
})
it('a value call passing less than the intrinsic gas should appear to revert', async () => {
const Factory__PayableConstant: ContractFactory = await ethers.getContractFactory(
'PayableConstant',
wallet
)
const PayableConstant: Contract = await Factory__PayableConstant.deploy()
await PayableConstant.deployTransaction.wait()
const sendAmount = 15
const [
success,
returndata,
] = await ValueCalls0.callStatic.sendWithDataAndGas(
PayableConstant.address,
sendAmount,
PayableConstant.interface.encodeFunctionData('returnValue'),
CALL_WITH_VALUE_INTRINSIC_GAS - 1,
{
gasLimit: 2_000_000,
}
)
expect(success).to.eq(false)
expect(returndata).to.eq('0x')
})
it('a value call which runs out of gas does not out-of-gas the parent', async () => {
const Factory__TestOOG: ContractFactory = await ethers.getContractFactory(
'TestOOG',
wallet
)
const TestOOG: Contract = await Factory__TestOOG.deploy()
await TestOOG.deployTransaction.wait()
const sendAmount = 15
// Implicitly test that this call is not rejected
const [
success,
returndata,
] = await ValueCalls0.callStatic.sendWithDataAndGas(
TestOOG.address,
sendAmount,
TestOOG.interface.encodeFunctionData('runOutOfGas'),
CALL_WITH_VALUE_INTRINSIC_GAS * 2,
{
gasLimit: 2_000_000,
}
)
expect(success).to.eq(false)
expect(returndata).to.eq('0x')
})
})
})
})
......@@ -5,8 +5,8 @@ import { Direction } from './shared/watcher-utils'
import { PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils'
import { OptimismEnv } from './shared/env'
const DEFAULT_TEST_GAS_L1 = 230_000
const DEFAULT_TEST_GAS_L2 = 825_000
const DEFAULT_TEST_GAS_L1 = 330_000
const DEFAULT_TEST_GAS_L2 = 1_000_000
// TX size enforced by CTC:
const MAX_ROLLUP_TX_SIZE = 50_000
......@@ -50,13 +50,13 @@ describe('Native ETH Integration Tests', async () => {
const amount = utils.parseEther('0.5')
const addr = '0x' + '1234'.repeat(10)
const gas = await env.ovmEth.estimateGas.transfer(addr, amount)
expect(gas).to.be.deep.eq(BigNumber.from(6430020))
expect(gas).to.be.deep.eq(BigNumber.from(6430021))
})
it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.5')
const gas = await env.ovmEth.estimateGas.withdraw(amount, 0, '0xFFFF')
expect(gas).to.be.deep.eq(BigNumber.from(6580050))
expect(gas).to.be.deep.eq(BigNumber.from(6580054))
})
})
......
......@@ -4,10 +4,10 @@ import {
TxGasPrice,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { Wallet, BigNumber, Contract } from 'ethers'
import { Wallet, BigNumber, Contract, ContractFactory } from 'ethers'
import { ethers } from 'hardhat'
import chai, { expect } from 'chai'
import { sleep, l2Provider, l1Provider } from './shared/utils'
import { sleep, l2Provider, l1Provider, fundUser } from './shared/utils'
import chaiAsPromised from 'chai-as-promised'
import { OptimismEnv } from './shared/env'
import {
......@@ -154,7 +154,7 @@ describe('Basic RPC tests', () => {
})
it('should correctly report OOG for contract creations', async () => {
const factory = await ethers.getContractFactory('TestOOG')
const factory = await ethers.getContractFactory('TestOOGInConstructor')
await expect(factory.connect(wallet).deploy()).to.be.rejectedWith(
'gas required exceeds allowance'
......@@ -207,6 +207,32 @@ describe('Basic RPC tests', () => {
'Contract creation code contains unsafe opcodes. Did you use the right compiler or pass an unsafe constructor argument?'
)
})
it('should allow eth_calls with nonzero value', async () => {
// Deploy a contract to check msg.value of the call
const Factory__ValueContext: ContractFactory = await ethers.getContractFactory(
'ValueContext',
wallet
)
const ValueContext: Contract = await Factory__ValueContext.deploy()
await ValueContext.deployTransaction.wait()
// Fund account to call from
const from = wallet.address
const value = 15
await fundUser(env.watcher, env.gateway, value, from)
// Do the call and check msg.value
const data = ValueContext.interface.encodeFunctionData('getCallValue')
const res = await provider.call({
to: ValueContext.address,
from,
data,
value,
})
expect(res).to.eq(BigNumber.from(value))
})
})
describe('eth_getTransactionReceipt', () => {
......@@ -236,7 +262,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract creations', async () => {
const req: TransactionRequest = {
...revertingDeployTx,
gasLimit: 17700899, // override gas estimation
gasLimit: 27700899, // override gas estimation
}
const tx = await wallet.sendTransaction(req)
......@@ -353,7 +379,7 @@ describe('Basic RPC tests', () => {
to: DEFAULT_TRANSACTION.to,
value: 0,
})
expect(estimate).to.be.eq(5920012)
expect(estimate).to.be.eq(5920013)
})
it('should return a gas estimate that grows with the size of data', async () => {
......
......@@ -100,6 +100,11 @@ func EncodeSimulatedMessage(msg Message, timestamp, blockNumber *big.Int, execut
to = &common.Address{0}
}
value := msg.Value()
if value == nil {
value = common.Big0
}
tx := ovmTransaction{
timestamp,
blockNumber,
......@@ -114,6 +119,7 @@ func EncodeSimulatedMessage(msg Message, timestamp, blockNumber *big.Int, execut
var args = []interface{}{
tx,
from,
value,
stateManager.Address,
}
......
......@@ -321,14 +321,6 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction)
if len(signedTx.Data()) > b.MaxCallDataSize {
return fmt.Errorf("Calldata cannot be larger than %d, sent %d", b.MaxCallDataSize, len(signedTx.Data()))
}
// If there is a value field set then reject transactions that
// contain calldata. The feature of sending transactions with value
// and calldata will be added in the future.
if signedTx.Value().Cmp(common.Big0) != 0 {
if len(signedTx.Data()) > 0 {
return errors.New("Cannot send transactions with value and calldata")
}
}
}
return b.eth.syncService.ValidateAndApplySequencerTransaction(signedTx)
}
......
......@@ -47,6 +47,16 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
* Public Functions *
********************/
/**
* No-op fallback mirrors behavior of calling an EOA on L1.
*/
fallback()
external
payable
{
return;
}
/**
* Executes a signed transaction.
* @param _transaction Signed EIP155 transaction.
......@@ -121,24 +131,6 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
// cases, but since this is a contract we'd end up bumping the nonce twice.
Lib_ExecutionManagerWrapper.ovmINCREMENTNONCE();
// Value transfer currently only supported for CALL but not for CREATE.
if (_transaction.value > 0) {
// TEMPORARY: Block value transfer if the transaction has input data.
require(
_transaction.data.length == 0,
"Value is nonzero but input data was provided."
);
require(
OVM_ETH(Lib_PredeployAddresses.OVM_ETH).transfer(
_transaction.to,
_transaction.value
),
"Value could not be transferred to recipient."
);
return (true, bytes(""));
} else {
// NOTE: Upgrades are temporarily disabled because users can, in theory, modify their EOA
// so that they don't have to pay any fees to the sequencer. Function will remain disabled
// until a robust solution is in place.
......@@ -147,8 +139,7 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
"Calls to self are disabled until upgradability is re-enabled."
);
return _transaction.to.call(_transaction.data);
}
return _transaction.to.call{value: _transaction.value}(_transaction.data);
}
}
}
......@@ -39,6 +39,7 @@ contract OVM_ProxyEOA {
fallback()
external
payable
{
(bool success, bytes memory returndata) = getImplementation().delegatecall(msg.data);
......
......@@ -6,6 +6,7 @@ pragma experimental ABIEncoderV2;
/* Library Imports */
import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol";
import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.sol";
import { Lib_Bytes32Utils } from "../../libraries/utils/Lib_Bytes32Utils.sol";
import { Lib_EthUtils } from "../../libraries/utils/Lib_EthUtils.sol";
import { Lib_ErrorUtils } from "../../libraries/utils/Lib_ErrorUtils.sol";
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
......@@ -14,10 +15,14 @@ import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployA
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";
/* Contract Imports */
import { OVM_DeployerWhitelist } from "../predeploys/OVM_DeployerWhitelist.sol";
/* External Imports */
import { Math } from "@openzeppelin/contracts/math/Math.sol";
/**
* @title OVM_ExecutionManager
* @dev The Execution Manager (EM) is the core of our OVM implementation, and provides a sandboxed
......@@ -66,6 +71,15 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
uint256 constant NUISANCE_GAS_PER_CONTRACT_BYTE = 100;
uint256 constant MIN_GAS_FOR_INVALID_STATE_ACCESS = 30000;
/**************************
* Native Value Constants *
**************************/
// Public so we can access and make assertions in integration tests.
uint256 public constant CALL_WITH_VALUE_INTRINSIC_GAS = 90000;
/**************************
* Default Context Values *
**************************/
......@@ -220,6 +234,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(, bytes memory returndata) = ovmCALL(
_transaction.gasLimit - gasMeterConfig.minTransactionGasLimit,
_transaction.entrypoint,
0,
_transaction.data
);
......@@ -269,6 +284,21 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return messageContext.ovmADDRESS;
}
/**
* @notice Overrides CALLVALUE.
* @return _CALLVALUE Value sent along with the call according to the current message context.
*/
function ovmCALLVALUE()
override
public
view
returns (
uint256 _CALLVALUE
)
{
return messageContext.ovmCALLVALUE;
}
/**
* @notice Overrides TIMESTAMP.
* @return _TIMESTAMP Value of the TIMESTAMP within the transaction context.
......@@ -418,7 +448,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return _createContract(
contractAddress,
_bytecode
_bytecode,
MessageType.ovmCREATE
);
}
......@@ -458,7 +489,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return _createContract(
contractAddress,
_bytecode
_bytecode,
MessageType.ovmCREATE2
);
}
......@@ -591,6 +623,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
* @notice Overrides CALL.
* @param _gasLimit Amount of gas to be passed into this call.
* @param _address Address of the contract to call.
* @param _value ETH value to pass with the call.
* @param _calldata Data to send along with the call.
* @return _success Whether or not the call returned (rather than reverted).
* @return _returndata Data returned by the call.
......@@ -598,6 +631,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
function ovmCALL(
uint256 _gasLimit,
address _address,
uint256 _value,
bytes memory _calldata
)
override
......@@ -612,12 +646,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address;
nextMessageContext.ovmCALLVALUE = _value;
return _callContract(
nextMessageContext,
_gasLimit,
_address,
_calldata
_calldata,
MessageType.ovmCALL
);
}
......@@ -635,24 +671,26 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory _calldata
)
override
external
public
fixedGasDiscount(80000)
returns (
bool _success,
bytes memory _returndata
)
{
// STATICCALL updates the CALLER, updates the ADDRESS, and runs in a static context.
// STATICCALL updates the CALLER, updates the ADDRESS, and runs in a static, valueless context.
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address;
nextMessageContext.isStatic = true;
nextMessageContext.ovmCALLVALUE = 0;
return _callContract(
nextMessageContext,
_gasLimit,
_address,
_calldata
_calldata,
MessageType.ovmSTATICCALL
);
}
......@@ -670,7 +708,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory _calldata
)
override
external
public
fixedGasDiscount(40000)
returns (
bool _success,
......@@ -684,6 +722,36 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
nextMessageContext,
_gasLimit,
_address,
_calldata,
MessageType.ovmDELEGATECALL
);
}
/**
* @notice Legacy ovmCALL function which did not support ETH value; this maintains backwards compatibility.
* @param _gasLimit Amount of gas to be passed into this call.
* @param _address Address of the contract to call.
* @param _calldata Data to send along with the call.
* @return _success Whether or not the call returned (rather than reverted).
* @return _returndata Data returned by the call.
*/
function ovmCALL(
uint256 _gasLimit,
address _address,
bytes memory _calldata
)
override
public
returns(
bool _success,
bytes memory _returndata
)
{
// Legacy ovmCALL assumed always-0 value.
return ovmCALL(
_gasLimit,
_address,
0,
_calldata
);
}
......@@ -809,6 +877,63 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
);
}
/***************************************
* Public Functions: ETH Value Opcodes *
***************************************/
/**
* @notice Overrides BALANCE.
* NOTE: In the future, this could be optimized to directly invoke EM._getContractStorage(...).
* @param _contract Address of the contract to query the OVM_ETH balance of.
* @return _BALANCE OVM_ETH balance of the requested contract.
*/
function ovmBALANCE(
address _contract
)
override
public
returns (
uint256 _BALANCE
)
{
// Easiest way to get the balance is query OVM_ETH as normal.
bytes memory balanceOfCalldata = abi.encodeWithSelector(
IUniswapV2ERC20.balanceOf.selector,
_contract
);
// Static call because this should be a read-only query.
(bool success, bytes memory returndata) = ovmSTATICCALL(
gasleft(),
Lib_PredeployAddresses.OVM_ETH,
balanceOfCalldata
);
// All balanceOf queries should successfully return a uint, otherwise this must be an OOG.
if (!success || returndata.length != 32) {
_revertWithFlag(RevertFlag.OUT_OF_GAS);
}
// Return the decoded balance.
return abi.decode(returndata, (uint256));
}
/**
* @notice Overrides SELFBALANCE.
* @return _BALANCE OVM_ETH balance of the requesting contract.
*/
function ovmSELFBALANCE()
override
external
returns (
uint256 _BALANCE
)
{
return ovmBALANCE(ovmADDRESS());
}
/***************************************
* Public Functions: Execution Context *
***************************************/
......@@ -839,10 +964,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
{
// From an OVM semantics perspective, this will appear identical to
// the deployer ovmCALLing the whitelist. This is fine--in a sense, we are forcing them to.
(bool success, bytes memory data) = ovmCALL(
(bool success, bytes memory data) = ovmSTATICCALL(
gasleft(),
Lib_PredeployAddresses.DEPLOYER_WHITELIST,
abi.encodeWithSignature("isDeployerAllowed(address)", _deployerAddress)
abi.encodeWithSelector(
OVM_DeployerWhitelist.isDeployerAllowed.selector,
_deployerAddress
)
);
bool isAllowed = abi.decode(data, (bool));
......@@ -864,7 +992,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
*/
function _createContract(
address _contractAddress,
bytes memory _bytecode
bytes memory _bytecode,
MessageType _messageType
)
internal
returns (
......@@ -888,7 +1017,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
gasleft(),
_contractAddress,
_bytecode,
true
_messageType
);
// Yellow paper requires that address returned is zero if the contract deployment fails.
......@@ -911,7 +1040,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
MessageContext memory _nextMessageContext,
uint256 _gasLimit,
address _contract,
bytes memory _calldata
bytes memory _calldata,
MessageType _messageType
)
internal
returns (
......@@ -940,7 +1070,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
_gasLimit,
codeContractAddress,
_calldata,
false
_messageType
);
}
......@@ -949,19 +1079,20 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
* Ensures that OVM-related measures are enforced, including L2 gas refunds, nuisance gas, and flagged reversions.
*
* @param _nextMessageContext Message context to be used for the external message.
* @param _gasLimit Amount of gas to be passed into this message.
* @param _gasLimit Amount of gas to be passed into this message. NOTE: this argument is overwritten in some cases to avoid stack-too-deep.
* @param _contract OVM address being called or deployed to
* @param _data Data for the message (either calldata or creation code)
* @param _isCreate Whether this is a create-type message.
* @param _messageType What type of ovmOPCODE this message corresponds to.
* @return Whether or not the message (either a call or deployment) succeeded.
* @return Data returned by the message.
*/
function _handleExternalMessage(
MessageContext memory _nextMessageContext,
// NOTE: this argument is overwritten in some cases to avoid stack-too-deep.
uint256 _gasLimit,
address _contract,
bytes memory _data,
bool _isCreate
MessageType _messageType
)
internal
returns (
......@@ -969,6 +1100,43 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory
)
{
uint256 messageValue = _nextMessageContext.ovmCALLVALUE;
// If there is value in this message, we need to transfer the ETH over before switching contexts.
if (
messageValue > 0
&& _isValueType(_messageType)
) {
// Handle out-of-intrinsic gas consistent with EVM behavior -- the subcall "appears to revert" if we don't have enough gas to transfer the ETH.
// Similar to dynamic gas cost of value exceeding gas here:
// https://github.com/ethereum/go-ethereum/blob/c503f98f6d5e80e079c1d8a3601d188af2a899da/core/vm/interpreter.go#L268-L273
if (gasleft() < CALL_WITH_VALUE_INTRINSIC_GAS) {
return (false, hex"");
}
// If there *is* enough gas to transfer ETH, then we need to make sure this amount of gas is reserved (i.e. not
// given to the _contract.call below) to guarantee that _handleExternalMessage can't run out of gas.
// In particular, in the event that the call fails, we will need to transfer the ETH back to the sender.
// Taking the lesser of _gasLimit and gasleft() - CALL_WITH_VALUE_INTRINSIC_GAS guarantees that the second
// _attemptForcedEthTransfer below, if needed, always has enough gas to succeed.
_gasLimit = Math.min(
_gasLimit,
gasleft() - CALL_WITH_VALUE_INTRINSIC_GAS // Cannot overflow due to the above check.
);
// Now transfer the value of the call.
// The target is interpreted to be the next message's ovmADDRESS account.
bool transferredOvmEth = _attemptForcedEthTransfer(
_nextMessageContext.ovmADDRESS,
messageValue
);
// If the ETH transfer fails (should only be possible in the case of insufficient balance), then treat this as a revert.
// This mirrors EVM behavior, see https://github.com/ethereum/go-ethereum/blob/2dee31930c9977af2a9fcb518fb9838aa609a7cf/core/vm/evm.go#L298
if (!transferredOvmEth) {
return (false, hex"");
}
}
// We need to switch over to our next message context for the duration of this call.
MessageContext memory prevMessageContext = messageContext;
_switchMessageContext(prevMessageContext, _nextMessageContext);
......@@ -989,14 +1157,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bool success;
bytes memory returndata;
if (_isCreate) {
if (_isCreateType(_messageType)) {
// safeCREATE() is a function which replicates a CREATE message, but uses return values
// Which match that of CALL (i.e. bool, bytes). This allows many security checks to be
// to be shared between untrusted call and create call frames.
(success, returndata) = address(this).call(
(success, returndata) = address(this).call{gas: _gasLimit}(
abi.encodeWithSelector(
this.safeCREATE.selector,
_gasLimit,
_data,
_contract
)
......@@ -1005,7 +1172,29 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(success, returndata) = _contract.call{gas: _gasLimit}(_data);
}
// Switch back to the original message context now that we're out of the call.
// If the message threw an exception, its value should be returned back to the sender.
// So, we force it back, BEFORE returning the messageContext to the previous addresses.
// This operation is part of the reason we "reserved the intrinsic gas" above.
if (
messageValue > 0
&& _isValueType(_messageType)
&& !success
) {
bool transferredOvmEth = _attemptForcedEthTransfer(
prevMessageContext.ovmADDRESS,
messageValue
);
// Since we transferred it in above and the call reverted, the transfer back should always pass.
// This code path should NEVER be triggered since we sent `messageValue` worth of OVM_ETH into the target
// and reserved sufficient gas to execute the transfer, but in case there is some edge case which has
// been missed, we revert the entire frame (and its parent) to make sure the ETH gets sent back.
if (!transferredOvmEth) {
_revertWithFlag(RevertFlag.OUT_OF_GAS);
}
}
// Switch back to the original message context now that we're out of the call and all OVM_ETH is in the right place.
_switchMessageContext(_nextMessageContext, prevMessageContext);
// Assuming there were no reverts, the message record should be accurate here. We'll update
......@@ -1042,10 +1231,12 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
}
// INTENTIONAL_REVERT needs to pass up the user-provided return data encoded into the
// flag, *not* the full encoded flag. All other revert types return no data.
// flag, *not* the full encoded flag. Additionally, we surface custom error messages
// to developers in the case of unsafe creations for improved devex.
// All other revert types return no data.
if (
flag == RevertFlag.INTENTIONAL_REVERT
|| _isCreate
|| flag == RevertFlag.UNSAFE_BYTECODE
) {
returndata = returndataFromFlag;
} else {
......@@ -1077,12 +1268,10 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
* Having this step occur as a separate call frame also allows us to easily revert the
* contract deployment in the event that the code is unsafe.
*
* @param _gasLimit Amount of gas to be passed into this creation.
* @param _creationCode Code to pass into CREATE for deployment.
* @param _address OVM address being deployed to.
*/
function safeCREATE(
uint _gasLimit,
bytes memory _creationCode,
address _address
)
......@@ -1098,13 +1287,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
// Note: in the EVM, this case burns all allotted gas. For improved
// developer experience, we do return the remaining gas.
_revertWithFlag(
RevertFlag.CREATE_COLLISION,
Lib_ErrorUtils.encodeRevertString("A contract has already been deployed to this address")
RevertFlag.CREATE_COLLISION
);
}
// Check the creation bytecode against the OVM_SafetyChecker.
if (ovmSafetyChecker.isBytecodeSafe(_creationCode) == false) {
// Note: in the EVM, this case burns all allotted gas. For improved
// developer experience, we do return the remaining gas.
_revertWithFlag(
RevertFlag.UNSAFE_BYTECODE,
Lib_ErrorUtils.encodeRevertString("Contract creation code contains unsafe opcodes. Did you use the right compiler or pass an unsafe constructor argument?")
......@@ -1146,6 +1336,46 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
);
}
/******************************************
* Internal Functions: Value Manipulation *
******************************************/
/**
* Invokes an ovmCALL to OVM_ETH.transfer on behalf of the current ovmADDRESS, allowing us to force movement of OVM_ETH in correspondence with ETH's native value functionality.
* WARNING: this will send on behalf of whatever the messageContext.ovmADDRESS is in storage at the time of the call.
* NOTE: In the future, this could be optimized to directly invoke EM._setContractStorage(...).
* @param _to Amount of OVM_ETH to be sent.
* @param _value Amount of OVM_ETH to send.
* @return _success Whether or not the transfer worked.
*/
function _attemptForcedEthTransfer(
address _to,
uint256 _value
)
internal
returns(
bool _success
)
{
bytes memory transferCalldata = abi.encodeWithSelector(
IUniswapV2ERC20.transfer.selector,
_to,
_value
);
// OVM_ETH inherits from the UniswapV2ERC20 standard. In this implementation, its return type
// is a boolean. However, the implementation always returns true if it does not revert.
// Thus, success of the call frame is sufficient to infer success of the transfer itself.
(bool success, ) = ovmCALL(
gasleft(),
Lib_PredeployAddresses.OVM_ETH,
0,
transferCalldata
);
return success;
}
/******************************************
* Internal Functions: State Manipulation *
******************************************/
......@@ -1817,20 +2047,23 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
)
internal
{
// Avoid unnecessary the SSTORE.
// These conditionals allow us to avoid unneccessary SSTOREs. However, they do mean that the current storage
// value for the messageContext MUST equal the _prevMessageContext argument, or an SSTORE might be erroneously skipped.
if (_prevMessageContext.ovmCALLER != _nextMessageContext.ovmCALLER) {
messageContext.ovmCALLER = _nextMessageContext.ovmCALLER;
}
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.ovmADDRESS != _nextMessageContext.ovmADDRESS) {
messageContext.ovmADDRESS = _nextMessageContext.ovmADDRESS;
}
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.isStatic != _nextMessageContext.isStatic) {
messageContext.isStatic = _nextMessageContext.isStatic;
}
if (_prevMessageContext.ovmCALLVALUE != _nextMessageContext.ovmCALLVALUE) {
messageContext.ovmCALLVALUE = _nextMessageContext.ovmCALLVALUE;
}
}
/**
......@@ -1877,6 +2110,52 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
ovmStateManager = iOVM_StateManager(address(0));
}
/******************************************
* Internal Functions: Message Typechecks *
******************************************/
/**
* Returns whether or not the given message type is a CREATE-type.
* @param _messageType the message type in question.
*/
function _isCreateType(
MessageType _messageType
)
internal
pure
returns(
bool
)
{
return (
_messageType == MessageType.ovmCREATE
|| _messageType == MessageType.ovmCREATE2
);
}
/**
* Returns whether or not the given message type (potentially) requires the transfer of ETH value along with the message.
* @param _messageType the message type in question.
*/
function _isValueType(
MessageType _messageType
)
internal
pure
returns(
bool
)
{
// ovmSTATICCALL and ovmDELEGATECALL types do not accept or transfer value.
return (
_messageType == MessageType.ovmCALL
|| _messageType == MessageType.ovmCREATE
|| _messageType == MessageType.ovmCREATE2
);
}
/*****************************
* L2-only Helper Functions *
*****************************/
......@@ -1886,10 +2165,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
* This function will throw an exception in all cases other than when used as a custom entrypoint in L2 Geth to simulate eth_call.
* @param _transaction the message transaction to simulate.
* @param _from the OVM account the simulated call should be from.
* @param _value the amount of ETH value to send.
* @param _ovmStateManager the address of the OVM_StateManager precompile in the L2 state.
*/
function simulateMessage(
Lib_OVMCodec.Transaction memory _transaction,
address _from,
uint256 _value,
iOVM_StateManager _ovmStateManager
)
external
......@@ -1900,12 +2182,15 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
// Prevent this call from having any effect unless in a custom-set VM frame
require(msg.sender == address(0));
// Initialize the EM's internal state, ignoring nuisance gas.
ovmStateManager = _ovmStateManager;
_initContext(_transaction);
messageRecord.nuisanceGasLeft = uint(-1);
// Set the ovmADDRESS to the _from so that the subsequent call frame "comes from" them.
messageContext.ovmADDRESS = _from;
// Execute the desired message.
bool isCreate = _transaction.entrypoint == address(0);
if (isCreate) {
(address created, bytes memory revertData) = ovmCREATE(_transaction.data);
......@@ -1920,6 +2205,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(bool success, bytes memory returndata) = ovmCALL(
_transaction.gasLimit,
_transaction.entrypoint,
_value,
_transaction.data
);
return abi.encode(success, returndata);
......
......@@ -22,6 +22,7 @@ contract OVM_ExecutionManagerWrapper {
fallback()
external
payable
{
bytes memory data = msg.data;
assembly {
......
......@@ -29,6 +29,14 @@ interface iOVM_ExecutionManager {
PREV_EPOCH_L1TOL2_QUEUE_GAS
}
enum MessageType {
ovmCALL,
ovmSTATICCALL,
ovmDELEGATECALL,
ovmCREATE,
ovmCREATE2
}
/***********
* Structs *
***********/
......@@ -60,6 +68,7 @@ interface iOVM_ExecutionManager {
struct MessageContext {
address ovmCALLER;
address ovmADDRESS;
uint256 ovmCALLVALUE;
bool isStatic;
}
......@@ -84,6 +93,7 @@ interface iOVM_ExecutionManager {
function ovmCALLER() external view returns (address _caller);
function ovmADDRESS() external view returns (address _address);
function ovmCALLVALUE() external view returns (uint _callValue);
function ovmTIMESTAMP() external view returns (uint256 _timestamp);
function ovmNUMBER() external view returns (uint256 _number);
function ovmGASLIMIT() external view returns (uint256 _gasLimit);
......@@ -126,7 +136,9 @@ interface iOVM_ExecutionManager {
* Contract Calling Opcodes *
****************************/
// Valueless ovmCALL for maintaining backwards compatibility with legacy OVM bytecode.
function ovmCALL(uint256 _gasLimit, address _address, bytes memory _calldata) external returns (bool _success, bytes memory _returndata);
function ovmCALL(uint256 _gasLimit, address _address, uint256 _value, bytes memory _calldata) external returns (bool _success, bytes memory _returndata);
function ovmSTATICCALL(uint256 _gasLimit, address _address, bytes memory _calldata) external returns (bool _success, bytes memory _returndata);
function ovmDELEGATECALL(uint256 _gasLimit, address _address, bytes memory _calldata) external returns (bool _success, bytes memory _returndata);
......@@ -148,6 +160,14 @@ interface iOVM_ExecutionManager {
function ovmEXTCODEHASH(address _contract) external returns (bytes32 _hash);
/*********************
* ETH Value Opcodes *
*********************/
function ovmBALANCE(address _contract) external returns (uint256 _balance);
function ovmSELFBALANCE() external returns (uint256 _balance);
/***************************************
* Public Functions: Execution Context *
***************************************/
......
......@@ -158,6 +158,82 @@ library Lib_ExecutionManagerWrapper {
return abi.decode(returndata, (address));
}
/**
* Calls the value-enabled ovmCALL opcode.
* @param _gasLimit Amount of gas to be passed into this call.
* @param _address Address of the contract to call.
* @param _value ETH value to pass with the call.
* @param _calldata Data to send along with the call.
* @return _success Whether or not the call returned (rather than reverted).
* @return _returndata Data returned by the call.
*/
function ovmCALL(
uint256 _gasLimit,
address _address,
uint256 _value,
bytes memory _calldata
)
internal
returns (
bool,
bytes memory
)
{
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmCALL(uint256,address,uint256,bytes)",
_gasLimit,
_address,
_value,
_calldata
)
);
return abi.decode(returndata, (bool, bytes));
}
/**
* Calls the ovmBALANCE opcode.
* @param _address OVM account to query the balance of.
* @return Balance of the account.
*/
function ovmBALANCE(
address _address
)
internal
returns (
uint256
)
{
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmBALANCE(address)",
_address
)
);
return abi.decode(returndata, (uint256));
}
/**
* Calls the ovmCALLVALUE opcode.
* @return Value of the current call frame.
*/
function ovmCALLVALUE()
internal
returns (
uint256
)
{
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmCALLVALUE()"
)
);
return abi.decode(returndata, (uint256));
}
/*********************
* Private Functions *
......
......@@ -53,7 +53,7 @@ const config: HardhatUserConfig = {
},
},
ovm: {
solcVersion: '0.7.6',
solcVersion: '0.7.6+commit.3b061308',
},
typechain: {
outDir: 'dist/types',
......
......@@ -49,7 +49,18 @@ describe('OVM_ECDSAContractAccount', () => {
Mock__OVM_ETH.smocked.transfer.will.return.with(true)
})
describe('fallback()', () => {
describe('fallback', async () => {
it('should successfully accept value sent to it', async () => {
await expect(
wallet.sendTransaction({
to: OVM_ECDSAContractAccount.address,
value: 1,
})
).to.not.be.reverted
})
})
describe('execute()', () => {
it(`should successfully execute an EIP155Transaction`, async () => {
const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction)
......@@ -132,37 +143,36 @@ describe('OVM_ECDSAContractAccount', () => {
})
it(`should transfer value if value is greater than 0`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, data: '0x' }
const value = 100
const valueRecipient = '0x' + '34'.repeat(20)
const transaction = {
...DEFAULT_EIP155_TX,
to: valueRecipient,
value,
data: '0x',
}
const encodedTransaction = await wallet.signTransaction(transaction)
// fund the contract account
await wallet.sendTransaction({
to: OVM_ECDSAContractAccount.address,
value: value * 10,
gasLimit: 1_000_000,
})
const receipientBalanceBefore = await wallet.provider.getBalance(
valueRecipient
)
await OVM_ECDSAContractAccount.execute(
LibEIP155TxStruct(encodedTransaction)
)
const recipientBalanceAfter = await wallet.provider.getBalance(
valueRecipient
)
// First call transfers fee, second transfers value (since value > 0).
expect(
toPlainObject(Mock__OVM_ETH.smocked.transfer.calls[1])
).to.deep.include({
to: transaction.to,
value: BigNumber.from(transaction.value),
})
})
it(`should revert if the value is not transferred to the recipient`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, data: '0x' }
const encodedTransaction = await wallet.signTransaction(transaction)
Mock__OVM_ETH.smocked.transfer.will.return.with((to, value) => {
if (to === transaction.to) {
return false
} else {
return true
}
})
await expect(
OVM_ECDSAContractAccount.execute(LibEIP155TxStruct(encodedTransaction))
).to.be.revertedWith('Value could not be transferred to recipient.')
recipientBalanceAfter.sub(receipientBalanceBefore).toNumber()
).to.eq(value)
})
it(`should revert if trying to send value with a contract creation`, async () => {
......@@ -174,15 +184,6 @@ describe('OVM_ECDSAContractAccount', () => {
).to.be.revertedWith('Value transfer in contract creation not supported.')
})
it(`should revert if trying to send value with non-empty transaction data`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, data: '0x1234' }
const encodedTransaction = await wallet.signTransaction(transaction)
await expect(
OVM_ECDSAContractAccount.execute(LibEIP155TxStruct(encodedTransaction))
).to.be.revertedWith('Value is nonzero but input data was provided.')
})
// NOTE: Upgrades are disabled for now but will be re-enabled at a later point in time. See
// comment in OVM_ECDSAContractAccount.sol for additional information.
it(`should revert if trying call itself`, async () => {
......
......@@ -7,7 +7,7 @@ import { MockContract, smockit } from '@eth-optimism/smock'
import { toPlainObject } from 'lodash'
/* Internal Imports */
import { getContractInterface, predeploys } from '../../../../src'
import { predeploys } from '../../../../src'
import { DEFAULT_EIP155_TX, LibEIP155TxStruct } from '../../../helpers'
describe('OVM_ProxyEOA', () => {
......
......@@ -110,7 +110,7 @@ describe('OVM_ExecutionManager gas consumption', () => {
)
console.log(`calculated gas cost of ${gasCost}`)
const benchmark: number = 106_000
const benchmark: number = 110_000
expect(gasCost).to.be.lte(benchmark)
expect(gasCost).to.be.gte(
benchmark - 1_000,
......
/* Internal Imports */
import { remove0x, toHexString } from '@eth-optimism/core-utils'
import { ethers } from 'ethers'
import { predeploys } from '../../../../../src'
import {
ExecutionManagerTestRunner,
TestDefinition,
OVM_TX_GAS_LIMIT,
NON_NULL_BYTES32,
REVERT_FLAGS,
VERIFIED_EMPTY_CONTRACT_HASH,
} from '../../../../helpers'
const uniswapERC20BalanceOfStorageLayoutKey =
'0000000000000000000000000000000000000000000000000000000000000005'
// TODO: use fancy chugsplash storage getter once possible
const getOvmEthBalanceSlot = (addressOrPlaceholder: string): string => {
let address: string
if (addressOrPlaceholder.startsWith('$DUMMY_OVM_ADDRESS_')) {
address = ExecutionManagerTestRunner.getDummyAddress(addressOrPlaceholder)
} else {
address = addressOrPlaceholder
}
const balanceOfSlotPreimage =
ethers.utils.hexZeroPad(address, 32) + uniswapERC20BalanceOfStorageLayoutKey
const balanceOfSlot = ethers.utils.keccak256(balanceOfSlotPreimage)
return balanceOfSlot
}
const INITIAL_BALANCE = 1234
const CALL_VALUE = 69
const test_nativeETH: TestDefinition = {
name: 'Basic tests for ovmCALL',
preState: {
ExecutionManager: {
ovmStateManager: '$OVM_STATE_MANAGER',
ovmSafetyChecker: '$OVM_SAFETY_CHECKER',
messageRecord: {
nuisanceGasLeft: OVM_TX_GAS_LIMIT,
},
},
StateManager: {
owner: '$OVM_EXECUTION_MANAGER',
accounts: {
$DUMMY_OVM_ADDRESS_1: {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_CALL_HELPER',
},
$DUMMY_OVM_ADDRESS_2: {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_CALL_HELPER',
},
$DUMMY_OVM_ADDRESS_3: {
codeHash: VERIFIED_EMPTY_CONTRACT_HASH,
ethAddress: '0x' + '00'.repeat(20),
},
},
contractStorage: {
[predeploys.OVM_ETH]: {
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_1')]: {
getStorageXOR: true,
value: toHexString(INITIAL_BALANCE),
},
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_2')]: {
getStorageXOR: true,
value: '0x00',
},
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_3')]: {
getStorageXOR: true,
value: '0x00',
},
},
},
verifiedContractStorage: {
[predeploys.OVM_ETH]: {
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_1')]: true,
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_2')]: true,
[getOvmEthBalanceSlot('$DUMMY_OVM_ADDRESS_3')]: true,
},
},
},
},
parameters: [
{
name: 'ovmCALL(ADDRESS_1) => ovmBALANCE(ADDRESS_1)',
steps: [
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_1',
subSteps: [
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE,
},
],
},
expectedReturnStatus: true,
},
],
},
{
name: 'ovmCALL(ADDRESS_1) => ovmCALL(EMPTY_ACCOUNT, value)',
steps: [
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_1',
subSteps: [
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_3',
value: CALL_VALUE,
calldata: '0x',
},
expectedReturnStatus: true,
},
// Check balances are still applied:
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE - CALL_VALUE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_3',
},
expectedReturnStatus: true,
expectedReturnValue: CALL_VALUE,
},
],
},
expectedReturnStatus: true,
},
],
},
{
name: 'ovmCALL(ADDRESS_1) => ovmCALL(ADDRESS_2, value) [successful call]',
steps: [
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_1',
subSteps: [
// expected initial balances:
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: 0,
},
// do the call with some value
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_2',
value: CALL_VALUE,
subSteps: [
// check that the ovmCALLVALUE is updated
{
functionName: 'ovmCALLVALUE',
expectedReturnValue: CALL_VALUE,
},
// check that the balances have been updated
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE - CALL_VALUE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: CALL_VALUE,
},
],
},
expectedReturnStatus: true,
},
// check that the ovmCALLVALUE is reset back to 0
{
functionName: 'ovmCALLVALUE',
expectedReturnValue: 0,
},
// check that the balances have persisted
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE - CALL_VALUE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: CALL_VALUE,
},
],
},
expectedReturnStatus: true,
},
],
},
{
name: 'ovmCALL(ADDRESS_1) => ovmCALL(ADDRESS_2, value) [reverting call]',
steps: [
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_1',
subSteps: [
// expected initial balances:
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: 0,
},
// do the call with some value
{
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
target: '$DUMMY_OVM_ADDRESS_2',
value: CALL_VALUE,
subSteps: [
// check that the ovmCALLVALUE is updated
{
functionName: 'ovmCALLVALUE',
expectedReturnValue: CALL_VALUE,
},
// check that the balances have been updated
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE - CALL_VALUE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: CALL_VALUE,
},
// now revert everything
{
functionName: 'ovmREVERT',
expectedReturnStatus: false,
expectedReturnValue: {
flag: REVERT_FLAGS.INTENTIONAL_REVERT,
onlyValidateFlag: true,
},
},
],
},
expectedReturnStatus: true,
expectedReturnValue: {
ovmSuccess: false,
returnData: '0x',
},
},
// check that the ovmCALLVALUE is reset back to 0
{
functionName: 'ovmCALLVALUE',
expectedReturnValue: 0,
},
// check that the balances have NOT persisted
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_1',
},
expectedReturnStatus: true,
expectedReturnValue: INITIAL_BALANCE,
},
{
functionName: 'ovmBALANCE',
functionParams: {
address: '$DUMMY_OVM_ADDRESS_2',
},
expectedReturnStatus: true,
expectedReturnValue: 0,
},
],
},
expectedReturnStatus: true,
},
],
},
],
}
const runner = new ExecutionManagerTestRunner()
runner.run(test_nativeETH)
......@@ -12,12 +12,12 @@ import {
ParsedTestStep,
TestParameter,
TestStep,
TestStep_CALL,
TestStep_CALLType,
TestStep_Run,
isRevertFlagError,
isTestStep_SSTORE,
isTestStep_SLOAD,
isTestStep_CALL,
isTestStep_CALLType,
isTestStep_CREATE,
isTestStep_CREATE2,
isTestStep_CREATEEOA,
......@@ -27,7 +27,9 @@ import {
isTestStep_EXTCODESIZE,
isTestStep_EXTCODEHASH,
isTestStep_EXTCODECOPY,
isTestStep_BALANCE,
isTestStep_REVERT,
isTestStep_CALL,
} from './test.types'
import { encodeRevertData, REVERT_FLAGS } from '../codec'
import {
......@@ -49,6 +51,7 @@ export class ExecutionManagerTestRunner {
Factory__Helper_TestRunner_CREATE: ContractFactory
OVM_DeployerWhitelist: Contract
OVM_ProxyEOA: Contract
OVM_ETH: Contract
} = {
OVM_SafetyChecker: undefined,
OVM_StateManager: undefined,
......@@ -57,6 +60,7 @@ export class ExecutionManagerTestRunner {
Factory__Helper_TestRunner_CREATE: undefined,
OVM_DeployerWhitelist: undefined,
OVM_ProxyEOA: undefined,
OVM_ETH: undefined,
}
// Default pre-state with contract deployer whitelist NOT initialized.
......@@ -68,6 +72,10 @@ export class ExecutionManagerTestRunner {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_DEPLOYER_WHITELIST',
},
[predeploys.OVM_ETH]: {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_ETH',
},
[predeploys.OVM_ProxyEOA]: {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_PROXY_EOA',
......@@ -227,6 +235,14 @@ export class ExecutionManagerTestRunner {
this.contracts.OVM_DeployerWhitelist = DeployerWhitelist
const OvmEth = await getContractFactory(
'OVM_ETH',
AddressManager.signer,
true
).deploy(ethers.constants.AddressZero, ethers.constants.AddressZero)
this.contracts.OVM_ETH = OvmEth
this.contracts.OVM_ProxyEOA = await getContractFactory(
'OVM_ProxyEOA',
AddressManager.signer,
......@@ -282,6 +298,8 @@ export class ExecutionManagerTestRunner {
return this.contracts.Helper_TestRunner.address
} else if (kv === '$OVM_DEPLOYER_WHITELIST') {
return this.contracts.OVM_DeployerWhitelist.address
} else if (kv === '$OVM_ETH') {
return this.contracts.OVM_ETH.address
} else if (kv === '$OVM_PROXY_EOA') {
return this.contracts.OVM_ProxyEOA.address
} else if (kv.startsWith('$DUMMY_OVM_ADDRESS_')) {
......@@ -328,7 +346,7 @@ export class ExecutionManagerTestRunner {
if (step.functionParams.data) {
calldata = step.functionParams.data
} else {
const runStep: TestStep_CALL = {
const runStep: TestStep_CALLType = {
functionName: 'ovmCALL',
functionParams: {
gasLimit: OVM_TX_GAS_LIMIT,
......@@ -362,9 +380,12 @@ export class ExecutionManagerTestRunner {
await toRun
}
} else {
await this.contracts.OVM_ExecutionManager.ovmCALL(
await this.contracts.OVM_ExecutionManager[
'ovmCALL(uint256,address,uint256,bytes)'
](
OVM_TX_GAS_LIMIT,
ExecutionManagerTestRunner.getDummyAddress('$DUMMY_OVM_ADDRESS_1'),
0,
this.contracts.Helper_TestRunner.interface.encodeFunctionData(
'runSingleTestStep',
[this.parseTestStep(step)]
......@@ -398,7 +419,7 @@ export class ExecutionManagerTestRunner {
return false
} else if (isTestStep_Context(step)) {
return true
} else if (isTestStep_CALL(step)) {
} else if (isTestStep_CALLType(step)) {
if (
isRevertFlagError(step.expectedReturnValue) &&
(step.expectedReturnValue.flag === REVERT_FLAGS.INVALID_STATE_ACCESS ||
......@@ -435,13 +456,12 @@ export class ExecutionManagerTestRunner {
isTestStep_EXTCODESIZE(step) ||
isTestStep_EXTCODEHASH(step) ||
isTestStep_EXTCODECOPY(step) ||
isTestStep_BALANCE(step) ||
isTestStep_CREATEEOA(step)
) {
functionParams = Object.values(step.functionParams)
} else if (isTestStep_CALL(step)) {
functionParams = [
step.functionParams.gasLimit,
step.functionParams.target,
} else if (isTestStep_CALLType(step)) {
const innnerCalldata =
step.functionParams.calldata ||
this.contracts.Helper_TestRunner.interface.encodeFunctionData(
'runMultipleTestSteps',
......@@ -450,8 +470,22 @@ export class ExecutionManagerTestRunner {
return this.parseTestStep(subStep)
}),
]
),
)
// only ovmCALL accepts a value parameter.
if (isTestStep_CALL(step)) {
functionParams = [
step.functionParams.gasLimit,
step.functionParams.target,
step.functionParams.value || 0,
innnerCalldata,
]
} else {
functionParams = [
step.functionParams.gasLimit,
step.functionParams.target,
innnerCalldata,
]
}
} else if (isTestStep_CREATE(step)) {
functionParams = [
this.contracts.Factory__Helper_TestRunner_CREATE.getDeployTransaction(
......@@ -475,8 +509,16 @@ export class ExecutionManagerTestRunner {
functionParams = [step.revertData || '0x']
}
// legacy ovmCALL causes multiple matching functions without the full signature
let functionName
if (step.functionName === 'ovmCALL') {
functionName = 'ovmCALL(uint256,address,uint256,bytes)'
} else {
functionName = step.functionName
}
return this.contracts.OVM_ExecutionManager.interface.encodeFunctionData(
step.functionName,
functionName,
functionParams
)
}
......@@ -500,7 +542,7 @@ export class ExecutionManagerTestRunner {
}
let returnData: any[] = []
if (isTestStep_CALL(step)) {
if (isTestStep_CALLType(step)) {
if (step.expectedReturnValue === '0x00') {
return step.expectedReturnValue
} else if (
......@@ -540,8 +582,16 @@ export class ExecutionManagerTestRunner {
}
}
// legacy ovmCALL causes multiple matching functions without the full signature
let functionName
if (step.functionName === 'ovmCALL') {
functionName = 'ovmCALL(uint256,address,uint256,bytes)'
} else {
functionName = step.functionName
}
return this.contracts.OVM_ExecutionManager.interface.encodeFunctionResult(
step.functionName,
functionName,
returnData
)
}
......
......@@ -11,6 +11,7 @@ export type ContextOpcode =
| 'ovmGASLIMIT'
| 'ovmCHAINID'
| 'ovmGETNONCE'
| 'ovmCALLVALUE'
type CallOpcode = 'ovmCALL' | 'ovmSTATICCALL' | 'ovmDELEGATECALL'
......@@ -68,6 +69,15 @@ interface TestStep_EXTCODECOPY {
expectedReturnValue: string | RevertFlagError
}
interface TestStep_BALANCE {
functionName: 'ovmBALANCE'
functionParams: {
address: string
}
expectedReturnStatus: boolean
expectedReturnValue: number | RevertFlagError
}
interface TestStep_SSTORE {
functionName: 'ovmSSTORE'
functionParams: {
......@@ -93,11 +103,12 @@ interface TestStep_INCREMENTNONCE {
expectedReturnValue?: RevertFlagError
}
export interface TestStep_CALL {
export interface TestStep_CALLType {
functionName: CallOpcode
functionParams: {
gasLimit: number | BigNumber
target: string
value?: number | BigNumber
calldata?: string
subSteps?: TestStep[]
}
......@@ -174,13 +185,14 @@ export type TestStep =
| TestStep_SSTORE
| TestStep_SLOAD
| TestStep_INCREMENTNONCE
| TestStep_CALL
| TestStep_CALLType
| TestStep_CREATE
| TestStep_CREATE2
| TestStep_CREATEEOA
| TestStep_EXTCODESIZE
| TestStep_EXTCODEHASH
| TestStep_EXTCODECOPY
| TestStep_BALANCE
| TestStep_REVERT
| TestStep_evm
......@@ -220,6 +232,7 @@ export const isTestStep_Context = (
'ovmCHAINID',
'ovmL1QUEUEORIGIN',
'ovmGETNONCE',
'ovmCALLVALUE',
].includes(step.functionName)
}
......@@ -255,16 +268,28 @@ export const isTestStep_EXTCODECOPY = (
return step.functionName === 'ovmEXTCODECOPY'
}
export const isTestStep_BALANCE = (
step: TestStep
): step is TestStep_BALANCE => {
return step.functionName === 'ovmBALANCE'
}
export const isTestStep_REVERT = (step: TestStep): step is TestStep_REVERT => {
return step.functionName === 'ovmREVERT'
}
export const isTestStep_CALL = (step: TestStep): step is TestStep_CALL => {
export const isTestStep_CALLType = (
step: TestStep
): step is TestStep_CALLType => {
return ['ovmCALL', 'ovmSTATICCALL', 'ovmDELEGATECALL'].includes(
step.functionName
)
}
export const isTestStep_CALL = (step: TestStep): boolean => {
return step.functionName === 'ovmCALL'
}
export const isTestStep_CREATE = (step: TestStep): step is TestStep_CREATE => {
return step.functionName === 'ovmCREATE'
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment