Commit b284dc39 authored by smartcontracts's avatar smartcontracts Committed by Kelvin Fichter

refactor[contracts]: Turn ExecutionManagerWrapper into a predeployed contract (#808)

* wip: Started working on L2 contract testing revamp

* test: clean tests for ProxyEOA

* style: clean imports for ProxyEOA tests

* test: port tests for ECDSAContractAccount

* fix tests and add wrapper to dump

* fix: add em wrapper to l2 deploy

* ffix: add comments to wrapper contract

* fix: add more comments

* fix: add smock comment for unbind

* Update packages/smock/src/smockit/binding.ts
parent 245136f1
// SPDX-License-Identifier: MIT
// @unsupported: evm
pragma solidity >0.5.0 <0.8.0;
pragma experimental ABIEncoderV2;
......@@ -74,7 +73,7 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
// recovered address of the user who signed this message. This is how we manage to shim
// account abstraction even though the user isn't a contract.
require(
transaction.sender() == address(this),
transaction.sender() == Lib_ExecutionManagerWrapper.ovmADDRESS(),
"Signature provided for EOA transaction execution is invalid."
);
......@@ -138,6 +137,7 @@ contract OVM_ECDSAContractAccount is iOVM_ECDSAContractAccount {
),
"Value could not be transferred to recipient."
);
return (true, bytes(""));
} else {
return transaction.to.call(transaction.data);
......
// SPDX-License-Identifier: MIT
// @unsupported: evm
pragma solidity >0.5.0 <0.8.0;
/* Library Imports */
import { Lib_Bytes32Utils } from "../../libraries/utils/Lib_Bytes32Utils.sol";
import { Lib_ExecutionManagerWrapper } from "../../libraries/wrappers/Lib_ExecutionManagerWrapper.sol";
/**
* @title OVM_ProxyEOA
......@@ -72,7 +72,7 @@ contract OVM_ProxyEOA {
external
{
require(
msg.sender == address(this),
msg.sender == Lib_ExecutionManagerWrapper.ovmADDRESS(),
"EOAs can only upgrade their own EOA implementation"
);
......@@ -86,6 +86,7 @@ contract OVM_ProxyEOA {
*/
function getImplementation()
public
view
returns (
address
)
......
// SPDX-License-Identifier: MIT
// @unsupported: evm
pragma solidity >0.5.0 <0.8.0;
/* Library Imports */
import { Lib_ErrorUtils } from "../../libraries/utils/Lib_ErrorUtils.sol";
/**
* @title OVM_ExecutionManagerWrapper
* @dev This contract is a thin wrapper around the `kall` builtin. By making this contract a
* predeployed contract, we can restrict evm solc incompatibility to this one contract. Other
* contracts will typically call this contract via `Lib_ExecutionManagerWrapper`.
*
* Compiler used: optimistic-solc
* Runtime target: OVM
*/
contract OVM_ExecutionManagerWrapper {
/*********************
* Fallback Function *
*********************/
fallback()
external
{
bytes memory data = msg.data;
assembly {
// kall is a custom yul builtin within optimistic-solc that allows us to directly call
// the execution manager (since `call` would be compiled).
kall(add(data, 0x20), mload(data), 0x0, 0x0)
// Standard returndata loading code.
let size := returndatasize()
let returndata := mload(0x40)
mstore(0x40, add(returndata, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(returndata, size)
returndatacopy(add(returndata, 0x20), 0x0, size)
// kall automatically reverts if the underlying call fails, so we only need to handle
// the success case.
return(add(returndata, 0x20), mload(returndata))
}
}
}
// SPDX-License-Identifier: MIT
// @unsupported: evm
pragma solidity >0.5.0 <0.8.0;
/* Library Imports */
......
// SPDX-License-Identifier: MIT
// @unsupported: evm
pragma solidity >0.5.0 <0.8.0;
/* Library Imports */
......@@ -7,6 +6,9 @@ import { Lib_ErrorUtils } from "../utils/Lib_ErrorUtils.sol";
/**
* @title Lib_ExecutionManagerWrapper
* @dev This library acts as a utility for easily calling the OVM_ExecutionManagerWrapper, the
* predeployed contract which exposes the `kall` builtin. Effectively, this contract allows the
* user to trigger OVM opcodes by directly calling the OVM_ExecutionManger.
*
* Compiler used: solc
* Runtime target: OVM
......@@ -20,7 +22,7 @@ library Lib_ExecutionManagerWrapper {
/**
* Performs a safe ovmCREATE call.
* @param _bytecode Code for the new contract.
* @return _contract Address of the created contract.
* @return Address of the created contract.
*/
function ovmCREATE(
bytes memory _bytecode
......@@ -31,7 +33,7 @@ library Lib_ExecutionManagerWrapper {
bytes memory
)
{
bytes memory returndata = _safeExecutionManagerInteraction(
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmCREATE(bytes)",
_bytecode
......@@ -43,15 +45,15 @@ library Lib_ExecutionManagerWrapper {
/**
* Performs a safe ovmGETNONCE call.
* @return _nonce Result of calling ovmGETNONCE.
* @return Result of calling ovmGETNONCE.
*/
function ovmGETNONCE()
internal
returns (
uint256 _nonce
uint256
)
{
bytes memory returndata = _safeExecutionManagerInteraction(
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmGETNONCE()"
)
......@@ -66,7 +68,7 @@ library Lib_ExecutionManagerWrapper {
function ovmINCREMENTNONCE()
internal
{
_safeExecutionManagerInteraction(
_callWrapperContract(
abi.encodeWithSignature(
"ovmINCREMENTNONCE()"
)
......@@ -88,7 +90,7 @@ library Lib_ExecutionManagerWrapper {
)
internal
{
_safeExecutionManagerInteraction(
_callWrapperContract(
abi.encodeWithSignature(
"ovmCREATEEOA(bytes32,uint8,bytes32,bytes32)",
_messageHash,
......@@ -109,7 +111,7 @@ library Lib_ExecutionManagerWrapper {
address
)
{
bytes memory returndata = _safeExecutionManagerInteraction(
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmL1TXORIGIN()"
)
......@@ -128,7 +130,7 @@ library Lib_ExecutionManagerWrapper {
uint256
)
{
bytes memory returndata = _safeExecutionManagerInteraction(
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmCHAINID()"
)
......@@ -137,6 +139,25 @@ library Lib_ExecutionManagerWrapper {
return abi.decode(returndata, (uint256));
}
/**
* Performs a safe ovmADDRESS call.
* @return Result of calling ovmADDRESS.
*/
function ovmADDRESS()
internal
returns (
address
)
{
bytes memory returndata = _callWrapperContract(
abi.encodeWithSignature(
"ovmADDRESS()"
)
);
return abi.decode(returndata, (address));
}
/*********************
* Private Functions *
......@@ -145,9 +166,9 @@ library Lib_ExecutionManagerWrapper {
/**
* Performs an ovm interaction and the necessary safety checks.
* @param _calldata Data to send to the OVM_ExecutionManager (encoded with sighash).
* @return _returndata Data sent back by the OVM_ExecutionManager.
* @return Data sent back by the OVM_ExecutionManager.
*/
function _safeExecutionManagerInteraction(
function _callWrapperContract(
bytes memory _calldata
)
private
......@@ -155,17 +176,14 @@ library Lib_ExecutionManagerWrapper {
bytes memory
)
{
bytes memory returndata;
(bool success, bytes memory returndata) = 0x420000000000000000000000000000000000000B.delegatecall(_calldata);
if (success == true) {
return returndata;
} else {
assembly {
// kall is a custom yul builtin within optimistic-solc that allows us to directly call
// the execution manager (since `call` would be compiled).
kall(add(_calldata, 0x20), mload(_calldata), 0x0, 0x0)
let size := returndatasize()
returndata := mload(0x40)
mstore(0x40, add(returndata, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(returndata, size)
returndatacopy(add(returndata, 0x20), 0x0, size)
revert(add(returndata, 0x20), mload(returndata))
}
}
return returndata;
}
}
......@@ -251,5 +251,12 @@ export const makeContractDeployConfig = async (
OVM_ProxyEOA: {
factory: getContractFactory('OVM_ProxyEOA', undefined, true),
},
OVM_ExecutionManagerWrapper: {
factory: getContractFactory(
'OVM_ExecutionManagerWrapper',
undefined,
true
),
},
}
}
......@@ -17,5 +17,6 @@ export const predeploys = {
OVM_L2CrossDomainMessenger: '0x4200000000000000000000000000000000000007',
Lib_AddressManager: '0x4200000000000000000000000000000000000008',
OVM_ProxyEOA: '0x4200000000000000000000000000000000000009',
OVM_ExecutionManagerWrapper: '0x420000000000000000000000000000000000000B',
ERC1820Registry: '0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24',
}
......@@ -136,6 +136,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise<any> => {
'OVM_ExecutionManager',
'OVM_StateManager',
'OVM_ETH',
'OVM_ExecutionManagerWrapper',
],
deployOverrides: {},
waitForReceipts: false,
......@@ -152,6 +153,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise<any> => {
'OVM_ETH',
'OVM_ECDSAContractAccount',
'OVM_ProxyEOA',
'OVM_ExecutionManagerWrapper',
]
const deploymentResult = await deploy(config)
......@@ -174,7 +176,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise<any> => {
for (let i = 0; i < Object.keys(deploymentResult.contracts).length; i++) {
const name = Object.keys(deploymentResult.contracts)[i]
const contract = deploymentResult.contracts[name]
let code
let code: string
if (ovmCompiled.includes(name)) {
const ovmDeployedBytecode = getContractDefinition(name, true)
.deployedBytecode
......
......@@ -4,129 +4,49 @@ import { expect } from '../../../setup'
import { ethers, waffle } from 'hardhat'
import { ContractFactory, Contract, Wallet, BigNumber } from 'ethers'
import { MockContract, smockit } from '@eth-optimism/smock'
import { fromHexString, toHexString } from '@eth-optimism/core-utils'
import { toPlainObject } from 'lodash'
/* Internal Imports */
import {
NON_ZERO_ADDRESS,
DEFAULT_EIP155_TX,
decodeSolidityError,
} from '../../../helpers'
import {
getContractFactory,
getContractInterface,
predeploys,
} from '../../../../src'
const callPredeploy = async (
Helper_PredeployCaller: Contract,
predeploy: Contract,
functionName: string,
functionParams?: any[],
gasLimit?: number
): Promise<any> => {
if (gasLimit) {
return Helper_PredeployCaller.callPredeploy(
predeploy.address,
predeploy.interface.encodeFunctionData(
functionName,
functionParams || []
),
{ gasLimit }
)
}
return Helper_PredeployCaller.callPredeploy(
predeploy.address,
predeploy.interface.encodeFunctionData(functionName, functionParams || [])
)
}
const iOVM_ETH = getContractInterface('OVM_ETH')
import { DEFAULT_EIP155_TX } from '../../../helpers'
import { predeploys } from '../../../../src'
describe('OVM_ECDSAContractAccount', () => {
let wallet: Wallet
let badWallet: Wallet
before(async () => {
const provider = waffle.provider
;[wallet, badWallet] = provider.getWallets()
;[wallet] = provider.getWallets()
})
let Mock__OVM_ExecutionManager: MockContract
let Helper_PredeployCaller: Contract
let Mock__OVM_ETH: MockContract
before(async () => {
Mock__OVM_ExecutionManager = await smockit(
await ethers.getContractFactory('OVM_ExecutionManager')
)
Helper_PredeployCaller = await (
await ethers.getContractFactory('Helper_PredeployCaller')
).deploy()
Helper_PredeployCaller.setTarget(Mock__OVM_ExecutionManager.address)
Mock__OVM_ExecutionManager = await smockit('OVM_ExecutionManager', {
address: predeploys.OVM_ExecutionManagerWrapper,
})
Mock__OVM_ETH = await smockit('OVM_ETH', {
address: predeploys.OVM_ETH,
})
})
let Factory__OVM_ECDSAContractAccount: ContractFactory
before(async () => {
Factory__OVM_ECDSAContractAccount = getContractFactory(
'OVM_ECDSAContractAccount',
wallet,
true
Factory__OVM_ECDSAContractAccount = await ethers.getContractFactory(
'OVM_ECDSAContractAccount'
)
})
let OVM_ECDSAContractAccount: Contract
beforeEach(async () => {
OVM_ECDSAContractAccount = await Factory__OVM_ECDSAContractAccount.deploy()
})
Mock__OVM_ExecutionManager.smocked.ovmADDRESS.will.return.with(
await wallet.getAddress()
)
Mock__OVM_ExecutionManager.smocked.ovmEXTCODESIZE.will.return.with(1)
beforeEach(async () => {
Mock__OVM_ExecutionManager.smocked.ovmCHAINID.will.return.with(420)
Mock__OVM_ExecutionManager.smocked.ovmGETNONCE.will.return.with(100)
Mock__OVM_ExecutionManager.smocked.ovmCALL.will.return.with(
(gasLimit, target, data) => {
if (target === predeploys.OVM_ETH) {
return [
true,
'0x0000000000000000000000000000000000000000000000000000000000000001',
]
} else {
return [true, '0x']
}
}
)
Mock__OVM_ExecutionManager.smocked.ovmSTATICCALL.will.return.with(
(gasLimit, target, data) => {
// Duplicating the behavior of the ecrecover precompile.
if (target === '0x0000000000000000000000000000000000000001') {
const databuf = fromHexString(data)
let addr: string
try {
addr = ethers.utils.recoverAddress(databuf.slice(0, 32), {
v: BigNumber.from(databuf.slice(32, 64)).toNumber(),
r: toHexString(databuf.slice(64, 96)),
s: toHexString(databuf.slice(96, 128)),
})
} catch (err) {
addr = ethers.constants.AddressZero
}
const ret = ethers.utils.defaultAbiCoder.encode(['address'], [addr])
return [true, ret]
} else {
return [true, '0x']
}
}
)
Mock__OVM_ExecutionManager.smocked.ovmCREATE.will.return.with([
NON_ZERO_ADDRESS,
'0x',
])
Mock__OVM_ExecutionManager.smocked.ovmCALLER.will.return.with(
NON_ZERO_ADDRESS
Mock__OVM_ExecutionManager.smocked.ovmADDRESS.will.return.with(
await wallet.getAddress()
)
Mock__OVM_ETH.smocked.transfer.will.return.with(true)
})
describe('fallback()', () => {
......@@ -134,29 +54,14 @@ describe('OVM_ECDSAContractAccount', () => {
const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction]
)
// The ovmCALL is the 2nd call because the first call transfers the fee.
const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[1]
expect(ovmCALL._address).to.equal(DEFAULT_EIP155_TX.to)
expect(ovmCALL._calldata).to.equal(DEFAULT_EIP155_TX.data)
await OVM_ECDSAContractAccount.execute(encodedTransaction)
})
it(`should ovmCREATE if EIP155Transaction.to is zero address`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, to: '' }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction]
)
await OVM_ECDSAContractAccount.execute(encodedTransaction)
const ovmCREATE: any =
Mock__OVM_ExecutionManager.smocked.ovmCREATE.calls[0]
......@@ -170,15 +75,9 @@ describe('OVM_ECDSAContractAccount', () => {
'0x' + '00'.repeat(65)
)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction]
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith(
'Signature provided for EOA transaction execution is invalid.'
)
})
......@@ -187,15 +86,9 @@ describe('OVM_ECDSAContractAccount', () => {
const transaction = { ...DEFAULT_EIP155_TX, nonce: 99 }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction]
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith(
'Transaction nonce does not match the expected nonce.'
)
})
......@@ -204,15 +97,9 @@ describe('OVM_ECDSAContractAccount', () => {
const transaction = { ...DEFAULT_EIP155_TX, chainId: 421 }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction]
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith(
'Lib_EIP155Tx: Transaction signed with wrong chain ID'
)
})
......@@ -222,155 +109,72 @@ describe('OVM_ECDSAContractAccount', () => {
const transaction = { ...DEFAULT_EIP155_TX, gasLimit: 200000000 }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
'Gas is not sufficient to execute the transaction.'
)
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction, {
gasLimit: 40000000,
})
).to.be.revertedWith('Gas is not sufficient to execute the transaction.')
})
it(`should revert if fee is not transferred to the relayer`, async () => {
const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction)
Mock__OVM_ExecutionManager.smocked.ovmCALL.will.return.with(
(gasLimit, target, data) => {
if (target === predeploys.OVM_ETH) {
return [
true,
'0x0000000000000000000000000000000000000000000000000000000000000000',
]
} else {
return [true, '0x']
}
}
)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
Mock__OVM_ETH.smocked.transfer.will.return.with(false)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
'Fee was not transferred to relayer.'
)
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith('Fee was not transferred to relayer.')
})
it(`should transfer value if value is greater than 0`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, data: '0x' }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
await OVM_ECDSAContractAccount.execute(encodedTransaction)
// First call transfers fee, second transfers value (since value > 0).
const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[1]
expect(ovmCALL._address).to.equal(predeploys.OVM_ETH)
expect(ovmCALL._calldata).to.equal(
iOVM_ETH.encodeFunctionData('transfer', [
transaction.to,
transaction.value,
])
)
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_ExecutionManager.smocked.ovmCALL.will.return.with(
(gasLimit, target, data) => {
if (target === predeploys.OVM_ETH) {
const [recipient, amount] = iOVM_ETH.decodeFunctionData(
'transfer',
data
)
if (recipient === transaction.to) {
return [
true,
'0x0000000000000000000000000000000000000000000000000000000000000000',
]
Mock__OVM_ETH.smocked.transfer.will.return.with((to, value) => {
if (to === transaction.to) {
return false
} else {
return [
true,
'0x0000000000000000000000000000000000000000000000000000000000000001',
]
return true
}
} else {
return [true, '0x']
}
}
)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
})
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
'Value could not be transferred to recipient.'
)
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith('Value could not be transferred to recipient.')
})
it(`should revert if trying to send value with a contract creation`, async () => {
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, to: '' }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
'Value transfer in contract creation not supported.'
)
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).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, to: '' }
const transaction = { ...DEFAULT_EIP155_TX, value: 1234, data: '0x1234' }
const encodedTransaction = await wallet.signTransaction(transaction)
await callPredeploy(
Helper_PredeployCaller,
OVM_ECDSAContractAccount,
'execute',
[encodedTransaction],
40000000
)
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
'Value transfer in contract creation not supported.'
)
await expect(
OVM_ECDSAContractAccount.execute(encodedTransaction)
).to.be.revertedWith('Value is nonzero but input data was provided.')
})
})
})
import { expect } from '../../../setup'
/* External Imports */
import { ethers, waffle } from 'hardhat'
import { ContractFactory, Contract, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import { ContractFactory, Contract, Signer } from 'ethers'
import { MockContract, smockit } from '@eth-optimism/smock'
import { remove0x } from '@eth-optimism/core-utils'
import { toPlainObject } from 'lodash'
/* Internal Imports */
import { decodeSolidityError } from '../../../helpers'
import { getContractInterface, getContractFactory } from '../../../../src'
const callPredeploy = async (
Helper_PredeployCaller: Contract,
predeploy: Contract,
functionName: string,
functionParams?: any[],
ethCall: boolean = false
): Promise<any> => {
if (ethCall) {
return Helper_PredeployCaller.callStatic.callPredeployAbi(
predeploy.address,
predeploy.interface.encodeFunctionData(functionName, functionParams || [])
)
}
return Helper_PredeployCaller.callPredeploy(
predeploy.address,
predeploy.interface.encodeFunctionData(functionName, functionParams || [])
)
}
const addrToBytes32 = (addr: string) => '0x' + '00'.repeat(12) + remove0x(addr)
const eoaDefaultAddr = '0x4200000000000000000000000000000000000003'
import { getContractInterface, predeploys } from '../../../../src'
describe('OVM_ProxyEOA', () => {
let wallet: Wallet
let signer: Signer
before(async () => {
const provider = waffle.provider
;[wallet] = provider.getWallets()
;[signer] = await ethers.getSigners()
})
let Mock__OVM_ExecutionManager: MockContract
let Mock__OVM_ECDSAContractAccount: MockContract
let Helper_PredeployCaller: Contract
before(async () => {
Mock__OVM_ExecutionManager = await smockit(
await ethers.getContractFactory('OVM_ExecutionManager')
)
Helper_PredeployCaller = await (
await ethers.getContractFactory('Helper_PredeployCaller')
).deploy()
Helper_PredeployCaller.setTarget(Mock__OVM_ExecutionManager.address)
Mock__OVM_ECDSAContractAccount = await smockit(
getContractInterface('OVM_ECDSAContractAccount', true)
)
Mock__OVM_ExecutionManager = await smockit('OVM_ExecutionManager', {
address: predeploys.OVM_ExecutionManagerWrapper,
})
Mock__OVM_ECDSAContractAccount = await smockit('OVM_ECDSAContractAccount', {
address: predeploys.OVM_ECDSAContractAccount,
})
})
let OVM_ProxyEOAFactory: ContractFactory
let Factory__OVM_ProxyEOA: ContractFactory
before(async () => {
OVM_ProxyEOAFactory = getContractFactory('OVM_ProxyEOA', wallet, true)
Factory__OVM_ProxyEOA = await ethers.getContractFactory('OVM_ProxyEOA')
})
let OVM_ProxyEOA: Contract
beforeEach(async () => {
OVM_ProxyEOA = await OVM_ProxyEOAFactory.deploy()
Mock__OVM_ExecutionManager.smocked.ovmADDRESS.will.return.with(
OVM_ProxyEOA.address
)
Mock__OVM_ExecutionManager.smocked.ovmCALLER.will.return.with(
OVM_ProxyEOA.address
)
OVM_ProxyEOA = await Factory__OVM_ProxyEOA.deploy()
})
describe('getImplementation()', () => {
it(`should be created with implementation at predeploy address`, async () => {
const eoaDefaultAddrBytes32 = addrToBytes32(eoaDefaultAddr)
Mock__OVM_ExecutionManager.smocked.ovmSLOAD.will.return.with(
eoaDefaultAddrBytes32
expect(await OVM_ProxyEOA.getImplementation()).to.equal(
predeploys.OVM_ECDSAContractAccount
)
const implAddrBytes32 = await callPredeploy(
Helper_PredeployCaller,
OVM_ProxyEOA,
'getImplementation',
[],
true
)
expect(implAddrBytes32).to.equal(eoaDefaultAddrBytes32)
})
})
describe('upgrade()', () => {
const implSlotKey =
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' //bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
it(`should upgrade the proxy implementation`, async () => {
const newImpl = `0x${'81'.repeat(20)}`
const newImplBytes32 = addrToBytes32(newImpl)
await callPredeploy(Helper_PredeployCaller, OVM_ProxyEOA, 'upgrade', [
newImpl,
])
const ovmSSTORE: any =
Mock__OVM_ExecutionManager.smocked.ovmSSTORE.calls[0]
expect(ovmSSTORE._key).to.equal(implSlotKey)
expect(ovmSSTORE._value).to.equal(newImplBytes32)
Mock__OVM_ExecutionManager.smocked.ovmADDRESS.will.return.with(
await signer.getAddress()
)
await expect(OVM_ProxyEOA.upgrade(newImpl)).to.not.be.reverted
expect(await OVM_ProxyEOA.getImplementation()).to.equal(newImpl)
})
it(`should not allow upgrade of the proxy implementation by another account`, async () => {
Mock__OVM_ExecutionManager.smocked.ovmCALLER.will.return.with(
await wallet.getAddress()
)
const newImpl = `0x${'81'.repeat(20)}`
await callPredeploy(Helper_PredeployCaller, OVM_ProxyEOA, 'upgrade', [
newImpl,
])
const ovmREVERT: any =
Mock__OVM_ExecutionManager.smocked.ovmREVERT.calls[0]
expect(decodeSolidityError(ovmREVERT._data)).to.equal(
Mock__OVM_ExecutionManager.smocked.ovmADDRESS.will.return.with(
ethers.constants.AddressZero
)
await expect(OVM_ProxyEOA.upgrade(newImpl)).to.be.revertedWith(
'EOAs can only upgrade their own EOA implementation'
)
})
})
describe('fallback()', () => {
it(`should call delegateCall with right calldata`, async () => {
Mock__OVM_ExecutionManager.smocked.ovmSLOAD.will.return.with(
addrToBytes32(Mock__OVM_ECDSAContractAccount.address)
const data = Mock__OVM_ECDSAContractAccount.interface.encodeFunctionData(
'execute',
['0x12341234']
)
Mock__OVM_ExecutionManager.smocked.ovmDELEGATECALL.will.return.with([
true,
'0x1234',
])
const calldata = '0xdeadbeef'
await Helper_PredeployCaller.callPredeploy(OVM_ProxyEOA.address, calldata)
const ovmDELEGATECALL: any =
Mock__OVM_ExecutionManager.smocked.ovmDELEGATECALL.calls[0]
expect(ovmDELEGATECALL._address).to.equal(
Mock__OVM_ECDSAContractAccount.address
)
expect(ovmDELEGATECALL._calldata).to.equal(calldata)
await signer.sendTransaction({
to: OVM_ProxyEOA.address,
data,
})
expect(
toPlainObject(Mock__OVM_ECDSAContractAccount.smocked.execute.calls[0])
).to.deep.include({
_encodedTransaction: '0x12341234',
})
})
it.skip(`should return data from fallback`, async () => {
//TODO test return data from fallback
// TODO: test return data from fallback
})
it.skip(`should revert in fallback`, async () => {
//TODO test reversion from fallback
// TODO: test reversion from fallback
})
})
})
......@@ -2,154 +2,113 @@ import { expect } from '../../../setup'
/* External Imports */
import { waffle, ethers } from 'hardhat'
import { ContractFactory, Wallet, Contract, BigNumber } from 'ethers'
import { smockit, MockContract } from '@eth-optimism/smock'
import { fromHexString, toHexString } from '@eth-optimism/core-utils'
import { ContractFactory, Wallet, Contract, Signer } from 'ethers'
import { smockit, MockContract, unbind } from '@eth-optimism/smock'
import { toPlainObject } from 'lodash'
/* Internal Imports */
import { DEFAULT_EIP155_TX } from '../../../helpers'
import { getContractInterface, getContractFactory } from '../../../../src'
import { getContractInterface, predeploys } from '../../../../src'
describe('OVM_SequencerEntrypoint', () => {
const iOVM_ECDSAContractAccount = getContractInterface(
'OVM_ECDSAContractAccount'
)
let wallet: Wallet
before(async () => {
const provider = waffle.provider
;[wallet] = provider.getWallets()
})
let signer: Signer
before(async () => {
;[signer] = await ethers.getSigners()
})
let Mock__OVM_ExecutionManager: MockContract
let Helper_PredeployCaller: Contract
before(async () => {
Mock__OVM_ExecutionManager = await smockit(
await ethers.getContractFactory('OVM_ExecutionManager')
)
Mock__OVM_ExecutionManager = await smockit('OVM_ExecutionManager', {
address: predeploys.OVM_ExecutionManagerWrapper,
})
Mock__OVM_ExecutionManager.smocked.ovmCHAINID.will.return.with(420)
Mock__OVM_ExecutionManager.smocked.ovmCALL.will.return.with(
(gasLimit, target, data) => {
if (target === wallet.address) {
return [
true,
iOVM_ECDSAContractAccount.encodeFunctionResult('execute', [
true,
'0x',
]),
]
} else {
return [true, '0x']
}
}
)
Mock__OVM_ExecutionManager.smocked.ovmSTATICCALL.will.return.with(
(gasLimit, target, data) => {
// Duplicating the behavior of the ecrecover precompile.
if (target === '0x0000000000000000000000000000000000000001') {
const databuf = fromHexString(data)
const addr = ethers.utils.recoverAddress(databuf.slice(0, 32), {
v: BigNumber.from(databuf.slice(32, 64)).toNumber(),
r: toHexString(databuf.slice(64, 96)),
s: toHexString(databuf.slice(96, 128)),
})
const ret = ethers.utils.defaultAbiCoder.encode(['address'], [addr])
return [true, ret]
} else {
return [true, '0x']
}
}
)
Helper_PredeployCaller = await (
await ethers.getContractFactory('Helper_PredeployCaller')
).deploy()
Helper_PredeployCaller.setTarget(Mock__OVM_ExecutionManager.address)
Mock__OVM_ExecutionManager.smocked.ovmCREATEEOA.will.return()
})
let OVM_SequencerEntrypointFactory: ContractFactory
let Factory__OVM_SequencerEntrypoint: ContractFactory
before(async () => {
OVM_SequencerEntrypointFactory = getContractFactory(
'OVM_SequencerEntrypoint',
wallet,
true
Factory__OVM_SequencerEntrypoint = await ethers.getContractFactory(
'OVM_SequencerEntrypoint'
)
})
const iOVM_ECDSAContractAccount = getContractInterface(
'OVM_ECDSAContractAccount',
true
)
let OVM_SequencerEntrypoint: Contract
beforeEach(async () => {
OVM_SequencerEntrypoint = await OVM_SequencerEntrypointFactory.deploy()
Mock__OVM_ExecutionManager.smocked.ovmEXTCODESIZE.will.return.with(1)
Mock__OVM_ExecutionManager.smocked.ovmREVERT.will.revert()
OVM_SequencerEntrypoint = await Factory__OVM_SequencerEntrypoint.deploy()
})
describe('fallback()', async () => {
it('should call EIP155', async () => {
it('should call ovmCREATEEOA when ovmEXTCODESIZE returns 0', async () => {
const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction)
await Helper_PredeployCaller.callPredeploy(
OVM_SequencerEntrypoint.address,
encodedTransaction
)
// Just unbind the smock in case it's there during this test for some reason.
await unbind(await wallet.getAddress())
const expectedEOACalldata = iOVM_ECDSAContractAccount.encodeFunctionData(
'execute',
[encodedTransaction]
)
const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[0]
expect(ovmCALL._address).to.equal(await wallet.getAddress())
expect(ovmCALL._calldata).to.equal(expectedEOACalldata)
await signer.sendTransaction({
to: OVM_SequencerEntrypoint.address,
data: encodedTransaction,
})
it('should send correct calldata if tx is a create', async () => {
const transaction = { ...DEFAULT_EIP155_TX, to: '' }
const call: any = Mock__OVM_ExecutionManager.smocked.ovmCREATEEOA.calls[0]
const eoaAddress = ethers.utils.recoverAddress(call._messageHash, {
v: call._v + 27,
r: call._r,
s: call._s,
})
expect(eoaAddress).to.equal(await wallet.getAddress())
})
it('should call EIP155', async () => {
const transaction = DEFAULT_EIP155_TX
const encodedTransaction = await wallet.signTransaction(transaction)
await Helper_PredeployCaller.callPredeploy(
OVM_SequencerEntrypoint.address,
encodedTransaction
)
const Mock__wallet = await smockit(iOVM_ECDSAContractAccount, {
address: await wallet.getAddress(),
})
const expectedEOACalldata = iOVM_ECDSAContractAccount.encodeFunctionData(
'execute',
[encodedTransaction]
)
const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[0]
expect(ovmCALL._address).to.equal(await wallet.getAddress())
expect(ovmCALL._calldata).to.equal(expectedEOACalldata)
await signer.sendTransaction({
to: OVM_SequencerEntrypoint.address,
data: encodedTransaction,
})
it(`should call ovmCreateEOA when ovmEXTCODESIZE returns 0`, async () => {
let isFirstCheck = true
Mock__OVM_ExecutionManager.smocked.ovmEXTCODESIZE.will.return.with(() => {
if (isFirstCheck) {
isFirstCheck = false
return 0
} else {
return 1
}
expect(
toPlainObject(Mock__wallet.smocked.execute.calls[0])
).to.deep.include({
_encodedTransaction: encodedTransaction,
})
})
const transaction = DEFAULT_EIP155_TX
it('should send correct calldata if tx is a create', async () => {
const transaction = { ...DEFAULT_EIP155_TX, to: '' }
const encodedTransaction = await wallet.signTransaction(transaction)
await Helper_PredeployCaller.callPredeploy(
OVM_SequencerEntrypoint.address,
encodedTransaction
)
const Mock__wallet = await smockit(iOVM_ECDSAContractAccount, {
address: await wallet.getAddress(),
})
const call: any = Mock__OVM_ExecutionManager.smocked.ovmCREATEEOA.calls[0]
const eoaAddress = ethers.utils.recoverAddress(call._messageHash, {
v: call._v + 27,
r: call._r,
s: call._s,
await signer.sendTransaction({
to: OVM_SequencerEntrypoint.address,
data: encodedTransaction,
})
expect(eoaAddress).to.equal(await wallet.getAddress())
expect(
toPlainObject(Mock__wallet.smocked.execute.calls[0])
).to.deep.include({
_encodedTransaction: encodedTransaction,
})
})
})
})
......@@ -63,7 +63,12 @@ const initializeSmock = (provider: HardhatNetworkProvider): void => {
return
}
const target = fromFancyAddress(message.to)
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Check if the target address is a smocked contract.
if (!(target in vm._smockState.mocks)) {
......@@ -103,7 +108,13 @@ const initializeSmock = (provider: HardhatNetworkProvider): void => {
// contracts never create new sub-calls (meaning this `afterMessage` event corresponds directly
// to a `beforeMessage` event emitted during a call to a smock contract).
const message = vm._smockState.messages.pop()
const target = fromFancyAddress(message.to)
let target: string
if (message.delegatecall) {
target = fromFancyAddress(message._codeAddress)
} else {
target = fromFancyAddress(message.to)
}
// Not sure if this can ever actually happen? Just being safe.
if (!(target in vm._smockState.mocks)) {
......@@ -181,3 +192,32 @@ export const bindSmock = async (
Buffer.from('00', 'hex')
)
}
/**
* Detaches a smocked contract from a hardhat network provider.
* @param mock Smocked contract to detach to a provider, or an address.
* @param provider Hardhat network provider to detatch the contract from.
*/
export const unbindSmock = async (
mock: MockContract | string,
provider: HardhatNetworkProvider
): Promise<void> => {
if (!isSmockInitialized(provider)) {
initializeSmock(provider)
}
const vm: SmockedVM = (provider as any)._node._vm
const pStateManager = vm.pStateManager || vm.stateManager
// Add mock to our list of mocks currently attached to the VM.
const address = typeof mock === 'string' ? mock : mock.address.toLowerCase()
delete vm._smockState.mocks[address]
// Set the contract code for our mock to 0x00 == STOP. Need some non-empty contract code because
// Solidity will sometimes throw if it's calling something without code (I forget the exact
// scenario that causes this throw).
await pStateManager.putContractCode(
toFancyAddress(address),
Buffer.from('', 'hex')
)
}
......@@ -16,7 +16,7 @@ import {
SmockOptions,
SmockSpec,
} from './types'
import { bindSmock } from './binding'
import { bindSmock, unbindSmock } from './binding'
import { makeRandomAddress } from '../utils'
import { findBaseHardhatProvider } from '../common'
......@@ -304,3 +304,22 @@ export const smockit = async (
return contract
}
/**
* Unbinds a mock contract (meaning the contract will no longer behave as a mock).
* @param mock Mock contract or address to unbind.
*/
export const unbind = async (mock: MockContract | string): Promise<void> => {
// Only support native hardhat runtime, haven't bothered to figure it out for anything else.
if (hre.network.name !== 'hardhat') {
throw new Error(
`[smock]: smock is only compatible with the "hardhat" network, got: ${hre.network.name}`
)
}
// Find the provider object. See comments for `findBaseHardhatProvider`
const provider = findBaseHardhatProvider(hre)
// Unbind the contract.
await unbindSmock(mock, provider)
}
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