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 @@ ...@@ -2,10 +2,16 @@
pragma solidity >=0.7.0; pragma solidity >=0.7.0;
contract TestOOG { contract TestOOG {
constructor() { function runOutOfGas() public {
bytes32 h; bytes32 h;
for (uint256 i = 0; i < 100000; i++) { for (uint256 i = 0; i < 100000; i++) {
h = keccak256(abi.encodePacked(h)); 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 = { ...@@ -20,7 +20,7 @@ const config: HardhatUserConfig = {
}, },
solidity: '0.7.6', solidity: '0.7.6',
ovm: { ovm: {
solcVersion: '0.7.6', solcVersion: '0.7.6+commit.3b061308',
}, },
gasReporter: { gasReporter: {
enabled: enableGasReport, 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' ...@@ -5,8 +5,8 @@ import { Direction } from './shared/watcher-utils'
import { PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils' import { PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils'
import { OptimismEnv } from './shared/env' import { OptimismEnv } from './shared/env'
const DEFAULT_TEST_GAS_L1 = 230_000 const DEFAULT_TEST_GAS_L1 = 330_000
const DEFAULT_TEST_GAS_L2 = 825_000 const DEFAULT_TEST_GAS_L2 = 1_000_000
// TX size enforced by CTC: // TX size enforced by CTC:
const MAX_ROLLUP_TX_SIZE = 50_000 const MAX_ROLLUP_TX_SIZE = 50_000
...@@ -50,13 +50,13 @@ describe('Native ETH Integration Tests', async () => { ...@@ -50,13 +50,13 @@ describe('Native ETH Integration Tests', async () => {
const amount = utils.parseEther('0.5') const amount = utils.parseEther('0.5')
const addr = '0x' + '1234'.repeat(10) const addr = '0x' + '1234'.repeat(10)
const gas = await env.ovmEth.estimateGas.transfer(addr, amount) 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 () => { it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.5') const amount = utils.parseEther('0.5')
const gas = await env.ovmEth.estimateGas.withdraw(amount, 0, '0xFFFF') 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 { ...@@ -4,10 +4,10 @@ import {
TxGasPrice, TxGasPrice,
toRpcHexString, toRpcHexString,
} from '@eth-optimism/core-utils' } from '@eth-optimism/core-utils'
import { Wallet, BigNumber, Contract } from 'ethers' import { Wallet, BigNumber, Contract, ContractFactory } from 'ethers'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import chai, { expect } from 'chai' 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 chaiAsPromised from 'chai-as-promised'
import { OptimismEnv } from './shared/env' import { OptimismEnv } from './shared/env'
import { import {
...@@ -154,7 +154,7 @@ describe('Basic RPC tests', () => { ...@@ -154,7 +154,7 @@ describe('Basic RPC tests', () => {
}) })
it('should correctly report OOG for contract creations', async () => { 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( await expect(factory.connect(wallet).deploy()).to.be.rejectedWith(
'gas required exceeds allowance' 'gas required exceeds allowance'
...@@ -207,6 +207,32 @@ describe('Basic RPC tests', () => { ...@@ -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?' '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', () => { describe('eth_getTransactionReceipt', () => {
...@@ -236,7 +262,7 @@ describe('Basic RPC tests', () => { ...@@ -236,7 +262,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract creations', async () => { it('correctly exposes revert data for contract creations', async () => {
const req: TransactionRequest = { const req: TransactionRequest = {
...revertingDeployTx, ...revertingDeployTx,
gasLimit: 17700899, // override gas estimation gasLimit: 27700899, // override gas estimation
} }
const tx = await wallet.sendTransaction(req) const tx = await wallet.sendTransaction(req)
...@@ -353,7 +379,7 @@ describe('Basic RPC tests', () => { ...@@ -353,7 +379,7 @@ describe('Basic RPC tests', () => {
to: DEFAULT_TRANSACTION.to, to: DEFAULT_TRANSACTION.to,
value: 0, 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 () => { 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 ...@@ -100,6 +100,11 @@ func EncodeSimulatedMessage(msg Message, timestamp, blockNumber *big.Int, execut
to = &common.Address{0} to = &common.Address{0}
} }
value := msg.Value()
if value == nil {
value = common.Big0
}
tx := ovmTransaction{ tx := ovmTransaction{
timestamp, timestamp,
blockNumber, blockNumber,
...@@ -114,6 +119,7 @@ func EncodeSimulatedMessage(msg Message, timestamp, blockNumber *big.Int, execut ...@@ -114,6 +119,7 @@ func EncodeSimulatedMessage(msg Message, timestamp, blockNumber *big.Int, execut
var args = []interface{}{ var args = []interface{}{
tx, tx,
from, from,
value,
stateManager.Address, stateManager.Address,
} }
......
...@@ -321,14 +321,6 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) ...@@ -321,14 +321,6 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction)
if len(signedTx.Data()) > b.MaxCallDataSize { if len(signedTx.Data()) > b.MaxCallDataSize {
return fmt.Errorf("Calldata cannot be larger than %d, sent %d", b.MaxCallDataSize, len(signedTx.Data())) 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) return b.eth.syncService.ValidateAndApplySequencerTransaction(signedTx)
} }
......
...@@ -47,6 +47,16 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount { ...@@ -47,6 +47,16 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
* Public Functions * * Public Functions *
********************/ ********************/
/**
* No-op fallback mirrors behavior of calling an EOA on L1.
*/
fallback()
external
payable
{
return;
}
/** /**
* Executes a signed transaction. * Executes a signed transaction.
* @param _transaction Signed EIP155 transaction. * @param _transaction Signed EIP155 transaction.
...@@ -121,34 +131,15 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount { ...@@ -121,34 +131,15 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
// cases, but since this is a contract we'd end up bumping the nonce twice. // cases, but since this is a contract we'd end up bumping the nonce twice.
Lib_ExecutionManagerWrapper.ovmINCREMENTNONCE(); Lib_ExecutionManagerWrapper.ovmINCREMENTNONCE();
// Value transfer currently only supported for CALL but not for CREATE. // NOTE: Upgrades are temporarily disabled because users can, in theory, modify their EOA
if (_transaction.value > 0) { // so that they don't have to pay any fees to the sequencer. Function will remain disabled
// TEMPORARY: Block value transfer if the transaction has input data. // until a robust solution is in place.
require( require(
_transaction.data.length == 0, _transaction.to != Lib_ExecutionManagerWrapper.ovmADDRESS(),
"Value is nonzero but input data was provided." "Calls to self are disabled until upgradability is re-enabled."
); );
require( return _transaction.to.call{value: _transaction.value}(_transaction.data);
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.
require(
_transaction.to != Lib_ExecutionManagerWrapper.ovmADDRESS(),
"Calls to self are disabled until upgradability is re-enabled."
);
return _transaction.to.call(_transaction.data);
}
} }
} }
} }
...@@ -39,6 +39,7 @@ contract OVM_ProxyEOA { ...@@ -39,6 +39,7 @@ contract OVM_ProxyEOA {
fallback() fallback()
external external
payable
{ {
(bool success, bytes memory returndata) = getImplementation().delegatecall(msg.data); (bool success, bytes memory returndata) = getImplementation().delegatecall(msg.data);
......
...@@ -6,6 +6,7 @@ pragma experimental ABIEncoderV2; ...@@ -6,6 +6,7 @@ pragma experimental ABIEncoderV2;
/* Library Imports */ /* Library Imports */
import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol"; import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol";
import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.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_EthUtils } from "../../libraries/utils/Lib_EthUtils.sol";
import { Lib_ErrorUtils } from "../../libraries/utils/Lib_ErrorUtils.sol"; import { Lib_ErrorUtils } from "../../libraries/utils/Lib_ErrorUtils.sol";
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol"; import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
...@@ -14,10 +15,14 @@ import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployA ...@@ -14,10 +15,14 @@ import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployA
import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManager.sol"; import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManager.sol";
import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol"; import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol";
import { iOVM_SafetyChecker } from "../../iOVM/execution/iOVM_SafetyChecker.sol"; import { iOVM_SafetyChecker } from "../../iOVM/execution/iOVM_SafetyChecker.sol";
import { IUniswapV2ERC20 } from "../../libraries/standards/IUniswapV2ERC20.sol";
/* Contract Imports */ /* Contract Imports */
import { OVM_DeployerWhitelist } from "../predeploys/OVM_DeployerWhitelist.sol"; import { OVM_DeployerWhitelist } from "../predeploys/OVM_DeployerWhitelist.sol";
/* External Imports */
import { Math } from "@openzeppelin/contracts/math/Math.sol";
/** /**
* @title OVM_ExecutionManager * @title OVM_ExecutionManager
* @dev The Execution Manager (EM) is the core of our OVM implementation, and provides a sandboxed * @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 { ...@@ -66,6 +71,15 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
uint256 constant NUISANCE_GAS_PER_CONTRACT_BYTE = 100; uint256 constant NUISANCE_GAS_PER_CONTRACT_BYTE = 100;
uint256 constant MIN_GAS_FOR_INVALID_STATE_ACCESS = 30000; 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 * * Default Context Values *
**************************/ **************************/
...@@ -220,6 +234,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -220,6 +234,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(, bytes memory returndata) = ovmCALL( (, bytes memory returndata) = ovmCALL(
_transaction.gasLimit - gasMeterConfig.minTransactionGasLimit, _transaction.gasLimit - gasMeterConfig.minTransactionGasLimit,
_transaction.entrypoint, _transaction.entrypoint,
0,
_transaction.data _transaction.data
); );
...@@ -269,6 +284,21 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -269,6 +284,21 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return messageContext.ovmADDRESS; 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. * @notice Overrides TIMESTAMP.
* @return _TIMESTAMP Value of the TIMESTAMP within the transaction context. * @return _TIMESTAMP Value of the TIMESTAMP within the transaction context.
...@@ -418,7 +448,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -418,7 +448,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return _createContract( return _createContract(
contractAddress, contractAddress,
_bytecode _bytecode,
MessageType.ovmCREATE
); );
} }
...@@ -458,7 +489,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -458,7 +489,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
return _createContract( return _createContract(
contractAddress, contractAddress,
_bytecode _bytecode,
MessageType.ovmCREATE2
); );
} }
...@@ -591,6 +623,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -591,6 +623,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
* @notice Overrides CALL. * @notice Overrides CALL.
* @param _gasLimit Amount of gas to be passed into this call. * @param _gasLimit Amount of gas to be passed into this call.
* @param _address Address of the contract to 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. * @param _calldata Data to send along with the call.
* @return _success Whether or not the call returned (rather than reverted). * @return _success Whether or not the call returned (rather than reverted).
* @return _returndata Data returned by the call. * @return _returndata Data returned by the call.
...@@ -598,6 +631,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -598,6 +631,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
function ovmCALL( function ovmCALL(
uint256 _gasLimit, uint256 _gasLimit,
address _address, address _address,
uint256 _value,
bytes memory _calldata bytes memory _calldata
) )
override override
...@@ -612,12 +646,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -612,12 +646,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
MessageContext memory nextMessageContext = messageContext; MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS; nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address; nextMessageContext.ovmADDRESS = _address;
nextMessageContext.ovmCALLVALUE = _value;
return _callContract( return _callContract(
nextMessageContext, nextMessageContext,
_gasLimit, _gasLimit,
_address, _address,
_calldata _calldata,
MessageType.ovmCALL
); );
} }
...@@ -635,24 +671,26 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -635,24 +671,26 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory _calldata bytes memory _calldata
) )
override override
external public
fixedGasDiscount(80000) fixedGasDiscount(80000)
returns ( returns (
bool _success, bool _success,
bytes memory _returndata 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; MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS; nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address; nextMessageContext.ovmADDRESS = _address;
nextMessageContext.isStatic = true; nextMessageContext.isStatic = true;
nextMessageContext.ovmCALLVALUE = 0;
return _callContract( return _callContract(
nextMessageContext, nextMessageContext,
_gasLimit, _gasLimit,
_address, _address,
_calldata _calldata,
MessageType.ovmSTATICCALL
); );
} }
...@@ -670,7 +708,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -670,7 +708,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory _calldata bytes memory _calldata
) )
override override
external public
fixedGasDiscount(40000) fixedGasDiscount(40000)
returns ( returns (
bool _success, bool _success,
...@@ -684,6 +722,36 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -684,6 +722,36 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
nextMessageContext, nextMessageContext,
_gasLimit, _gasLimit,
_address, _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 _calldata
); );
} }
...@@ -809,6 +877,63 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -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 * * Public Functions: Execution Context *
***************************************/ ***************************************/
...@@ -839,10 +964,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -839,10 +964,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
{ {
// From an OVM semantics perspective, this will appear identical to // 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. // 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(), gasleft(),
Lib_PredeployAddresses.DEPLOYER_WHITELIST, Lib_PredeployAddresses.DEPLOYER_WHITELIST,
abi.encodeWithSignature("isDeployerAllowed(address)", _deployerAddress) abi.encodeWithSelector(
OVM_DeployerWhitelist.isDeployerAllowed.selector,
_deployerAddress
)
); );
bool isAllowed = abi.decode(data, (bool)); bool isAllowed = abi.decode(data, (bool));
...@@ -864,7 +992,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -864,7 +992,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
*/ */
function _createContract( function _createContract(
address _contractAddress, address _contractAddress,
bytes memory _bytecode bytes memory _bytecode,
MessageType _messageType
) )
internal internal
returns ( returns (
...@@ -888,7 +1017,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -888,7 +1017,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
gasleft(), gasleft(),
_contractAddress, _contractAddress,
_bytecode, _bytecode,
true _messageType
); );
// Yellow paper requires that address returned is zero if the contract deployment fails. // 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 { ...@@ -911,7 +1040,8 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
MessageContext memory _nextMessageContext, MessageContext memory _nextMessageContext,
uint256 _gasLimit, uint256 _gasLimit,
address _contract, address _contract,
bytes memory _calldata bytes memory _calldata,
MessageType _messageType
) )
internal internal
returns ( returns (
...@@ -940,7 +1070,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -940,7 +1070,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
_gasLimit, _gasLimit,
codeContractAddress, codeContractAddress,
_calldata, _calldata,
false _messageType
); );
} }
...@@ -949,19 +1079,20 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -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. * 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 _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 _contract OVM address being called or deployed to
* @param _data Data for the message (either calldata or creation code) * @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 Whether or not the message (either a call or deployment) succeeded.
* @return Data returned by the message. * @return Data returned by the message.
*/ */
function _handleExternalMessage( function _handleExternalMessage(
MessageContext memory _nextMessageContext, MessageContext memory _nextMessageContext,
// NOTE: this argument is overwritten in some cases to avoid stack-too-deep.
uint256 _gasLimit, uint256 _gasLimit,
address _contract, address _contract,
bytes memory _data, bytes memory _data,
bool _isCreate MessageType _messageType
) )
internal internal
returns ( returns (
...@@ -969,6 +1100,43 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -969,6 +1100,43 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bytes memory 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. // We need to switch over to our next message context for the duration of this call.
MessageContext memory prevMessageContext = messageContext; MessageContext memory prevMessageContext = messageContext;
_switchMessageContext(prevMessageContext, _nextMessageContext); _switchMessageContext(prevMessageContext, _nextMessageContext);
...@@ -989,14 +1157,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -989,14 +1157,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bool success; bool success;
bytes memory returndata; bytes memory returndata;
if (_isCreate) { if (_isCreateType(_messageType)) {
// safeCREATE() is a function which replicates a CREATE message, but uses return values // 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 // 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. // to be shared between untrusted call and create call frames.
(success, returndata) = address(this).call( (success, returndata) = address(this).call{gas: _gasLimit}(
abi.encodeWithSelector( abi.encodeWithSelector(
this.safeCREATE.selector, this.safeCREATE.selector,
_gasLimit,
_data, _data,
_contract _contract
) )
...@@ -1005,7 +1172,29 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -1005,7 +1172,29 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(success, returndata) = _contract.call{gas: _gasLimit}(_data); (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); _switchMessageContext(_nextMessageContext, prevMessageContext);
// Assuming there were no reverts, the message record should be accurate here. We'll update // 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 { ...@@ -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 // 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 ( if (
flag == RevertFlag.INTENTIONAL_REVERT flag == RevertFlag.INTENTIONAL_REVERT
|| _isCreate || flag == RevertFlag.UNSAFE_BYTECODE
) { ) {
returndata = returndataFromFlag; returndata = returndataFromFlag;
} else { } else {
...@@ -1077,12 +1268,10 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -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 * 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. * 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 _creationCode Code to pass into CREATE for deployment.
* @param _address OVM address being deployed to. * @param _address OVM address being deployed to.
*/ */
function safeCREATE( function safeCREATE(
uint _gasLimit,
bytes memory _creationCode, bytes memory _creationCode,
address _address address _address
) )
...@@ -1098,13 +1287,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -1098,13 +1287,14 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
// Note: in the EVM, this case burns all allotted gas. For improved // Note: in the EVM, this case burns all allotted gas. For improved
// developer experience, we do return the remaining gas. // developer experience, we do return the remaining gas.
_revertWithFlag( _revertWithFlag(
RevertFlag.CREATE_COLLISION, RevertFlag.CREATE_COLLISION
Lib_ErrorUtils.encodeRevertString("A contract has already been deployed to this address")
); );
} }
// Check the creation bytecode against the OVM_SafetyChecker. // Check the creation bytecode against the OVM_SafetyChecker.
if (ovmSafetyChecker.isBytecodeSafe(_creationCode) == false) { 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( _revertWithFlag(
RevertFlag.UNSAFE_BYTECODE, RevertFlag.UNSAFE_BYTECODE,
Lib_ErrorUtils.encodeRevertString("Contract creation code contains unsafe opcodes. Did you use the right compiler or pass an unsafe constructor argument?") 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 { ...@@ -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 * * Internal Functions: State Manipulation *
******************************************/ ******************************************/
...@@ -1817,20 +2047,23 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -1817,20 +2047,23 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
) )
internal 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) { if (_prevMessageContext.ovmCALLER != _nextMessageContext.ovmCALLER) {
messageContext.ovmCALLER = _nextMessageContext.ovmCALLER; messageContext.ovmCALLER = _nextMessageContext.ovmCALLER;
} }
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.ovmADDRESS != _nextMessageContext.ovmADDRESS) { if (_prevMessageContext.ovmADDRESS != _nextMessageContext.ovmADDRESS) {
messageContext.ovmADDRESS = _nextMessageContext.ovmADDRESS; messageContext.ovmADDRESS = _nextMessageContext.ovmADDRESS;
} }
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.isStatic != _nextMessageContext.isStatic) { if (_prevMessageContext.isStatic != _nextMessageContext.isStatic) {
messageContext.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 { ...@@ -1877,6 +2110,52 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
ovmStateManager = iOVM_StateManager(address(0)); 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 * * L2-only Helper Functions *
*****************************/ *****************************/
...@@ -1886,10 +2165,13 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -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. * 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 _transaction the message transaction to simulate.
* @param _from the OVM account the simulated call should be from. * @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( function simulateMessage(
Lib_OVMCodec.Transaction memory _transaction, Lib_OVMCodec.Transaction memory _transaction,
address _from, address _from,
uint256 _value,
iOVM_StateManager _ovmStateManager iOVM_StateManager _ovmStateManager
) )
external external
...@@ -1900,12 +2182,15 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -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 // Prevent this call from having any effect unless in a custom-set VM frame
require(msg.sender == address(0)); require(msg.sender == address(0));
// Initialize the EM's internal state, ignoring nuisance gas.
ovmStateManager = _ovmStateManager; ovmStateManager = _ovmStateManager;
_initContext(_transaction); _initContext(_transaction);
messageRecord.nuisanceGasLeft = uint(-1); messageRecord.nuisanceGasLeft = uint(-1);
// Set the ovmADDRESS to the _from so that the subsequent call frame "comes from" them.
messageContext.ovmADDRESS = _from; messageContext.ovmADDRESS = _from;
// Execute the desired message.
bool isCreate = _transaction.entrypoint == address(0); bool isCreate = _transaction.entrypoint == address(0);
if (isCreate) { if (isCreate) {
(address created, bytes memory revertData) = ovmCREATE(_transaction.data); (address created, bytes memory revertData) = ovmCREATE(_transaction.data);
...@@ -1920,6 +2205,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -1920,6 +2205,7 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
(bool success, bytes memory returndata) = ovmCALL( (bool success, bytes memory returndata) = ovmCALL(
_transaction.gasLimit, _transaction.gasLimit,
_transaction.entrypoint, _transaction.entrypoint,
_value,
_transaction.data _transaction.data
); );
return abi.encode(success, returndata); return abi.encode(success, returndata);
......
...@@ -22,6 +22,7 @@ contract OVM_ExecutionManagerWrapper { ...@@ -22,6 +22,7 @@ contract OVM_ExecutionManagerWrapper {
fallback() fallback()
external external
payable
{ {
bytes memory data = msg.data; bytes memory data = msg.data;
assembly { assembly {
......
...@@ -29,6 +29,14 @@ interface iOVM_ExecutionManager { ...@@ -29,6 +29,14 @@ interface iOVM_ExecutionManager {
PREV_EPOCH_L1TOL2_QUEUE_GAS PREV_EPOCH_L1TOL2_QUEUE_GAS
} }
enum MessageType {
ovmCALL,
ovmSTATICCALL,
ovmDELEGATECALL,
ovmCREATE,
ovmCREATE2
}
/*********** /***********
* Structs * * Structs *
***********/ ***********/
...@@ -60,6 +68,7 @@ interface iOVM_ExecutionManager { ...@@ -60,6 +68,7 @@ interface iOVM_ExecutionManager {
struct MessageContext { struct MessageContext {
address ovmCALLER; address ovmCALLER;
address ovmADDRESS; address ovmADDRESS;
uint256 ovmCALLVALUE;
bool isStatic; bool isStatic;
} }
...@@ -84,6 +93,7 @@ interface iOVM_ExecutionManager { ...@@ -84,6 +93,7 @@ interface iOVM_ExecutionManager {
function ovmCALLER() external view returns (address _caller); function ovmCALLER() external view returns (address _caller);
function ovmADDRESS() external view returns (address _address); function ovmADDRESS() external view returns (address _address);
function ovmCALLVALUE() external view returns (uint _callValue);
function ovmTIMESTAMP() external view returns (uint256 _timestamp); function ovmTIMESTAMP() external view returns (uint256 _timestamp);
function ovmNUMBER() external view returns (uint256 _number); function ovmNUMBER() external view returns (uint256 _number);
function ovmGASLIMIT() external view returns (uint256 _gasLimit); function ovmGASLIMIT() external view returns (uint256 _gasLimit);
...@@ -126,7 +136,9 @@ interface iOVM_ExecutionManager { ...@@ -126,7 +136,9 @@ interface iOVM_ExecutionManager {
* Contract Calling Opcodes * * 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, 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 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); function ovmDELEGATECALL(uint256 _gasLimit, address _address, bytes memory _calldata) external returns (bool _success, bytes memory _returndata);
...@@ -148,6 +160,14 @@ interface iOVM_ExecutionManager { ...@@ -148,6 +160,14 @@ interface iOVM_ExecutionManager {
function ovmEXTCODEHASH(address _contract) external returns (bytes32 _hash); 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 * * Public Functions: Execution Context *
***************************************/ ***************************************/
......
...@@ -158,6 +158,82 @@ library Lib_ExecutionManagerWrapper { ...@@ -158,6 +158,82 @@ library Lib_ExecutionManagerWrapper {
return abi.decode(returndata, (address)); 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 * * Private Functions *
......
...@@ -53,7 +53,7 @@ const config: HardhatUserConfig = { ...@@ -53,7 +53,7 @@ const config: HardhatUserConfig = {
}, },
}, },
ovm: { ovm: {
solcVersion: '0.7.6', solcVersion: '0.7.6+commit.3b061308',
}, },
typechain: { typechain: {
outDir: 'dist/types', outDir: 'dist/types',
......
...@@ -49,7 +49,18 @@ describe('OVM_ECDSAContractAccount', () => { ...@@ -49,7 +49,18 @@ describe('OVM_ECDSAContractAccount', () => {
Mock__OVM_ETH.smocked.transfer.will.return.with(true) 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 () => { it(`should successfully execute an EIP155Transaction`, async () => {
const transaction = DEFAULT_EIP155_TX const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction) const encodedTransaction = await wallet.signTransaction(transaction)
...@@ -132,37 +143,36 @@ describe('OVM_ECDSAContractAccount', () => { ...@@ -132,37 +143,36 @@ describe('OVM_ECDSAContractAccount', () => {
}) })
it(`should transfer value if value is greater than 0`, async () => { 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) 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( await OVM_ECDSAContractAccount.execute(
LibEIP155TxStruct(encodedTransaction) LibEIP155TxStruct(encodedTransaction)
) )
const recipientBalanceAfter = await wallet.provider.getBalance(
valueRecipient
)
// First call transfers fee, second transfers value (since value > 0).
expect( expect(
toPlainObject(Mock__OVM_ETH.smocked.transfer.calls[1]) recipientBalanceAfter.sub(receipientBalanceBefore).toNumber()
).to.deep.include({ ).to.eq(value)
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.')
}) })
it(`should revert if trying to send value with a contract creation`, async () => { it(`should revert if trying to send value with a contract creation`, async () => {
...@@ -174,15 +184,6 @@ describe('OVM_ECDSAContractAccount', () => { ...@@ -174,15 +184,6 @@ describe('OVM_ECDSAContractAccount', () => {
).to.be.revertedWith('Value transfer in contract creation not supported.') ).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 // 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. // comment in OVM_ECDSAContractAccount.sol for additional information.
it(`should revert if trying call itself`, async () => { it(`should revert if trying call itself`, async () => {
......
...@@ -7,7 +7,7 @@ import { MockContract, smockit } from '@eth-optimism/smock' ...@@ -7,7 +7,7 @@ import { MockContract, smockit } from '@eth-optimism/smock'
import { toPlainObject } from 'lodash' import { toPlainObject } from 'lodash'
/* Internal Imports */ /* Internal Imports */
import { getContractInterface, predeploys } from '../../../../src' import { predeploys } from '../../../../src'
import { DEFAULT_EIP155_TX, LibEIP155TxStruct } from '../../../helpers' import { DEFAULT_EIP155_TX, LibEIP155TxStruct } from '../../../helpers'
describe('OVM_ProxyEOA', () => { describe('OVM_ProxyEOA', () => {
......
...@@ -110,7 +110,7 @@ describe('OVM_ExecutionManager gas consumption', () => { ...@@ -110,7 +110,7 @@ describe('OVM_ExecutionManager gas consumption', () => {
) )
console.log(`calculated gas cost of ${gasCost}`) 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.lte(benchmark)
expect(gasCost).to.be.gte( expect(gasCost).to.be.gte(
benchmark - 1_000, 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 { ...@@ -12,12 +12,12 @@ import {
ParsedTestStep, ParsedTestStep,
TestParameter, TestParameter,
TestStep, TestStep,
TestStep_CALL, TestStep_CALLType,
TestStep_Run, TestStep_Run,
isRevertFlagError, isRevertFlagError,
isTestStep_SSTORE, isTestStep_SSTORE,
isTestStep_SLOAD, isTestStep_SLOAD,
isTestStep_CALL, isTestStep_CALLType,
isTestStep_CREATE, isTestStep_CREATE,
isTestStep_CREATE2, isTestStep_CREATE2,
isTestStep_CREATEEOA, isTestStep_CREATEEOA,
...@@ -27,7 +27,9 @@ import { ...@@ -27,7 +27,9 @@ import {
isTestStep_EXTCODESIZE, isTestStep_EXTCODESIZE,
isTestStep_EXTCODEHASH, isTestStep_EXTCODEHASH,
isTestStep_EXTCODECOPY, isTestStep_EXTCODECOPY,
isTestStep_BALANCE,
isTestStep_REVERT, isTestStep_REVERT,
isTestStep_CALL,
} from './test.types' } from './test.types'
import { encodeRevertData, REVERT_FLAGS } from '../codec' import { encodeRevertData, REVERT_FLAGS } from '../codec'
import { import {
...@@ -49,6 +51,7 @@ export class ExecutionManagerTestRunner { ...@@ -49,6 +51,7 @@ export class ExecutionManagerTestRunner {
Factory__Helper_TestRunner_CREATE: ContractFactory Factory__Helper_TestRunner_CREATE: ContractFactory
OVM_DeployerWhitelist: Contract OVM_DeployerWhitelist: Contract
OVM_ProxyEOA: Contract OVM_ProxyEOA: Contract
OVM_ETH: Contract
} = { } = {
OVM_SafetyChecker: undefined, OVM_SafetyChecker: undefined,
OVM_StateManager: undefined, OVM_StateManager: undefined,
...@@ -57,6 +60,7 @@ export class ExecutionManagerTestRunner { ...@@ -57,6 +60,7 @@ export class ExecutionManagerTestRunner {
Factory__Helper_TestRunner_CREATE: undefined, Factory__Helper_TestRunner_CREATE: undefined,
OVM_DeployerWhitelist: undefined, OVM_DeployerWhitelist: undefined,
OVM_ProxyEOA: undefined, OVM_ProxyEOA: undefined,
OVM_ETH: undefined,
} }
// Default pre-state with contract deployer whitelist NOT initialized. // Default pre-state with contract deployer whitelist NOT initialized.
...@@ -68,6 +72,10 @@ export class ExecutionManagerTestRunner { ...@@ -68,6 +72,10 @@ export class ExecutionManagerTestRunner {
codeHash: NON_NULL_BYTES32, codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_DEPLOYER_WHITELIST', ethAddress: '$OVM_DEPLOYER_WHITELIST',
}, },
[predeploys.OVM_ETH]: {
codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_ETH',
},
[predeploys.OVM_ProxyEOA]: { [predeploys.OVM_ProxyEOA]: {
codeHash: NON_NULL_BYTES32, codeHash: NON_NULL_BYTES32,
ethAddress: '$OVM_PROXY_EOA', ethAddress: '$OVM_PROXY_EOA',
...@@ -227,6 +235,14 @@ export class ExecutionManagerTestRunner { ...@@ -227,6 +235,14 @@ export class ExecutionManagerTestRunner {
this.contracts.OVM_DeployerWhitelist = DeployerWhitelist 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( this.contracts.OVM_ProxyEOA = await getContractFactory(
'OVM_ProxyEOA', 'OVM_ProxyEOA',
AddressManager.signer, AddressManager.signer,
...@@ -282,6 +298,8 @@ export class ExecutionManagerTestRunner { ...@@ -282,6 +298,8 @@ export class ExecutionManagerTestRunner {
return this.contracts.Helper_TestRunner.address return this.contracts.Helper_TestRunner.address
} else if (kv === '$OVM_DEPLOYER_WHITELIST') { } else if (kv === '$OVM_DEPLOYER_WHITELIST') {
return this.contracts.OVM_DeployerWhitelist.address return this.contracts.OVM_DeployerWhitelist.address
} else if (kv === '$OVM_ETH') {
return this.contracts.OVM_ETH.address
} else if (kv === '$OVM_PROXY_EOA') { } else if (kv === '$OVM_PROXY_EOA') {
return this.contracts.OVM_ProxyEOA.address return this.contracts.OVM_ProxyEOA.address
} else if (kv.startsWith('$DUMMY_OVM_ADDRESS_')) { } else if (kv.startsWith('$DUMMY_OVM_ADDRESS_')) {
...@@ -328,7 +346,7 @@ export class ExecutionManagerTestRunner { ...@@ -328,7 +346,7 @@ export class ExecutionManagerTestRunner {
if (step.functionParams.data) { if (step.functionParams.data) {
calldata = step.functionParams.data calldata = step.functionParams.data
} else { } else {
const runStep: TestStep_CALL = { const runStep: TestStep_CALLType = {
functionName: 'ovmCALL', functionName: 'ovmCALL',
functionParams: { functionParams: {
gasLimit: OVM_TX_GAS_LIMIT, gasLimit: OVM_TX_GAS_LIMIT,
...@@ -362,9 +380,12 @@ export class ExecutionManagerTestRunner { ...@@ -362,9 +380,12 @@ export class ExecutionManagerTestRunner {
await toRun await toRun
} }
} else { } else {
await this.contracts.OVM_ExecutionManager.ovmCALL( await this.contracts.OVM_ExecutionManager[
'ovmCALL(uint256,address,uint256,bytes)'
](
OVM_TX_GAS_LIMIT, OVM_TX_GAS_LIMIT,
ExecutionManagerTestRunner.getDummyAddress('$DUMMY_OVM_ADDRESS_1'), ExecutionManagerTestRunner.getDummyAddress('$DUMMY_OVM_ADDRESS_1'),
0,
this.contracts.Helper_TestRunner.interface.encodeFunctionData( this.contracts.Helper_TestRunner.interface.encodeFunctionData(
'runSingleTestStep', 'runSingleTestStep',
[this.parseTestStep(step)] [this.parseTestStep(step)]
...@@ -398,7 +419,7 @@ export class ExecutionManagerTestRunner { ...@@ -398,7 +419,7 @@ export class ExecutionManagerTestRunner {
return false return false
} else if (isTestStep_Context(step)) { } else if (isTestStep_Context(step)) {
return true return true
} else if (isTestStep_CALL(step)) { } else if (isTestStep_CALLType(step)) {
if ( if (
isRevertFlagError(step.expectedReturnValue) && isRevertFlagError(step.expectedReturnValue) &&
(step.expectedReturnValue.flag === REVERT_FLAGS.INVALID_STATE_ACCESS || (step.expectedReturnValue.flag === REVERT_FLAGS.INVALID_STATE_ACCESS ||
...@@ -435,23 +456,36 @@ export class ExecutionManagerTestRunner { ...@@ -435,23 +456,36 @@ export class ExecutionManagerTestRunner {
isTestStep_EXTCODESIZE(step) || isTestStep_EXTCODESIZE(step) ||
isTestStep_EXTCODEHASH(step) || isTestStep_EXTCODEHASH(step) ||
isTestStep_EXTCODECOPY(step) || isTestStep_EXTCODECOPY(step) ||
isTestStep_BALANCE(step) ||
isTestStep_CREATEEOA(step) isTestStep_CREATEEOA(step)
) { ) {
functionParams = Object.values(step.functionParams) functionParams = Object.values(step.functionParams)
} else if (isTestStep_CALL(step)) { } else if (isTestStep_CALLType(step)) {
functionParams = [ const innnerCalldata =
step.functionParams.gasLimit,
step.functionParams.target,
step.functionParams.calldata || step.functionParams.calldata ||
this.contracts.Helper_TestRunner.interface.encodeFunctionData( this.contracts.Helper_TestRunner.interface.encodeFunctionData(
'runMultipleTestSteps', 'runMultipleTestSteps',
[ [
step.functionParams.subSteps.map((subStep) => { step.functionParams.subSteps.map((subStep) => {
return this.parseTestStep(subStep) 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)) { } else if (isTestStep_CREATE(step)) {
functionParams = [ functionParams = [
this.contracts.Factory__Helper_TestRunner_CREATE.getDeployTransaction( this.contracts.Factory__Helper_TestRunner_CREATE.getDeployTransaction(
...@@ -475,8 +509,16 @@ export class ExecutionManagerTestRunner { ...@@ -475,8 +509,16 @@ export class ExecutionManagerTestRunner {
functionParams = [step.revertData || '0x'] 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( return this.contracts.OVM_ExecutionManager.interface.encodeFunctionData(
step.functionName, functionName,
functionParams functionParams
) )
} }
...@@ -500,7 +542,7 @@ export class ExecutionManagerTestRunner { ...@@ -500,7 +542,7 @@ export class ExecutionManagerTestRunner {
} }
let returnData: any[] = [] let returnData: any[] = []
if (isTestStep_CALL(step)) { if (isTestStep_CALLType(step)) {
if (step.expectedReturnValue === '0x00') { if (step.expectedReturnValue === '0x00') {
return step.expectedReturnValue return step.expectedReturnValue
} else if ( } else if (
...@@ -540,8 +582,16 @@ export class ExecutionManagerTestRunner { ...@@ -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( return this.contracts.OVM_ExecutionManager.interface.encodeFunctionResult(
step.functionName, functionName,
returnData returnData
) )
} }
......
...@@ -11,6 +11,7 @@ export type ContextOpcode = ...@@ -11,6 +11,7 @@ export type ContextOpcode =
| 'ovmGASLIMIT' | 'ovmGASLIMIT'
| 'ovmCHAINID' | 'ovmCHAINID'
| 'ovmGETNONCE' | 'ovmGETNONCE'
| 'ovmCALLVALUE'
type CallOpcode = 'ovmCALL' | 'ovmSTATICCALL' | 'ovmDELEGATECALL' type CallOpcode = 'ovmCALL' | 'ovmSTATICCALL' | 'ovmDELEGATECALL'
...@@ -68,6 +69,15 @@ interface TestStep_EXTCODECOPY { ...@@ -68,6 +69,15 @@ interface TestStep_EXTCODECOPY {
expectedReturnValue: string | RevertFlagError expectedReturnValue: string | RevertFlagError
} }
interface TestStep_BALANCE {
functionName: 'ovmBALANCE'
functionParams: {
address: string
}
expectedReturnStatus: boolean
expectedReturnValue: number | RevertFlagError
}
interface TestStep_SSTORE { interface TestStep_SSTORE {
functionName: 'ovmSSTORE' functionName: 'ovmSSTORE'
functionParams: { functionParams: {
...@@ -93,11 +103,12 @@ interface TestStep_INCREMENTNONCE { ...@@ -93,11 +103,12 @@ interface TestStep_INCREMENTNONCE {
expectedReturnValue?: RevertFlagError expectedReturnValue?: RevertFlagError
} }
export interface TestStep_CALL { export interface TestStep_CALLType {
functionName: CallOpcode functionName: CallOpcode
functionParams: { functionParams: {
gasLimit: number | BigNumber gasLimit: number | BigNumber
target: string target: string
value?: number | BigNumber
calldata?: string calldata?: string
subSteps?: TestStep[] subSteps?: TestStep[]
} }
...@@ -174,13 +185,14 @@ export type TestStep = ...@@ -174,13 +185,14 @@ export type TestStep =
| TestStep_SSTORE | TestStep_SSTORE
| TestStep_SLOAD | TestStep_SLOAD
| TestStep_INCREMENTNONCE | TestStep_INCREMENTNONCE
| TestStep_CALL | TestStep_CALLType
| TestStep_CREATE | TestStep_CREATE
| TestStep_CREATE2 | TestStep_CREATE2
| TestStep_CREATEEOA | TestStep_CREATEEOA
| TestStep_EXTCODESIZE | TestStep_EXTCODESIZE
| TestStep_EXTCODEHASH | TestStep_EXTCODEHASH
| TestStep_EXTCODECOPY | TestStep_EXTCODECOPY
| TestStep_BALANCE
| TestStep_REVERT | TestStep_REVERT
| TestStep_evm | TestStep_evm
...@@ -220,6 +232,7 @@ export const isTestStep_Context = ( ...@@ -220,6 +232,7 @@ export const isTestStep_Context = (
'ovmCHAINID', 'ovmCHAINID',
'ovmL1QUEUEORIGIN', 'ovmL1QUEUEORIGIN',
'ovmGETNONCE', 'ovmGETNONCE',
'ovmCALLVALUE',
].includes(step.functionName) ].includes(step.functionName)
} }
...@@ -255,16 +268,28 @@ export const isTestStep_EXTCODECOPY = ( ...@@ -255,16 +268,28 @@ export const isTestStep_EXTCODECOPY = (
return step.functionName === 'ovmEXTCODECOPY' 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 => { export const isTestStep_REVERT = (step: TestStep): step is TestStep_REVERT => {
return step.functionName === 'ovmREVERT' 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( return ['ovmCALL', 'ovmSTATICCALL', 'ovmDELEGATECALL'].includes(
step.functionName step.functionName
) )
} }
export const isTestStep_CALL = (step: TestStep): boolean => {
return step.functionName === 'ovmCALL'
}
export const isTestStep_CREATE = (step: TestStep): step is TestStep_CREATE => { export const isTestStep_CREATE = (step: TestStep): step is TestStep_CREATE => {
return step.functionName === 'ovmCREATE' 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