Commit 528f57f7 authored by Kelvin Fichter's avatar Kelvin Fichter

Initial commit

parents
node_modules/
artifacts/
cache/
/// <reference types="@nomiclabs/buidler-ethers/src/type-extensions" />
/// <reference types="@nomiclabs/buidler-waffle/src/type-extensions" />
\ No newline at end of file
import { usePlugin, BuidlerConfig } from '@nomiclabs/buidler/config'
import {
DEFAULT_ACCOUNTS_BUIDLER,
GAS_LIMIT,
} from './test/helpers/constants'
usePlugin('@nomiclabs/buidler-ethers')
usePlugin('@nomiclabs/buidler-waffle')
const config: BuidlerConfig = {
networks: {
buidlerevm: {
accounts: DEFAULT_ACCOUNTS_BUIDLER,
blockGasLimit: GAS_LIMIT * 2,
},
},
mocha: {
timeout: 50000,
},
solc: {
version: "0.7.0",
optimizer: { enabled: true, runs: 200 },
},
}
export default config
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Library Imports */
import { Lib_EthUtils } from "../../libraries/utils/Lib_EthUtils.sol";
/* Interface Imports */
import { iOVM_DataTypes } from "../../iOVM/codec/iOVM_DataTypes.sol";
import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManager.sol";
import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol";
import { iOVM_SafetyChecker } from "../../iOVM/execution/iOVM_SafetyChecker.sol";
/**
* @title OVM_ExecutionManager
*/
contract OVM_ExecutionManager is iOVM_ExecutionManager {
/********************************
* External Contract References *
********************************/
iOVM_SafetyChecker public ovmSafetyChecker;
iOVM_StateManager public ovmStateManager;
/*******************************
* Execution Context Variables *
*******************************/
GlobalContext internal globalContext;
TransactionContext internal transactionContext;
MessageContext internal messageContext;
TransactionRecord internal transactionRecord;
MessageRecord internal messageRecord;
/**************************
* Gas Metering Constants *
**************************/
uint256 constant NUISANCE_GAS_SLOAD = 20000;
uint256 constant NUISANCE_GAS_SSTORE = 20000;
uint256 constant NUISANCE_GAS_PER_CONTRACT_BYTE = 100;
uint256 constant MIN_GAS_FOR_INVALID_STATE_ACCESS = 30000;
/***************
* Constructor *
***************/
/**
* @param _ovmSafetyChecker Address of the iOVM_SafetyChecker implementation.
*/
constructor(
address _ovmSafetyChecker
) {
ovmSafetyChecker = iOVM_SafetyChecker(_ovmSafetyChecker);
}
/**********************
* Function Modifiers *
**********************/
/**
* Applies a net gas cost refund to a transaction to account for the difference in execution
* between L1 and L2.
* @param _cost Gas cost for the function after the refund.
*/
modifier netGasCost(
uint256 _cost
) {
uint256 preExecutionGas = gasleft();
_;
uint256 postExecutionGas = gasleft();
// We want to refund everything *except* the specified cost.
transactionRecord.ovmGasRefund += (
(preExecutionGas - postExecutionGas) - _cost
);
}
/************************************
* Transaction Execution Entrypoint *
************************************/
/**
* Starts the execution of a transaction via the OVM_ExecutionManager.
* @param _transaction Transaction data to be executed.
* @param _ovmStateManager iOVM_StateManager implementation providing account state.
*/
function run(
iOVM_DataTypes.OVMTransactionData memory _transaction,
address _ovmStateManager
)
override
public
{
}
/******************************
* Opcodes: Execution Context *
******************************/
/**
* @notice Overrides CALLER.
* @return _CALLER Address of the CALLER within the current message context.
*/
function ovmCALLER()
override
public
returns (
address _CALLER
)
{
return messageContext.ovmCALLER;
}
/**
* @notice Overrides ADDRESS.
* @return _ADDRESS Active ADDRESS within the current message context.
*/
function ovmADDRESS()
override
public
returns (
address _ADDRESS
)
{
return messageContext.ovmADDRESS;
}
/**
* @notice Overrides ORIGIN.
* @return _ORIGIN Address of the ORIGIN within the transaction context.
*/
function ovmORIGIN()
override
public
returns (
address _ORIGIN
)
{
return transactionContext.ovmORIGIN;
}
/**
* @notice Overrides TIMESTAMP.
* @return _TIMESTAMP Value of the TIMESTAMP within the transaction context.
*/
function ovmTIMESTAMP()
override
public
returns (
uint256 _TIMESTAMP
)
{
return transactionContext.ovmTIMESTAMP;
}
/**
* @notice Overrides GASLIMIT.
* @return _GASLIMIT Value of the block's GASLIMIT within the transaction context.
*/
function ovmGASLIMIT()
override
public
returns (
uint256 _GASLIMIT
)
{
return transactionContext.ovmGASLIMIT;
}
/**
* @notice Overrides CHAINID.
* @return _CHAINID Value of the chain's CHAINID within the global context.
*/
function ovmCHAINID()
override
public
returns (
uint256 _CHAINID
)
{
return globalContext.ovmCHAINID;
}
/********************
* Opcodes: Halting *
********************/
/**
* @notice Overrides REVERT.
* @param _data Bytes data to pass along with the REVERT.
*/
function ovmREVERT(
bytes memory _data
)
override
public
{
_revertWithFlag(RevertFlag.INTENTIONAL_REVERT, _data);
}
/******************************
* Opcodes: Contract Creation *
******************************/
/**
* @notice Overrides CREATE.
* @param _bytecode Code to be used to CREATE a new contract.
* @return _contract Address of the created contract.
*/
function ovmCREATE(
bytes memory _bytecode
)
override
public
netGasCost(40000 + _bytecode.length * 100)
returns (
address _contract
)
{
// Creator is always the current ADDRESS.
address creator = ovmADDRESS();
// Generate the correct CREATE address.
address contractAddress = Lib_EthUtils.getAddressForCREATE(
creator,
_getAccount(creator).nonce
);
_createContract(
contractAddress,
_bytecode
);
return contractAddress;
}
/**
* @notice Overrides CREATE2.
* @param _bytecode Code to be used to CREATE2 a new contract.
* @param _salt Value used to determine the contract's address.
* @return _contract Address of the created contract.
*/
function ovmCREATE2(
bytes memory _bytecode,
bytes32 _salt
)
override
public
netGasCost(40000 + _bytecode.length * 100)
returns (
address _contract
)
{
// Creator is always the current ADDRESS.
address creator = ovmADDRESS();
// Generate the correct CREATE2 address.
address contractAddress = Lib_EthUtils.getAddressForCREATE2(
creator,
_bytecode,
_salt
);
_createContract(
contractAddress,
_bytecode
);
return contractAddress;
}
/*********************************
* Opcodes: Contract Interaction *
*********************************/
/**
* @notice Overrides CALL.
* @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
netGasCost(100000)
returns (
bool _success,
bytes memory _returndata
)
{
// CALL updates the CALLER and ADDRESS.
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address;
return _callContract(
nextMessageContext,
_gasLimit,
_address,
_calldata
);
}
/**
* @notice Overrides STATICCALL.
* @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 ovmSTATICCALL(
uint256 _gasLimit,
address _address,
bytes memory _calldata
)
override
public
netGasCost(80000)
returns (
bool _success,
bytes memory _returndata
)
{
// STATICCALL updates the CALLER, updates the ADDRESS, and runs in a static context.
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmCALLER = nextMessageContext.ovmADDRESS;
nextMessageContext.ovmADDRESS = _address;
nextMessageContext.isStatic = true;
return _callContract(
nextMessageContext,
_gasLimit,
_address,
_calldata
);
}
/**
* @notice Overrides DELEGATECALL.
* @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 ovmDELEGATECALL(
uint256 _gasLimit,
address _address,
bytes memory _calldata
)
override
public
netGasCost(40000)
returns (
bool _success,
bytes memory _returndata
)
{
// DELEGATECALL does not change anything about the message context.
MessageContext memory nextMessageContext = messageContext;
return _callContract(
nextMessageContext,
_gasLimit,
_address,
_calldata
);
}
/************************************
* Opcodes: Contract Storage Access *
************************************/
/**
* @notice Overrides SLOAD.
* @param _key 32 byte key of the storage slot to load.
* @return _value 32 byte value of the requested storage slot.
*/
function ovmSLOAD(
bytes32 _key
)
override
public
netGasCost(40000)
returns (
bytes32 _value
)
{
// We always SLOAD from the storage of ADDRESS.
address contractAddress = ovmADDRESS();
return _getContractStorage(
contractAddress,
_key
);
}
/**
* @notice Overrides SSTORE.
* @param _key 32 byte key of the storage slot to set.
* @param _value 32 byte value for the storage slot.
*/
function ovmSSTORE(
bytes32 _key,
bytes32 _value
)
override
public
netGasCost(60000)
{
// We always SSTORE to the storage of ADDRESS.
address contractAddress = ovmADDRESS();
_putContractStorage(
contractAddress,
_key,
_value
);
}
/*********************************
* Opcodes: Contract Code Access *
*********************************/
/**
* @notice Overrides EXTCODECOPY.
* @param _contract Address of the contract to copy code from.
* @param _offset Offset in bytes from the start of contract code to copy beyond.
* @param _length Total number of bytes to copy from the contract's code.
* @return _code Bytes of code copied from the requested contract.
*/
function ovmEXTCODECOPY(
address _contract,
uint256 _offset,
uint256 _length
)
override
public
returns (
bytes memory _code
)
{
// `ovmEXTCODECOPY` is the only overridden opcode capable of producing exactly one byte of
// return data. By blocking reads of one byte, we're able to use the condition that an
// OVM_ExecutionManager function return value having a length of exactly one byte indicates
// an error without an explicit revert. If users were able to read a single byte, they
// could forcibly trigger behavior that should only be available to this contract.
uint256 length = _length == 1 ? 2 : _length;
return Lib_EthUtils.getCode(
_getAccount(_contract).ethAddress,
_offset,
_length
);
}
/**
* @notice Overrides EXTCODESIZE.
* @param _contract Address of the contract to query the size of.
* @return _EXTCODESIZE Size of the requested contract in bytes.
*/
function ovmEXTCODESIZE(
address _contract
)
override
public
returns (
uint256 _EXTCODESIZE
)
{
return Lib_EthUtils.getCodeSize(
_getAccount(_contract).ethAddress
);
}
/**
* @notice Overrides EXTCODEHASH.
* @param _contract Address of the contract to query the hash of.
* @return _EXTCODEHASH Size of the requested contract in bytes.
*/
function ovmEXTCODEHASH(
address _contract
)
override
public
returns (
bytes32 _EXTCODEHASH
)
{
return Lib_EthUtils.getCodeHash(
_getAccount(_contract).ethAddress
);
}
/**************************************
* Public Functions: Execution Safety *
**************************************/
/**
* Performs the logic to create a contract and revert under various potential conditions.
* @dev This function is implemented as `public` because we need to be able to revert a
* contract creation without losing information about exactly *why* the contract reverted.
* In particular, we want to be sure that contracts cannot trigger an INVALID_STATE_ACCESS
* flag and then revert to reset the flag. We're able to do this by making an external
* call from `ovmCREATE` and `ovmCREATE2` to `safeCREATE`, which can capture and relay
* information before reverting.
* @param _address Address of the contract to associate with the one being created.
* @param _bytecode Code to be used to create the new contract.
*/
function safeCREATE(
address _address,
bytes memory _bytecode
)
override
public
{
// Since this function is public, anyone can attempt to directly call it. We need to make
// sure that the OVM_ExecutionManager itself is the only party that can actually try to
// call this function.
if (msg.sender != address(this)) {
return;
}
// Check the creation bytecode against the OVM_SafetyChecker.
if (ovmSafetyChecker.isBytecodeSafe(_bytecode) == false) {
_revertWithFlag(RevertFlag.UNSAFE_BYTECODE);
}
// We always need to initialize the contract with the default account values.
_initPendingAccount(_address);
// Actually deploy the contract and retrieve its address. This step is hiding a lot of
// complexity because we need to ensure that contract creation *never* reverts by itself.
// We cover this partially by storing a revert flag and returning (instead of reverting)
// when we know that we're inside a contract's creation code.
address ethAddress = Lib_EthUtils.createContract(_bytecode);
// Here we pull out the revert flag that would've been set during creation code. Now that
// we're out of creation code again, we can just revert normally while passing the flag
// through the revert data.
if (messageRecord.revertFlag != RevertFlag.DID_NOT_REVERT) {
_revertWithFlag(messageRecord.revertFlag);
}
// Again simply checking that the deployed code is safe too. Contracts can generate
// arbitrary deployment code, so there's no easy way to analyze this beforehand.
bytes memory deployedCode = Lib_EthUtils.getCode(ethAddress);
if (ovmSafetyChecker.isBytecodeSafe(deployedCode) == false) {
_revertWithFlag(RevertFlag.UNSAFE_BYTECODE);
}
// Contract creation didn't need to be reverted and the bytecode is safe. We finish up by
// associating the desired address with the newly created contract's code hash and address.
_commitPendingAccount(
_address,
ethAddress,
keccak256(deployedCode)
);
}
/********************************************
* Internal Functions: Contract Interaction *
********************************************/
/**
* Creates a new contract and associates it with some contract address.
* @param _contractAddress Address to associate the created contract with.
* @param _bytecode Bytecode to be used to create the contract.
*/
function _createContract(
address _contractAddress,
bytes memory _bytecode
)
internal
{
// We always update the nonce of the creating account, even if the creation fails.
_incrementAccountNonce(ovmADDRESS());
// We're stepping into a CREATE or CREATE2, so we need to update ADDRESS to point
// to the contract's associated address.
MessageContext memory nextMessageContext = messageContext;
nextMessageContext.ovmADDRESS = _contractAddress;
// Run `safeCREATE` in a new EVM message so that our changes can be reflected even if
// `safeCREATE` reverts.
_handleExternalInteraction(
nextMessageContext,
gasleft(),
address(this),
abi.encodeWithSignature(
"safeCREATE(address,bytes)",
_contractAddress,
_bytecode
)
);
// Need to make sure that this flag is reset so that it isn't propagated to creations in
// some parent EVM message.
messageRecord.revertFlag = RevertFlag.DID_NOT_REVERT;
}
/**
* Calls the deployed contract associated with a given address.
* @param _nextMessageContext Message context to be used for the call.
* @param _gasLimit Amount of gas to be passed into this call.
* @param _contract Address used to resolve the deployed contract.
* @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 _callContract(
MessageContext memory _nextMessageContext,
uint256 _gasLimit,
address _contract,
bytes memory _calldata
)
internal
returns (
bool _success,
bytes memory _returndata
)
{
return _handleExternalInteraction(
_nextMessageContext,
_gasLimit,
_getAccount(_contract).ethAddress,
_calldata
);
}
/**
* Handles the logic of making an external call and parsing revert information.
* @param _nextMessageContext Message context to be used for the call.
* @param _gasLimit Amount of gas to be passed into this call.
* @param _target Address of the contract to call.
* @param _data 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 _handleExternalInteraction(
MessageContext memory _nextMessageContext,
uint256 _gasLimit,
address _target,
bytes memory _data
)
internal
returns (
bool _success,
bytes memory _returndata
)
{
// We need to switch over to our next message context for the duration of this call.
MessageContext memory prevMessageContext = messageContext;
_switchMessageContext(prevMessageContext, _nextMessageContext);
// Nuisance gas is a system used to bound the ability for an attacker to make fraud proofs
// expensive by touching a lot of different accounts or storage slots. Since most contracts
// only use a few storage slots during any given transaction, this shouldn't be a limiting
// factor.
uint256 prevNuisanceGasLeft = messageRecord.nuisanceGasLeft;
uint256 nuisanceGasLimit = _getNuisanceGasLimit(_gasLimit);
messageRecord.nuisanceGasLeft = nuisanceGasLimit;
// Make the call and make sure to pass in the gas limit. Another instance of hidden
// complexity. `_target` is guaranteed to be a safe contract, meaning its return/revert
// behavior can be controlled. In particular, we enforce that flags are passed through
// revert data as to retrieve execution metadata that would normally be reverted out of
// existence.
(bool success, bytes memory returndata) = _target.call{gas: _gasLimit}(_data);
// Reverts at this point are completely OK, but we need to make a few updates based on the
// information passed through the revert.
if (success == false) {
(
RevertFlag flag,
uint256 nuisanceGasLeft,
uint256 ovmGasRefund,
) = _decodeRevertData(returndata);
// INVALID_STATE_ACCESS is the only flag that triggers an immediate abort of the
// parent EVM message. This behavior is necessary because INVALID_STATE_ACCESS must
// halt any further transaction execution that could impact the execution result.
if (flag == RevertFlag.INVALID_STATE_ACCESS) {
_revertWithFlag(flag);
}
// INTENTIONAL_REVERT and UNSAFE_BYTECODE aren't dependent on the input state, so we
// can just handle them like standard reverts. Our only change here is to record the
// gas refund reported by the call (enforced by safety checking).
if (
flag == RevertFlag.INTENTIONAL_REVERT
|| flag == RevertFlag.UNSAFE_BYTECODE
) {
transactionRecord.ovmGasRefund = ovmGasRefund;
}
// Reverts mean we need to use up whatever "nuisance gas" was used by the call.
// EXCEEDS_NUISANCE_GAS explicitly reduces the remaining nuisance gas for this message
// to zero. OUT_OF_GAS is a "pseudo" flag given that messages return no data when they
// run out of gas, so we have to treat this like EXCEEDS_NUISANCE_GAS. All other flags
// will simply pass up the remaining nuisance gas.
messageRecord.nuisanceGasLeft = prevNuisanceGasLeft - (nuisanceGasLimit - nuisanceGasLeft);
}
// Switch back to the original message context now that we're out of the call.
_switchMessageContext(_nextMessageContext, prevMessageContext);
return (
success,
returndata
);
}
/******************************************
* Internal Functions: State Manipulation *
******************************************/
/**
* Retrieves an account from the OVM_StateManager.
* @param _address Address of the account to retrieve.
* @return _account Retrieved account object.
*/
function _getAccount(
address _address
)
internal
returns (
iOVM_DataTypes.OVMAccount memory _account
)
{
// We need to make sure that the transaction isn't trying to access an account that hasn't
// been provided to the OVM_StateManager. We'll immediately abort if this is the case.
_checkInvalidStateAccess(
ovmStateManager.hasAccount(_address)
);
// Check whether the account has been loaded before and mark it as loaded if not. We need
// this because "nuisance gas" only applies to the first time that an account is loaded.
(
bool _wasAccountAlreadyLoaded
) = ovmStateManager.testAndSetAccountLoaded(_address);
// Actually retrieve the account.
iOVM_DataTypes.OVMAccount memory account = ovmStateManager.getAccount(_address);
// If we hadn't already loaded the account, then we'll need to charge "nuisance gas" based
// on the size of the contract code.
if (_wasAccountAlreadyLoaded == false) {
_useNuisanceGas(
Lib_EthUtils.getCodeSize(account.ethAddress) * NUISANCE_GAS_PER_CONTRACT_BYTE
);
}
return account;
}
/**
* Increments the nonce of an account.
* @param _address Address of the account to bump.
*/
function _incrementAccountNonce(
address _address
)
internal
{
ovmStateManager.incrementAccountNonce(_address);
}
/**
* Creates the default account object for the given address.
* @param _address Address of the account create.
*/
function _initPendingAccount(
address _address
)
internal
{
ovmStateManager.initPendingAccount(_address);
}
/**
* Stores additional relevant data for a new account, thereby "committing" it to the state.
* This function is only called during `ovmCREATE` and `ovmCREATE2` after a successful contract
* creation.
* @param _address Address of the account to commit.
* @param _ethAddress Address of the associated deployed contract.
* @param _codeHash Hash of the code stored at the address.
*/
function _commitPendingAccount(
address _address,
address _ethAddress,
bytes32 _codeHash
)
internal
{
// Check whether the account has been changed before and mark it as changed if not. We need
// this because "nuisance gas" only applies to the first time that an account is changed.
(
bool _wasAccountAlreadyChanged
) = ovmStateManager.testAndSetAccountChanged(_address);
// If we hadn't already changed the account, then we'll need to charge "nuisance gas" based
// on the size of the contract code.
if (_wasAccountAlreadyChanged == false) {
_useNuisanceGas(
Lib_EthUtils.getCodeSize(_ethAddress) * NUISANCE_GAS_PER_CONTRACT_BYTE
);
}
// Actually commit the contract.
ovmStateManager.commitPendingAccount(
_address,
_ethAddress,
_codeHash
);
}
/**
* Retrieves the value of a storage slot.
* @param _contract Address of the contract to query.
* @param _key 32 byte key of the storage slot.
* @return _value 32 byte storage slot value.
*/
function _getContractStorage(
address _contract,
bytes32 _key
)
internal
returns (
bytes32 _value
)
{
// We need to make sure that the transaction isn't trying to access storage that hasn't
// been provided to the OVM_StateManager. We'll immediately abort if this is the case.
_checkInvalidStateAccess(
ovmStateManager.hasContractStorage(_contract, _key)
);
// Check whether the slot has been loaded before and mark it as loaded if not. We need
// this because "nuisance gas" only applies to the first time that a slot is loaded.
(
bool _wasContractStorageAlreadyLoaded
) = ovmStateManager.testAndSetContractStorageLoaded(_contract, _key);
// If we hadn't already loaded the account, then we'll need to charge some fixed amount of
// "nuisance gas".
if (_wasContractStorageAlreadyLoaded == false) {
_useNuisanceGas(NUISANCE_GAS_SLOAD);
}
// Actually retrieve the storage slot.
return ovmStateManager.getContractStorage(_contract, _key);
}
/**
* Sets the value of a storage slot.
* @param _contract Address of the contract to modify.
* @param _key 32 byte key of the storage slot.
* @param _value 32 byte storage slot value.
*/
function _putContractStorage(
address _contract,
bytes32 _key,
bytes32 _value
)
internal
{
// Check whether the slot has been changed before and mark it as changed if not. We need
// this because "nuisance gas" only applies to the first time that a slot is changed.
(
bool _wasContractStorageAlreadyChanged
) = ovmStateManager.testAndSetContractStorageChanged(_contract, _key);
// If we hadn't already changed the account, then we'll need to charge some fixed amount of
// "nuisance gas".
if (_wasContractStorageAlreadyChanged == false) {
_useNuisanceGas(NUISANCE_GAS_SSTORE);
}
// Actually modify the storage slot.
ovmStateManager.putContractStorage(_contract, _key, _value);
}
/************************************
* Internal Functions: Revert Logic *
************************************/
/**
* Simple encoding for revert data.
* @param _flag Flag to revert with.
* @param _data Additional user-provided revert data.
* @return _revertdata Encoded revert data.
*/
function _encodeRevertData(
RevertFlag _flag,
bytes memory _data
)
internal
returns (
bytes memory _revertdata
)
{
// Running out of gas will return no data, so simulating it shouldn't either.
if (_flag == RevertFlag.OUT_OF_GAS) {
return bytes('');
}
// Just ABI encode the rest of the parameters.
return abi.encode(
_flag,
messageRecord.nuisanceGasLeft,
transactionRecord.ovmGasRefund,
_data
);
}
/**
* Simple decoding for revert data.
* @param _revertdata Revert data to decode.
* @return _flag Flag used to revert.
* @return _nuisanceGasLeft Amount of nuisance gas unused by the message.
* @return _ovmGasRefund Amount of gas refunded during the message.
* @return _data Additional user-provided revert data.
*/
function _decodeRevertData(
bytes memory _revertdata
)
internal
returns (
RevertFlag _flag,
uint256 _nuisanceGasLeft,
uint256 _ovmGasRefund,
bytes memory _data
)
{
// A length of zero means the call ran out of gas, just return empty data.
if (_revertdata.length == 0) {
return (
RevertFlag.OUT_OF_GAS,
0,
0,
bytes('')
);
}
// ABI decode the incoming data.
return abi.decode(_revertdata, (RevertFlag, uint256, uint256, bytes));
}
/**
* Causes a message to revert or abort.
* @param _flag Flag to revert with.
* @param _data Additional user-provided data.
*/
function _revertWithFlag(
RevertFlag _flag,
bytes memory _data
)
internal
{
// We don't want to revert when we're inside a CREATE or CREATE2, because those opcodes
// fail silently (we can't pass any data upwards). Instead, we set a flag and return a
// *single* byte, something the OVM_ExecutionManager will not return in any other case.
// We're thereby allowed to communicate failure without allowing contracts to trick us into
// thinking there was a failure.
if (_inCreationContext()) {
messageRecord.revertFlag = _flag;
assembly {
return(0, 1)
}
}
// If we're not inside a CREATE or CREATE2, we can simply encode the necessary data and
// revert normally.
bytes memory revertdata = _encodeRevertData(
_flag,
_data
);
assembly {
revert(add(revertdata, 0x20), mload(revertdata))
}
}
/**
* Causes a message to revert or abort.
* @param _flag Flag to revert with.
*/
function _revertWithFlag(
RevertFlag _flag
)
internal
{
_revertWithFlag(_flag, bytes(''));
}
/**
* Checks for an attempt to access some inaccessible state.
* @param _condition Result of some function that checks for bad access.
*/
function _checkInvalidStateAccess(
bool _condition
)
internal
{
// Another case of hidden complexity. If we didn't enforce this requirement, then a
// contract could pass in just enough gas to cause this to fail on L1 but not on L2.
// A contract could use this behavior to prevent the OVM_ExecutionManager from detecting
// an invalid state access. Reverting with OUT_OF_GAS allows us to also charge for the
// full message nuisance gas as to generally disincentivize this attack.
if (gasleft() < MIN_GAS_FOR_INVALID_STATE_ACCESS) {
_revertWithFlag(RevertFlag.OUT_OF_GAS);
}
// We have enough gas to comfortably run this revert, so do it.
if (_condition == false) {
_revertWithFlag(RevertFlag.INVALID_STATE_ACCESS);
}
}
/******************************************
* Internal Functions: Nuisance Gas Logic *
******************************************/
/**
* Computes the nuisance gas limit from the gas limit.
* @dev This function is currently using a naive implementation whereby the nuisance gas limit
* is set to exactly equal the lesser of the gas limit or remaining gas. It's likely that
* this implementation is perfectly fine, but we may change this formula later.
* @param _gasLimit Gas limit to compute from.
* @return _nuisanceGasLimit Computed nuisance gas limit.
*/
function _getNuisanceGasLimit(
uint256 _gasLimit
)
internal
view
returns (
uint256 _nuisanceGasLimit
)
{
return _gasLimit < gasleft() ? _gasLimit : gasleft();
}
/**
* Uses a certain amount of nuisance gas.
* @param _amount Amount of nuisance gas to use.
*/
function _useNuisanceGas(
uint256 _amount
)
internal
{
// Essentially the same as a standard OUT_OF_GAS, except we also retain a record of the gas
// refund to be given at the end of the transaction.
if (messageRecord.nuisanceGasLeft < _amount) {
_revertWithFlag(RevertFlag.EXCEEDS_NUISANCE_GAS);
}
messageRecord.nuisanceGasLeft -= _amount;
}
/*****************************************
* Internal Functions: Execution Context *
*****************************************/
/**
* Swaps over to a new message context.
* @param _prevMessageContext Context we're switching from.
* @param _nextMessageContext Context we're switching to.
*/
function _switchMessageContext(
MessageContext memory _prevMessageContext,
MessageContext memory _nextMessageContext
)
internal
{
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.ovmCALLER != _nextMessageContext.ovmCALLER) {
messageContext.ovmCALLER = _nextMessageContext.ovmCALLER;
}
// Avoid unnecessary the SSTORE.
if (_prevMessageContext.isStatic != _nextMessageContext.isStatic) {
messageContext.isStatic = _nextMessageContext.isStatic;
}
}
/**
* Checks whether we're inside contract creation code.
* @return _inCreation Whether or not we're in a contract creation.
*/
function _inCreationContext()
internal
returns (
bool _inCreation
)
{
// An interesting "hack" of sorts. Since the contract doesn't exist yet, it won't have any
// stored contract code. A simple-but-elegant way to detect this condition.
return (
ovmADDRESS() != address(0)
&& ovmEXTCODESIZE(ovmADDRESS()) == 0
);
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Interface Imports */
import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol";
import { iOVM_DataTypes } from "../../iOVM/codec/iOVM_DataTypes.sol";
contract OVM_StateManager is iOVM_StateManager {
enum ItemState {
ITEM_UNTOUCHED,
ITEM_LOADED,
ITEM_CHANGED
}
mapping (address => iOVM_DataTypes.OVMAccount) public accounts;
mapping (address => iOVM_DataTypes.OVMAccount) public pendingAccounts;
mapping (address => mapping (bytes32 => bytes32)) public contractStorage;
mapping (address => mapping (bytes32 => bool)) public verifiedContractStorage;
mapping (bytes32 => ItemState) public itemStates;
function putAccount(
address _address,
iOVM_DataTypes.OVMAccount memory _account
)
override
public
{
accounts[_address] = _account;
}
function getAccount(address _address)
override
public
returns (
iOVM_DataTypes.OVMAccount memory _account
)
{
return accounts[_address];
}
function hasAccount(
address _address
)
override
public
returns (
bool _exists
)
{
return getAccount(_address).codeHash != bytes32(0);
}
function incrementAccountNonce(
address _address
)
override
public
{
accounts[_address].nonce += 1;
}
function initPendingAccount(
address _address
)
override
public
{
iOVM_DataTypes.OVMAccount storage account = accounts[_address];
account.nonce = 1;
account.codeHash = keccak256(hex'80');
}
function commitPendingAccount(
address _address,
address _ethAddress,
bytes32 _codeHash
)
override
public
{
iOVM_DataTypes.OVMAccount storage account = accounts[_address];
account.ethAddress = _ethAddress;
account.codeHash = _codeHash;
}
function putContractStorage(
address _contract,
bytes32 _key,
bytes32 _value
)
override
public
{
contractStorage[_contract][_key] = _value;
verifiedContractStorage[_contract][_key] = true;
}
function getContractStorage(
address _contract,
bytes32 _key
)
override
public
returns (
bytes32 _value
)
{
return contractStorage[_contract][_key];
}
function hasContractStorage(
address _contract,
bytes32 _key
)
override
public
returns (
bool _exists
)
{
return verifiedContractStorage[_contract][_key];
}
function testAndSetAccountLoaded(
address _address
)
override
public
returns (
bool _wasAccountAlreadyLoaded
)
{
return _testItemState(
keccak256(abi.encodePacked(_address)),
ItemState.ITEM_LOADED
);
}
function testAndSetAccountChanged(
address _address
)
override
public
returns (
bool _wasAccountAlreadyChanged
)
{
return _testItemState(
keccak256(abi.encodePacked(_address)),
ItemState.ITEM_CHANGED
);
}
function testAndSetContractStorageLoaded(
address _contract,
bytes32 _key
)
override
public
returns (
bool _wasContractStorageAlreadyLoaded
)
{
return _testItemState(
keccak256(abi.encodePacked(_contract, _key)),
ItemState.ITEM_LOADED
);
}
function testAndSetContractStorageChanged(
address _contract,
bytes32 _key
)
override
public
returns (
bool _wasContractStorageAlreadyChanged
)
{
return _testItemState(
keccak256(abi.encodePacked(_contract, _key)),
ItemState.ITEM_CHANGED
);
}
/*
* Internal Functions
*/
function _testItemState(
bytes32 _item,
ItemState _minItemState
)
internal
returns (
bool _wasItemState
)
{
ItemState itemState = itemStates[_item];
bool wasItemState = itemState >= _minItemState;
if (wasItemState == false) {
itemStates[_item] = _minItemState;
}
return wasItemState;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
interface iOVM_DataTypes {
struct OVMAccount {
uint256 nonce;
uint256 balance;
bytes32 storageRoot;
bytes32 codeHash;
address ethAddress;
}
struct EVMAccount {
uint256 nonce;
uint256 balance;
bytes32 storageRoot;
bytes32 codeHash;
}
struct OVMChainBatchHeader {
uint256 batchIndex;
bytes32 batchRoot;
uint256 batchSize;
uint256 prevTotalElements;
bytes extraData;
}
struct OVMChainInclusionProof {
uint256 index;
bytes32[] siblings;
}
struct OVMTransactionData {
uint256 timestamp;
uint256 queueOrigin;
address entrypoint;
address origin;
address msgSender;
uint256 gasLimit;
bytes data;
}
struct OVMProofMatrix {
bool checkNonce;
bool checkBalance;
bool checkStorageRoot;
bool checkCodeHash;
}
struct OVMQueueElement {
uint256 timestamp;
bytes32 batchRoot;
bool isL1ToL2Batch;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Interface Imports */
import { iOVM_DataTypes } from "../codec/iOVM_DataTypes.sol";
interface iOVM_ExecutionManager {
enum RevertFlag {
DID_NOT_REVERT,
OUT_OF_GAS,
INTENTIONAL_REVERT,
EXCEEDS_NUISANCE_GAS,
INVALID_STATE_ACCESS,
UNSAFE_BYTECODE
}
struct GlobalContext {
uint256 ovmCHAINID;
}
struct TransactionContext {
address ovmORIGIN;
uint256 ovmTIMESTAMP;
uint256 ovmGASLIMIT;
uint256 ovmTXGASLIMIT;
uint256 ovmQUEUEORIGIN;
}
struct TransactionRecord {
uint256 ovmGasRefund;
}
struct MessageContext {
address ovmCALLER;
address ovmADDRESS;
bool isStatic;
}
struct MessageRecord {
uint256 nuisanceGasLeft;
RevertFlag revertFlag;
}
function run(
iOVM_DataTypes.OVMTransactionData calldata _transaction,
address _txStateManager
) external;
/*******************
* Context Opcodes *
*******************/
function ovmCALLER() external returns (address _caller);
function ovmADDRESS() external returns (address _address);
function ovmORIGIN() external returns (address _origin);
function ovmTIMESTAMP() external returns (uint256 _timestamp);
function ovmGASLIMIT() external returns (uint256 _gasLimit);
function ovmCHAINID() external returns (uint256 _chainId);
/*******************
* Halting Opcodes *
*******************/
function ovmREVERT(bytes memory _data) external;
/*****************************
* Contract Creation Opcodes *
*****************************/
function ovmCREATE(bytes memory _bytecode) external returns (address _contract);
function ovmCREATE2(bytes memory _bytecode, bytes32 _salt) external returns (address _contract);
function safeCREATE(address _address, bytes memory _bytecode) external;
/****************************
* Contract Calling Opcodes *
****************************/
function ovmCALL(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);
/****************************
* Contract Storage Opcodes *
****************************/
function ovmSLOAD(bytes32 _key) external returns (bytes32 _value);
function ovmSSTORE(bytes32 _key, bytes32 _value) external;
/*************************
* Contract Code Opcodes *
*************************/
function ovmEXTCODECOPY(address _contract, uint256 _offset, uint256 _length) external returns (bytes memory _code);
function ovmEXTCODESIZE(address _contract) external returns (uint256 _size);
function ovmEXTCODEHASH(address _contract) external returns (bytes32 _hash);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
interface iOVM_SafetyChecker {
function isBytecodeSafe(bytes memory _bytecode) external view returns (bool _safe);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Interface Imports */
import { iOVM_DataTypes } from "../codec/iOVM_DataTypes.sol";
interface iOVM_StateManager {
function putAccount(address _address, iOVM_DataTypes.OVMAccount memory _account) external;
function getAccount(address _address) external returns (iOVM_DataTypes.OVMAccount memory _account);
function hasAccount(address _address) external returns (bool _exists);
function incrementAccountNonce(address _address) external;
function initPendingAccount(address _address) external;
function commitPendingAccount(address _address, address _ethAddress, bytes32 _codeHash) external;
function putContractStorage(address _contract, bytes32 _key, bytes32 _value) external;
function getContractStorage(address _contract, bytes32 _key) external returns (bytes32 _value);
function hasContractStorage(address _contract, bytes32 _key) external returns (bool _exists);
function testAndSetAccountLoaded(address _address) external returns (bool _wasAccountAlreadyLoaded);
function testAndSetAccountChanged(address _address) external returns (bool _wasAccountAlreadyChanged);
function testAndSetContractStorageLoaded(address _contract, bytes32 _key) external returns (bool _wasContractStorageAlreadyLoaded);
function testAndSetContractStorageChanged(address _contract, bytes32 _key) external returns (bool _wasContractStorageAlreadyChanged);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/**
* @title RLPReader
* @author Hamdi Allam hamdi.allam97@gmail.com
*/
library Lib_RLPReader {
/*
* Data Structures
*/
struct RLPItem {
uint len;
uint memPtr;
}
/*
* Contract Constants
*/
uint8 constant private STRING_SHORT_START = 0x80;
uint8 constant private STRING_LONG_START = 0xb8;
uint8 constant private LIST_SHORT_START = 0xc0;
uint8 constant private LIST_LONG_START = 0xf8;
uint8 constant private WORD_SIZE = 32;
/*
* Public Functions
*/
/**
* @param item RLP encoded bytes
*/
function toRlpItem(
bytes memory item
)
internal
pure
returns (RLPItem memory)
{
uint memPtr;
assembly {
memPtr := add(item, 0x20)
}
return RLPItem(item.length, memPtr);
}
/**
* @param item RLP encoded bytes
*/
function rlpLen(
RLPItem memory item
)
internal
pure
returns (uint)
{
return item.len;
}
/**
* @param item RLP encoded bytes
*/
function payloadLen(
RLPItem memory item
)
internal
pure
returns (uint)
{
return item.len - _payloadOffset(item.memPtr);
}
/**
* @param item RLP encoded list in bytes
*/
function toList(
RLPItem memory item
)
internal
pure
returns (RLPItem[] memory result)
{
require(isList(item));
uint items = numItems(item);
result = new RLPItem[](items);
uint memPtr = item.memPtr + _payloadOffset(item.memPtr);
uint dataLen;
for (uint i = 0; i < items; i++) {
dataLen = _itemLength(memPtr);
result[i] = RLPItem(dataLen, memPtr);
memPtr = memPtr + dataLen;
}
}
// @return indicator whether encoded payload is a list. negate this function call for isData.
function isList(
RLPItem memory item
)
internal
pure
returns (bool)
{
if (item.len == 0) return false;
uint8 byte0;
uint memPtr = item.memPtr;
assembly {
byte0 := byte(0, mload(memPtr))
}
if (byte0 < LIST_SHORT_START)
return false;
return true;
}
/** RLPItem conversions into data types **/
// @returns raw rlp encoding in bytes
function toRlpBytes(
RLPItem memory item
)
internal
pure
returns (bytes memory)
{
bytes memory result = new bytes(item.len);
if (result.length == 0) return result;
uint ptr;
assembly {
ptr := add(0x20, result)
}
copy(item.memPtr, ptr, item.len);
return result;
}
// any non-zero byte is considered true
function toBoolean(
RLPItem memory item
)
internal
pure
returns (bool)
{
require(item.len == 1);
uint result;
uint memPtr = item.memPtr;
assembly {
result := byte(0, mload(memPtr))
}
return result == 0 ? false : true;
}
function toAddress(
RLPItem memory item
)
internal
pure
returns (address)
{
// 1 byte for the length prefix
require(item.len == 21);
return address(toUint(item));
}
function toUint(
RLPItem memory item
)
internal
pure
returns (uint)
{
require(item.len > 0 && item.len <= 33);
uint offset = _payloadOffset(item.memPtr);
uint len = item.len - offset;
uint result;
uint memPtr = item.memPtr + offset;
assembly {
result := mload(memPtr)
// shfit to the correct location if neccesary
if lt(len, 32) {
result := div(result, exp(256, sub(32, len)))
}
}
return result;
}
// enforces 32 byte length
function toUintStrict(
RLPItem memory item
)
internal
pure
returns (uint)
{
// one byte prefix
require(item.len == 33);
uint result;
uint memPtr = item.memPtr + 1;
assembly {
result := mload(memPtr)
}
return result;
}
function toBytes(
RLPItem memory item
)
internal
pure
returns (bytes memory)
{
require(item.len > 0);
uint offset = _payloadOffset(item.memPtr);
uint len = item.len - offset; // data length
bytes memory result = new bytes(len);
uint destPtr;
assembly {
destPtr := add(0x20, result)
}
copy(item.memPtr + offset, destPtr, len);
return result;
}
/*
* Private Functions
*/
// @return number of payload items inside an encoded list.
function numItems(
RLPItem memory item
)
private
pure
returns (uint)
{
if (item.len == 0) return 0;
uint count = 0;
uint currPtr = item.memPtr + _payloadOffset(item.memPtr);
uint endPtr = item.memPtr + item.len;
while (currPtr < endPtr) {
currPtr = currPtr + _itemLength(currPtr); // skip over an item
count++;
}
return count;
}
// @return entire rlp item byte length
function _itemLength(
uint memPtr
)
private
pure
returns (uint len)
{
uint byte0;
assembly {
byte0 := byte(0, mload(memPtr))
}
if (byte0 < STRING_SHORT_START)
return 1;
else if (byte0 < STRING_LONG_START)
return byte0 - STRING_SHORT_START + 1;
else if (byte0 < LIST_SHORT_START) {
assembly {
let byteLen := sub(byte0, 0xb7) // number of bytes the actual length is
memPtr := add(memPtr, 1) // skip over the first byte
/* 32 byte word size */
let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len
len := add(dataLen, add(byteLen, 1))
}
}
else if (byte0 < LIST_LONG_START) {
return byte0 - LIST_SHORT_START + 1;
}
else {
assembly {
let byteLen := sub(byte0, 0xf7)
memPtr := add(memPtr, 1)
let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length
len := add(dataLen, add(byteLen, 1))
}
}
}
// @return number of bytes until the data
function _payloadOffset(
uint memPtr
)
private
pure
returns (uint)
{
uint byte0;
assembly {
byte0 := byte(0, mload(memPtr))
}
if (byte0 < STRING_SHORT_START)
return 0;
else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START))
return 1;
else if (byte0 < LIST_SHORT_START) // being explicit
return byte0 - (STRING_LONG_START - 1) + 1;
else
return byte0 - (LIST_LONG_START - 1) + 1;
}
/*
* @param src Pointer to source
* @param dest Pointer to destination
* @param len Amount of memory to copy from the source
*/
function copy(
uint src,
uint dest,
uint len
)
private
pure
{
if (len == 0) return;
// copy as many word sizes as possible
for (; len >= WORD_SIZE; len -= WORD_SIZE) {
assembly {
mstore(dest, mload(src))
}
src += WORD_SIZE;
dest += WORD_SIZE;
}
// left over bytes. Mask is used to remove unwanted bytes from the word
uint mask = 256 ** (WORD_SIZE - len) - 1;
assembly {
let srcpart := and(mload(src), not(mask)) // zero out src
let destpart := and(mload(dest), mask) // retrieve the bytes
mstore(dest, or(destpart, srcpart))
}
}
}
\ No newline at end of file
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/**
* Source: https://github.com/omisego/plasma-mvp/blob/master/plasma/root_chain/contracts/RLPEncode.sol
* @title RLPEncode
* @dev A simple RLP encoding library.
* @author Bakaoh
*/
library Lib_RLPWriter {
/*
* Public Functions
*/
/**
* RLP encodes a byte string.
* @param self The byte string to encode.
* @return The RLP encoded string in bytes.
*/
function encodeBytes(
bytes memory self
)
internal
pure
returns (bytes memory)
{
bytes memory encoded;
if (self.length == 1 && uint8(self[0]) < 128) {
encoded = self;
} else {
encoded = concat(encodeLength(self.length, 128), self);
}
return encoded;
}
/**
* RLP encodes a list of RLP encoded byte byte strings.
* @param self The list of RLP encoded byte strings.
* @return The RLP encoded list of items in bytes.
*/
function encodeList(
bytes[] memory self
)
internal
pure
returns (bytes memory)
{
bytes memory list = flatten(self);
return concat(encodeLength(list.length, 192), list);
}
/**
* RLP encodes a string.
* @param self The string to encode.
* @return The RLP encoded string in bytes.
*/
function encodeString(
string memory self
)
internal
pure
returns (bytes memory)
{
return encodeBytes(bytes(self));
}
/**
* RLP encodes an address.
* @param self The address to encode.
* @return The RLP encoded address in bytes.
*/
function encodeAddress(
address self
)
internal
pure
returns (bytes memory)
{
bytes memory inputBytes;
assembly {
let m := mload(0x40)
mstore(add(m, 20), xor(0x140000000000000000000000000000000000000000, self))
mstore(0x40, add(m, 52))
inputBytes := m
}
return encodeBytes(inputBytes);
}
/**
* RLP encodes a uint.
* @param self The uint to encode.
* @return The RLP encoded uint in bytes.
*/
function encodeUint(
uint self
)
internal
pure
returns (bytes memory)
{
return encodeBytes(toBinary(self));
}
/**
* RLP encodes an int.
* @param self The int to encode.
* @return The RLP encoded int in bytes.
*/
function encodeInt(
int self
)
internal
pure
returns (bytes memory)
{
return encodeUint(uint(self));
}
/**
* RLP encodes a bool.
* @param self The bool to encode.
* @return The RLP encoded bool in bytes.
*/
function encodeBool(
bool self
)
internal
pure
returns (bytes memory)
{
bytes memory encoded = new bytes(1);
encoded[0] = (self ? bytes1(0x01) : bytes1(0x80));
return encoded;
}
/*
* Private Functions
*/
/**
* Encode the first byte, followed by the `len` in binary form if `length` is more than 55.
* @param len The length of the string or the payload.
* @param offset 128 if item is string, 192 if item is list.
* @return RLP encoded bytes.
*/
function encodeLength(
uint len,
uint offset
)
private
pure
returns (bytes memory)
{
bytes memory encoded;
if (len < 56) {
encoded = new bytes(1);
encoded[0] = byte(uint8(len) + uint8(offset));
} else {
uint lenLen;
uint i = 1;
while (len / i != 0) {
lenLen++;
i *= 256;
}
encoded = new bytes(lenLen + 1);
encoded[0] = byte(uint8(lenLen) + uint8(offset) + 55);
for(i = 1; i <= lenLen; i++) {
encoded[i] = byte(uint8((len / (256**(lenLen-i))) % 256));
}
}
return encoded;
}
/**
* Encode integer in big endian binary form with no leading zeroes.
* @notice TODO: This should be optimized with assembly to save gas costs.
* @param _x The integer to encode.
* @return RLP encoded bytes.
*/
function toBinary(
uint _x
)
private
pure
returns (bytes memory)
{
bytes memory b = new bytes(32);
assembly {
mstore(add(b, 32), _x)
}
uint i = 0;
for (; i < 32; i++) {
if (b[i] != 0) {
break;
}
}
bytes memory res = new bytes(32 - i);
for (uint j = 0; j < res.length; j++) {
res[j] = b[i++];
}
return res;
}
/**
* Copies a piece of memory to another location.
* @notice From: https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol.
* @param _dest Destination location.
* @param _src Source location.
* @param _len Length of memory to copy.
*/
function memcpy(
uint _dest,
uint _src,
uint _len
)
private
pure
{
uint dest = _dest;
uint src = _src;
uint len = _len;
for(; len >= 32; len -= 32) {
assembly {
mstore(dest, mload(src))
}
dest += 32;
src += 32;
}
uint mask = 256 ** (32 - len) - 1;
assembly {
let srcpart := and(mload(src), not(mask))
let destpart := and(mload(dest), mask)
mstore(dest, or(destpart, srcpart))
}
}
/**
* Flattens a list of byte strings into one byte string.
* @notice From: https://github.com/sammayo/solidity-rlp-encoder/blob/master/RLPEncode.sol.
* @param _list List of byte strings to flatten.
* @return The flattened byte string.
*/
function flatten(
bytes[] memory _list
)
private
pure
returns (bytes memory)
{
if (_list.length == 0) {
return new bytes(0);
}
uint len;
uint i = 0;
for (; i < _list.length; i++) {
len += _list[i].length;
}
bytes memory flattened = new bytes(len);
uint flattenedPtr;
assembly { flattenedPtr := add(flattened, 0x20) }
for(i = 0; i < _list.length; i++) {
bytes memory item = _list[i];
uint listPtr;
assembly { listPtr := add(item, 0x20)}
memcpy(flattenedPtr, listPtr, item.length);
flattenedPtr += _list[i].length;
}
return flattened;
}
/**
* Concatenates two bytes.
* @notice From: https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol.
* @param _preBytes First byte string.
* @param _postBytes Second byte string.
* @return Both byte string combined.
*/
function concat(
bytes memory _preBytes,
bytes memory _postBytes
)
private
pure
returns (bytes memory)
{
bytes memory tempBytes;
assembly {
tempBytes := mload(0x40)
let length := mload(_preBytes)
mstore(tempBytes, length)
let mc := add(tempBytes, 0x20)
let end := add(mc, length)
for {
let cc := add(_preBytes, 0x20)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
length := mload(_postBytes)
mstore(tempBytes, add(length, mload(tempBytes)))
mc := end
end := add(mc, length)
for {
let cc := add(_postBytes, 0x20)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
mstore(0x40, and(
add(add(end, iszero(add(length, mload(_preBytes)))), 31),
not(31)
))
}
return tempBytes;
}
}
\ No newline at end of file
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Library Imports */
import { Lib_RLPWriter } from "../rlp/Lib_RLPWriter.sol";
library Lib_EthUtils {
function getCode(
address _address,
uint256 _offset,
uint256 _length
)
internal
view
returns (
bytes memory _code
)
{
assembly {
_code := mload(0x40)
mstore(0x40, add(_code, add(_length, 0x20)))
mstore(_code, _length)
extcodecopy(_address, add(_code, 0x20), _offset, _length)
}
return _code;
}
function getCode(
address _address
)
internal
view
returns (
bytes memory _code
)
{
return getCode(
_address,
0,
getCodeSize(_address)
);
}
function getCodeSize(
address _address
)
internal
view
returns (
uint256 _codeSize
)
{
assembly {
_codeSize := extcodesize(_address)
}
return _codeSize;
}
function getCodeHash(
address _address
)
internal
view
returns (
bytes32 _codeHash
)
{
assembly {
_codeHash := extcodehash(_address)
}
return _codeHash;
}
function createContract(
bytes memory _code
)
internal
returns (
address _created
)
{
assembly {
_created := create(
0,
add(_code, 0x20),
mload(_code)
)
}
return _created;
}
function getAddressForCREATE(
address _creator,
uint256 _nonce
)
internal
view
returns (
address _address
)
{
bytes[] memory encoded = new bytes[](2);
encoded[0] = Lib_RLPWriter.encodeAddress(_creator);
encoded[1] = Lib_RLPWriter.encodeUint(_nonce);
bytes memory encodedList = Lib_RLPWriter.encodeList(encoded);
return getAddressFromHash(keccak256(encodedList));
}
function getAddressForCREATE2(
address _creator,
bytes memory _bytecode,
bytes32 _salt
)
internal
view
returns (address _address)
{
bytes32 hashedData = keccak256(abi.encodePacked(
byte(0xff),
_creator,
_salt,
keccak256(_bytecode)
));
return getAddressFromHash(hashedData);
}
/**
* Determines an address from a 32 byte hash. Since addresses are only
* 20 bytes, we need to retrieve the last 20 bytes from the original
* hash. Converting to uint256 and then uint160 gives us these bytes.
* @param _hash Hash to convert to an address.
* @return Hash converted to an address.
*/
function getAddressFromHash(
bytes32 _hash
)
private
pure
returns (address)
{
return address(bytes20(uint160(uint256(_hash))));
}
}
pragma solidity >=0.7.0;
pragma experimental ABIEncoderV2;
// note: this pattern breaks if an action leads to reversion, since the result will not be stored.
// Thus, only the final action in deployedActions can be an ovmREVERT, and the result must be checked via the callee.
contract MockOvmCodeContract {
bytes[] public EMReturnValuesInConstructor;
bytes[][] public EMReturnValuesInDeployed;
bytes[][] public callDatasForEMInDeployed;
bytes[] public returnDataForCodeContractInDeployed;
uint public callIndex = 0;
bytes public constrRet0;
bytes public constrRet1;
bytes public constCall0;
constructor(bytes[] memory _callsToEMInConstructor, bytes[][] memory _calldatasToEMInDeployed, bytes[] memory _returnDataForCodeContractInDeployed) {
require(_calldatasToEMInDeployed.length == _returnDataForCodeContractInDeployed.length, "Invalid behavior requested for mock code contract: mismatch between number of calldata batches and returndata for post-deployment behavior.");
callDatasForEMInDeployed = _calldatasToEMInDeployed;
returnDataForCodeContractInDeployed = _returnDataForCodeContractInDeployed;
bytes[] memory callsToDoNow = _callsToEMInConstructor;
bytes[] memory returnVals = doEMCalls(callsToDoNow);
constCall0 = callsToDoNow[0];
constrRet0 = returnVals[0];
constrRet1 = returnVals[1];
for (uint i = 0; i < returnVals.length; i++) {
EMReturnValuesInConstructor.push(returnVals[i]);
}
}
fallback() external {
bytes[] memory calldatas = callDatasForEMInDeployed[callIndex];
bytes[] memory returndatas = doEMCalls(calldatas);
EMReturnValuesInDeployed.push();
for (uint i = 0; i < returndatas.length; i++) {
EMReturnValuesInDeployed[callIndex].push(returndatas[i]);
}
bytes memory dataToReturn = returnDataForCodeContractInDeployed[callIndex];
callIndex++;
uint returnLength = dataToReturn.length;
assembly {
return(add(dataToReturn, 0x20), returnLength)
}
}
function doEMCalls(bytes[] memory _calldatas) internal returns(bytes[] memory) {
bytes[] memory calldatas = _calldatas;
bytes[] memory results = new bytes[](calldatas.length);
for (uint i = 0; i < calldatas.length; i++) {
bytes memory data = calldatas[i];
bytes memory result = callExecutionManager(data);
results[i] = result;
}
return results;
}
function callExecutionManager (bytes memory _data) internal returns (bytes memory actionResult) {
uint dataLength = _data.length;
uint returnedLength;
assembly {
function isContextCREATE() -> isCREATE {
isCREATE := iszero(extcodesize(address()))
}
// Note: this function is the only way that the opcodes REVERT, CALLER, EXTCODESIZE, ADDRESS can appear in a code contract which passes SafetyChecker.isBytecodeSafe().
// The static analysis enforces that the EXACT functionality below is implemented by comparing to a reference bytestring.
function doSafeExecutionManagerCall(argOff, argLen, retOffset, retLen) {
let success := call(gas(), caller(), 0, argOff, argLen, retOffset, retLen)
if iszero(success) {
mstore(0, 0x2a2a7adb00000000000000000000000000000000000000000000000000000000) // ovmREVERT(bytes) methodId
returndatacopy(4, 0, 32)
let secondsuccess := call(gas(), caller(), 0, 0, 36, 0, 0)
if iszero(secondsuccess) {
returndatacopy(0, 0, 32)
revert(0, 32)
}
// ovmREVERT will only succeed if we are in a CREATE context, in which case we abort by deploying a single byte contract.
mstore(0,0)
return(0, 1)
}
}
doSafeExecutionManagerCall(add(_data, 0x20), dataLength, 0, 0)
returnedLength := returndatasize()
}
bytes memory returned = new bytes(returnedLength);
assembly {
returndatacopy(add(returned, 0x20), 0, returndatasize())
}
return returned;
}
}
\ No newline at end of file
# Execution Managager Integration/State Tests
## General notes
- run everything below with invalid state accesses automatically and assert invalid state access handled in ALL cases
- run everything below through a state manager proxy which consumes a different amount of gas and check that the **OVM** gas values are not different
## Test Cases
- CALL-types
- for all: call an undeployed contract and make sure it errors or whatevs (or maybe that's just a unit test)
- ovmCALL
- -> ovmCALLER
- -> ovmADDRESS
- -> SLOAD
- -> SSTORE
- -> CREATE/2
- ovmSTATICCALL
- -> ovmCALLER
- -> ovmADDRESS
- -> SLOAD
- -> SSTORE (fail)
- -> CREATE/2 (fail)
- -> ovmCALL -> ovmSSTORE
- -> ovmCALL -> ovmCREATE
- -> ovmSTATICCALL -> RETURN -> SLOAD (fails)
- ovmDELEGATECALL
- -> ovmCALLER
- -> ovmADDRESS
- -> SLOAD
- -> SSTORE
- -> CREATE/2
- -> ovmDELEGATECALL -> ovmCALLER
- -> ovmDELEGATECALL -> ovmADDRESS
- Code-related
- CREATE-types
- do we just duplicate these exactly for CREATE and CREATE2? Probably
- ovmCREATE -> success -> ovmEXTCODE{SIZE,HASH,COPY}
- ovmCREATE -> fail (ovmREVERT, NOT out of gas/invalid jump) -> ovmEXTCODE{SIZE,HASH,COPY}
- ovmCREATE -> fail -> ovmCALL what was attempted to be created (fail)
- ovmCREATE -> ovmCREATE (during constructor) -> success -> success (check right address for inner deployment)
- ovmCREATE -> ovmCALL(in constructor) -> ovmSSTORE, return -> ovmREVERT (deployment fails, storage not modified, but state access gas correctly increased)
- ovmCREATE -> ovmCREATE (during constructor) -> success -> fail (outer contract)
- "creator" does ovmCREATE -> invalid jumpdest -> creator out-of-gasses (or at least, appears to--really it will revert with no data, so there will be some gas left)
- "creator" does ovmCREATE -> initcode does ovmCREATE -> invalid jumpdest -> creator out-of-gasses (or at least, appears to--really it will revert with no data, so there will be some gas left) AKA same as above but nested CREATEs
- OVM gas metering
- do everything for both queue origins/flip flopped roles:
- blocks transactions whose gas limit would put the cumulative gas over the max for the current epoch
- starts a new epoch and allows tx through if the above would have held true, but new epoch has begun
- allows transaction through queue B even if queue A cumulative gas would have blocked it
- out of gas
- ovmCALL -> [ovmCALL(gas()/2) -> out of gas] -> SSTORE (does not out of gas parent)
- State access limiting logic
- ovmCALL(gas()/2) -> ovmCALL(gas()) -> out of gas -> return(someVal) -> SSTORE(someVal)
- this one shows that if a subcall out-of-gasses, but you do not do any further state accesses, then you can still return, and if your parent has a bigger allocation left, they can still ovmSSTORE
- ovmSSTORE, repeated max times, ovmCALL(gas()) -> ovmSSTORE -> fails (even though max gas allotment was given, the parent already used them up)
- ovmCALL(gas/2) -> ovmCREATE, out of gas -> SSTORE succeeds
- ovmCALL(gas) -> ovmCREATE, out of gas -> SSTORE fails
- ovmCALL(gas) -> ovmCREATE, ovmREVERT (in init) -> SSTORE succeeds
- ovmCALL(gas) -> ovmCREATE, ovmSSTORE(max times), ovmREVERT -> ovmSSTORE fails (max allocated in reverting CREATE)
- ovmCALL(gas) -> ovmCREATE -> ovmCREATE ovmSSTORE(max times), ovmREVERT -> deploy -> ovmSSTORE fails (propogates through a failed CREATE inside a successful CREATE
- ovmCALL(gas) -> ovmCREATE -> ovmCREATE, ovmSLOAD(max times), inner deploy success -> outer deploy fail -> ovmSSTORE fails (propogates through a successful create inside a failed create)
- ovmCREATE -> ovmCALL, ovmSSTORE (max), return -> ovmSSTORE fails
- ovmCREATE -> ovmCALL(gas/2) -> ovmCREATE, out of gas, call reverts (as if out of gas) -> ovmSSTORE (success in constructor)
- Explicit invalid state access tests
- CALL -> CALL, ISA
- CALL -> CALL, CALL, ISA
- CREATE -> CREATE, ISA
- CREATE -> CREATE -> CREATE ISA
- CREATE -> CALL, ISA
- CALL -> CREATE, ISA
- CALL -> CREATE -> CALL, ISA
- CREATE -> CALL -> CREATE, ISA
\ No newline at end of file
{
"name": "optimism",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "yarn run build:contracts",
"build:contracts": "buidler compile",
"test": "yarn run test:contracts",
"test:contracts": "buidler test \"test/contracts/OVM/execution/OVM_ExecutionManager/opcodes/OVM_ExecutionManager.opcodes.calling.spec.ts\""
},
"devDependencies": {
"@nomiclabs/buidler": "^1.4.4",
"@nomiclabs/buidler-ethers": "^2.0.0",
"@nomiclabs/buidler-waffle": "^2.0.0",
"@types/chai": "^4.2.12",
"@types/mocha": "^8.0.3",
"@types/node": "^14.6.0",
"assert": "^2.0.0",
"chai": "^4.2.0",
"ethereum-waffle": "3.0.0",
"ethers": "5.0.0",
"mocha": "^8.1.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
}
}
export const toHexString = (buf: Buffer | string): string => {
return '0x' + fromHexString(buf).toString('hex')
}
export const fromHexString = (str: string | Buffer): Buffer => {
if (typeof str === 'string' && str.startsWith('0x')) {
return Buffer.from(str.slice(2), 'hex')
}
return Buffer.from(str)
}
export const makeHexString = (byte: string, len: number): string => {
return '0x' + byte.repeat(len)
}
export const makeAddress = (byte: string): string => {
return makeHexString(byte, 20)
}
import { ethers } from '@nomiclabs/buidler'
export const encodeRevertData = (
flag: number,
data: string = '0x',
nuisanceGasLeft: number = 0,
ovmGasRefund: number = 0
): string => {
return ethers.utils.defaultAbiCoder.encode(
['uint256','uint256','uint256','bytes'],
[flag, nuisanceGasLeft, ovmGasRefund, data]
)
}
export const REVERT_FLAGS = {
DID_NOT_REVERT: 0,
OUT_OF_GAS: 1,
INTENTIONAL_REVERT: 2,
EXCEEDS_NUISANCE_GAS: 3,
INVALID_STATE_ACCESS: 4,
UNSAFE_BYTECODE: 5
}
import * as path from 'path'
import bre from '@nomiclabs/buidler'
import { Compiler } from '@nomiclabs/buidler/internal/solidity/compiler'
export interface SolidityCompiler {
version: () => string
compile: any
}
export interface ContractSource {
path: string
content: string
}
export const getDefaultCompiler = async (): Promise<SolidityCompiler> => {
const compiler = new Compiler(
bre.config.solc.version,
path.join(bre.config.paths.cache, 'compilers')
)
return compiler.getSolc()
}
export const compile = async (
sources: ContractSource[],
compiler?: SolidityCompiler
): Promise<any> => {
compiler = compiler || (await getDefaultCompiler())
const compilerInput = {
language: 'Solidity',
sources: sources.reduce((parsed, source) => {
parsed[source.path] = {
content: source.content,
}
return parsed
}, {}),
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
}
return JSON.parse(compiler.compile(JSON.stringify(compilerInput)))
}
/* External Imports */
import { ethers } from 'ethers'
import { defaultAccounts } from 'ethereum-waffle'
/* Internal Imports */
import { makeHexString, makeAddress } from './byte-utils'
export const DEFAULT_ACCOUNTS = defaultAccounts
export const DEFAULT_ACCOUNTS_BUIDLER = defaultAccounts.map((account) => {
return {
balance: ethers.BigNumber.from(account.balance).toHexString(),
privateKey: account.secretKey,
}
})
export const GAS_LIMIT = 1_000_000_000
export const NULL_BYTES32 = makeHexString('00', 32)
export const NON_NULL_BYTES32 = makeHexString('11', 32)
export const ZERO_ADDRESS = makeAddress('00')
export const NON_ZERO_ADDRESS = makeAddress('11')
/* Internal Imports */
import { DUMMY_BYTES32 } from './bytes32'
import { ZERO_ADDRESS, NON_ZERO_ADDRESS } from '../constants'
import { makeAddress } from '../byte-utils'
import { OVMAccount } from '../types/ovm-types'
export const DUMMY_ACCOUNTS: Array<{
address: string,
data: OVMAccount
}> = [
{
address: makeAddress('12'),
data: {
nonce: 123,
balance: 456,
storageRoot: DUMMY_BYTES32[0],
codeHash: DUMMY_BYTES32[1],
ethAddress: ZERO_ADDRESS,
},
},
{
address: makeAddress('21'),
data: {
nonce: 321,
balance: 654,
storageRoot: DUMMY_BYTES32[2],
codeHash: DUMMY_BYTES32[3],
ethAddress: NON_ZERO_ADDRESS,
},
}
]
/* External Imports */
import { ethers } from 'ethers'
export const DUMMY_BYTES32: string[] = Array.from(
{
length: 10,
},
(_, i) => {
return ethers.utils.keccak256(`0x0${i}`)
}
)
/* Internal Imports */
import { NON_ZERO_ADDRESS } from '../constants'
export const DUMMY_CONTEXT = {
GLOBAL: {
ovmCHAINID: 11
},
TRANSACTION: {
ovmORIGIN: NON_ZERO_ADDRESS,
ovmTIMESTAMP: 22,
ovmGASLIMIT: 33,
ovmTXGASLIMIT: 44,
ovmQUEUEORIGIN: 55,
},
MESSAGE: {
ovmCALLER: NON_ZERO_ADDRESS,
ovmADDRESS: NON_ZERO_ADDRESS,
ovmSTATICCTX: true
}
}
export * from './accounts'
export * from './bytes32'
export * from './context'
export * from './dummy'
export * from './types'
export * from './constants'
export * from './proxy'
export * from './mocks'
export * from './buffer-utils'
export * from './byte-utils'
export * from './codec'
export * from './mock-contract.types'
export * from './mock-generation'
/* External Imports */
import bre from '@nomiclabs/buidler'
import { ethers, Contract, ContractFactory } from 'ethers'
/* Internal Imports */
import { toHexString, fromHexString } from '../buffer-utils'
import { MockContract, MockContractFunction } from './mock-contract.types'
/**
* Binds logic to the buidler node that checks for calls to mock contracts and
* replaces return values. Runs once as to avoid repeatedly hijacking functions
* for each new mock contract.
*/
export const bindMockWatcherToVM = (): void => {
const node = bre.network.provider['_node' as any]
// No need to bind here if we've already done so.
if (node.__calls) {
return
}
const vmTracer = node['_vmTracer' as any]
const vm = node['_vm' as any]
// Set up some things we'll need for later.
let txid: string
let messages: Array<{
address: string
sighash: string
calldata: string
}> = []
node.__calls = {}
node.__contracts = {}
// Modify the vm.runTx function to capture an ID for each transaction.
const originalRunTx = vm.runTx.bind(vm)
const modifiedRunTx = async (opts: any): Promise<any> => {
// Buidler runs transactions multiple times (e.g., for gas estimation).
// Here we're computing a unique ID for each transaction (based on sender,
// nonce, and transaction data) so that we don't log calls multiple times.
txid = ethers.utils.keccak256(
'0x' +
opts.tx._from.toString('hex') +
opts.tx.nonce.toString('hex') +
opts.tx.data.toString('hex')
)
// Wipe the calls for this txid to avoid duplicate results.
node.__calls[txid] = {}
return originalRunTx(opts)
}
vm['runTx' as any] = modifiedRunTx.bind(vm)
// Modify the pre-message handler to capture calldata.
const originalBeforeMessageHandler = vmTracer['_beforeMessageHandler' as any]
const modifiedBeforeMessageHandler = async (message: any, next: any) => {
// We only care about capturing if we're sending to one of our mocks.
const address = message.to
? toHexString(message.to).toLowerCase()
: undefined
const contract = node.__contracts[address]
if (address && contract) {
const calldata = toHexString(message.data.slice(4))
let sighash = toHexString(message.data.slice(0, 4))
if (contract.__sigmap) {
sighash = contract.__sigmap[sighash]
message.data.write(sighash.slice(2), 0, 4, 'hex')
}
// Store the message for use in the post-message handler.
messages.push({
address,
sighash,
calldata,
})
// Basic existence checks.
if (!node.__calls[txid][address]) {
node.__calls[txid][address] = {}
}
if (!node.__calls[txid][address][sighash]) {
node.__calls[txid][address][sighash] = []
}
// Add the data to the per-sighash array.
node.__calls[txid][address][sighash].push(toHexString(message.data))
}
originalBeforeMessageHandler(message, next)
}
// Modify the post-message handler to insert the correct return data.
const originalAfterMessageHandler = vmTracer['_afterMessageHandler' as any]
const modifiedAfterMessageHandler = async (result: any, next: any) => {
// We don't need to do anything if we haven't stored any mock messages.
if (messages.length > 0) {
// We need to look at the messages backwards since the first result will
// correspond to the last message on the stack.
const message = messages.pop()
const contract: Contract = node.__contracts[message.address]
const fn: MockContractFunction = contract.__fns[message.sighash]
// Compute our return values.
const inputParams = contract.__spec
? contract.__spec.interface.decodeFunctionData(
fn.functionName,
contract.__spec.interface.getSighash(fn.functionName) + message.calldata.slice(2)
)
: ethers.utils.defaultAbiCoder.decode(
fn.inputTypes,
message.calldata
)
const returnValues = Array.isArray(fn.returnValues)
? fn.returnValues
: await fn.returnValues(...inputParams)
const returnBuffer = fromHexString(
contract.__spec
? contract.__spec.interface.encodeFunctionResult(
fn.functionName,
returnValues
)
: ethers.utils.defaultAbiCoder.encode(fn.outputTypes, returnValues)
)
// Set the return value to match our computed value.
result.execResult.returnValue = returnBuffer
}
originalAfterMessageHandler(result, next)
}
// Disable tracing to remove the old handlers before adding new ones.
vmTracer.disableTracing()
vmTracer['_beforeMessageHandler' as any] = modifiedBeforeMessageHandler.bind(
vmTracer
)
vmTracer['_afterMessageHandler' as any] = modifiedAfterMessageHandler.bind(
vmTracer
)
vmTracer.enableTracing()
}
/**
* Binds a mock contract to the VM and inserts necessary functions.
* @param mock Mock contract to bind.
* @param fns Contract functions associated with the mock.
*/
export const bindMockContractToVM = (
mock: MockContract,
fns: MockContractFunction[],
spec: MockContractFunction[] | Contract | ContractFactory
): void => {
const node = bre.network.provider['_node' as any]
node.__contracts[mock.address.toLowerCase()] = mock
const getCalls = (functionName: string): string[] => {
const calls: {
[sighash: string]: string[]
} = {}
for (const txid of Object.keys(node.__calls)) {
for (const address of Object.keys(node.__calls[txid])) {
if (address === mock.address.toLowerCase()) {
for (const sighash of Object.keys(node.__calls[txid][address])) {
const txcalls = node.__calls[txid][address][sighash]
calls[sighash] = calls[sighash]
? calls[sighash].concat(txcalls)
: txcalls
}
}
}
}
const sighash = mock.interface.getSighash(functionName)
return calls[sighash] || []
}
if (!Array.isArray(spec)) {
;(mock as any).__spec = spec
;(mock as any).__sigmap = Object.keys(mock.interface.functions).reduce((sigmap, fn) => {
fn = fn.split('(')[0]
sigmap[spec.interface.getSighash(fn)] = mock.interface.getSighash(fn)
return sigmap
}, {})
}
;(mock as any).getCallCount = (functionName: string): number => {
return getCalls(functionName).length
}
;(mock as any).getCallData = (
functionName: string,
callIndex: number
): any[] => {
const calls = getCalls(functionName)
if (calls.length <= callIndex) {
throw new Error('Provided function call index does not exist.')
}
const iface = mock.__spec ? mock.__spec.interface : mock.interface
const calldata = iface.getSighash(functionName) + calls[callIndex].slice(10)
return iface
.decodeFunctionData(functionName, calldata)
.map((element) => {
return element
})
}
;(mock as any).setReturnValues = (
functionName: string,
returnValues: any[] | ((...params: any[]) => any[])
): void => {
mock.__fns[
mock.interface.getSighash(functionName)
].returnValues = returnValues
}
;(mock as any).__fns = fns.reduce((fnmap, fn) => {
fnmap[mock.interface.getSighash(fn.functionName)] = fn
return fnmap
}, {})
}
/* External Imports */
import { Contract } from 'ethers'
export interface MockContractFunction {
functionName: string
inputTypes?: string[]
outputTypes?: string[]
returnValues?: any[] | ((...params: any[]) => any[] | Promise<any>)
}
export interface MockContract extends Contract {
getCallCount: (functionName: string) => number
getCallData: (functionName: string, callIndex: number) => any[]
setReturnValues: (
functionName: string,
returnValues: any[] | ((...params: any[]) => any[])
) => void
__fns: {
[sighash: string]: MockContractFunction
}
}
/* External Imports */
import { ethers } from '@nomiclabs/buidler'
import { Signer, Contract, ContractFactory } from 'ethers'
import { FunctionFragment, ParamType } from 'ethers/lib/utils'
/* Internal Imports */
import { MockContract, MockContractFunction } from './mock-contract.types'
import { bindMockContractToVM, bindMockWatcherToVM } from './mock-binding'
import {
SolidityCompiler,
getDefaultCompiler,
compile,
} from '../compilation'
/**
* Generates contract code for a mock contract.
* @param fns Mock contract function definitions.
* @param contractName Name for the contract.
* @param compilerVersion Compiler version being used.
* @returns Contract code.
*/
const getSolidityMockContractCode = (
fns: MockContractFunction[],
contractName: string,
compilerVersion: string
): string => {
return `
pragma solidity ${compilerVersion};
contract ${contractName} {
${fns
.map((fn) => {
return `
function ${fn.functionName}(${fn.inputTypes
.map((inputType, idx) => {
return `${inputType} _${idx}`
})
.join(', ')})
public
{
return;
}
`
})
.join('\n')}
}
`
}
/**
* Checks that a mock contract function definition is valid.
* @param fn Mock contract function definition.
* @returns Whether or not the function is valid.
*/
const isValidMockContractFunction = (fn: MockContractFunction): boolean => {
return (
fn.inputTypes &&
fn.outputTypes &&
fn.returnValues &&
(!Array.isArray(fn.returnValues) ||
fn.outputTypes.length === fn.returnValues.length)
)
}
/**
* Basic sanitization for mock function definitions
* @param fn Mock contract function definition to sanitize.
* @returns Sanitized definition.
*/
export const sanitizeMockContractFunction = (
fn: MockContractFunction
): MockContractFunction => {
const sanitized = {
functionName: fn.functionName,
inputTypes: fn.inputTypes || [],
outputTypes: fn.outputTypes || [],
returnValues: fn.returnValues || [],
}
if (!isValidMockContractFunction(sanitized)) {
throw new Error(
'Provided MockContract function is invalid. Please check your mock definition.'
)
}
return sanitized
}
/**
* Basic sanitization for mock function definitions
* @param fns Mock contract function definitions to sanitize.
* @returns Sanitized definitions.
*/
const sanitizeMockContractFunctions = (
fns: MockContractFunction[]
): MockContractFunction[] => {
return fns.map((fn) => {
return sanitizeMockContractFunction(fn)
})
}
/**
* Gets mock return values for a set of output types.
* @param outputTypes Output types as ethers param types.
* @returns Mock return values.
*/
const getMockReturnValues = (outputTypes: ParamType[]): string[] => {
return outputTypes.map((outputType) => {
return outputType.type === outputType.baseType
? '0x' + '00'.repeat(32)
: '0x' + '00'.repeat(64)
})
}
/**
* Converts an ethers function fragment to a mock function.
* @param fn Function fragment to convert.
* @returns Generated mock function.
*/
const getMockFunctionFromFragment = (
fn: FunctionFragment
): MockContractFunction => {
return {
functionName: fn.name,
inputTypes: [],
outputTypes: [],
returnValues: getMockReturnValues(fn.outputs),
}
}
/**
* Generates mock functions from a contract spec.
* @param spec Contract or factory used as the spec.
* @returns Array of mock functions.
*/
const getFnsFromContractSpec = (
spec: Contract | ContractFactory
): MockContractFunction[] => {
return Object.values(spec.interface.functions)
.filter((fn) => {
return fn.type === 'function'
})
.map((fn) => {
return getMockFunctionFromFragment(fn)
})
}
/**
* Generates a mock contract for testing.
* @param spec Mock contract function definitions or contract to base on.
* @param signer Signer to use to deploy the mock.
* @param compiler Optional compiler instance to use.
* @returns Generated mock contract instance.
*/
export const getMockContract = async (
spec: MockContractFunction[] | Contract | ContractFactory | string,
signer?: Signer,
compiler?: SolidityCompiler
): Promise<MockContract> => {
if (typeof spec === 'string') {
spec = await ethers.getContractFactory(spec)
}
if (!Array.isArray(spec)) {
signer = signer || spec.signer
}
if (!signer) {
throw new Error('You must provide a signer.')
}
compiler = compiler || (await getDefaultCompiler())
const fns = Array.isArray(spec)
? sanitizeMockContractFunctions(spec)
: getFnsFromContractSpec(spec)
const contractName = 'MockContract'
const contractPath = contractName + '.sol'
const contractCode = getSolidityMockContractCode(
fns,
contractName,
'^' + compiler.version().split('+')[0]
)
const compilerOutput = await compile(
[
{
path: contractPath,
content: contractCode,
},
],
compiler
)
const MockContractJSON = compilerOutput.contracts[contractPath][contractName]
const MockContractFactory = new ethers.ContractFactory(
MockContractJSON.abi,
MockContractJSON.evm.bytecode.object,
signer
)
const originalDefinePropertyFn = Object.defineProperty
Object.defineProperty = (object: any, name: string, props: any): void => {
if (props.writable === false) {
props.writable = true
}
originalDefinePropertyFn(object, name, props)
}
const MockContract = (await MockContractFactory.deploy()) as MockContract
Object.defineProperty = originalDefinePropertyFn
bindMockWatcherToVM()
bindMockContractToVM(MockContract, fns, spec)
return MockContract
}
/* External Imports */
import { ethers } from '@nomiclabs/buidler'
import { Contract } from 'ethers'
const getLibraryConfig = (ProxyManager: Contract): any => {
return [
{
name: 'Lib_ByteUtils',
params: []
},
{
name: 'Lib_EthUtils',
params: [ProxyManager.address]
},
{
name: 'Lib_RLPReader',
params: []
},
{
name: 'Lib_RLPWriter',
params: []
}
]
}
export const makeProxies = async (
Proxy_Manager: Contract,
names: string[]
): Promise<void> => {
for (const name of names) {
if (await Proxy_Manager['hasProxy(string)'](name)) {
continue
}
const Factory__Proxy_Forwarder = await ethers.getContractFactory(
'Proxy_Forwarder'
)
const Proxy_Forwarder = await Factory__Proxy_Forwarder.deploy(
Proxy_Manager.address
)
await Proxy_Manager.setProxy(
name,
Proxy_Forwarder.address
)
}
}
export const setProxyTarget = async (
Proxy_Manager: Contract,
name: string,
target: Contract
): Promise<void> => {
await makeProxies(Proxy_Manager, [name])
await Proxy_Manager.setTarget(
name,
target.address
)
}
export const getProxyManager = async (): Promise<Contract> => {
const Factory__Proxy_Manager = await ethers.getContractFactory(
'Proxy_Manager'
)
const Proxy_Manager = await Factory__Proxy_Manager.deploy()
const libraryConfig = getLibraryConfig(Proxy_Manager)
await makeProxies(
Proxy_Manager,
libraryConfig.map((config) => {
return config.name
})
)
for (const config of libraryConfig) {
const Factory__Lib_Contract = await ethers.getContractFactory(
config.name
)
const Lib_Contract = await Factory__Lib_Contract.deploy(
...config.params
)
await Proxy_Manager.setTarget(
config.name,
Lib_Contract.address
)
}
return Proxy_Manager
}
export interface OVMAccount {
nonce: number
balance: number
storageRoot: string
codeHash: string
ethAddress: string
}
export const toOVMAccount = (result: any[]): OVMAccount => {
return {
nonce: result[0].toNumber(),
balance: result[1].toNumber(),
storageRoot: result[2],
codeHash: result[3],
ethAddress: result[4],
}
}
/* External Imports */
import chai = require('chai')
import { solidity } from 'ethereum-waffle'
chai.use(solidity)
const should = chai.should()
const expect = chai.expect
export { should, expect }
{
"compilerOptions": {
"outDir": "./build",
"baseUrl": "./",
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["*.ts", "**/*.ts", "artifacts/*.json"],
"exclude": ["./build", "node_modules"],
"files": [
"./buidler.config.ts",
"./buidler-env.d.ts",
"./node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts",
"./node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts"
]
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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