Commit d8a82f47 authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

Integration Tests (#33)

* test: add the tests

* ci: enable integration test

* ci: always run integration tests

* ci: build integration tests deps

* ci: cache integration test deps

* ci: properly cache yarn deps

* fix(integration-tests): clone the provider instead of referencing

Otherwise it interferes with test isolation

* chore: yarn lint
parent 27487c1d
...@@ -5,12 +5,6 @@ on: ...@@ -5,12 +5,6 @@ on:
branches: branches:
- master - master
pull_request: pull_request:
branches:
- master
defaults:
run:
working-directory: ./ops
jobs: jobs:
integration: integration:
...@@ -22,13 +16,38 @@ jobs: ...@@ -22,13 +16,38 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build the base layer dependencies - name: Build the base layer dependencies
working-directory: ./ops
run: docker-compose build --parallel -- builder l2geth l1_chain run: docker-compose build --parallel -- builder l2geth l1_chain
- name: Build the other services - name: Build the other services
working-directory: ./ops
run: docker-compose build --parallel -- deployer dtl batch_submitter run: docker-compose build --parallel -- deployer dtl batch_submitter
- name: Bring the stack up and wait for the sequencer to be ready - name: Bring the stack up and wait for the sequencer to be ready
working-directory: ./ops
run: docker-compose up -d && ./scripts/wait-for-sequencer.sh run: docker-compose up -d && ./scripts/wait-for-sequencer.sh
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install integration test dependencies
working-directory: ./integration-tests
# only install dependencies if the cache was invalidated
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Build deps for the integration tests
run: yarn build
- name: Run the integration tests - name: Run the integration tests
run: echo "Integration tests will be run here" working-directory: ./integration-tests
run: yarn test:integration
...@@ -34,17 +34,21 @@ jobs: ...@@ -34,17 +34,21 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
# START DEPENDENCY CACHING - name: Get yarn cache directory path
- name: Cache root deps id: yarn-cache-dir-path
uses: actions/cache@v1 run: echo "::set-output name=dir::$(yarn cache dir)"
id: cache_base
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package.json') }}
# END DEPENDENCY CACHING - uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies - name: Install Dependencies
# only install dependencies if there was a change in the deps
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install run: yarn install
- name: Build - name: Build
......
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2 <0.8.0;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize, which returns 0 for contracts in
// construction, since the code is only stored at the end of the
// constructor execution.
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(account) }
return size > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
// solhint-disable-next-line avoid-low-level-calls, avoid-call-value
(bool success, ) = recipient.call{ value: amount }("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain`call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.call{ value: value }(data);
return _verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) {
require(isContract(target), "Address: static call to non-contract");
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.staticcall(data);
return _verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
require(isContract(target), "Address: delegate call to non-contract");
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.delegatecall(data);
return _verifyCallResult(success, returndata, errorMessage);
}
function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) {
if (success) {
return returndata;
} else {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
// solhint-disable-next-line no-inline-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity >0.6.0 <0.8.0;
/**
* @title ERC20
* @dev A super simple ERC20 implementation!
*/
contract ChainlinkERC20 {
/**********
* Events *
**********/
event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
/*************
* Variables *
*************/
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowances;
// Some optional extra goodies.
uint256 public totalSupply;
string public name;
/***************
* Constructor *
***************/
/**
* @param _initialSupply Initial maximum token supply.
* @param _name A name for our ERC20 (technically optional, but it's fun ok jeez).
*/
function init(
uint256 _initialSupply,
string memory _name
)
public
{
balances[msg.sender] = _initialSupply;
totalSupply = _initialSupply;
name = _name;
}
/********************
* Public Functions *
********************/
/**
* Checks the balance of an address.
* @param _owner Address to check a balance for.
* @return Balance of the address.
*/
function balanceOf(
address _owner
)
external
view
returns (
uint256
)
{
return balances[_owner];
}
/**
* Transfers a balance from your account to someone else's account!
* @param _to Address to transfer a balance to.
* @param _amount Amount to transfer to the other account.
* @return true if the transfer was successful.
*/
function transfer(
address _to,
uint256 _amount
)
external
returns (
bool
)
{
require(
balances[msg.sender] >= _amount,
"You don't have enough balance to make this transfer!"
);
balances[msg.sender] -= _amount;
balances[_to] += _amount;
emit Transfer(
msg.sender,
_to,
_amount
);
return true;
}
/**
* Transfers a balance from someone else's account to another account. You need an allowance
* from the sending account for this to work!
* @param _from Account to transfer a balance from.
* @param _to Account to transfer a balance to.
* @param _amount Amount to transfer to the other account.
* @return true if the transfer was successful.
*/
function transferFrom(
address _from,
address _to,
uint256 _amount
)
external
returns (
bool
)
{
require(
balances[_from] >= _amount,
"Can't transfer from the desired account because it doesn't have enough balance."
);
require(
allowances[_from][msg.sender] >= _amount,
"Can't transfer from the desired account because you don't have enough of an allowance."
);
balances[_to] += _amount;
balances[_from] -= _amount;
emit Transfer(
_from,
_to,
_amount
);
return true;
}
/**
* Approves an account to spend some amount from your account.
* @param _spender Account to approve a balance for.
* @param _amount Amount to allow the account to spend from your account.
* @return true if the allowance was successful.
*/
function approve(
address _spender,
uint256 _amount
)
external
returns (
bool
)
{
allowances[msg.sender][_spender] = _amount;
emit Approval(
msg.sender,
_spender,
_amount
);
return true;
}
/**
* Checks how much a given account is allowed to spend from another given account.
* @param _owner Address of the account to check an allowance from.
* @param _spender Address of the account trying to spend from the owner.
* @return Allowance for the spender from the owner.
*/
function allowance(
address _owner,
address _spender
)
external
view
returns (
uint256
)
{
return allowances[_owner][_spender];
}
function testRequire()
external
view
returns (
uint256
)
{
require(false, "This is an revert string");
return balances[msg.sender];
}
}
// SPDX-License-Identifier: MIT
// solhint-disable-next-line compiler-version
pragma solidity >=0.4.24 <0.8.0;
import "./Address.sol";
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {UpgradeableProxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Modifier to protect an initializer function from being invoked twice.
*/
modifier initializer() {
require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized");
bool isTopLevelCall = !_initializing;
if (isTopLevelCall) {
_initializing = true;
_initialized = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
}
}
/// @dev Returns true if and only if the function is running in the constructor
function _isConstructor() private view returns (bool) {
return !Address.isContract(address(this));
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
/**
* @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM
* instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to
* be specified by overriding the virtual {_implementation} function.
*
* Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
* different contract through the {_delegate} function.
*
* The success and return data of the delegated call will be returned back to the caller of the proxy.
*/
abstract contract Proxy {
/**
* @dev Delegates the current call to `implementation`.
*
* This function does not return to its internall call site, it will return directly to the external caller.
*/
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
/**
* @dev This is a virtual function that should be overriden so it returns the address to which the fallback function
* and {_fallback} should delegate.
*/
function _implementation() internal view virtual returns (address);
/**
* @dev Delegates the current call to the address returned by `_implementation()`.
*
* This function does not return to its internall call site, it will return directly to the external caller.
*/
function _fallback() internal virtual {
_beforeFallback();
_delegate(_implementation());
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
* function in the contract matches the call data.
*/
fallback () external payable virtual {
_fallback();
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data
* is empty.
*/
receive () external payable virtual {
_fallback();
}
/**
* @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback`
* call, or as part of the Solidity `fallback` or `receive` functions.
*
* If overriden should call `super._beforeFallback()`.
*/
function _beforeFallback() internal virtual {
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
import "./Proxy.sol";
import "./Address.sol";
/**
* @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an
* implementation address that can be changed. This address is stored in storage in the location specified by
* https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the
* implementation behind the proxy.
*
* Upgradeability is only provided internally through {_upgradeTo}. For an externally upgradeable proxy see
* {TransparentUpgradeableProxy}.
*/
contract UpgradeableProxy is Proxy {
/**
* @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
*
* If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded
* function call, and allows initializating the storage of the proxy like a Solidity constructor.
*/
constructor(address _logic, bytes memory _data) public payable {
assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
_setImplementation(_logic);
if(_data.length > 0) {
Address.functionDelegateCall(_logic, _data);
}
}
/**
* @dev Emitted when the implementation is upgraded.
*/
event Upgraded(address indexed implementation);
/**
* @dev Storage slot with the address of the current implementation.
* This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
* validated in the constructor.
*/
bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
/**
* @dev Returns the current implementation address.
*/
function _implementation() internal view virtual override returns (address impl) {
bytes32 slot = _IMPLEMENTATION_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly {
impl := sload(slot)
}
}
/**
* @dev Upgrades the proxy to a new implementation.
*
* Emits an {Upgraded} event.
*/
function _upgradeTo(address newImplementation) internal virtual {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
/**
* @dev Stores a new address in the EIP1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract");
bytes32 slot = _IMPLEMENTATION_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly {
sstore(slot, newImplementation)
}
}
}
import { HardhatUserConfig } from 'hardhat/types'
// Hardhat plugins
import '@nomiclabs/hardhat-ethers'
import '@eth-optimism/hardhat-ovm'
const config: HardhatUserConfig = {
mocha: {
timeout: 100000,
},
}
export default config
{
"name": "@eth-optimism/sequencer-interactions",
"version": "0.0.1",
"description": "[Optimism] Integration Tests: Sequencer Interactions",
"author": "Optimism PBC",
"license": "MIT",
"scripts": {
"lint": "yarn lint:fix && yarn lint:check",
"lint:check": "tslint --format stylish --project .",
"lint:fix": "prettier --config ./prettier-config.json --write 'test/**/*.ts'",
"test:integration": "TARGET=ovm hardhat test"
},
"devDependencies": {
"@eth-optimism/contracts": "^0.1.11",
"@eth-optimism/core-utils": "^0.1.10",
"@eth-optimism/hardhat-ovm": "^0.0.1",
"@ethersproject/providers": "^5.0.24",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"chai": "^4.3.3",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.0.32",
"hardhat": "^2.1.2",
"lodash": "^4.17.21",
"mocha": "^8.3.1"
}
}
../prettier-config.json
\ No newline at end of file
/* Imports: Internal */
import { getContractFactory } from '@eth-optimism/contracts'
import { injectL2Context } from './utils'
/* Imports: External */
import { Contract, Signer, Wallet, providers } from 'ethers'
import { expect } from 'chai'
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// This test ensures that the transactions which get `enqueue`d get
// added to the L2 blocks by the Sync Service (which queries the DTL)
describe('Queue Ingestion', () => {
const RETRIES = 20
const numTxs = 5
let startBlock: number
let endBlock: number
let l1Signer: Signer
let l2Provider: providers.JsonRpcProvider
let addressResolver: Contract
let canonicalTransactionChain: Contract
const receipts = []
before(async () => {
const httpPort = 8545
const l1HttpPort = 9545
l2Provider = injectL2Context(
new providers.JsonRpcProvider(`http://localhost:${httpPort}`)
)
l1Signer = new Wallet(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
new providers.JsonRpcProvider(`http://localhost:${l1HttpPort}`)
)
const addressResolverAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
addressResolver = getContractFactory('Lib_AddressManager')
.connect(l1Signer)
.attach(addressResolverAddress)
const ctcAddress = await addressResolver.getAddress(
'OVM_CanonicalTransactionChain'
)
canonicalTransactionChain = getContractFactory(
'OVM_CanonicalTransactionChain'
).attach(ctcAddress)
})
// The transactions are enqueue'd with a `to` address of i.repeat(40)
// meaning that the `to` value is different each iteration in a deterministic
// way. They need to be inserted into the L2 chain in an ascending order.
// Keep track of the receipts so that the blockNumber can be compared
// against the `L1BlockNumber` on the tx objects.
before(async () => {
// Keep track of the L2 tip before submitting any transactions so that
// the subsequent transactions can be queried for in the next test
startBlock = (await l2Provider.getBlockNumber()) + 1
endBlock = startBlock + numTxs - 1
// Enqueue some transactions by building the calldata and then sending
// the transaction to Layer 1
for (let i = 0; i < numTxs; i++) {
const input = ['0x' + `${i}`.repeat(40), 500_000, `0x0${i}`]
const calldata = canonicalTransactionChain.interface.encodeFunctionData(
'enqueue',
input
)
const txResponse = await l1Signer.sendTransaction({
data: calldata,
to: canonicalTransactionChain.address,
})
const receipt = await txResponse.wait()
receipts.push(receipt)
}
})
// The batch submitter will notice that there are transactions
// that are in the queue and submit them. L2 will pick up the
// sequencer batch appended event and play the transactions.
it('should order transactions correctly', async () => {
// Wait until each tx from the previous test has
// been executed
let i: number
for (i = 0; i < RETRIES; i++) {
const tip = await l2Provider.getBlockNumber()
if (tip >= endBlock) {
break
}
await sleep(1000)
}
if (i === RETRIES) {
throw new Error(
'timed out waiting for queued transactions to be inserted'
)
}
const from = await l1Signer.getAddress()
// Keep track of an index into the receipts list and
// increment it for each block fetched.
let receiptIndex = 0
// Fetch blocks
for (i = 0; i < numTxs; i++) {
const block = await l2Provider.getBlock(startBlock + i)
const hash = block.transactions[0]
// Use as any hack because additional properties are
// added to the transaction response
const tx = await (l2Provider.getTransaction(hash) as any)
// The `to` addresses are defined in the previous test and
// increment sequentially.
expect(tx.to).to.be.equal('0x' + `${i}`.repeat(40))
// The transaction type is EIP155
expect(tx.txType).to.be.equal('EIP155')
// The queue origin is Layer 1
expect(tx.queueOrigin).to.be.equal('l1')
// the L1TxOrigin is equal to the Layer one from
expect(tx.l1TxOrigin).to.be.equal(from.toLowerCase())
expect(typeof tx.l1BlockNumber).to.be.equal('number')
// Get the receipt and increment the recept index
const receipt = receipts[receiptIndex++]
expect(tx.l1BlockNumber).to.be.equal(receipt.blockNumber)
}
})
})
import { expect } from 'chai'
import { ethers } from 'hardhat'
/* Imports: External */
import { Contract, Wallet, providers } from 'ethers'
describe('Reading events from proxy contracts', () => {
let l2Provider: providers.JsonRpcProvider
let l2Wallet: Wallet
before(async () => {
const httpPort = 8545
l2Provider = new providers.JsonRpcProvider(`http://localhost:${httpPort}`)
l2Wallet = Wallet.createRandom().connect(l2Provider)
})
// helper to query the transfers
const _queryFilterTransfer = async (
queryContract: Contract,
filterContract: Contract
) => {
// Get the filter
const filter = filterContract.filters.Transfer(null, null, null)
// Query the filter
return queryContract.queryFilter(filter, 0, 'latest')
}
let ProxyERC20: Contract
let ERC20: Contract
beforeEach(async () => {
// Set up our contract factories in advance.
const Factory__ERC20 = await ethers.getContractFactory(
'ChainlinkERC20',
l2Wallet
)
const Factory__UpgradeableProxy = await ethers.getContractFactory(
'UpgradeableProxy',
l2Wallet
)
// Deploy the underlying ERC20 implementation.
ERC20 = await Factory__ERC20.deploy()
await ERC20.deployTransaction.wait()
// Deploy the upgradeable proxy and execute the init function.
ProxyERC20 = await Factory__UpgradeableProxy.deploy(
ERC20.address,
ERC20.interface.encodeFunctionData('init', [
1000, // initial supply
'Cool Token Name Goes Here', // token name
])
)
await ProxyERC20.deployTransaction.wait()
ProxyERC20 = new ethers.Contract(
ProxyERC20.address,
ERC20.interface,
l2Wallet
)
})
it('should read transfer events from a proxy ERC20', async () => {
// Make two transfers.
const recipient = '0x0000000000000000000000000000000000000000'
const transfer1 = await ProxyERC20.transfer(recipient, 1)
await transfer1.wait()
const transfer2 = await ProxyERC20.transfer(recipient, 1)
await transfer2.wait()
// Make sure events are being emitted in the right places.
expect((await _queryFilterTransfer(ERC20, ERC20)).length).to.eq(0)
expect((await _queryFilterTransfer(ERC20, ProxyERC20)).length).to.eq(0)
expect((await _queryFilterTransfer(ProxyERC20, ERC20)).length).to.eq(2)
expect((await _queryFilterTransfer(ProxyERC20, ProxyERC20)).length).to.eq(2)
})
})
import { remove0x } from '@eth-optimism/core-utils'
import { JsonRpcProvider } from '@ethersproject/providers'
import cloneDeep from 'lodash/cloneDeep'
import { utils, providers, Transaction } from 'ethers'
/**
* Helper for adding additional L2 context to transactions
*/
export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => {
const provider = cloneDeep(l1Provider)
const format = provider.formatter.transaction.bind(provider.formatter)
provider.formatter.transaction = (transaction) => {
const tx = format(transaction)
const sig = utils.joinSignature(tx)
const hash = sighashEthSign(tx)
tx.from = utils.verifyMessage(hash, sig)
return tx
}
// Pass through the state root
const blockFormat = provider.formatter.block.bind(provider.formatter)
provider.formatter.block = (block) => {
const b = blockFormat(block)
b.stateRoot = block.stateRoot
return b
}
// Pass through the state root and additional tx data
const blockWithTransactions = provider.formatter.blockWithTransactions.bind(
provider.formatter
)
provider.formatter.blockWithTransactions = (block) => {
const b = blockWithTransactions(block)
b.stateRoot = block.stateRoot
for (let i = 0; i < b.transactions.length; i++) {
b.transactions[i].l1BlockNumber = block.transactions[i].l1BlockNumber
if (b.transactions[i].l1BlockNumber != null) {
b.transactions[i].l1BlockNumber = parseInt(
b.transactions[i].l1BlockNumber,
16
)
}
b.transactions[i].l1TxOrigin = block.transactions[i].l1TxOrigin
b.transactions[i].txType = block.transactions[i].txType
b.transactions[i].queueOrigin = block.transactions[i].queueOrigin
}
return b
}
// Handle additional tx data
const formatTxResponse = provider.formatter.transactionResponse.bind(
provider.formatter
)
provider.formatter.transactionResponse = (transaction) => {
const tx = formatTxResponse(transaction) as any
tx.txType = transaction.txType
tx.queueOrigin = transaction.queueOrigin
tx.rawTransaction = transaction.rawTransaction
tx.l1BlockNumber = transaction.l1BlockNumber
if (tx.l1BlockNumber != null) {
tx.l1BlockNumber = parseInt(tx.l1BlockNumber, 16)
}
tx.l1TxOrigin = transaction.l1TxOrigin
return tx
}
return provider
}
function serializeEthSignTransaction(transaction: Transaction): any {
const encoded = utils.defaultAbiCoder.encode(
['uint256', 'uint256', 'uint256', 'uint256', 'address', 'bytes'],
[
transaction.nonce,
transaction.gasLimit,
transaction.gasPrice,
transaction.chainId,
transaction.to,
transaction.data,
]
)
return Buffer.from(encoded.slice(2), 'hex')
}
// Use this function as input to `eth_sign`. It does not
// add the prefix because `eth_sign` does that. It does
// serialize the transaction and hash the serialized
// transaction.
function sighashEthSign(transaction: any): Buffer {
const serialized = serializeEthSignTransaction(transaction)
const hash = remove0x(utils.keccak256(serialized))
return Buffer.from(hash, 'hex')
}
{
"extends": "../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": ["./test"],
"files": ["./hardhat.config.ts"]
}
{
"extends": "../tslint.base.json",
"rules": {
"array-type": false,
"class-name": false
}
}
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/*" "packages/*",
"integration-tests"
], ],
"private": true, "private": true,
"devDependencies": { "devDependencies": {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@eth-optimism/*": ["packages/*/src"] "@eth-optimism/*": ["packages/*/src", "integration-tests/*/test"]
}, },
"skipLibCheck": true "skipLibCheck": true
} }
......
This diff is collapsed.
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