diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6a9cf9003ad6..a4915f014439 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -45,18 +45,18 @@ jobs: run: docker-compose run integration_tests # Examples Tests - - name: Test & deploy hardhat-example on Ethereum (regression) - working-directory: ./examples/hardhat - run: | - yarn - yarn deploy - yarn test:integration - - - name: Test & deploy hardhat-example on Optimistic Ethereum - working-directory: ./examples/hardhat - run: | - yarn deploy:ovm - yarn test:integration:ovm + # - name: Test & deploy hardhat-example on Ethereum (regression) + # working-directory: ./examples/hardhat + # run: | + # yarn + # yarn deploy + # yarn test:integration + + # - name: Test & deploy hardhat-example on Optimistic Ethereum + # working-directory: ./examples/hardhat + # run: | + # yarn deploy:ovm + # yarn test:integration:ovm - name: Test & deploy waffle-example on Ethereum (regression) working-directory: ./examples/waffle @@ -86,28 +86,28 @@ jobs: yarn test:integration:ovm yarn deploy:ovm - - name: Test l1-l2-deposit-withdrawal example on Optimistic Ethereum with cross-domain message passing - working-directory: ./examples/l1-l2-deposit-withdrawal - run: | - yarn - yarn compile - yarn compile:ovm - yarn test:integration:ovm + # - name: Test l1-l2-deposit-withdrawal example on Optimistic Ethereum with cross-domain message passing + # working-directory: ./examples/l1-l2-deposit-withdrawal + # run: | + # yarn + # yarn compile + # yarn compile:ovm + # yarn test:integration:ovm + + - name: Collect docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v1 + with: + images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth,ethereumoptimism/integration-tests' + dest: './logs' - # - name: Collect docker logs on failure - # if: failure() - # uses: jwalton/gh-docker-logs@v1 - # with: - # images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth,ethereumoptimism/integration-tests' - # dest: './logs' - - # - name: Tar logs - # if: failure() - # run: tar cvzf ./logs.tgz ./logs - - # - name: Upload logs to GitHub - # if: failure() - # uses: actions/upload-artifact@master - # with: - # name: logs.tgz - # path: ./logs.tgz \ No newline at end of file + - name: Tar logs + if: failure() + run: tar cvzf ./logs.tgz ./logs + + - name: Upload logs to GitHub + if: failure() + uses: actions/upload-artifact@master + with: + name: logs.tgz + path: ./logs.tgz diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml index 82585ff8dc8e..20f8f351b051 100644 --- a/.github/workflows/publish-canary.yml +++ b/.github/workflows/publish-canary.yml @@ -21,6 +21,7 @@ jobs: message-relayer: ${{ steps.packages.outputs.message-relayer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} contracts: ${{ steps.packages.outputs.contracts }} + canary-docker-tag: ${{ steps.docker-image-name.outputs.canary-docker-tag }} steps: - name: Check out source code @@ -72,9 +73,9 @@ jobs: node ops/scripts/ci-versions.js ${{ toJSON(steps.changesets.outputs.publishedPackages) }} - name: Docker Image Name - id: docker_image_name + id: docker-image-name run: | - if [ -z "${CUSTOM_IMAGE_NAME}" ] + if [ ${CUSTOM_IMAGE_NAME} == '' ] then echo "::set-output name=canary-docker-tag::${GITHUB_SHA::8}" else @@ -89,7 +90,7 @@ jobs: # while also allowing for parallelization (i.e. `l2geth` not depending on `builder`) # and all jobs executing in parallel once `builder` is built l2geth: - name: Publish L2Geth Version ${{ needs.canary-publish.outputs.l2geth }} + name: Publish L2Geth Version ${{ needs.canary-publish.outputs.canary-docker-tag }} needs: canary-publish if: needs.canary-publish.outputs.l2geth != '' runs-on: ubuntu-latest @@ -112,7 +113,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.geth push: true - tags: ethereumoptimism/l2geth:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/l2geth:${{ needs.canary-publish.outputs.canary-docker-tag }} # pushes the base builder image to dockerhub builder: @@ -127,6 +128,8 @@ jobs: data-transport-layer: ${{ needs.canary-publish.outputs.data-transport-layer }} contracts: ${{ needs.canary-publish.outputs.contracts }} integration-tests: ${{ needs.canary-publish.outputs.integration-tests }} + canary-docker-tag: ${{ needs.canary-publish.outputs.canary-docker-tag }} + steps: - name: Checkout @@ -149,7 +152,7 @@ jobs: tags: ethereumoptimism/builder message-relayer: - name: Publish Message Relayer Version ${{ needs.builder.outputs.message-relayer }} + name: Publish Message Relayer Version ${{ needs.builder.outputs.canary-docker-tag }} needs: builder if: needs.builder.outputs.message-relayer != '' runs-on: ubuntu-latest @@ -172,10 +175,10 @@ jobs: context: . file: ./ops/docker/Dockerfile.message-relayer push: true - tags: ethereumoptimism/message-relayer:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/message-relayer:${{ needs.builder.outputs.canary-docker-tag }} batch-submitter: - name: Publish Batch Submitter Version ${{ needs.builder.outputs.batch-submitter }} + name: Publish Batch Submitter Version ${{ needs.builder.outputs.canary-docker-tag }} needs: builder if: needs.builder.outputs.batch-submitter != '' runs-on: ubuntu-latest @@ -198,10 +201,10 @@ jobs: context: . file: ./ops/docker/Dockerfile.batch-submitter push: true - tags: ethereumoptimism/batch-submitter:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/batch-submitter:${{ needs.builder.outputs.canary-docker-tag }} data-transport-layer: - name: Publish Data Transport Layer Version ${{ needs.builder.outputs.data-transport-layer }} + name: Publish Data Transport Layer Version ${{ needs.builder.outputs.canary-docker-tag }} needs: builder if: needs.builder.outputs.data-transport-layer != '' runs-on: ubuntu-latest @@ -224,10 +227,10 @@ jobs: context: . file: ./ops/docker/Dockerfile.data-transport-layer push: true - tags: ethereumoptimism/data-transport-layer:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/data-transport-layer:${{ needs.builder.outputs.canary-docker-tag }} contracts: - name: Publish Deployer Version ${{ needs.builder.outputs.contracts }} + name: Publish Deployer Version ${{ needs.builder.outputs.canary-docker-tag }} needs: builder if: needs.builder.outputs.contracts != '' runs-on: ubuntu-latest @@ -250,7 +253,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.deployer push: true - tags: ethereumoptimism/deployer:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/deployer:${{ needs.builder.outputs.canary-docker-tag }} integration_tests: name: Publish Integration tests ${{ needs.builder.outputs.integration-tests }} @@ -276,4 +279,4 @@ jobs: context: . file: ./ops/docker/Dockerfile.integration-tests push: true - tags: ethereumoptimism/integration-tests:${{ steps.docker_image_name.outputs.canary-docker-tag }} + tags: ethereumoptimism/integration-tests:${{ needs.builder.outputs.canary-docker-tag }} diff --git a/.github/workflows/sync-tests.yml b/.github/workflows/sync-tests.yml index 6dfcb599a3dc..2e3c94d37f3c 100644 --- a/.github/workflows/sync-tests.yml +++ b/.github/workflows/sync-tests.yml @@ -1,13 +1,6 @@ name: sync-tests -on: - push: - branches: - - 'master' - - 'develop' - - 'regenesis/*' - pull_request: - workflow_dispatch: +on: workflow_dispatch jobs: integration-sync-test: @@ -41,22 +34,23 @@ jobs: working-directory: ./integration-tests run: | yarn + yarn build:integration yarn test:sync - # - name: Collect docker logs on failure - # if: failure() - # uses: jwalton/gh-docker-logs@v1 - # with: - # images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth' - # dest: './logs' - - # - name: Tar logs - # if: failure() - # run: tar cvzf ./logs.tgz ./logs - - # - name: Upload logs to GitHub - # if: failure() - # uses: actions/upload-artifact@master - # with: - # name: logs.tgz - # path: ./logs.tgz + - name: Collect docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v1 + with: + images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth' + dest: './logs' + + - name: Tar logs + if: failure() + run: tar cvzf ./logs.tgz ./logs + + - name: Upload logs to GitHub + if: failure() + uses: actions/upload-artifact@master + with: + name: logs.tgz + path: ./logs.tgz diff --git a/.gitignore b/.gitignore index 3b96ad85ee8d..e031e635cb48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,16 +20,8 @@ packages/contracts/hardhat* packages/data-transport-layer/db -packages/omgx/wallet/wallet/build - # vim *.swp .env -env.js -env.yml -env.js - -.serverless -packages/omgx/wallet/deployment/local/addresses.json -packages/omgx/wallet/wallet/src/deployment/local/addresses.json +*.log diff --git a/README.md b/README.md index 0f4d185446df..dc179472a6e7 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,49 @@ yarn build:integration yarn test:integration ``` +## Branching Model and Releases + + + +### Active Branches + +| Branch | Status | +| --------------- | -------------------------------------------------------------------------------- | +| [master](https://github.com/ethereum-optimism/optimism/tree/master/) | Accepts PRs from `develop` when we intend to deploy to mainnet. | +| [develop](https://github.com/ethereum-optimism/optimism/tree/develop/) | Accepts PRs that are compatible with `master` OR from `regenesis/X.X.X` branches. | +| regenesis/X.X.X | Accepts PRs for all changes, particularly those not backwards compatible with `develop` and `master`. | + +### Overview + +We generally follow [this Git branching model](https://nvie.com/posts/a-successful-git-branching-model/). +Please read the linked post if you're planning to make frequent PRs into this repository (e.g., people working at/with Optimism). + +### The `master` branch + +The `master` branch contains the code for our latest "stable" releases. +Updates from `master` always come from the `develop` branch. +We only ever update the `master` branch when we intend to deploy code within the `develop` to the Optimistic Ethereum mainnet. +Our update process takes the form of a PR merging the `develop` branch into the `master` branch. + +### The `develop` branch + +Our primary development branch is [`develop`](https://github.com/ethereum-optimism/optimism/tree/develop/). +`develop` contains the most up-to-date software that remains backwards compatible with our latest experimental [network deployments](https://community.optimism.io/docs/developers/networks.html). +If you're making a backwards compatible change, please direct your pull request towards `develop`. + +**Changes to contracts within `packages/contracts/contracts/optimistic-ethereum` are usually NOT considered backwards compatible and SHOULD be made against a release candidate branch**. +Some exceptions to this rule exist for cases in which we absolutely must deploy some new contract after a release candidate branch has already been fully deployed. +If you're changing or adding a contract and you're unsure about which branch to make a PR into, default to using the latest release candidate branch. +See below for info about release candidate branches. + +### Release candidate branches + +Branches marked `regenesis/X.X.X` are **release candidate branches**. +Changes that are not backwards compatible and all changes to contracts within `packages/contracts/contracts/optimistic-ethereum` MUST be directed towards a release candidate branch. +Release candidates are merged into `develop` and then into `master` once they've been fully deployed. +We may sometimes have more than one active `regenesis/X.X.X` branch if we're in the middle of a deployment. +See table in the **Active Branches** section above to find the right branch to target. + ## Additional Reference Material ### Running contract static analysis diff --git a/integration-tests/CHANGELOG.md b/integration-tests/CHANGELOG.md index ea2181a934dd..723c586e379d 100644 --- a/integration-tests/CHANGELOG.md +++ b/integration-tests/CHANGELOG.md @@ -1,5 +1,27 @@ # @eth-optimism/integration-tests +## 0.1.1 + +### Patch Changes + +- 40b99a6e: Add new RPC endpoint `rollup_gasPrices` + +## 0.1.0 + +### Minor Changes + +- e04de624: Add support for ovmCALL with nonzero ETH value + +### Patch Changes + +- 25f09abd: Adds ERC1271 support to default contract account +- 5fc728da: Add a new Standard Token Bridge, to handle deposits and withdrawals of any ERC20 token. + For projects developing a custom bridge, if you were previously importing `iAbs_BaseCrossDomainMessenger`, you should now + import `iOVM_CrossDomainMessenger`. +- c43b33ec: Add WETH9 compatible deposit and withdraw functions to OVM_ETH +- e045f582: Adds new SequencerFeeVault contract to store generated fees +- b8e2d685: Add replica sync test to integration tests; handle 0 L2 blocks in DTL + ## 0.0.7 ### Patch Changes diff --git a/integration-tests/package.json b/integration-tests/package.json index 12061c3b9d4a..a17433a472b5 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/integration-tests", - "version": "0.0.7", + "version": "0.1.1", "description": "[Optimism] Integration Tests", "private": true, "author": "Optimism PBC", @@ -17,8 +17,8 @@ "clean": "rimraf cache artifacts artifacts-ovm cache-ovm" }, "devDependencies": { - "@eth-optimism/contracts": "^0.3.5", - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/contracts": "^0.4.1", + "@eth-optimism/core-utils": "^0.4.6", "@eth-optimism/hardhat-ovm": "^0.2.2", "@ethersproject/providers": "^5.0.24", "@nomiclabs/hardhat-ethers": "^2.0.2", diff --git a/integration-tests/sync-tests/sync-verifier.spec.ts b/integration-tests/sync-tests/sync-verifier.spec.ts deleted file mode 100644 index a989a83005c4..000000000000 --- a/integration-tests/sync-tests/sync-verifier.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import chai, { expect } from 'chai' -import { Wallet, BigNumber, providers } from 'ethers' -import { injectL2Context } from '@eth-optimism/core-utils' - -import { sleep, l2Provider, verifierProvider } from '../test/shared/utils' -import { OptimismEnv } from '../test/shared/env' -import { DockerComposeNetwork } from '../test/shared/docker-compose' - -describe('Syncing a verifier', () => { - let env: OptimismEnv - let wallet: Wallet - let verifier: DockerComposeNetwork - let provider: providers.JsonRpcProvider - - const sequencerProvider = injectL2Context(l2Provider) - - /* Helper functions */ - - const waitForBatchSubmission = async ( - totalElementsBefore: BigNumber - ): Promise => { - // Wait for batch submission to happen by watching the CTC - let totalElementsAfter = (await env.ctc.getTotalElements()) as BigNumber - while (totalElementsBefore.eq(totalElementsAfter)) { - await sleep(500) - totalElementsAfter = (await env.ctc.getTotalElements()) as BigNumber - } - return totalElementsAfter - } - - const startVerifier = async () => { - // Bring up new verifier - verifier = new DockerComposeNetwork(['verifier']) - await verifier.up({ commandOptions: ['--scale', 'verifier=1'] }) - - // Wait for verifier to be looping - let logs = await verifier.logs() - while (!logs.out.includes('Starting Verifier Loop')) { - await sleep(500) - logs = await verifier.logs() - } - - provider = injectL2Context(verifierProvider) - } - - const syncVerifier = async (sequencerBlockNumber: number) => { - // Wait until verifier has caught up to the sequencer - let latestVerifierBlock = (await provider.getBlock('latest')) as any - while (latestVerifierBlock.number < sequencerBlockNumber) { - await sleep(500) - latestVerifierBlock = (await provider.getBlock('latest')) as any - } - - return provider.getBlock(sequencerBlockNumber) - } - - before(async () => { - env = await OptimismEnv.new() - wallet = env.l2Wallet - }) - - describe('Basic transactions', () => { - after(async () => { - await verifier.stop('verifier') - await verifier.rm() - }) - - it('should sync dummy transaction', async () => { - const totalElementsBefore = - (await env.ctc.getTotalElements()) as BigNumber - - const tx = { - to: '0x' + '1234'.repeat(10), - gasLimit: 4000000, - gasPrice: 0, - data: '0x', - value: 0, - } - const result = await wallet.sendTransaction(tx) - await result.wait() - - const totalElementsAfter = await waitForBatchSubmission( - totalElementsBefore - ) - expect(totalElementsAfter.gt(totalElementsAfter)) - - const latestSequencerBlock = (await sequencerProvider.getBlock( - 'latest' - )) as any - - await startVerifier() - - const matchingVerifierBlock = (await syncVerifier( - latestSequencerBlock.number - )) as any - - expect(matchingVerifierBlock.stateRoot).to.eq( - latestSequencerBlock.stateRoot - ) - }) - - it('should have matching block data', async () => { - const sequencerTip = await sequencerProvider.getBlock('latest') - const verifierTip = await provider.getBlock('latest') - - expect(sequencerTip.number).to.deep.eq(verifierTip.number) - expect(sequencerTip.hash).to.deep.eq(verifierTip.hash) - }) - }) -}) diff --git a/integration-tests/test/fee-payment.spec.ts b/integration-tests/test/fee-payment.spec.ts index f7de4d3611a2..0e0c878fcc70 100644 --- a/integration-tests/test/fee-payment.spec.ts +++ b/integration-tests/test/fee-payment.spec.ts @@ -1,18 +1,33 @@ import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' chai.use(chaiAsPromised) -import { BigNumber, utils } from 'ethers' -import { OptimismEnv } from './shared/env' + +/* Imports: External */ +import { BigNumber, Contract, utils } from 'ethers' import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils' +import { predeploys, getContractInterface } from '@eth-optimism/contracts' + +/* Imports: Internal */ +import { OptimismEnv } from './shared/env' +import { Direction } from './shared/watcher-utils' describe('Fee Payment Integration Tests', async () => { - let env: OptimismEnv const other = '0x1234123412341234123412341234123412341234' + let env: OptimismEnv before(async () => { env = await OptimismEnv.new() }) + let ovmSequencerFeeVault: Contract + before(async () => { + ovmSequencerFeeVault = new Contract( + predeploys.OVM_SequencerFeeVault, + getContractInterface('OVM_SequencerFeeVault'), + env.l2Wallet + ) + }) + it(`Should return a gasPrice of ${TxGasPrice.toString()} wei`, async () => { const gasPrice = await env.l2Wallet.getGasPrice() expect(gasPrice).to.deep.eq(TxGasPrice) @@ -36,6 +51,9 @@ describe('Fee Payment Integration Tests', async () => { it('Paying a nonzero but acceptable gasPrice fee', async () => { const amount = utils.parseEther('0.5') const balanceBefore = await env.l2Wallet.getBalance() + const feeVaultBalanceBefore = await env.l2Wallet.provider.getBalance( + ovmSequencerFeeVault.address + ) expect(balanceBefore.gt(amount)) const tx = await env.ovmEth.transfer(other, amount) @@ -43,10 +61,53 @@ describe('Fee Payment Integration Tests', async () => { expect(receipt.status).to.eq(1) const balanceAfter = await env.l2Wallet.getBalance() + const feeVaultBalanceAfter = await env.l2Wallet.provider.getBalance( + ovmSequencerFeeVault.address + ) + const expectedFeePaid = tx.gasPrice.mul(tx.gasLimit) + // The fee paid MUST be the receipt.gasUsed, and not the tx.gasLimit // https://github.com/ethereum-optimism/optimism/blob/0de7a2f9c96a7c4860658822231b2d6da0fefb1d/packages/contracts/contracts/optimistic-ethereum/OVM/accounts/OVM_ECDSAContractAccount.sol#L103 - expect(balanceBefore.sub(balanceAfter)).to.be.deep.eq( - tx.gasPrice.mul(tx.gasLimit).add(amount) + expect(balanceBefore.sub(balanceAfter)).to.deep.equal( + expectedFeePaid.add(amount) + ) + + // Make sure the fee was transferred to the vault. + expect(feeVaultBalanceAfter.sub(feeVaultBalanceBefore)).to.deep.equal( + expectedFeePaid + ) + }) + + it('should not be able to withdraw fees before the minimum is met', async () => { + await expect(ovmSequencerFeeVault.withdraw()).to.be.rejected + }) + + it('should be able to withdraw fees back to L1 once the minimum is met', async () => { + const l1FeeWallet = await ovmSequencerFeeVault.l1FeeWallet() + const balanceBefore = await env.l1Wallet.provider.getBalance(l1FeeWallet) + + // Transfer the minimum required to withdraw. + await env.ovmEth.transfer( + ovmSequencerFeeVault.address, + await ovmSequencerFeeVault.MIN_WITHDRAWAL_AMOUNT() + ) + + const vaultBalance = await env.ovmEth.balanceOf( + ovmSequencerFeeVault.address + ) + + // Submit the withdrawal. + const withdrawTx = await ovmSequencerFeeVault.withdraw({ + gasPrice: 0, // Need a gasprice of 0 or the balances will include the fee paid during this tx. + }) + + // Wait for the withdrawal to be relayed to L1. + await env.waitForXDomainTransaction(withdrawTx, Direction.L2ToL1) + + // Balance difference should be equal to old L2 balance. + const balanceAfter = await env.l1Wallet.provider.getBalance(l1FeeWallet) + expect(balanceAfter.sub(balanceBefore)).to.deep.equal( + BigNumber.from(vaultBalance) ) }) }) diff --git a/integration-tests/test/native-eth.spec.ts b/integration-tests/test/native-eth.spec.ts index 68c27f52a639..81bf6dc809b0 100644 --- a/integration-tests/test/native-eth.spec.ts +++ b/integration-tests/test/native-eth.spec.ts @@ -1,10 +1,21 @@ +import { predeploys } from '@eth-optimism/contracts' + import { expect } from 'chai' import { Wallet, utils, BigNumber } from 'ethers' import { Direction } from './shared/watcher-utils' -import { PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils' +import { + expectApprox, + fundUser, + PROXY_SEQUENCER_ENTRYPOINT_ADDRESS, +} from './shared/utils' import { OptimismEnv } from './shared/env' +const DEFAULT_TEST_GAS_L1 = 330_000 +const DEFAULT_TEST_GAS_L2 = 1_300_000 +// TX size enforced by CTC: +const MAX_ROLLUP_TX_SIZE = 50_000 + describe('Native ETH Integration Tests', async () => { let env: OptimismEnv let l1Bob: Wallet @@ -20,8 +31,8 @@ describe('Native ETH Integration Tests', async () => { const sequencerBalance = await _env.ovmEth.balanceOf( PROXY_SEQUENCER_ENTRYPOINT_ADDRESS ) - const l1GatewayBalance = await _env.l1Wallet.provider.getBalance( - _env.gateway.address + const l1BridgeBalance = await _env.l1Wallet.provider.getBalance( + _env.l1Bridge.address ) return { @@ -29,7 +40,7 @@ describe('Native ETH Integration Tests', async () => { l2UserBalance, l1BobBalance, l2BobBalance, - l1GatewayBalance, + l1BridgeBalance, sequencerBalance, } } @@ -45,29 +56,65 @@ describe('Native ETH Integration Tests', async () => { const amount = utils.parseEther('0.5') const addr = '0x' + '1234'.repeat(10) const gas = await env.ovmEth.estimateGas.transfer(addr, amount) - expect(gas).to.be.deep.eq(BigNumber.from(6430020)) + // Expect gas to be less than or equal to the target plus 1% + expectApprox(gas, 6430020, 1) }) it('Should estimate gas for ETH withdraw', async () => { const amount = utils.parseEther('0.5') - const gas = await env.ovmEth.estimateGas.withdraw(amount) - expect(gas).to.be.deep.eq(BigNumber.from(6140049)) + const gas = await env.l2Bridge.estimateGas.withdraw( + predeploys.OVM_ETH, + amount, + 0, + '0xFFFF' + ) + // Expect gas to be less than or equal to the target plus 1% + expectApprox(gas, 6700060, 1) }) }) - it('deposit', async () => { + it('receive', async () => { + const depositAmount = 10 + const preBalances = await getBalances(env) + const { tx, receipt } = await env.waitForXDomainTransaction( + env.l1Wallet.sendTransaction({ + to: env.l1Bridge.address, + value: depositAmount, + gasLimit: DEFAULT_TEST_GAS_L1, + }), + Direction.L1ToL2 + ) + + const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice) + const postBalances = await getBalances(env) + + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.add(depositAmount) + ) + expect(postBalances.l2UserBalance).to.deep.eq( + preBalances.l2UserBalance.add(depositAmount) + ) + expect(postBalances.l1UserBalance).to.deep.eq( + preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount)) + ) + }) + + it('depositETH', async () => { const depositAmount = 10 const preBalances = await getBalances(env) const { tx, receipt } = await env.waitForXDomainTransaction( - env.gateway.deposit({ value: depositAmount }), + env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, '0xFFFF', { + value: depositAmount, + gasLimit: DEFAULT_TEST_GAS_L1, + }), Direction.L1ToL2 ) const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice) const postBalances = await getBalances(env) - expect(postBalances.l1GatewayBalance).to.deep.eq( - preBalances.l1GatewayBalance.add(depositAmount) + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.add(depositAmount) ) expect(postBalances.l2UserBalance).to.deep.eq( preBalances.l2UserBalance.add(depositAmount) @@ -77,12 +124,13 @@ describe('Native ETH Integration Tests', async () => { ) }) - it('depositTo', async () => { + it('depositETHTo', async () => { const depositAmount = 10 const preBalances = await getBalances(env) const depositReceipts = await env.waitForXDomainTransaction( - env.gateway.depositTo(l2Bob.address, { + env.l1Bridge.depositETHTo(l2Bob.address, DEFAULT_TEST_GAS_L2, '0xFFFF', { value: depositAmount, + gasLimit: DEFAULT_TEST_GAS_L1, }), Direction.L1ToL2 ) @@ -91,8 +139,8 @@ describe('Native ETH Integration Tests', async () => { depositReceipts.tx.gasPrice ) const postBalances = await getBalances(env) - expect(postBalances.l1GatewayBalance).to.deep.eq( - preBalances.l1GatewayBalance.add(depositAmount) + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.add(depositAmount) ) expect(postBalances.l2BobBalance).to.deep.eq( preBalances.l2BobBalance.add(depositAmount) @@ -102,6 +150,49 @@ describe('Native ETH Integration Tests', async () => { ) }) + it('deposit passes with a large data argument', async () => { + const ASSUMED_L2_GAS_LIMIT = 8_000_000 + const depositAmount = 10 + const preBalances = await getBalances(env) + + // Set data length slightly less than MAX_ROLLUP_TX_SIZE + // to allow for encoding and other arguments + const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE - 500) + const { tx, receipt } = await env.waitForXDomainTransaction( + env.l1Bridge.depositETH(ASSUMED_L2_GAS_LIMIT, data, { + value: depositAmount, + gasLimit: 4_000_000, + }), + Direction.L1ToL2 + ) + + const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice) + const postBalances = await getBalances(env) + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.add(depositAmount) + ) + expect(postBalances.l2UserBalance).to.deep.eq( + preBalances.l2UserBalance.add(depositAmount) + ) + expect(postBalances.l1UserBalance).to.deep.eq( + preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount)) + ) + }) + + it('depositETH fails with a TOO large data argument', async () => { + const depositAmount = 10 + + const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE + 1) + await expect( + env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, data, { + value: depositAmount, + gasLimit: 4_000_000, + }) + ).to.be.revertedWith( + 'Transaction data size exceeds maximum for rollup transaction.' + ) + }) + it('withdraw', async () => { const withdrawAmount = BigNumber.from(3) const preBalances = await getBalances(env) @@ -111,15 +202,20 @@ describe('Native ETH Integration Tests', async () => { ) const receipts = await env.waitForXDomainTransaction( - env.ovmEth.withdraw(withdrawAmount), + env.l2Bridge.withdraw( + predeploys.OVM_ETH, + withdrawAmount, + DEFAULT_TEST_GAS_L2, + '0xFFFF' + ), Direction.L2ToL1 ) const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice) const postBalances = await getBalances(env) - expect(postBalances.l1GatewayBalance).to.deep.eq( - preBalances.l1GatewayBalance.sub(withdrawAmount) + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.sub(withdrawAmount) ) expect(postBalances.l2UserBalance).to.deep.eq( preBalances.l2UserBalance.sub(withdrawAmount.add(fee)) @@ -140,15 +236,21 @@ describe('Native ETH Integration Tests', async () => { ) const receipts = await env.waitForXDomainTransaction( - env.ovmEth.withdrawTo(l1Bob.address, withdrawAmount), + env.l2Bridge.withdrawTo( + predeploys.OVM_ETH, + l1Bob.address, + withdrawAmount, + DEFAULT_TEST_GAS_L2, + '0xFFFF' + ), Direction.L2ToL1 ) const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice) const postBalances = await getBalances(env) - expect(postBalances.l1GatewayBalance).to.deep.eq( - preBalances.l1GatewayBalance.sub(withdrawAmount) + expect(postBalances.l1BridgeBalance).to.deep.eq( + preBalances.l1BridgeBalance.sub(withdrawAmount) ) expect(postBalances.l2UserBalance).to.deep.eq( preBalances.l2UserBalance.sub(withdrawAmount.add(fee)) @@ -162,8 +264,9 @@ describe('Native ETH Integration Tests', async () => { // 1. deposit const amount = utils.parseEther('1') await env.waitForXDomainTransaction( - env.gateway.deposit({ + env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, '0xFFFF', { value: amount, + gasLimit: DEFAULT_TEST_GAS_L1, }), Direction.L1ToL2 ) @@ -180,7 +283,14 @@ describe('Native ETH Integration Tests', async () => { // 3. do withdrawal const withdrawnAmount = utils.parseEther('0.95') const receipts = await env.waitForXDomainTransaction( - env.ovmEth.connect(other).withdraw(withdrawnAmount), + env.l2Bridge + .connect(other) + .withdraw( + predeploys.OVM_ETH, + withdrawnAmount, + DEFAULT_TEST_GAS_L1, + '0xFFFF' + ), Direction.L2ToL1 ) @@ -193,4 +303,81 @@ describe('Native ETH Integration Tests', async () => { expect(l1BalanceAfter).to.deep.eq(l1BalanceBefore.add(withdrawnAmount)) expect(l2BalanceAfter).to.deep.eq(amount.sub(withdrawnAmount).sub(fee)) }) + + describe('WETH9 functionality', async () => { + let initialBalance: BigNumber + const value = 10 + + beforeEach(async () => { + await fundUser(env.watcher, env.l1Bridge, value, env.l2Wallet.address) + initialBalance = await env.l2Wallet.provider.getBalance( + env.l2Wallet.address + ) + }) + + it('successfully deposits', async () => { + const depositTx = await env.ovmEth.deposit({ value, gasPrice: 0 }) + const receipt = await depositTx.wait() + + expect( + await env.l2Wallet.provider.getBalance(env.l2Wallet.address) + ).to.equal(initialBalance) + expect(receipt.events.length).to.equal(4) + + // The first transfer event is fee payment + const [ + , + firstTransferEvent, + secondTransferEvent, + depositEvent, + ] = receipt.events + + expect(firstTransferEvent.event).to.equal('Transfer') + expect(firstTransferEvent.args.from).to.equal(env.l2Wallet.address) + expect(firstTransferEvent.args.to).to.equal(env.ovmEth.address) + expect(firstTransferEvent.args.value).to.equal(value) + + expect(secondTransferEvent.event).to.equal('Transfer') + expect(secondTransferEvent.args.from).to.equal(env.ovmEth.address) + expect(secondTransferEvent.args.to).to.equal(env.l2Wallet.address) + expect(secondTransferEvent.args.value).to.equal(value) + + expect(depositEvent.event).to.equal('Deposit') + expect(depositEvent.args.dst).to.equal(env.l2Wallet.address) + expect(depositEvent.args.wad).to.equal(value) + }) + + it('successfully deposits on fallback', async () => { + const fallbackTx = await env.l2Wallet.sendTransaction({ + to: env.ovmEth.address, + value, + gasPrice: 0, + }) + const receipt = await fallbackTx.wait() + expect(receipt.status).to.equal(1) + expect( + await env.l2Wallet.provider.getBalance(env.l2Wallet.address) + ).to.equal(initialBalance) + }) + + it('successfully withdraws', async () => { + const withdrawTx = await env.ovmEth.withdraw(value, { gasPrice: 0 }) + const receipt = await withdrawTx.wait() + expect( + await env.l2Wallet.provider.getBalance(env.l2Wallet.address) + ).to.equal(initialBalance) + expect(receipt.events.length).to.equal(2) + + // The first transfer event is fee payment + const depositEvent = receipt.events[1] + expect(depositEvent.event).to.equal('Withdrawal') + expect(depositEvent.args.src).to.equal(env.l2Wallet.address) + expect(depositEvent.args.wad).to.equal(value) + }) + + it('reverts on invalid withdraw', async () => { + await expect(env.ovmEth.withdraw(initialBalance.add(1), { gasPrice: 0 })) + .to.be.reverted + }) + }) }) diff --git a/integration-tests/test/rpc.spec.ts b/integration-tests/test/rpc.spec.ts index fd321864e13b..122740747bff 100644 --- a/integration-tests/test/rpc.spec.ts +++ b/integration-tests/test/rpc.spec.ts @@ -4,10 +4,16 @@ import { TxGasPrice, toRpcHexString, } from '@eth-optimism/core-utils' -import { Wallet, BigNumber, Contract } from 'ethers' +import { Wallet, BigNumber, Contract, ContractFactory } from 'ethers' import { ethers } from 'hardhat' import chai, { expect } from 'chai' -import { sleep, l2Provider, l1Provider } from './shared/utils' +import { + sleep, + l2Provider, + DEFAULT_TRANSACTION, + fundUser, + expectApprox, +} from './shared/utils' import chaiAsPromised from 'chai-as-promised' import { OptimismEnv } from './shared/env' import { @@ -22,14 +28,6 @@ describe('Basic RPC tests', () => { let env: OptimismEnv let wallet: Wallet - const DEFAULT_TRANSACTION = { - to: '0x' + '1234'.repeat(10), - gasLimit: 33600000000001, - gasPrice: 0, - data: '0x', - value: 0, - } - const provider = injectL2Context(l2Provider) let Reverter: Contract @@ -154,7 +152,7 @@ describe('Basic RPC tests', () => { }) it('should correctly report OOG for contract creations', async () => { - const factory = await ethers.getContractFactory('TestOOG') + const factory = await ethers.getContractFactory('TestOOGInConstructor') await expect(factory.connect(wallet).deploy()).to.be.rejectedWith( 'gas required exceeds allowance' @@ -207,6 +205,32 @@ describe('Basic RPC tests', () => { 'Contract creation code contains unsafe opcodes. Did you use the right compiler or pass an unsafe constructor argument?' ) }) + + it('should allow eth_calls with nonzero value', async () => { + // Deploy a contract to check msg.value of the call + const Factory__ValueContext: ContractFactory = await ethers.getContractFactory( + 'ValueContext', + wallet + ) + const ValueContext: Contract = await Factory__ValueContext.deploy() + await ValueContext.deployTransaction.wait() + + // Fund account to call from + const from = wallet.address + const value = 15 + await fundUser(env.watcher, env.l1Bridge, value, from) + + // Do the call and check msg.value + const data = ValueContext.interface.encodeFunctionData('getCallValue') + const res = await provider.call({ + to: ValueContext.address, + from, + data, + value, + }) + + expect(res).to.eq(BigNumber.from(value)) + }) }) describe('eth_getTransactionReceipt', () => { @@ -236,7 +260,7 @@ describe('Basic RPC tests', () => { it('correctly exposes revert data for contract creations', async () => { const req: TransactionRequest = { ...revertingDeployTx, - gasLimit: 17700899, // override gas estimation + gasLimit: 27700899, // override gas estimation } const tx = await wallet.sendTransaction(req) @@ -264,7 +288,6 @@ describe('Basic RPC tests', () => { await result.wait() const transaction = (await provider.getTransaction(result.hash)) as any - expect(transaction.txType).to.equal('EIP155') expect(transaction.queueOrigin).to.equal('sequencer') expect(transaction.transactionIndex).to.be.eq(0) expect(transaction.gasLimit).to.be.deep.eq(BigNumber.from(tx.gasLimit)) @@ -285,7 +308,6 @@ describe('Basic RPC tests', () => { expect(block.number).to.not.equal(0) expect(typeof block.stateRoot).to.equal('string') expect(block.transactions.length).to.equal(1) - expect(block.transactions[0].txType).to.equal('EIP155') expect(block.transactions[0].queueOrigin).to.equal('sequencer') expect(block.transactions[0].l1TxOrigin).to.equal(null) }) @@ -321,7 +343,7 @@ describe('Basic RPC tests', () => { describe('eth_chainId', () => { it('should get the correct chainid', async () => { const { chainId } = await provider.getNetwork() - expect(chainId).to.be.eq(28) + expect(chainId).to.be.eq(420) }) }) @@ -355,7 +377,8 @@ describe('Basic RPC tests', () => { to: DEFAULT_TRANSACTION.to, value: 0, }) - expect(estimate).to.be.eq(5920012) + // Expect gas to be less than or equal to the target plus 1% + expectApprox(estimate, 5920012, 1) }) it('should return a gas estimate that grows with the size of data', async () => { @@ -403,4 +426,15 @@ describe('Basic RPC tests', () => { .reverted }) }) + + describe('rollup_gasPrices', () => { + it('should return the L1 and L2 gas prices', async () => { + const result = await provider.send('rollup_gasPrices', []); + const l1GasPrice = await env.l1Wallet.provider.getGasPrice() + const l2GasPrice = await env.gasPriceOracle.gasPrice() + + expect(BigNumber.from(result.l1GasPrice)).to.deep.eq(l1GasPrice) + expect((BigNumber.from(result.l2GasPrice))).to.deep.eq(l2GasPrice) + }) + }) }) diff --git a/l2geth/CHANGELOG.md b/l2geth/CHANGELOG.md index 95f2630fce25..15136ed6a93c 100644 --- a/l2geth/CHANGELOG.md +++ b/l2geth/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.4.1 + +### Patch Changes + +- 40b99a6e: Add new RPC endpoint `rollup_gasPrices` + +## 0.4.0 + +### Minor Changes + +- e04de624: Add support for ovmCALL with nonzero ETH value + +### Patch Changes + +- 01646a0a: Add new config `ROLLUP_GAS_PRICE_ORACLE_OWNER_ADDRESS` to set the owner of the gas price oracle at runtime +- 8fee7bed: Add extra overflow protection for the DTL types +- 5fc728da: Add a new Standard Token Bridge, to handle deposits and withdrawals of any ERC20 token. + For projects developing a custom bridge, if you were previously importing `iAbs_BaseCrossDomainMessenger`, you should now + import `iOVM_CrossDomainMessenger`. +- 257deb70: Prevent overflows in abi encoding of ovm codec transaction from geth types.Transaction +- 08873674: Update queueOrigin type +- 01646a0a: Removes config options that are no longer required. `ROLLUP_DATAPRICE`, `ROLLUP_EXECUTION_PRICE`, `ROLLUP_GAS_PRICE_ORACLE_ADDRESS` and `ROLLUP_ENABLE_L2_GAS_POLLING`. The oracle was moved to a predeploy 0x42.. address and polling is always enabled as it no longer needs to be backwards compatible +- 0a7f5a46: Removes the gas refund for unused gas in geth since it is instead managed in the smart contracts +- e045f582: Adds new SequencerFeeVault contract to store generated fees +- 25a5dbdd: Removes the SignatureHashType from l2geth as it is deprecated and no longer required. + ## 0.3.9 ### Patch Changes diff --git a/l2geth/eth/gasprice/rollup_gasprice.go b/l2geth/eth/gasprice/rollup_gasprice.go index 7b0a062e6c41..ac4166dea95e 100644 --- a/l2geth/eth/gasprice/rollup_gasprice.go +++ b/l2geth/eth/gasprice/rollup_gasprice.go @@ -17,10 +17,12 @@ type RollupOracle struct { } // NewRollupOracle returns an initialized RollupOracle -func NewRollupOracle(l1GasPrice *big.Int, l2GasPrice *big.Int) *RollupOracle { +func NewRollupOracle() *RollupOracle { return &RollupOracle{ - l1GasPrice: l1GasPrice, - l2GasPrice: l2GasPrice, + l1GasPrice: new(big.Int), + l2GasPrice: new(big.Int), + l1GasPriceLock: sync.RWMutex{}, + l2GasPriceLock: sync.RWMutex{}, } } diff --git a/l2geth/package.json b/l2geth/package.json index 424e231e21df..153ff28abc91 100644 --- a/l2geth/package.json +++ b/l2geth/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/l2geth", - "version": "0.3.9", + "version": "0.4.1", "private": true, "devDependencies": {} } diff --git a/l2geth/rollup/sync_service.go b/l2geth/rollup/sync_service.go index 27bcfd4e131d..b59ebe4b0e42 100644 --- a/l2geth/rollup/sync_service.go +++ b/l2geth/rollup/sync_service.go @@ -31,6 +31,7 @@ var errShortRemoteTip = errors.New("Unexpected remote less than tip") // L2GasPrice slot refers to the storage slot that the execution price is stored // in the L2 predeploy contract, the GasPriceOracle var l2GasPriceSlot = common.BigToHash(big.NewInt(1)) +var l2GasPriceOracleAddress = common.HexToAddress("0x420000000000000000000000000000000000000F") // SyncService implements the main functionality around pulling in transactions // and executing them. It can be configured to run in both sequencer mode and in @@ -57,8 +58,6 @@ type SyncService struct { timestampRefreshThreshold time.Duration chainHeadCh chan core.ChainHeadEvent backend Backend - gpoAddress common.Address - enableL2GasPolling bool enforceFees bool } @@ -112,8 +111,6 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co pollInterval: pollInterval, timestampRefreshThreshold: timestampRefreshThreshold, backend: cfg.Backend, - gpoAddress: cfg.GasPriceOracleAddress, - enableL2GasPolling: cfg.EnableL2GasPolling, enforceFees: cfg.EnforceFees, } @@ -435,11 +432,6 @@ func (s *SyncService) updateL1GasPrice() error { // price oracle at the state that corresponds to the state root. If no state // root is passed in, then the tip is used. func (s *SyncService) updateL2GasPrice(hash *common.Hash) error { - // TODO(mark): this is temporary and will be able to be rmoved when the - // OVM_GasPriceOracle is moved into the predeploy contracts - if !s.enableL2GasPolling { - return nil - } var state *state.StateDB var err error if hash != nil { @@ -450,7 +442,7 @@ func (s *SyncService) updateL2GasPrice(hash *common.Hash) error { if err != nil { return err } - result := state.GetState(s.gpoAddress, l2GasPriceSlot) + result := state.GetState(l2GasPriceOracleAddress, l2GasPriceSlot) s.RollupGpo.SetL2GasPrice(result.Big()) return nil } @@ -649,7 +641,7 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error { // Queue Origin L1 to L2 transactions must have a timestamp that is set by // the L1 block that holds the transaction. This should never happen but is // a sanity check to prevent fraudulent execution. - if tx.QueueOrigin().Uint64() == uint64(types.QueueOriginL1ToL2) { + if tx.QueueOrigin() == types.QueueOriginL1ToL2 { if tx.L1Timestamp() == 0 { return fmt.Errorf("Queue origin L1 to L2 transaction without a timestamp: %s", tx.Hash().Hex()) } @@ -676,7 +668,7 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error { bn := tx.L1BlockNumber() s.SetLatestL1Timestamp(ts) s.SetLatestL1BlockNumber(bn.Uint64()) - log.Debug("Updating OVM context based on new transaction", "timestamp", ts, "blocknumber", bn.Uint64(), "queue-origin", tx.QueueOrigin().Uint64()) + log.Debug("Updating OVM context based on new transaction", "timestamp", ts, "blocknumber", bn.Uint64(), "queue-origin", tx.QueueOrigin()) } else if tx.L1Timestamp() < s.GetLatestL1Timestamp() { log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex()) } @@ -795,11 +787,8 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction log.Trace("Sequencer transaction validation", "hash", tx.Hash().Hex()) qo := tx.QueueOrigin() - if qo == nil { - return errors.New("invalid transaction with no queue origin") - } - if qo.Uint64() != uint64(types.QueueOriginSequencer) { - return fmt.Errorf("invalid transaction with queue origin %d", qo.Uint64()) + if qo != types.QueueOriginSequencer { + return fmt.Errorf("invalid transaction with queue origin %d", qo) } err := s.txpool.ValidateTx(tx) if err != nil { @@ -1028,4 +1017,4 @@ func stringify(i *uint64) string { // validation and applies the transaction func (s *SyncService) IngestTransaction(tx *types.Transaction) error { return s.applyTransaction(tx) -} \ No newline at end of file +} diff --git a/l2geth/rollup/sync_service_test.go b/l2geth/rollup/sync_service_test.go index 705539be7fe4..62e543d690f4 100644 --- a/l2geth/rollup/sync_service_test.go +++ b/l2geth/rollup/sync_service_test.go @@ -121,7 +121,6 @@ func TestSyncServiceTransactionEnqueued(t *testing.T) { l1BlockNumber, timestamp, &l1TxOrigin, - types.SighashEIP155, types.QueueOriginL1ToL2, &index, &queueIndex, @@ -178,7 +177,6 @@ func TestTransactionToTipNoIndex(t *testing.T) { l1BlockNumber, timestamp, &l1TxOrigin, - types.SighashEIP155, types.QueueOriginL1ToL2, nil, // The index is `nil`, expect it to be set afterwards nil, @@ -518,8 +516,6 @@ func TestSyncServiceL2GasPrice(t *testing.T) { if err != nil { t.Fatal(err) } - service.enableL2GasPolling = true - service.gpoAddress = common.HexToAddress("0xF20b338752976878754518183873602902360704") price, err := service.RollupGpo.SuggestL2GasPrice(context.Background()) if err != nil { @@ -535,7 +531,7 @@ func TestSyncServiceL2GasPrice(t *testing.T) { t.Fatal("Cannot get state db") } l2GasPrice := big.NewInt(100000000000) - state.SetState(service.gpoAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice)) + state.SetState(l2GasPriceOracleAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice)) root, _ := state.Commit(false) service.updateL2GasPrice(&root) @@ -571,7 +567,6 @@ func TestSyncServiceSync(t *testing.T) { l1BlockNumber, timestamp, &l1TxOrigin, - types.SighashEIP155, types.QueueOriginL1ToL2, &index, &queueIndex, @@ -623,7 +618,6 @@ func TestInitializeL1ContextPostGenesis(t *testing.T) { l1BlockNumber, timestamp, &l1TxOrigin, - types.SighashEIP155, types.QueueOriginL1ToL2, &index, &queueIndex, @@ -699,7 +693,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err) } - service.RollupGpo = gasprice.NewRollupOracle(big.NewInt(0), big.NewInt(0)) + service.RollupGpo = gasprice.NewRollupOracle() txCh := make(chan core.NewTxsEvent, 1) sub := service.SubscribeNewTxsEvent(txCh) @@ -721,7 +715,7 @@ type mockClient struct { func setupMockClient(service *SyncService, responses map[string]interface{}) { client := newMockClient(responses) service.client = client - service.RollupGpo = gasprice.NewRollupOracle(big.NewInt(0), big.NewInt(0)) + service.RollupGpo = gasprice.NewRollupOracle() } func newMockClient(responses map[string]interface{}) *mockClient { @@ -871,7 +865,6 @@ func mockTx() *types.Transaction { l1BlockNumber, timestamp, &l1TxOrigin, - types.SighashEIP155, types.QueueOriginSequencer, nil, nil, diff --git a/ops/README.md b/ops/README.md index b4c9639930c1..6838b666ec53 100644 --- a/ops/README.md +++ b/ops/README.md @@ -18,9 +18,8 @@ Also available for testing is the `rpc-proxy` service in the `docker-compose-rpc The base stack can be started and stopped with a command like this (there is no need to specify the default docker-compose.yml) ``` - -docker-compose up --build --detach - +docker-compose \ + up --build --detach ``` To start the stack with monitoring enabled, just add the metric composition file. @@ -31,30 +30,20 @@ docker-compose \ up --build --detach ``` -Optionally, run a verifier along the rest of the stack. +Optionally, run a verifier along the rest of the stack. Run a replica with the same command by switching the service name! ``` - -docker-compose up --scale verifier=1 --build --detach - +docker-compose up --scale \ + verifier=1 \ + --build --detach ``` + A Makefile has been provided for convience. The following targets are available. - make up - make down - make up-metrics - make down-metrics -Running the integration tests - -``` - -docker-compose run integration_tests - -``` - -``` - - ## Authentication Influxdb has authentication disabled. diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml index 64a8488bee6b..9791365d6404 100644 --- a/ops/docker-compose.yml +++ b/ops/docker-compose.yml @@ -31,11 +31,9 @@ services: environment: FRAUD_PROOF_WINDOW_SECONDS: 0 L1_NODE_WEB3_URL: http://l1_chain:8545 - # these keys are hardhat's first 3 accounts, DO NOT use in production + # these keys are hardhat's first 2 accounts, DO NOT use in production DEPLOYER_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" SEQUENCER_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - PROPOSER_PRIVATE_KEY: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" - RELAYER_PRIVATE_KEY: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" # skip compilation when run in docker-compose, since the contracts # were already compiled in the builder step NO_COMPILE: 1 @@ -64,8 +62,8 @@ services: # connect to the 2 layers DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT: http://l1_chain:8545 DATA_TRANSPORT_LAYER__L2_RPC_ENDPOINT: http://l2geth:8545 - DATA_TRANSPORT_LAYER__L2_CHAIN_ID: 28 DATA_TRANSPORT_LAYER__SYNC_FROM_L2: 'true' + DATA_TRANSPORT_LAYER__L2_CHAIN_ID: 420 ports: - ${DTL_PORT:-7878}:7878 @@ -109,11 +107,10 @@ services: L1_NODE_WEB3_URL: http://l1_chain:8545 L2_NODE_WEB3_URL: http://l2geth:8545 URL: http://deployer:8081/addresses.json - WHITELIST_ENDPOINT: https://api-message-relayer.rinkeby.omgx.network/get.whitelist # a funded hardhat account - L1_WALLET_KEY: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" + L1_WALLET_KEY: "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97" RETRIES: 60 - POLLING_INTERVAL: 1500 + POLLING_INTERVAL: 500 GET_LOGS_INTERVAL: 500 batch_submitter: @@ -133,7 +130,6 @@ services: L2_NODE_WEB3_URL: http://l2geth:8545 URL: http://deployer:8081/addresses.json SEQUENCER_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - PROPOSER_PRIVATE_KEY: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" verifier: depends_on: @@ -146,25 +142,46 @@ services: build: context: .. dockerfile: ./ops/docker/Dockerfile.geth - # override with the geth script and the env vars required for it entrypoint: sh ./geth.sh env_file: - ./envs/geth.env environment: ETH1_HTTP: http://l1_chain:8545 ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json - # used for getting the addresses URL: http://deployer:8081/addresses.json - # connecting to the DTL ROLLUP_CLIENT_HTTP: http://dtl:7878 + ROLLUP_BACKEND: 'l1' ETH1_CTC_DEPLOYMENT_HEIGHT: 8 RETRIES: 60 - # IS_VERIFIER: "true" - ROLLUP_BACKEND: 'l2' ROLLUP_VERIFIER_ENABLE: 'true' ports: - ${VERIFIER_HTTP_PORT:-8547}:8545 - ${VERIFIER_WS_PORT:-8548}:8546 + + replica: + depends_on: + - dtl + image: ethereumoptimism/l2geth + deploy: + replicas: 0 + build: + context: .. + dockerfile: ./ops/docker/Dockerfile.geth + entrypoint: sh ./geth.sh + env_file: + - ./envs/geth.env + environment: + ETH1_HTTP: http://l1_chain:8545 + ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json + URL: http://deployer:8081/addresses.json + ROLLUP_CLIENT_HTTP: http://dtl:7878 + ROLLUP_BACKEND: 'l2' + ROLLUP_VERIFIER_ENABLE: 'true' + ETH1_CTC_DEPLOYMENT_HEIGHT: 8 + RETRIES: 60 + ports: + - ${L2GETH_HTTP_PORT:-8549}:8545 + - ${L2GETH_WS_PORT:-8550}:8546 integration_tests: image: ethereumoptimism/integration-tests diff --git a/package.json b/package.json index 92416f01f65e..676e8cde2f5e 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,19 @@ "author": "Optimism PBC", "license": "MIT", "workspaces": { - "packages": [ - "packages/*", - "packages/omgx/*", - "l2geth", - "integration-tests", - "specs" - ], - "nohoist": ["examples/*"] + "packages": [ + "packages/*", + "l2geth", + "integration-tests", + "specs" + ], + "nohoist": [ + "examples/*" + ] }, "private": true, "devDependencies": { + "husky": "^6.0.0", "lerna": "^4.0.0", "patch-package": "^6.4.7" }, @@ -27,6 +29,8 @@ "lint:check": "yarn lerna run lint:check", "lint:fix": "yarn lerna run lint:fix", "postinstall": "patch-package", + "ready": "yarn lint && yarn test", + "prepare": "husky install", "release": "yarn build && yarn changeset publish" }, "dependencies": { diff --git a/packages/batch-submitter/CHANGELOG.md b/packages/batch-submitter/CHANGELOG.md index 74311a997ba9..ab121529aaf0 100644 --- a/packages/batch-submitter/CHANGELOG.md +++ b/packages/batch-submitter/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.3.6 + +### Patch Changes + +- f87a2d00: Use dashes instead of colons in contract names +- 52d02b14: Add failure metrics to batch submitter +- 31f517a2: Improved logging of batch submission timeout logs +- 5c89c45f: Move the metric prefix string to a label #1047 +- Updated dependencies [25f09abd] +- Updated dependencies [dd8edc7b] +- Updated dependencies [c87e4c74] +- Updated dependencies [db0dbfb2] +- Updated dependencies [7f5936a8] +- Updated dependencies [f87a2d00] +- Updated dependencies [85da4979] +- Updated dependencies [57ca21a2] +- Updated dependencies [5fc728da] +- Updated dependencies [2e72fd90] +- Updated dependencies [c43b33ec] +- Updated dependencies [26bc63ad] +- Updated dependencies [a0d9e565] +- Updated dependencies [2bd49730] +- Updated dependencies [38355a3b] +- Updated dependencies [3c2c32e1] +- Updated dependencies [d9644c34] +- Updated dependencies [48ece14c] +- Updated dependencies [e04de624] +- Updated dependencies [014dea71] +- Updated dependencies [fa29b03e] +- Updated dependencies [6b46c8ba] +- Updated dependencies [e045f582] +- Updated dependencies [5c89c45f] +- Updated dependencies [df5ff890] +- Updated dependencies [e29fab10] +- Updated dependencies [c2a04893] +- Updated dependencies [baacda34] + - @eth-optimism/contracts@0.4.0 + - @eth-optimism/core-utils@0.4.6 + - @eth-optimism/common-ts@0.1.4 + ## 0.3.5 ### Patch Changes diff --git a/packages/batch-submitter/package.json b/packages/batch-submitter/package.json index 7c365149e514..279771a9eccb 100644 --- a/packages/batch-submitter/package.json +++ b/packages/batch-submitter/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/batch-submitter", - "version": "0.3.5", + "version": "0.3.6", "private": true, "description": "[Optimism] Batch submission for sequencer & aggregators", "main": "dist/index", @@ -31,9 +31,9 @@ "url": "https://github.com/ethereum-optimism/optimism-monorepo.git" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.3", - "@eth-optimism/contracts": "^0.3.5", - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/common-ts": "^0.1.4", + "@eth-optimism/contracts": "^0.4.0", + "@eth-optimism/core-utils": "^0.4.6", "@eth-optimism/ynatm": "^0.2.2", "@ethersproject/abstract-provider": "^5.0.5", "@ethersproject/providers": "^5.0.14", @@ -46,7 +46,7 @@ "prom-client": "^13.1.0" }, "devDependencies": { - "@eth-optimism/smock": "^1.1.5", + "@eth-optimism/smock": "^1.1.6", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/bluebird": "^3.5.34", diff --git a/packages/common-ts/CHANGELOG.md b/packages/common-ts/CHANGELOG.md index 60781b255f87..8bcba4ec14a7 100644 --- a/packages/common-ts/CHANGELOG.md +++ b/packages/common-ts/CHANGELOG.md @@ -1,5 +1,11 @@ # @eth-optimism/common-ts +## 0.1.4 + +### Patch Changes + +- 5c89c45f: Move the metric prefix string to a label #1047 + ## 0.1.3 ### Patch Changes diff --git a/packages/common-ts/package.json b/packages/common-ts/package.json index 9388e2d30e36..c69a22413bc0 100644 --- a/packages/common-ts/package.json +++ b/packages/common-ts/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/common-ts", - "version": "0.1.3", + "version": "0.1.4", "main": "dist/index", "files": [ "dist/*" diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md index 857f869507d8..0c56b8d3a0fa 100644 --- a/packages/contracts/CHANGELOG.md +++ b/packages/contracts/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 0.4.1 + +### Patch Changes + +- 98e02cfa: Add 0.4.0 deployment artifacts + +## 0.4.0 + +### Minor Changes + +- db0dbfb2: Disables EOA contract upgrades until further notice +- 5fc728da: Add a new Standard Token Bridge, to handle deposits and withdrawals of any ERC20 token. + For projects developing a custom bridge, if you were previously importing `iAbs_BaseCrossDomainMessenger`, you should now + import `iOVM_CrossDomainMessenger`. +- 2e72fd90: Update AddressSet event to speed search up a bit. Breaks AddressSet API. +- e04de624: Add support for ovmCALL with nonzero ETH value + +### Patch Changes + +- 25f09abd: Adds ERC1271 support to default contract account +- dd8edc7b: Update the ECDSAContractAccount import path in the `contract-data.ts` file for connecting ethers contracts to the L2 contracts +- c87e4c74: Migrated from tslint to eslint. The preference for lint exceptions is as follows: line level, block level, file level, package level. +- 7f5936a8: Apply consistent styling to constants +- f87a2d00: Use dashes instead of colons in contract names +- 85da4979: Replaces RingBuffer with a simpler Buffer library +- 57ca21a2: "Adds connectL1Contracts and connectL2Contracts utility functions" +- c43b33ec: Add WETH9 compatible deposit and withdraw functions to OVM_ETH +- 26bc63ad: Deploy new Goerli contracts at d3e743aa7a406c583f7d76f4fda607f592d03e47 +- a0d9e565: ECDSA account interface contract moved to predeploys dir +- 2bd49730: Deploy v0.4.0 rc to Kovan +- 38355a3b: Moved contracts in the "accounts" folder into the "predeploys" folder +- 3c2c32e1: Use predeploy constants lib for EM wrapper +- 48ece14c: Adds a temporary way to fund hardhat accounts when testing locally +- 014dea71: Removes one-off GasPriceOracle deployment file +- fa29b03e: Updates the deployment of the L1MultiMessageRelayer to NOT set the OVM_L2MessageRelayer address in the AddressManager +- 6b46c8ba: Disable upgradability from the ECDSA account instead of the EOA proxy. +- e045f582: Adds new SequencerFeeVault contract to store generated fees +- e29fab10: Token gateways pass additional information: sender and arbitrary data. +- c2a04893: Do not RLP decode the transaction in the OVM_ECDSAContractAccount +- baacda34: Introduce the L1ChugSplashProxy contract +- Updated dependencies [d9644c34] +- Updated dependencies [df5ff890] + - @eth-optimism/core-utils@0.4.6 + ## 0.3.5 ### Patch Changes diff --git a/packages/contracts/package.json b/packages/contracts/package.json index ee109fe460d7..4483255d97f2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/contracts", - "version": "0.3.5", + "version": "0.4.1", "main": "dist/index", "files": [ "dist/**/*.js", @@ -37,7 +37,7 @@ "posttest:slither": "rm -f @openzeppelin && rm -f @ens && rm -f hardhat", "lint": "yarn lint:fix && yarn lint:check", "lint:fix": "yarn run lint:fix:typescript", - "lint:fix:typescript": "prettier --config .prettierrc.json --write \"hardhat.config.ts\" \"{src,test}/**/*.ts\"", + "lint:fix:typescript": "prettier --config .prettierrc.json --write \"hardhat.config.ts\" \"{src,test,deploy}/**/*.ts\"", "lint:check": "yarn run lint:typescript", "lint:typescript": "eslint -c .eslintrc.js --ext .ts --format stylish .", "clean": "rm -rf ./dist ./artifacts ./artifacts-ovm ./cache ./cache-ovm ./tsconfig.build.tsbuildinfo", @@ -50,7 +50,7 @@ "generate-markdown": "node \"./scripts/generate-markdown.js\"" }, "dependencies": { - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/core-utils": "^0.4.6", "@ethersproject/abstract-provider": "^5.0.8", "@ethersproject/abstract-signer": "^5.1.0", "@ethersproject/contracts": "^5.0.5", @@ -59,7 +59,8 @@ "devDependencies": { "@codechecks/client": "0.1.10-beta", "@eth-optimism/hardhat-ovm": "^0.2.2", - "@eth-optimism/smock": "^1.1.5", + "@eth-optimism/smock": "^1.1.6", + "@ethersproject/transactions": "^5.0.31", "@nomiclabs/hardhat-ethers": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^3.3.0", diff --git a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ERC20Gateway.spec.ts b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ERC20Gateway.spec.ts deleted file mode 100644 index 7de77ab4d2f8..000000000000 --- a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ERC20Gateway.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { expect } from '../../../../setup' - -/* External Imports */ -import { ethers } from 'hardhat' -import { Signer, ContractFactory, Contract, constants } from 'ethers' -import { smockit, MockContract, smoddit } from '@eth-optimism/smock' - -/* Internal Imports */ -import { NON_ZERO_ADDRESS } from '../../../../helpers' - -const INITIAL_TOTAL_L1_SUPPLY = 3000 - -const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' -const ERR_INVALID_X_DOMAIN_MSG_SENDER = - 'OVM_XCHAIN: wrong sender of cross-domain message' - -describe('OVM_L1ERC20Gateway', () => { - // init signers - let alice: Signer - let bob: Signer - - // we can just make up this string since it's on the "other" Layer - let Mock__OVM_L2DepositedERC20: MockContract - let Factory__L1ERC20: ContractFactory - let L1ERC20: Contract - before(async () => { - ;[alice, bob] = await ethers.getSigners() - - Mock__OVM_L2DepositedERC20 = await smockit( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ) - - // deploy an ERC20 contract on L1 - Factory__L1ERC20 = await smoddit('UniswapV2ERC20') - - L1ERC20 = await Factory__L1ERC20.deploy('L1ERC20', 'ERC') - - const aliceAddress = await alice.getAddress() - await L1ERC20.smodify.put({ - totalSupply: INITIAL_TOTAL_L1_SUPPLY, - balanceOf: { - [aliceAddress]: INITIAL_TOTAL_L1_SUPPLY, - }, - }) - }) - - let OVM_L1ERC20Gateway: Contract - let Mock__OVM_L1CrossDomainMessenger: MockContract - let finalizeDepositGasLimit: number - beforeEach(async () => { - // Create a special signer which will enable us to send messages from the L1Messenger contract - let l1MessengerImpersonator: Signer - ;[l1MessengerImpersonator, alice, bob] = await ethers.getSigners() - // Get a new mock L1 messenger - Mock__OVM_L1CrossDomainMessenger = await smockit( - await ethers.getContractFactory('OVM_L1CrossDomainMessenger'), - { address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls - ) - - // Deploy the contract under test - OVM_L1ERC20Gateway = await ( - await ethers.getContractFactory('OVM_L1ERC20Gateway') - ).deploy( - L1ERC20.address, - Mock__OVM_L2DepositedERC20.address, - Mock__OVM_L1CrossDomainMessenger.address - ) - - finalizeDepositGasLimit = await OVM_L1ERC20Gateway.getFinalizeDepositL2Gas() - }) - - describe('finalizeWithdrawal', () => { - it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { - // Deploy new gateway, initialize with random messenger - OVM_L1ERC20Gateway = await ( - await ethers.getContractFactory('OVM_L1ERC20Gateway') - ).deploy( - L1ERC20.address, - Mock__OVM_L2DepositedERC20.address, - NON_ZERO_ADDRESS - ) - - await expect( - OVM_L1ERC20Gateway.finalizeWithdrawal(constants.AddressZero, 1) - ).to.be.revertedWith(ERR_INVALID_MESSENGER) - }) - - it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2ERC20Gateway)', async () => { - Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - () => NON_ZERO_ADDRESS - ) - - await expect( - OVM_L1ERC20Gateway.finalizeWithdrawal(constants.AddressZero, 1, { - from: Mock__OVM_L1CrossDomainMessenger.address, - }) - ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) - }) - - it('should credit funds to the withdrawer and not use too much gas', async () => { - // make sure no balance at start of test - await expect(await L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) - - const withdrawalAmount = 100 - Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - () => Mock__OVM_L2DepositedERC20.address - ) - - await L1ERC20.transfer(OVM_L1ERC20Gateway.address, withdrawalAmount) - - const res = await OVM_L1ERC20Gateway.finalizeWithdrawal( - NON_ZERO_ADDRESS, - withdrawalAmount, - { from: Mock__OVM_L1CrossDomainMessenger.address } - ) - - await expect(await L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal( - withdrawalAmount - ) - - const gasUsed = ( - await OVM_L1ERC20Gateway.provider.getTransactionReceipt(res.hash) - ).gasUsed - - const OVM_L2DepositedERC20 = await ( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ).deploy(constants.AddressZero, '', '') - const defaultFinalizeWithdrawalGas = - await OVM_L2DepositedERC20.getFinalizeWithdrawalL1Gas() - await expect(gasUsed.gt((defaultFinalizeWithdrawalGas * 11) / 10)) - }) - - it.skip('finalizeWithdrawalAndCall(): should should credit funds to the withdrawer, and forward from and data', async () => { - // TODO: implement this functionality in a future update - expect.fail() - }) - }) - - describe('deposits', () => { - const INITIAL_DEPOSITER_BALANCE = 100_000 - let depositer: string - const depositAmount = 1_000 - - beforeEach(async () => { - // Deploy the L1 ERC20 token, Alice will receive the full initialSupply - L1ERC20 = await Factory__L1ERC20.deploy('L1ERC20', 'ERC') - - // get a new mock L1 messenger - Mock__OVM_L1CrossDomainMessenger = await smockit( - await ethers.getContractFactory('OVM_L1CrossDomainMessenger') - ) - - // Deploy the contract under test: - OVM_L1ERC20Gateway = await ( - await ethers.getContractFactory('OVM_L1ERC20Gateway') - ).deploy( - L1ERC20.address, - Mock__OVM_L2DepositedERC20.address, - Mock__OVM_L1CrossDomainMessenger.address - ) - - // the Signer sets approve for the L1 Gateway - await L1ERC20.approve(OVM_L1ERC20Gateway.address, depositAmount) - depositer = await L1ERC20.signer.getAddress() - - await L1ERC20.smodify.put({ - balanceOf: { - [depositer]: INITIAL_DEPOSITER_BALANCE, - }, - }) - }) - - it('deposit() escrows the deposit amount and sends the correct deposit message', async () => { - // alice calls deposit on the gateway and the L1 gateway calls transferFrom on the token - await OVM_L1ERC20Gateway.deposit(depositAmount) - const depositCallToMessenger = - Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] - - const depositerBalance = await L1ERC20.balanceOf(depositer) - expect(depositerBalance).to.equal( - INITIAL_DEPOSITER_BALANCE - depositAmount - ) - - // gateway's balance is increased - const gatewayBalance = await L1ERC20.balanceOf(OVM_L1ERC20Gateway.address) - expect(gatewayBalance).to.equal(depositAmount) - - // Check the correct cross-chain call was sent: - // Message should be sent to the L2ERC20Gateway on L2 - expect(depositCallToMessenger._target).to.equal( - Mock__OVM_L2DepositedERC20.address - ) - // Message data should be a call telling the L2ERC20Gateway to finalize the deposit - - // the L1 gateway sends the correct message to the L1 messenger - expect(depositCallToMessenger._message).to.equal( - await Mock__OVM_L2DepositedERC20.interface.encodeFunctionData( - 'finalizeDeposit', - [depositer, depositAmount] - ) - ) - expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) - }) - - it('depositTo() escrows the deposit amount and sends the correct deposit message', async () => { - // depositor calls deposit on the gateway and the L1 gateway calls transferFrom on the token - const bobsAddress = await bob.getAddress() - await OVM_L1ERC20Gateway.depositTo(bobsAddress, depositAmount) - const depositCallToMessenger = - Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] - - const depositerBalance = await L1ERC20.balanceOf(depositer) - expect(depositerBalance).to.equal( - INITIAL_DEPOSITER_BALANCE - depositAmount - ) - - // gateway's balance is increased - const gatewayBalance = await L1ERC20.balanceOf(OVM_L1ERC20Gateway.address) - expect(gatewayBalance).to.equal(depositAmount) - - // Check the correct cross-chain call was sent: - // Message should be sent to the L2ERC20Gateway on L2 - expect(depositCallToMessenger._target).to.equal( - Mock__OVM_L2DepositedERC20.address - ) - // Message data should be a call telling the L2ERC20Gateway to finalize the deposit - - // the L1 gateway sends the correct message to the L1 messenger - expect(depositCallToMessenger._message).to.equal( - await Mock__OVM_L2DepositedERC20.interface.encodeFunctionData( - 'finalizeDeposit', - [bobsAddress, depositAmount] - ) - ) - expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) - }) - }) -}) diff --git a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ETHGateway.spec.ts b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ETHGateway.spec.ts deleted file mode 100644 index 98d64620cfad..000000000000 --- a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1ETHGateway.spec.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { expect } from '../../../../setup' - -/* External Imports */ -import { ethers } from 'hardhat' -import { Signer, Contract, constants } from 'ethers' -import { smockit, MockContract } from '@eth-optimism/smock' - -/* Internal Imports */ -import { NON_ZERO_ADDRESS, makeAddressManager } from '../../../../helpers' - -const L1_MESSENGER_NAME = 'Proxy__OVM_L1CrossDomainMessenger' - -const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' -const ERR_INVALID_X_DOMAIN_MSG_SENDER = - 'OVM_XCHAIN: wrong sender of cross-domain message' -const ERR_ALREADY_INITIALIZED = 'Contract has already been initialized.' - -describe('OVM_L1ETHGateway', () => { - // init signers - let l1MessengerImpersonator: Signer - let alice: Signer - let bob: Signer - - let AddressManager: Contract - before(async () => { - AddressManager = await makeAddressManager() - }) - - // we can just make up this string since it's on the "other" Layer - let Mock__OVM_L2DepositedERC20: MockContract - before(async () => { - ;[l1MessengerImpersonator, alice, bob] = await ethers.getSigners() - - Mock__OVM_L2DepositedERC20 = await smockit( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ) - }) - - let OVM_L1ETHGateway: Contract - let Mock__OVM_L1CrossDomainMessenger: MockContract - let finalizeDepositGasLimit: number - beforeEach(async () => { - // Get a new mock L1 messenger - Mock__OVM_L1CrossDomainMessenger = await smockit( - await ethers.getContractFactory('OVM_L1CrossDomainMessenger'), - { address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls - ) - - // Deploy the contract under test and initialize - OVM_L1ETHGateway = await ( - await ethers.getContractFactory('OVM_L1ETHGateway') - ).deploy() - await OVM_L1ETHGateway.initialize( - AddressManager.address, - Mock__OVM_L2DepositedERC20.address - ) - - finalizeDepositGasLimit = await OVM_L1ETHGateway.getFinalizeDepositL2Gas() - }) - - describe('initialize', () => { - it('Should only be callable once', async () => { - await expect( - OVM_L1ETHGateway.initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero - ) - ).to.be.revertedWith(ERR_ALREADY_INITIALIZED) - }) - }) - - describe('finalizeWithdrawal', () => { - it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { - // Deploy new gateway, initialize with random messenger - await expect( - OVM_L1ETHGateway.connect(alice).finalizeWithdrawal( - constants.AddressZero, - 1 - ) - ).to.be.revertedWith(ERR_INVALID_MESSENGER) - }) - - it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2ETHGateway)', async () => { - await AddressManager.setAddress( - L1_MESSENGER_NAME, - Mock__OVM_L1CrossDomainMessenger.address - ) - - OVM_L1ETHGateway = await ( - await ethers.getContractFactory('OVM_L1ETHGateway') - ).deploy() - await OVM_L1ETHGateway.initialize( - AddressManager.address, - Mock__OVM_L2DepositedERC20.address - ) - - Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - NON_ZERO_ADDRESS - ) - - await expect( - OVM_L1ETHGateway.finalizeWithdrawal(constants.AddressZero, 1) - ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) - }) - - it('should credit funds to the withdrawer and not use too much gas', async () => { - // make sure no balance at start of test - await expect( - await ethers.provider.getBalance(NON_ZERO_ADDRESS) - ).to.be.equal(0) - - const withdrawalAmount = 100 - Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - () => Mock__OVM_L2DepositedERC20.address - ) - - // thanks Alice - await OVM_L1ETHGateway.connect(alice).deposit({ - value: ethers.utils.parseEther('1.0'), - gasPrice: 0, - }) - - const res = await OVM_L1ETHGateway.finalizeWithdrawal( - NON_ZERO_ADDRESS, - withdrawalAmount, - { from: Mock__OVM_L1CrossDomainMessenger.address } - ) - - await expect( - await ethers.provider.getBalance(NON_ZERO_ADDRESS) - ).to.be.equal(withdrawalAmount) - - const gasUsed = ( - await OVM_L1ETHGateway.provider.getTransactionReceipt(res.hash) - ).gasUsed - - // Deploy this just for the getter - const OVM_L2DepositedERC20 = await ( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ).deploy(constants.AddressZero, '', '') - - await expect( - gasUsed.gt( - ((await OVM_L2DepositedERC20.getFinalizeWithdrawalL1Gas()) * 11) / 10 - ) - ) - }) - - it.skip('finalizeWithdrawalAndCall(): should should credit funds to the withdrawer, and forward from and data', async () => { - // TODO: implement this functionality in a future update - expect.fail() - }) - }) - - describe('deposits', () => { - const depositAmount = 1_000 - - beforeEach(async () => { - // Deploy the L1 ETH token, Alice will receive the full initialSupply - - // get a new mock L1 messenger and set in AM - Mock__OVM_L1CrossDomainMessenger = await smockit( - await ethers.getContractFactory('OVM_L1CrossDomainMessenger') - ) - await AddressManager.setAddress( - L1_MESSENGER_NAME, - Mock__OVM_L1CrossDomainMessenger.address - ) - - // Deploy the contract under test and initialize - OVM_L1ETHGateway = await ( - await ethers.getContractFactory('OVM_L1ETHGateway') - ).deploy() - await OVM_L1ETHGateway.initialize( - AddressManager.address, - Mock__OVM_L2DepositedERC20.address - ) - }) - - it('deposit() escrows the deposit amount and sends the correct deposit message', async () => { - const depositer = await alice.getAddress() - const initialBalance = await ethers.provider.getBalance(depositer) - - // alice calls deposit on the gateway and the L1 gateway calls transferFrom on the token - await OVM_L1ETHGateway.connect(alice).deposit({ - value: depositAmount, - gasPrice: 0, - }) - - const depositCallToMessenger = - Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] - - const depositerBalance = await ethers.provider.getBalance(depositer) - - expect(depositerBalance).to.equal(initialBalance.sub(depositAmount)) - - // gateway's balance is increased - const gatewayBalance = await ethers.provider.getBalance( - OVM_L1ETHGateway.address - ) - expect(gatewayBalance).to.equal(depositAmount) - - // Check the correct cross-chain call was sent: - // Message should be sent to the L2ETHGateway on L2 - expect(depositCallToMessenger._target).to.equal( - Mock__OVM_L2DepositedERC20.address - ) - // Message data should be a call telling the L2ETHGateway to finalize the deposit - - // the L1 gateway sends the correct message to the L1 messenger - expect(depositCallToMessenger._message).to.equal( - await Mock__OVM_L2DepositedERC20.interface.encodeFunctionData( - 'finalizeDeposit', - [depositer, depositAmount] - ) - ) - expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) - }) - - it('depositTo() escrows the deposit amount and sends the correct deposit message', async () => { - // depositor calls deposit on the gateway and the L1 gateway calls transferFrom on the token - const bobsAddress = await bob.getAddress() - const aliceAddress = await alice.getAddress() - const initialBalance = await ethers.provider.getBalance(aliceAddress) - - await OVM_L1ETHGateway.connect(alice).depositTo(bobsAddress, { - value: depositAmount, - gasPrice: 0, - }) - const depositCallToMessenger = - Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] - - const depositerBalance = await ethers.provider.getBalance(aliceAddress) - expect(depositerBalance).to.equal(initialBalance.sub(depositAmount)) - - // gateway's balance is increased - const gatewayBalance = await ethers.provider.getBalance( - OVM_L1ETHGateway.address - ) - expect(gatewayBalance).to.equal(depositAmount) - - // Check the correct cross-chain call was sent: - // Message should be sent to the L2ETHGateway on L2 - expect(depositCallToMessenger._target).to.equal( - Mock__OVM_L2DepositedERC20.address - ) - // Message data should be a call telling the L2ETHGateway to finalize the deposit - - // the L1 gateway sends the correct message to the L1 messenger - expect(depositCallToMessenger._message).to.equal( - await Mock__OVM_L2DepositedERC20.interface.encodeFunctionData( - 'finalizeDeposit', - [bobsAddress, depositAmount] - ) - ) - expect(depositCallToMessenger._gasLimit).to.equal(finalizeDepositGasLimit) - }) - }) - describe('migrating ETH', () => { - const migrateAmount = 1_000 - - beforeEach(async () => { - await OVM_L1ETHGateway.donateETH({ value: migrateAmount }) - const gatewayBalance = await ethers.provider.getBalance( - OVM_L1ETHGateway.address - ) - expect(gatewayBalance).to.equal(migrateAmount) - }) - it('should successfully migrate ETH to new gateway', async () => { - const New_OVM_L1ETHGateway = await ( - await ethers.getContractFactory('OVM_L1ETHGateway') - ).deploy() - await New_OVM_L1ETHGateway.initialize( - AddressManager.address, - Mock__OVM_L2DepositedERC20.address - ) - await OVM_L1ETHGateway.migrateEth(New_OVM_L1ETHGateway.address) - const newGatewayBalance = await ethers.provider.getBalance( - New_OVM_L1ETHGateway.address - ) - expect(newGatewayBalance).to.equal(migrateAmount) - }) - it('should not allow migrating ETH from non-owner', async () => { - const New_OVM_L1ETHGateway = await ( - await ethers.getContractFactory('OVM_L1ETHGateway') - ).deploy() - await New_OVM_L1ETHGateway.initialize( - AddressManager.address, - Mock__OVM_L2DepositedERC20.address - ) - await expect( - OVM_L1ETHGateway.connect(bob).migrateEth(New_OVM_L1ETHGateway.address) - ).to.be.revertedWith('Only the owner can migrate ETH') - }) - }) -}) diff --git a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1StandardBridge.spec.ts b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1StandardBridge.spec.ts new file mode 100644 index 000000000000..5170186ac00e --- /dev/null +++ b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L1StandardBridge.spec.ts @@ -0,0 +1,540 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract, constants } from 'ethers' +import { Interface } from 'ethers/lib/utils' +import { smockit, MockContract, smoddit } from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../../../../helpers' +import { getContractInterface, predeploys } from '../../../../../src' + +const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' +const ERR_INVALID_X_DOMAIN_MSG_SENDER = + 'OVM_XCHAIN: wrong sender of cross-domain message' +const ERR_ALREADY_INITIALIZED = 'Contract has already been initialized.' +const DUMMY_L2_ERC20_ADDRESS = ethers.utils.getAddress('0x' + 'abba'.repeat(10)) +const DUMMY_L2_BRIDGE_ADDRESS = ethers.utils.getAddress( + '0x' + 'acdc'.repeat(10) +) + +const INITIAL_TOTAL_L1_SUPPLY = 5000 +const FINALIZATION_GAS = 1_200_000 + +describe('OVM_L1StandardBridge', () => { + // init signers + let l1MessengerImpersonator: Signer + let alice: Signer + let bob: Signer + let bobsAddress + let aliceAddress + + // we can just make up this string since it's on the "other" Layer + let Mock__OVM_ETH: MockContract + let Factory__L1ERC20: ContractFactory + let IL2ERC20Bridge: Interface + before(async () => { + ;[l1MessengerImpersonator, alice, bob] = await ethers.getSigners() + + Mock__OVM_ETH = await smockit(await ethers.getContractFactory('OVM_ETH')) + + // deploy an ERC20 contract on L1 + Factory__L1ERC20 = await smoddit( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20' + ) + + // get an L2ER20Bridge Interface + IL2ERC20Bridge = getContractInterface('iOVM_L2ERC20Bridge') + + aliceAddress = await alice.getAddress() + bobsAddress = await bob.getAddress() + }) + + let L1ERC20: Contract + let OVM_L1StandardBridge: Contract + let Mock__OVM_L1CrossDomainMessenger: MockContract + beforeEach(async () => { + // Get a new mock L1 messenger + Mock__OVM_L1CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L1CrossDomainMessenger'), + { address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls + ) + + // Deploy the contract under test + OVM_L1StandardBridge = await ( + await ethers.getContractFactory('OVM_L1StandardBridge') + ).deploy() + await OVM_L1StandardBridge.initialize( + Mock__OVM_L1CrossDomainMessenger.address, + DUMMY_L2_BRIDGE_ADDRESS + ) + + L1ERC20 = await Factory__L1ERC20.deploy('L1ERC20', 'ERC') + await L1ERC20.smodify.put({ + _totalSupply: INITIAL_TOTAL_L1_SUPPLY, + _balances: { + [aliceAddress]: INITIAL_TOTAL_L1_SUPPLY, + }, + }) + }) + + describe('initialize', () => { + it('Should only be callable once', async () => { + await expect( + OVM_L1StandardBridge.initialize( + ethers.constants.AddressZero, + DUMMY_L2_BRIDGE_ADDRESS + ) + ).to.be.revertedWith(ERR_ALREADY_INITIALIZED) + }) + }) + + describe('ETH deposits', () => { + const depositAmount = 1_000 + + it('depositETH() escrows the deposit amount and sends the correct deposit message', async () => { + const depositer = await alice.getAddress() + const initialBalance = await ethers.provider.getBalance(depositer) + + // alice calls deposit on the bridge and the L1 bridge calls transferFrom on the token + await OVM_L1StandardBridge.connect(alice).depositETH( + FINALIZATION_GAS, + NON_NULL_BYTES32, + { + value: depositAmount, + gasPrice: 0, + } + ) + + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await ethers.provider.getBalance(depositer) + + expect(depositerBalance).to.equal(initialBalance.sub(depositAmount)) + + // bridge's balance is increased + const bridgeBalance = await ethers.provider.getBalance( + OVM_L1StandardBridge.address + ) + expect(bridgeBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2 bridge + expect(depositCallToMessenger._target).to.equal(DUMMY_L2_BRIDGE_ADDRESS) + // Message data should be a call telling the L2ETHToken to finalize the deposit + + // the L1 bridge sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + IL2ERC20Bridge.encodeFunctionData('finalizeDeposit', [ + constants.AddressZero, + predeploys.OVM_ETH, + depositer, + depositer, + depositAmount, + NON_NULL_BYTES32, + ]) + ) + expect(depositCallToMessenger._gasLimit).to.equal(FINALIZATION_GAS) + }) + + it('depositETHTo() escrows the deposit amount and sends the correct deposit message', async () => { + // depositor calls deposit on the bridge and the L1 bridge calls transferFrom on the token + const initialBalance = await ethers.provider.getBalance(aliceAddress) + + await OVM_L1StandardBridge.connect(alice).depositETHTo( + bobsAddress, + FINALIZATION_GAS, + NON_NULL_BYTES32, + { + value: depositAmount, + gasPrice: 0, + } + ) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await ethers.provider.getBalance(aliceAddress) + expect(depositerBalance).to.equal(initialBalance.sub(depositAmount)) + + // bridge's balance is increased + const bridgeBalance = await ethers.provider.getBalance( + OVM_L1StandardBridge.address + ) + expect(bridgeBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2 bridge + expect(depositCallToMessenger._target).to.equal(DUMMY_L2_BRIDGE_ADDRESS) + // Message data should be a call telling the L2ETHToken to finalize the deposit + + // the L1 bridge sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + IL2ERC20Bridge.encodeFunctionData('finalizeDeposit', [ + constants.AddressZero, + predeploys.OVM_ETH, + aliceAddress, + bobsAddress, + depositAmount, + NON_NULL_BYTES32, + ]) + ) + expect(depositCallToMessenger._gasLimit).to.equal(FINALIZATION_GAS) + }) + + it('cannot depositETH from a contract account', async () => { + expect( + OVM_L1StandardBridge.depositETH(FINALIZATION_GAS, NON_NULL_BYTES32, { + value: depositAmount, + gasPrice: 0, + }) + ).to.be.revertedWith('Account not EOA') + }) + }) + + describe('ETH withdrawals', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { + // Deploy new bridge, initialize with random messenger + await expect( + OVM_L1StandardBridge.connect(alice).finalizeETHWithdrawal( + constants.AddressZero, + constants.AddressZero, + 1, + NON_NULL_BYTES32, + { + from: aliceAddress, + } + ) + ).to.be.revertedWith(ERR_INVALID_MESSENGER) + }) + + it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2ETHToken)', async () => { + OVM_L1StandardBridge = await ( + await ethers.getContractFactory('OVM_L1StandardBridge') + ).deploy() + await OVM_L1StandardBridge.initialize( + Mock__OVM_L1CrossDomainMessenger.address, + DUMMY_L2_BRIDGE_ADDRESS + ) + + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + '0x' + '22'.repeat(20) + ) + + await expect( + OVM_L1StandardBridge.finalizeETHWithdrawal( + constants.AddressZero, + constants.AddressZero, + 1, + NON_NULL_BYTES32, + { + from: Mock__OVM_L1CrossDomainMessenger.address, + } + ) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should credit funds to the withdrawer and not use too much gas', async () => { + // make sure no balance at start of test + expect(await ethers.provider.getBalance(NON_ZERO_ADDRESS)).to.be.equal(0) + + const withdrawalAmount = 100 + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => DUMMY_L2_BRIDGE_ADDRESS + ) + + // thanks Alice + await OVM_L1StandardBridge.connect(alice).depositETH( + FINALIZATION_GAS, + NON_NULL_BYTES32, + { + value: ethers.utils.parseEther('1.0'), + gasPrice: 0, + } + ) + + await OVM_L1StandardBridge.finalizeETHWithdrawal( + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + withdrawalAmount, + NON_NULL_BYTES32, + { + from: Mock__OVM_L1CrossDomainMessenger.address, + } + ) + + expect(await ethers.provider.getBalance(NON_ZERO_ADDRESS)).to.be.equal( + withdrawalAmount + ) + }) + }) + + describe('ERC20 deposits', () => { + const depositAmount = 1_000 + + beforeEach(async () => { + await L1ERC20.connect(alice).approve( + OVM_L1StandardBridge.address, + depositAmount + ) + }) + + it('depositERC20() escrows the deposit amount and sends the correct deposit message', async () => { + // alice calls deposit on the bridge and the L1 bridge calls transferFrom on the token + await OVM_L1StandardBridge.connect(alice).depositERC20( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await L1ERC20.balanceOf(aliceAddress) + + expect(depositerBalance).to.equal(INITIAL_TOTAL_L1_SUPPLY - depositAmount) + + // bridge's balance is increased + const bridgeBalance = await L1ERC20.balanceOf( + OVM_L1StandardBridge.address + ) + expect(bridgeBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2 bridge + expect(depositCallToMessenger._target).to.equal(DUMMY_L2_BRIDGE_ADDRESS) + // Message data should be a call telling the L2DepositedERC20 to finalize the deposit + + // the L1 bridge sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + IL2ERC20Bridge.encodeFunctionData('finalizeDeposit', [ + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + aliceAddress, + aliceAddress, + depositAmount, + NON_NULL_BYTES32, + ]) + ) + expect(depositCallToMessenger._gasLimit).to.equal(FINALIZATION_GAS) + }) + + it('depositERC20To() escrows the deposit amount and sends the correct deposit message', async () => { + // depositor calls deposit on the bridge and the L1 bridge calls transferFrom on the token + await OVM_L1StandardBridge.connect(alice).depositERC20To( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + bobsAddress, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + const depositCallToMessenger = + Mock__OVM_L1CrossDomainMessenger.smocked.sendMessage.calls[0] + + const depositerBalance = await L1ERC20.balanceOf(aliceAddress) + expect(depositerBalance).to.equal(INITIAL_TOTAL_L1_SUPPLY - depositAmount) + + // bridge's balance is increased + const bridgeBalance = await L1ERC20.balanceOf( + OVM_L1StandardBridge.address + ) + expect(bridgeBalance).to.equal(depositAmount) + + // Check the correct cross-chain call was sent: + // Message should be sent to the L2DepositedERC20 on L2 + expect(depositCallToMessenger._target).to.equal(DUMMY_L2_BRIDGE_ADDRESS) + // Message data should be a call telling the L2DepositedERC20 to finalize the deposit + + // the L1 bridge sends the correct message to the L1 messenger + expect(depositCallToMessenger._message).to.equal( + IL2ERC20Bridge.encodeFunctionData('finalizeDeposit', [ + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + aliceAddress, + bobsAddress, + depositAmount, + NON_NULL_BYTES32, + ]) + ) + expect(depositCallToMessenger._gasLimit).to.equal(FINALIZATION_GAS) + }) + + it('cannot depositERC20 from a contract account', async () => { + expect( + OVM_L1StandardBridge.depositERC20( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('Account not EOA') + }) + + describe('Handling ERC20.transferFrom() failures that revert ', () => { + let MOCK__L1ERC20: MockContract + + before(async () => { + // Deploy the L1 ERC20 token, Alice will receive the full initialSupply + MOCK__L1ERC20 = await smockit( + await Factory__L1ERC20.deploy('L1ERC20', 'ERC') + ) + MOCK__L1ERC20.smocked.transferFrom.will.revert() + }) + + it('depositERC20(): will revert if ERC20.transferFrom() reverts', async () => { + await expect( + OVM_L1StandardBridge.connect(alice).depositERC20( + MOCK__L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('SafeERC20: low-level call failed') + }) + + it('depositERC20To(): will revert if ERC20.transferFrom() reverts', async () => { + await expect( + OVM_L1StandardBridge.connect(alice).depositERC20To( + MOCK__L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + bobsAddress, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('SafeERC20: low-level call failed') + }) + + it('depositERC20To(): will revert if the L1 ERC20 has no code or is zero address', async () => { + await expect( + OVM_L1StandardBridge.connect(alice).depositERC20To( + ethers.constants.AddressZero, + DUMMY_L2_ERC20_ADDRESS, + bobsAddress, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('Address: call to non-contract') + }) + }) + + describe('Handling ERC20.transferFrom failures that return false', () => { + let MOCK__L1ERC20: MockContract + before(async () => { + MOCK__L1ERC20 = await smockit( + await Factory__L1ERC20.deploy('L1ERC20', 'ERC') + ) + MOCK__L1ERC20.smocked.transferFrom.will.return.with(false) + }) + + it('deposit(): will revert if ERC20.transferFrom() returns false', async () => { + await expect( + OVM_L1StandardBridge.connect(alice).depositERC20( + MOCK__L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('SafeERC20: ERC20 operation did not succeed') + }) + + it('depositTo(): will revert if ERC20.transferFrom() returns false', async () => { + await expect( + OVM_L1StandardBridge.depositERC20To( + MOCK__L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + bobsAddress, + depositAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith('SafeERC20: ERC20 operation did not succeed') + }) + }) + }) + + describe('ERC20 withdrawals', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L1 account', async () => { + await expect( + OVM_L1StandardBridge.connect(alice).finalizeERC20Withdrawal( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + constants.AddressZero, + constants.AddressZero, + 1, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith(ERR_INVALID_MESSENGER) + }) + + it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L2DepositedERC20)', async () => { + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + '0x' + '22'.repeat(20) + ) + + await expect( + OVM_L1StandardBridge.finalizeERC20Withdrawal( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + constants.AddressZero, + constants.AddressZero, + 1, + NON_NULL_BYTES32, + { + from: Mock__OVM_L1CrossDomainMessenger.address, + } + ) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should credit funds to the withdrawer and not use too much gas', async () => { + // First Alice will 'donate' some tokens so that there's a balance to be withdrawn + const withdrawalAmount = 10 + await L1ERC20.connect(alice).approve( + OVM_L1StandardBridge.address, + withdrawalAmount + ) + + await OVM_L1StandardBridge.connect(alice).depositERC20( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + withdrawalAmount, + FINALIZATION_GAS, + NON_NULL_BYTES32 + ) + + expect(await L1ERC20.balanceOf(OVM_L1StandardBridge.address)).to.be.equal( + withdrawalAmount + ) + + // make sure no balance at start of test + expect(await L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal(0) + + Mock__OVM_L1CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => DUMMY_L2_BRIDGE_ADDRESS + ) + + await OVM_L1StandardBridge.finalizeERC20Withdrawal( + L1ERC20.address, + DUMMY_L2_ERC20_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + withdrawalAmount, + NON_NULL_BYTES32, + { from: Mock__OVM_L1CrossDomainMessenger.address } + ) + + expect(await L1ERC20.balanceOf(NON_ZERO_ADDRESS)).to.be.equal( + withdrawalAmount + ) + }) + }) +}) diff --git a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts deleted file mode 100644 index 5c2b4a52371e..000000000000 --- a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2DepositedERC20.spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { expect } from '../../../../setup' - -/* External Imports */ -import { ethers } from 'hardhat' -import { Signer, ContractFactory, Contract, constants } from 'ethers' -import { - smockit, - MockContract, - smoddit, - ModifiableContract, -} from '@eth-optimism/smock' - -/* Internal Imports */ -import { NON_ZERO_ADDRESS } from '../../../../helpers' - -const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' -const ERR_INVALID_X_DOMAIN_MSG_SENDER = - 'OVM_XCHAIN: wrong sender of cross-domain message' -const MOCK_L1GATEWAY_ADDRESS: string = - '0x1234123412341234123412341234123412341234' - -describe('OVM_L2DepositedERC20', () => { - let alice: Signer - let bob: Signer - let Factory__OVM_L1ERC20Gateway: ContractFactory - before(async () => { - ;[alice, bob] = await ethers.getSigners() - Factory__OVM_L1ERC20Gateway = await ethers.getContractFactory( - 'OVM_L1ERC20Gateway' - ) - }) - - let OVM_L2DepositedERC20: Contract - let Mock__OVM_L2CrossDomainMessenger: MockContract - let finalizeWithdrawalGasLimit: number - beforeEach(async () => { - // Create a special signer which will enable us to send messages from the L2Messenger contract - const [l2MessengerImpersonator] = await ethers.getSigners() - - // Get a new mock L2 messenger - Mock__OVM_L2CrossDomainMessenger = await smockit( - await ethers.getContractFactory('OVM_L2CrossDomainMessenger'), - // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls - { address: await l2MessengerImpersonator.getAddress() } - ) - - // Deploy the contract under test - OVM_L2DepositedERC20 = await ( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ).deploy(Mock__OVM_L2CrossDomainMessenger.address, 'ovmWETH', 'oWETH') - - // initialize the L2 Gateway with the L1G ateway addrss - await OVM_L2DepositedERC20.init(MOCK_L1GATEWAY_ADDRESS) - - finalizeWithdrawalGasLimit = - await OVM_L2DepositedERC20.getFinalizeWithdrawalL1Gas() - }) - - // test the transfer flow of moving a token from L2 to L1 - describe('finalizeDeposit', () => { - it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { - // Deploy new gateway, initialize with random messenger - OVM_L2DepositedERC20 = await ( - await ethers.getContractFactory('OVM_L2DepositedERC20') - ).deploy(NON_ZERO_ADDRESS, 'ovmWETH', 'oWETH') - await OVM_L2DepositedERC20.init(NON_ZERO_ADDRESS) - - await expect( - OVM_L2DepositedERC20.finalizeDeposit(constants.AddressZero, 0) - ).to.be.revertedWith(ERR_INVALID_MESSENGER) - }) - - it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L1ERC20Gateway)', async () => { - Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - NON_ZERO_ADDRESS - ) - - await expect( - OVM_L2DepositedERC20.finalizeDeposit(constants.AddressZero, 0, { - from: Mock__OVM_L2CrossDomainMessenger.address, - }) - ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) - }) - - it('should credit funds to the depositor', async () => { - const depositAmount = 100 - Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( - () => MOCK_L1GATEWAY_ADDRESS - ) - - await OVM_L2DepositedERC20.finalizeDeposit( - await alice.getAddress(), - depositAmount, - { from: Mock__OVM_L2CrossDomainMessenger.address } - ) - - const aliceBalance = await OVM_L2DepositedERC20.balanceOf( - await alice.getAddress() - ) - aliceBalance.should.equal(depositAmount) - }) - }) - - describe('withdrawals', () => { - const INITIAL_TOTAL_SUPPLY = 100_000 - const ALICE_INITIAL_BALANCE = 50_000 - const withdrawAmount = 1_000 - let SmoddedL2Gateway: ModifiableContract - beforeEach(async () => { - // Deploy a smodded gateway so we can give some balances to withdraw - SmoddedL2Gateway = await ( - await smoddit('OVM_L2DepositedERC20', alice) - ).deploy(Mock__OVM_L2CrossDomainMessenger.address, 'ovmWETH', 'oWETH') - await SmoddedL2Gateway.init(MOCK_L1GATEWAY_ADDRESS) - - // Populate the initial state with a total supply and some money in alice's balance - const aliceAddress = await alice.getAddress() - SmoddedL2Gateway.smodify.put({ - totalSupply: INITIAL_TOTAL_SUPPLY, - balanceOf: { - [aliceAddress]: ALICE_INITIAL_BALANCE, - }, - }) - }) - - it('withdraw() burns and sends the correct withdrawal message', async () => { - await SmoddedL2Gateway.withdraw(withdrawAmount) - const withdrawalCallToMessenger = - Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] - - // Assert Alice's balance went down - const aliceBalance = await SmoddedL2Gateway.balanceOf( - await alice.getAddress() - ) - expect(aliceBalance).to.deep.equal( - ethers.BigNumber.from(ALICE_INITIAL_BALANCE - withdrawAmount) - ) - - // Assert totalSupply went down - const newTotalSupply = await SmoddedL2Gateway.totalSupply() - expect(newTotalSupply).to.deep.equal( - ethers.BigNumber.from(INITIAL_TOTAL_SUPPLY - withdrawAmount) - ) - - // Assert the correct cross-chain call was sent: - // Message should be sent to the L1ERC20Gateway on L1 - expect(withdrawalCallToMessenger._target).to.equal(MOCK_L1GATEWAY_ADDRESS) - // Message data should be a call telling the L1ERC20Gateway to finalize the withdrawal - expect(withdrawalCallToMessenger._message).to.equal( - await Factory__OVM_L1ERC20Gateway.interface.encodeFunctionData( - 'finalizeWithdrawal', - [await alice.getAddress(), withdrawAmount] - ) - ) - // Hardcoded gaslimit should be correct - expect(withdrawalCallToMessenger._gasLimit).to.equal( - finalizeWithdrawalGasLimit - ) - }) - - it('withdrawTo() burns and sends the correct withdrawal message', async () => { - await SmoddedL2Gateway.withdrawTo(await bob.getAddress(), withdrawAmount) - const withdrawalCallToMessenger = - Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] - - // Assert Alice's balance went down - const aliceBalance = await SmoddedL2Gateway.balanceOf( - await alice.getAddress() - ) - expect(aliceBalance).to.deep.equal( - ethers.BigNumber.from(ALICE_INITIAL_BALANCE - withdrawAmount) - ) - - // Assert totalSupply went down - const newTotalSupply = await SmoddedL2Gateway.totalSupply() - expect(newTotalSupply).to.deep.equal( - ethers.BigNumber.from(INITIAL_TOTAL_SUPPLY - withdrawAmount) - ) - - // Assert the correct cross-chain call was sent. - // Message should be sent to the L1ERC20Gateway on L1 - expect(withdrawalCallToMessenger._target).to.equal(MOCK_L1GATEWAY_ADDRESS) - // The message data should be a call telling the L1ERC20Gateway to finalize the withdrawal - expect(withdrawalCallToMessenger._message).to.equal( - await Factory__OVM_L1ERC20Gateway.interface.encodeFunctionData( - 'finalizeWithdrawal', - [await bob.getAddress(), withdrawAmount] - ) - ) - // Hardcoded gaslimit should be correct - expect(withdrawalCallToMessenger._gasLimit).to.equal( - finalizeWithdrawalGasLimit - ) - }) - }) - - // low priority todos: see question in contract - describe.skip('Initialization logic', () => { - it('should not allow calls to onlyInitialized functions', async () => { - // TODO - }) - - it('should only allow initialization once and emits initialized event', async () => { - // TODO - }) - }) -}) diff --git a/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2StandardBridge.spec.ts b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2StandardBridge.spec.ts new file mode 100644 index 000000000000..4b1000b53f5a --- /dev/null +++ b/packages/contracts/test/contracts/OVM/bridge/assets/OVM_L2StandardBridge.spec.ts @@ -0,0 +1,332 @@ +import { expect } from '../../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, ContractFactory, Contract } from 'ethers' +import { Interface } from 'ethers/lib/utils' +import { + smockit, + MockContract, + smoddit, + ModifiableContract, +} from '@eth-optimism/smock' + +/* Internal Imports */ +import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../../../../helpers' + +import { getContractInterface } from '../../../../../src' + +const ERR_INVALID_MESSENGER = 'OVM_XCHAIN: messenger contract unauthenticated' +const ERR_INVALID_X_DOMAIN_MSG_SENDER = + 'OVM_XCHAIN: wrong sender of cross-domain message' +const DUMMY_L1BRIDGE_ADDRESS: string = + '0x1234123412341234123412341234123412341234' +const DUMMY_L1TOKEN_ADDRESS: string = + '0x2234223412342234223422342234223422342234' + +describe('OVM_L2StandardBridge', () => { + let alice: Signer + let aliceAddress: string + let bob: Signer + let bobsAddress: string + let l2MessengerImpersonator: Signer + let Factory__OVM_L1StandardBridge: ContractFactory + let IL2ERC20Bridge: Interface + const INITIAL_TOTAL_SUPPLY = 100_000 + const ALICE_INITIAL_BALANCE = 50_000 + before(async () => { + // Create a special signer which will enable us to send messages from the L2Messenger contract + ;[alice, bob, l2MessengerImpersonator] = await ethers.getSigners() + aliceAddress = await alice.getAddress() + bobsAddress = await bob.getAddress() + Factory__OVM_L1StandardBridge = await ethers.getContractFactory( + 'OVM_L1StandardBridge' + ) + + // get an L2ER20Bridge Interface + IL2ERC20Bridge = getContractInterface('iOVM_L2ERC20Bridge') + }) + + let OVM_L2StandardBridge: Contract + let L2ERC20: Contract + let Mock__OVM_L2CrossDomainMessenger: MockContract + beforeEach(async () => { + // Get a new mock L2 messenger + Mock__OVM_L2CrossDomainMessenger = await smockit( + await ethers.getContractFactory('OVM_L2CrossDomainMessenger'), + // This allows us to use an ethers override {from: Mock__OVM_L2CrossDomainMessenger.address} to mock calls + { address: await l2MessengerImpersonator.getAddress() } + ) + + // Deploy the contract under test + OVM_L2StandardBridge = await ( + await ethers.getContractFactory('OVM_L2StandardBridge') + ).deploy(Mock__OVM_L2CrossDomainMessenger.address, DUMMY_L1BRIDGE_ADDRESS) + + // Deploy an L2 ERC20 + L2ERC20 = await ( + await ethers.getContractFactory('L2StandardERC20', alice) + ).deploy( + OVM_L2StandardBridge.address, + DUMMY_L1TOKEN_ADDRESS, + 'L2Token', + 'L2T' + ) + }) + + // test the transfer flow of moving a token from L2 to L1 + describe('finalizeDeposit', () => { + it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { + await expect( + OVM_L2StandardBridge.finalizeDeposit( + DUMMY_L1TOKEN_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + 0, + NON_NULL_BYTES32 + ) + ).to.be.revertedWith(ERR_INVALID_MESSENGER) + }) + + it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L1L1StandardBridge)', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + NON_ZERO_ADDRESS + ) + + await expect( + OVM_L2StandardBridge.connect(l2MessengerImpersonator).finalizeDeposit( + DUMMY_L1TOKEN_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + 0, + NON_NULL_BYTES32, + { + from: Mock__OVM_L2CrossDomainMessenger.address, + } + ) + ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MSG_SENDER) + }) + + it('should initialize a withdrawal if the L2 token is not compliant', async () => { + // Deploy a non compliant ERC20 + const NonCompliantERC20 = await ( + await ethers.getContractFactory( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20' + ) + ).deploy('L2Token', 'L2T') + + OVM_L2StandardBridge.connect(l2MessengerImpersonator).finalizeDeposit( + DUMMY_L1TOKEN_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + 0, + NON_NULL_BYTES32, + { + from: Mock__OVM_L2CrossDomainMessenger.address, + } + ) + + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => DUMMY_L1BRIDGE_ADDRESS + ) + + await OVM_L2StandardBridge.connect( + l2MessengerImpersonator + ).finalizeDeposit( + DUMMY_L1TOKEN_ADDRESS, + NonCompliantERC20.address, + aliceAddress, + bobsAddress, + 100, + NON_NULL_BYTES32, + { + from: Mock__OVM_L2CrossDomainMessenger.address, + } + ) + + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + expect(withdrawalCallToMessenger._target).to.equal(DUMMY_L1BRIDGE_ADDRESS) + expect(withdrawalCallToMessenger._message).to.equal( + Factory__OVM_L1StandardBridge.interface.encodeFunctionData( + 'finalizeERC20Withdrawal', + [ + DUMMY_L1TOKEN_ADDRESS, + NonCompliantERC20.address, + bobsAddress, + aliceAddress, + 100, + NON_NULL_BYTES32, + ] + ) + ) + }) + + it('should credit funds to the depositor', async () => { + const depositAmount = 100 + + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + () => DUMMY_L1BRIDGE_ADDRESS + ) + + await OVM_L2StandardBridge.connect( + l2MessengerImpersonator + ).finalizeDeposit( + DUMMY_L1TOKEN_ADDRESS, + L2ERC20.address, + aliceAddress, + bobsAddress, + depositAmount, + NON_NULL_BYTES32, + { + from: Mock__OVM_L2CrossDomainMessenger.address, + } + ) + + const bobsBalance = await L2ERC20.balanceOf(bobsAddress) + bobsBalance.should.equal(depositAmount) + }) + }) + + describe('withdrawals', () => { + const withdrawAmount = 1_000 + let SmoddedL2Token: ModifiableContract + beforeEach(async () => { + // Deploy a smodded gateway so we can give some balances to withdraw + SmoddedL2Token = await (await smoddit('L2StandardERC20', alice)).deploy( + OVM_L2StandardBridge.address, + DUMMY_L1TOKEN_ADDRESS, + 'L2Token', + 'L2T' + ) + + // Populate the initial state with a total supply and some money in alice's balance + SmoddedL2Token.smodify.put({ + _totalSupply: INITIAL_TOTAL_SUPPLY, + _balances: { + [aliceAddress]: ALICE_INITIAL_BALANCE, + }, + l2Bridge: OVM_L2StandardBridge.address, + }) + }) + + it('withdraw() burns and sends the correct withdrawal message', async () => { + await OVM_L2StandardBridge.withdraw( + SmoddedL2Token.address, + withdrawAmount, + 0, + NON_NULL_BYTES32 + ) + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert Alice's balance went down + const aliceBalance = await SmoddedL2Token.balanceOf( + await alice.getAddress() + ) + expect(aliceBalance).to.deep.equal( + ethers.BigNumber.from(ALICE_INITIAL_BALANCE - withdrawAmount) + ) + + // Assert totalSupply went down + const newTotalSupply = await SmoddedL2Token.totalSupply() + expect(newTotalSupply).to.deep.equal( + ethers.BigNumber.from(INITIAL_TOTAL_SUPPLY - withdrawAmount) + ) + + // Assert the correct cross-chain call was sent: + // Message should be sent to the L1L1StandardBridge on L1 + expect(withdrawalCallToMessenger._target).to.equal(DUMMY_L1BRIDGE_ADDRESS) + // Message data should be a call telling the L1L1StandardBridge to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + Factory__OVM_L1StandardBridge.interface.encodeFunctionData( + 'finalizeERC20Withdrawal', + [ + DUMMY_L1TOKEN_ADDRESS, + SmoddedL2Token.address, + await alice.getAddress(), + await alice.getAddress(), + withdrawAmount, + NON_NULL_BYTES32, + ] + ) + ) + // gaslimit should be correct + expect(withdrawalCallToMessenger._gasLimit).to.equal(0) + }) + + it('withdrawTo() burns and sends the correct withdrawal message', async () => { + await OVM_L2StandardBridge.withdrawTo( + SmoddedL2Token.address, + await bob.getAddress(), + withdrawAmount, + 0, + NON_NULL_BYTES32 + ) + const withdrawalCallToMessenger = + Mock__OVM_L2CrossDomainMessenger.smocked.sendMessage.calls[0] + + // Assert Alice's balance went down + const aliceBalance = await SmoddedL2Token.balanceOf( + await alice.getAddress() + ) + expect(aliceBalance).to.deep.equal( + ethers.BigNumber.from(ALICE_INITIAL_BALANCE - withdrawAmount) + ) + + // Assert totalSupply went down + const newTotalSupply = await SmoddedL2Token.totalSupply() + expect(newTotalSupply).to.deep.equal( + ethers.BigNumber.from(INITIAL_TOTAL_SUPPLY - withdrawAmount) + ) + + // Assert the correct cross-chain call was sent. + // Message should be sent to the L1L1StandardBridge on L1 + expect(withdrawalCallToMessenger._target).to.equal(DUMMY_L1BRIDGE_ADDRESS) + // The message data should be a call telling the L1L1StandardBridge to finalize the withdrawal + expect(withdrawalCallToMessenger._message).to.equal( + Factory__OVM_L1StandardBridge.interface.encodeFunctionData( + 'finalizeERC20Withdrawal', + [ + DUMMY_L1TOKEN_ADDRESS, + SmoddedL2Token.address, + await alice.getAddress(), + await bob.getAddress(), + withdrawAmount, + NON_NULL_BYTES32, + ] + ) + ) + // gas value is ignored and set to 0. + expect(withdrawalCallToMessenger._gasLimit).to.equal(0) + }) + }) + + describe('standard erc20', () => { + it('should not allow anyone but the L2 bridge to mint and burn', async () => { + expect(L2ERC20.connect(alice).mint(aliceAddress, 100)).to.be.revertedWith( + 'Only L2 Bridge can mint and burn' + ) + expect(L2ERC20.connect(alice).burn(aliceAddress, 100)).to.be.revertedWith( + 'Only L2 Bridge can mint and burn' + ) + }) + + it('should return the correct interface support', async () => { + const supportsERC165 = await L2ERC20.supportsInterface(0x01ffc9a7) + expect(supportsERC165).to.be.true + + const supportsL2TokenInterface = await L2ERC20.supportsInterface( + 0x1d1d8b63 + ) + expect(supportsL2TokenInterface).to.be.true + + const badSupports = await L2ERC20.supportsInterface(0xffffffff) + expect(badSupports).to.be.false + }) + }) +}) diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index aa33388023ee..b4e5ff40ae0e 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "resolveJsonModule": true + "resolveJsonModule": true, } } diff --git a/packages/core-utils/CHANGELOG.md b/packages/core-utils/CHANGELOG.md index 69d1bbdd6ec8..057a58f5c45f 100644 --- a/packages/core-utils/CHANGELOG.md +++ b/packages/core-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @eth-optimism/core-utils +## 0.4.6 + +### Patch Changes + +- d9644c34: Minor fix on watchers to pick up finalization of transactions on L1 +- df5ff890: improved watcher ability to find transactions during periods of high load + ## 0.4.5 ### Patch Changes diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json index 90407cc36c81..c2edc3479310 100644 --- a/packages/core-utils/package.json +++ b/packages/core-utils/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/core-utils", - "version": "0.4.5", + "version": "0.4.6", "main": "dist/index", "files": [ "dist/*" diff --git a/packages/data-transport-layer/CHANGELOG.md b/packages/data-transport-layer/CHANGELOG.md index cd696595dba6..f25045159f0e 100644 --- a/packages/data-transport-layer/CHANGELOG.md +++ b/packages/data-transport-layer/CHANGELOG.md @@ -1,5 +1,54 @@ # data transport layer +## 0.4.0 + +### Minor Changes + +- 2e72fd90: Update AddressSet event to speed search up a bit. Breaks AddressSet API. +- 8582fc16: Define L1 Starting block via OwnershipTransferred (occurring on block 1) rather than AddressSet (occuring on block 2 onwards) + +### Patch Changes + +- 0b91df42: Adds additional code into the DTL to defend against situations where an RPC provider might be missing an event. +- 8fee7bed: Add extra overflow protection for the DTL types +- ca7d65db: Removes a function that was previously used for backwards compatibility but is no longer necessary +- 16f68159: Have DTL log failed HTTP requests as ERROR instead of INFO +- a415d017: Updates the DTL to use the same L2 chain ID everywhere +- 29431d6a: Add highest L1 and L2 block number Gauge metrics to DTL +- 5c89c45f: Move the metric prefix string to a label #1047 +- b8e2d685: Add replica sync test to integration tests; handle 0 L2 blocks in DTL +- Updated dependencies [25f09abd] +- Updated dependencies [dd8edc7b] +- Updated dependencies [c87e4c74] +- Updated dependencies [db0dbfb2] +- Updated dependencies [7f5936a8] +- Updated dependencies [f87a2d00] +- Updated dependencies [85da4979] +- Updated dependencies [57ca21a2] +- Updated dependencies [5fc728da] +- Updated dependencies [2e72fd90] +- Updated dependencies [c43b33ec] +- Updated dependencies [26bc63ad] +- Updated dependencies [a0d9e565] +- Updated dependencies [2bd49730] +- Updated dependencies [38355a3b] +- Updated dependencies [3c2c32e1] +- Updated dependencies [d9644c34] +- Updated dependencies [48ece14c] +- Updated dependencies [e04de624] +- Updated dependencies [014dea71] +- Updated dependencies [fa29b03e] +- Updated dependencies [6b46c8ba] +- Updated dependencies [e045f582] +- Updated dependencies [5c89c45f] +- Updated dependencies [df5ff890] +- Updated dependencies [e29fab10] +- Updated dependencies [c2a04893] +- Updated dependencies [baacda34] + - @eth-optimism/contracts@0.4.0 + - @eth-optimism/core-utils@0.4.6 + - @eth-optimism/common-ts@0.1.4 + ## 0.3.6 ### Patch Changes diff --git a/packages/data-transport-layer/package.json b/packages/data-transport-layer/package.json index 7982c69b08f1..321e740100af 100644 --- a/packages/data-transport-layer/package.json +++ b/packages/data-transport-layer/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/data-transport-layer", - "version": "0.3.6", + "version": "0.4.0", "private": true, "main": "dist/index", "files": [ @@ -21,9 +21,9 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.3", - "@eth-optimism/contracts": "^0.3.5", - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/common-ts": "^0.1.4", + "@eth-optimism/contracts": "^0.4.0", + "@eth-optimism/core-utils": "^0.4.6", "@ethersproject/providers": "^5.0.21", "@ethersproject/transactions": "^5.0.21", "@sentry/node": "^6.3.1", @@ -52,6 +52,7 @@ "@types/mocha": "^8.2.2", "@types/node-fetch": "^2.5.8", "@types/workerpool": "^6.0.0", + "bfj": "^7.0.2", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", "chai": "^4.3.4", @@ -68,6 +69,7 @@ "mocha": "^8.3.2", "pino-pretty": "^4.7.1", "prettier": "^2.2.1", + "prom-client": "^13.1.0", "rimraf": "^3.0.2", "ts-node": "^9.1.1", "typescript": "^4.2.3" diff --git a/packages/data-transport-layer/src/db/transport-db.ts b/packages/data-transport-layer/src/db/transport-db.ts index ab98254bb06e..c1c50c4eac53 100644 --- a/packages/data-transport-layer/src/db/transport-db.ts +++ b/packages/data-transport-layer/src/db/transport-db.ts @@ -379,9 +379,7 @@ export class TransportDB { if (index === null) { return null } - let entry = await this.db.get(`${key}:index`, index) - entry = stringify(entry) - return entry + return this.db.get(`${key}:index`, index) } private async _getEntries( @@ -389,28 +387,6 @@ export class TransportDB { startIndex: number, endIndex: number ): Promise { - const entries = await this.db.range( - `${key}:index`, - startIndex, - endIndex - ) - const results = [] - for (const entry of entries) { - results.push(stringify(entry)) - } - return results - } -} - -const stringify = (entry) => { - if (entry === null || entry === undefined) { - return entry - } - if (entry.gasLimit) { - entry.gasLimit = BigNumber.from(entry.gasLimit).toString() - } - if (entry.decoded) { - entry.decoded.gasLimit = BigNumber.from(entry.decoded.gasLimit).toString() + return this.db.range(`${key}:index`, startIndex, endIndex) } - return entry } diff --git a/packages/data-transport-layer/src/services/l1-ingestion/service.ts b/packages/data-transport-layer/src/services/l1-ingestion/service.ts index b8b8cb4c39b2..f13309d54643 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/service.ts @@ -1,9 +1,10 @@ /* Imports: External */ import { fromHexString, EventArgsAddressSet } from '@eth-optimism/core-utils' -import { BaseService } from '@eth-optimism/common-ts' +import { BaseService, Metrics } from '@eth-optimism/common-ts' import { JsonRpcProvider } from '@ethersproject/providers' import { LevelUp } from 'levelup' import { ethers, constants } from 'ethers' +import { Gauge } from 'prom-client' /* Imports: Internal */ import { TransportDB } from '../../db/transport-db' @@ -21,9 +22,25 @@ import { handleEventsStateBatchAppended } from './handlers/state-batch-appended' import { L1DataTransportServiceOptions } from '../main/service' import { MissingElementError, EventName } from './handlers/errors' +interface L1IngestionMetrics { + highestSyncedL1Block: Gauge +} + +const registerMetrics = ({ + client, + registry, +}: Metrics): L1IngestionMetrics => ({ + highestSyncedL1Block: new client.Gauge({ + name: 'data_transport_layer_highest_synced_l1_block', + help: 'Highest Synced L1 Block Number', + registers: [registry], + }), +}) + export interface L1IngestionServiceOptions extends L1DataTransportServiceOptions { db: LevelUp + metrics: Metrics } const optionSettings = { @@ -54,6 +71,9 @@ const optionSettings = { return validators.isUrl(val) || validators.isJsonRpcProvider(val) }, }, + l2ChainId: { + validate: validators.isInteger, + }, } export class L1IngestionService extends BaseService { @@ -61,17 +81,20 @@ export class L1IngestionService extends BaseService { super('L1_Ingestion_Service', options, optionSettings) } + private l1IngestionMetrics: L1IngestionMetrics + private state: { db: TransportDB contracts: OptimismContracts l1RpcProvider: JsonRpcProvider startingL1BlockNumber: number - l2ChainId: number } = {} as any protected async _init(): Promise { this.state.db = new TransportDB(this.options.db) + this.l1IngestionMetrics = registerMetrics(this.metrics) + this.state.l1RpcProvider = typeof this.options.l1RpcProvider === 'string' ? new JsonRpcProvider(this.options.l1RpcProvider) @@ -116,10 +139,6 @@ export class L1IngestionService extends BaseService { this.options.addressManager ) - this.state.l2ChainId = ethers.BigNumber.from( - await this.state.contracts.OVM_ExecutionManager.ovmCHAINID() - ).toNumber() - const startingL1BlockNumber = await this.state.db.getStartingL1Block() if (startingL1BlockNumber) { this.state.startingL1BlockNumber = startingL1BlockNumber @@ -199,6 +218,8 @@ export class L1IngestionService extends BaseService { await this.state.db.setHighestSyncedL1Block(targetL1Block) + this.l1IngestionMetrics.highestSyncedL1Block.set(targetL1Block) + if ( currentL1Block - highestSyncedL1Block < this.options.logsPerPollingInterval @@ -240,6 +261,10 @@ export class L1IngestionService extends BaseService { lastGoodElement.blockNumber ) + this.l1IngestionMetrics.highestSyncedL1Block.set( + lastGoodElement.blockNumber + ) + // Something we should be keeping track of. this.logger.warn('recovering from a missing event', { eventName, @@ -281,13 +306,11 @@ export class L1IngestionService extends BaseService { // We need to figure out how to make this work without Infura. Mark and I think that infura is // doing some indexing of events beyond Geth's native capabilities, meaning some event logic // will only work on Infura and not on a local geth instance. Not great. - const addressSetEvents = ((await this.state.contracts.Lib_AddressManager.queryFilter( - this.state.contracts.Lib_AddressManager.filters.AddressSet(), + const addressSetEvents = await this.state.contracts.Lib_AddressManager.queryFilter( + this.state.contracts.Lib_AddressManager.filters.AddressSet(contractName), fromL1Block, toL1Block - )) as TypedEthersEvent[]).filter((event) => { - return event.args._name === contractName - }) + ) // We're going to parse things out in ranges because the address of a given contract may have // changed in the range provided by the user. @@ -343,7 +366,7 @@ export class L1IngestionService extends BaseService { const parsedEvent = await handlers.parseEvent( event, extraData, - this.state.l2ChainId + this.options.l2ChainId ) await handlers.storeEvent(parsedEvent, this.state.db) } @@ -370,21 +393,14 @@ export class L1IngestionService extends BaseService { contractName: string, blockNumber: number ): Promise { - // TODO: Should be much easier than this. Need to change the params of this event. - const relevantAddressSetEvents = ( - await this.state.contracts.Lib_AddressManager.queryFilter( - this.state.contracts.Lib_AddressManager.filters.AddressSet(), - this.state.startingL1BlockNumber - ) - ).filter((event) => { - return ( - event.args._name === contractName && event.blockNumber < blockNumber - ) - }) + const events = await this.state.contracts.Lib_AddressManager.queryFilter( + this.state.contracts.Lib_AddressManager.filters.AddressSet(contractName), + this.state.startingL1BlockNumber, + blockNumber + ) - if (relevantAddressSetEvents.length > 0) { - return relevantAddressSetEvents[relevantAddressSetEvents.length - 1].args - ._newAddress + if (events.length > 0) { + return events[events.length - 1].args._newAddress } else { // Address wasn't set before this. return constants.AddressZero @@ -396,7 +412,7 @@ export class L1IngestionService extends BaseService { for (let i = 0; i < currentL1Block; i += 1000000) { const events = await this.state.contracts.Lib_AddressManager.queryFilter( - this.state.contracts.Lib_AddressManager.filters.AddressSet(), + this.state.contracts.Lib_AddressManager.filters.OwnershipTransferred(), i, Math.min(i + 1000000, currentL1Block) ) diff --git a/packages/data-transport-layer/src/services/l2-ingestion/service.ts b/packages/data-transport-layer/src/services/l2-ingestion/service.ts index 3be21abf201b..50ea6e514874 100644 --- a/packages/data-transport-layer/src/services/l2-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l2-ingestion/service.ts @@ -1,10 +1,11 @@ /* Imports: External */ -import { BaseService } from '@eth-optimism/common-ts' +import { BaseService, Metrics } from '@eth-optimism/common-ts' import { JsonRpcProvider } from '@ethersproject/providers' import { BigNumber } from 'ethers' import { LevelUp } from 'levelup' import axios from 'axios' import bfj from 'bfj' +import { Gauge } from 'prom-client' /* Imports: Internal */ import { TransportDB } from '../../db/transport-db' @@ -12,6 +13,21 @@ import { sleep, toRpcHexString, validators } from '../../utils' import { L1DataTransportServiceOptions } from '../main/service' import { handleSequencerBlock } from './handlers/transaction' +interface L2IngestionMetrics { + highestSyncedL2Block: Gauge +} + +const registerMetrics = ({ + client, + registry, +}: Metrics): L2IngestionMetrics => ({ + highestSyncedL2Block: new client.Gauge({ + name: 'data_transport_layer_highest_synced_l2_block', + help: 'Highest Synced L2 Block Number', + registers: [registry], + }), +}) + export interface L2IngestionServiceOptions extends L1DataTransportServiceOptions { db: LevelUp @@ -52,6 +68,8 @@ export class L2IngestionService extends BaseService { super('L2_Ingestion_Service', options, optionSettings) } + private l2IngestionMetrics: L2IngestionMetrics + private state: { db: TransportDB l2RpcProvider: JsonRpcProvider @@ -64,6 +82,8 @@ export class L2IngestionService extends BaseService { ) } + this.l2IngestionMetrics = registerMetrics(this.metrics) + this.state.db = new TransportDB(this.options.db) this.state.l2RpcProvider = @@ -88,7 +108,11 @@ export class L2IngestionService extends BaseService { ) // We're already at the head, so no point in attempting to sync. - if (highestSyncedL2BlockNumber === targetL2Block) { + // Also wait on edge case of no L2 transactions + if ( + highestSyncedL2BlockNumber === targetL2Block || + currentL2Block === 0 + ) { await sleep(this.options.pollingInterval) continue } @@ -109,6 +133,8 @@ export class L2IngestionService extends BaseService { await this.state.db.setHighestSyncedUnconfirmedBlock(targetL2Block) + this.l2IngestionMetrics.highestSyncedL2Block.set(targetL2Block) + if ( currentL2Block - highestSyncedL2BlockNumber < this.options.transactionsPerPollingInterval @@ -206,4 +232,4 @@ export class L2IngestionService extends BaseService { await handleSequencerBlock.storeBlock(entry, this.state.db) } } -} \ No newline at end of file +} diff --git a/packages/data-transport-layer/src/services/main/service.ts b/packages/data-transport-layer/src/services/main/service.ts index b4d0839a360e..c8567a448d3b 100644 --- a/packages/data-transport-layer/src/services/main/service.ts +++ b/packages/data-transport-layer/src/services/main/service.ts @@ -1,5 +1,5 @@ /* Imports: External */ -import { BaseService, Logger } from '@eth-optimism/common-ts' +import { BaseService, Logger, Metrics } from '@eth-optimism/common-ts' import { LevelUp } from 'levelup' import level from 'level' @@ -31,7 +31,6 @@ export interface L1DataTransportServiceOptions { useSentry?: boolean sentryDsn?: string sentryTraceRate?: number - enableMetrics?: boolean defaultBackend: string } @@ -65,8 +64,18 @@ export class L1DataTransportService extends BaseService { private _initializeApp() { // TODO: Maybe pass this in as a parameter instead of creating it here? this.state.app = express() + if (this.options.useSentry) { this._initSentry() } - if (this.options.enableMetrics) { - this._initMetrics() - } + this.state.app.use(cors()) + + // Add prometheus middleware to express BEFORE route registering + this.state.app.use( + // This also serves metrics on port 3000 at /metrics + promBundle({ + // Provide metrics registry that other metrics uses + promRegistry: this.metrics.registry, + includeMethod: true, + includePath: true, + }) + ) + this._registerAllRoutes() + // Sentry error handling must be after all controllers // and before other error middleware if (this.options.useSentry) { @@ -148,25 +162,6 @@ export class L1TransportServer extends BaseService { this.state.app.use(Sentry.Handlers.tracingHandler()) } - /** - * Initialize Prometheus metrics collection and endpoint - */ - private _initMetrics() { - this.metrics = new Metrics({ - labels: { - environment: this.options.nodeEnv, - network: this.options.ethNetworkName, - release: this.options.release, - service: this.name, - }, - }) - const metricsMiddleware = promBundle({ - includeMethod: true, - includePath: true, - }) - this.state.app.use(metricsMiddleware) - } - /** * Registers a route on the server. * @@ -201,7 +196,7 @@ export class L1TransportServer extends BaseService { return res.json(json) } catch (e) { const elapsed = Date.now() - start - this.logger.info('Failed HTTP Request', { + this.logger.error('Failed HTTP Request', { method: req.method, url: req.url, elapsed, @@ -234,8 +229,7 @@ export class L1TransportServer extends BaseService { highestL2BlockNumber = await this.state.db.getHighestL2BlockNumber() break case 'l2': - currentL2Block = - await this.state.db.getLatestUnconfirmedTransaction() + currentL2Block = await this.state.db.getLatestUnconfirmedTransaction() highestL2BlockNumber = (await this.state.db.getHighestSyncedUnconfirmedBlock()) - 1 break @@ -478,12 +472,11 @@ export class L1TransportServer extends BaseService { } } - const transactions = - await this.state.db.getFullTransactionsByIndexRange( - BigNumber.from(batch.prevTotalElements).toNumber(), - BigNumber.from(batch.prevTotalElements).toNumber() + - BigNumber.from(batch.size).toNumber() - ) + const transactions = await this.state.db.getFullTransactionsByIndexRange( + BigNumber.from(batch.prevTotalElements).toNumber(), + BigNumber.from(batch.prevTotalElements).toNumber() + + BigNumber.from(batch.size).toNumber() + ) return { batch, @@ -507,12 +500,11 @@ export class L1TransportServer extends BaseService { } } - const transactions = - await this.state.db.getFullTransactionsByIndexRange( - BigNumber.from(batch.prevTotalElements).toNumber(), - BigNumber.from(batch.prevTotalElements).toNumber() + - BigNumber.from(batch.size).toNumber() - ) + const transactions = await this.state.db.getFullTransactionsByIndexRange( + BigNumber.from(batch.prevTotalElements).toNumber(), + BigNumber.from(batch.prevTotalElements).toNumber() + + BigNumber.from(batch.size).toNumber() + ) return { batch, diff --git a/packages/message-relayer/CHANGELOG.md b/packages/message-relayer/CHANGELOG.md index 0a8a41e3a2bb..3e8dc25f7e5a 100644 --- a/packages/message-relayer/CHANGELOG.md +++ b/packages/message-relayer/CHANGELOG.md @@ -1,5 +1,42 @@ # @eth-optimism/message-relayer +## 0.1.6 + +### Patch Changes + +- 735cd78f: Update relayer package JSON to correctly export all files in dist +- Updated dependencies [25f09abd] +- Updated dependencies [dd8edc7b] +- Updated dependencies [c87e4c74] +- Updated dependencies [db0dbfb2] +- Updated dependencies [7f5936a8] +- Updated dependencies [f87a2d00] +- Updated dependencies [85da4979] +- Updated dependencies [57ca21a2] +- Updated dependencies [5fc728da] +- Updated dependencies [2e72fd90] +- Updated dependencies [c43b33ec] +- Updated dependencies [26bc63ad] +- Updated dependencies [a0d9e565] +- Updated dependencies [2bd49730] +- Updated dependencies [38355a3b] +- Updated dependencies [3c2c32e1] +- Updated dependencies [d9644c34] +- Updated dependencies [48ece14c] +- Updated dependencies [e04de624] +- Updated dependencies [014dea71] +- Updated dependencies [fa29b03e] +- Updated dependencies [6b46c8ba] +- Updated dependencies [e045f582] +- Updated dependencies [5c89c45f] +- Updated dependencies [df5ff890] +- Updated dependencies [e29fab10] +- Updated dependencies [c2a04893] +- Updated dependencies [baacda34] + - @eth-optimism/contracts@0.4.0 + - @eth-optimism/core-utils@0.4.6 + - @eth-optimism/common-ts@0.1.4 + ## 0.1.5 ### Patch Changes diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index 9d6f1ffd4c32..442ee6c68214 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -1,11 +1,11 @@ { "name": "@eth-optimism/message-relayer", - "version": "0.1.5", + "version": "0.1.6", "description": "[Optimism] Cross Domain Message Relayer service", "main": "dist/index", "types": "dist/index", "files": [ - "dist/index" + "dist/*" ], "scripts": { "start": "node ./exec/run-message-relayer.js", @@ -29,9 +29,9 @@ "url": "https://github.com/ethereum-optimism/optimism.git" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.3", - "@eth-optimism/contracts": "^0.3.5", - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/common-ts": "^0.1.4", + "@eth-optimism/contracts": "^0.4.0", + "@eth-optimism/core-utils": "^0.4.6", "@sentry/node": "6.2.5", "bcfg": "^0.1.6", "dotenv": "^8.2.0", @@ -40,7 +40,7 @@ "rlp": "^2.2.6" }, "devDependencies": { - "@eth-optimism/smock": "^1.1.5", + "@eth-optimism/smock": "^1.1.6", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/chai": "^4.2.18", diff --git a/packages/message-relayer/src/exec/run.ts b/packages/message-relayer/src/exec/run.ts index 49fdde159a98..1cca64005d0a 100644 --- a/packages/message-relayer/src/exec/run.ts +++ b/packages/message-relayer/src/exec/run.ts @@ -70,14 +70,6 @@ const main = async () => { 'from-l2-transaction-index', parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0 ) - const BLACKLIST_ENDPOINT = config.str( - 'blacklist-endpoint', - env.BLACKLIST_ENDPOINT - ) || '' - const BLACKLIST_POLLING_INTERVAL = config.uint( - 'blacklist-polling-interval', - parseInt(env.BLACKLIST_POLLING_INTERVAL, 10) || 60000 - ) if (!ADDRESS_MANAGER_ADDRESS) { throw new Error('Must pass ADDRESS_MANAGER_ADDRESS') @@ -114,8 +106,6 @@ const main = async () => { l1StartOffset: L1_START_OFFSET, getLogsInterval: GET_LOGS_INTERVAL, logger, - blacklistEndpoint: BLACKLIST_ENDPOINT, - blacklistPollingInterval: BLACKLIST_POLLING_INTERVAL }) await service.start() diff --git a/packages/message-relayer/src/service.ts b/packages/message-relayer/src/service.ts index 14f67a025f41..547c661decfb 100644 --- a/packages/message-relayer/src/service.ts +++ b/packages/message-relayer/src/service.ts @@ -7,7 +7,11 @@ import { MerkleTree } from 'merkletreejs' import { fromHexString, sleep } from '@eth-optimism/core-utils' import { Logger, BaseService, Metrics } from '@eth-optimism/common-ts' -import { loadContract, loadContractFromManager } from '@eth-optimism/contracts' +import { + loadContract, + loadContractFromManager, + predeploys, +} from '@eth-optimism/contracts' import { StateRootBatchHeader, SentMessage, SentMessageProof } from './types' interface MessageRelayerOptions { @@ -46,11 +50,6 @@ interface MessageRelayerOptions { // A custom metrics tracker to manage metrics; default undefined metrics?: Metrics - - // blacklist - blacklistEndpoint?: string - - blacklistPollingInterval?: number } const optionSettings = { @@ -60,7 +59,6 @@ const optionSettings = { l2BlockOffset: { default: 1 }, l1StartOffset: { default: 0 }, getLogsInterval: { default: 2000 }, - blacklistPollingInterval: { default: 60000 } } export class MessageRelayerService extends BaseService { @@ -78,8 +76,6 @@ export class MessageRelayerService extends BaseService { OVM_L1CrossDomainMessenger: Contract OVM_L2CrossDomainMessenger: Contract OVM_L2ToL1MessagePasser: Contract - blacklist: Array - lastBlacklistPollingTimestamp: number } protected async _init(): Promise { @@ -89,7 +85,6 @@ export class MessageRelayerService extends BaseService { pollingInterval: this.options.pollingInterval, l2BlockOffset: this.options.l2BlockOffset, getLogsInterval: this.options.getLogsInterval, - blacklistPollingInterval: this.options.blacklistPollingInterval, }) // Need to improve this, sorry. this.state = {} as any @@ -137,7 +132,7 @@ export class MessageRelayerService extends BaseService { this.logger.info('Connecting to OVM_L2ToL1MessagePasser...') this.state.OVM_L2ToL1MessagePasser = loadContract( 'OVM_L2ToL1MessagePasser', - '0x4200000000000000000000000000000000000000', + predeploys.OVM_L2ToL1MessagePasser, this.options.l2RpcProvider ) this.logger.info('Connected to OVM_L2ToL1MessagePasser', { @@ -152,13 +147,11 @@ export class MessageRelayerService extends BaseService { this.state.lastFinalizedTxHeight = this.options.fromL2TransactionIndex || 0 this.state.nextUnfinalizedTxHeight = this.options.fromL2TransactionIndex || 0 - this.state.lastBlacklistPollingTimestamp = 0 } protected async _start(): Promise { while (this.running) { await sleep(this.options.pollingInterval) - await this._getBlacklist() try { // Check that the correct address is set in the address manager @@ -233,11 +226,6 @@ export class MessageRelayerService extends BaseService { continue } - if (this.state.blacklist.includes(message.target)) { - this.logger.info('Message not intended for target, skipping.') - continue - } - this.logger.info( 'Message not yet relayed. Attempting to generate a proof...' ) @@ -566,25 +554,4 @@ export class MessageRelayerService extends BaseService { } this.logger.info('Message successfully relayed to Layer 1!') } - - private async _getBlacklist(): Promise { - try { - if (this.options.blacklistEndpoint) { - if (this.state.lastBlacklistPollingTimestamp === 0 || - new Date().getTime() > this.state.lastBlacklistPollingTimestamp + this.options.blacklistPollingInterval - ) { - const response = await fetch(this.options.blacklistEndpoint); - const blacklist = await response.json(); - this.state.lastBlacklistPollingTimestamp = new Date().getTime(); - this.state.blacklist = blacklist; - this.logger.info('Found the blacklist', { blacklist }) - } - } else { - this.state.blacklist = []; - } - } catch { - this.logger.info('Failed to fetch the blacklist') - this.state.blacklist = []; - } - } } diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts index 1745a620f5f2..de668f24898d 100644 --- a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -58,7 +58,7 @@ describe('relay transaction generation functions', () => { .deploy(AddressManager.address, 0, 0) await AddressManager.setAddress( - 'OVM_ChainStorageContainer:SCC:batches', + 'OVM_ChainStorageContainer-SCC-batches', ChainStorageContainer.address ) diff --git a/packages/smock/CHANGELOG.md b/packages/smock/CHANGELOG.md index 1140eb913484..800a1779757b 100644 --- a/packages/smock/CHANGELOG.md +++ b/packages/smock/CHANGELOG.md @@ -1,5 +1,14 @@ # @eth-optimism/smock +## 1.1.6 + +### Patch Changes + +- 71349a4e: Minor smock patch to add support for hardhat 2.4.0 and up +- Updated dependencies [d9644c34] +- Updated dependencies [df5ff890] + - @eth-optimism/core-utils@0.4.6 + ## 1.1.5 ### Patch Changes diff --git a/packages/smock/package.json b/packages/smock/package.json index be16bcf0c645..fda76e0efad1 100644 --- a/packages/smock/package.json +++ b/packages/smock/package.json @@ -3,7 +3,7 @@ "files": [ "dist/src/*" ], - "version": "1.1.5", + "version": "1.1.6", "main": "dist/src/index", "types": "dist/src/index", "author": "Optimism PBC", @@ -26,7 +26,7 @@ "hardhat": "^2" }, "dependencies": { - "@eth-optimism/core-utils": "^0.4.5", + "@eth-optimism/core-utils": "^0.4.6", "bn.js": "^5.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index bcb6ed5a7aa0..a6bba0ca48ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -746,6 +746,17 @@ "@ethersproject/logger" "^5.1.0" "@ethersproject/rlp" "^5.1.0" +"@ethersproject/address@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.3.0.tgz#e53b69eacebf332e8175de814c5e6507d6932518" + integrity sha512-29TgjzEBK+gUEUAOfWCG7s9IxLNLCqvr+oDSk6L9TXD0VLvZJKhJV479tKQqheVA81OeGxfpdxYtUVH8hqlCvA== + dependencies: + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/rlp" "^5.3.0" + "@ethersproject/base64@5.1.0", "@ethersproject/base64@^5.0.0", "@ethersproject/base64@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.1.0.tgz#27240c174d0a4e13f6eae87416fd876caf7f42b6" @@ -770,6 +781,15 @@ "@ethersproject/logger" "^5.1.0" bn.js "^4.4.0" +"@ethersproject/bignumber@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.3.0.tgz#74ab2ec9c3bda4e344920565720a6ee9c794e9db" + integrity sha512-5xguJ+Q1/zRMgHgDCaqAexx/8DwDVLRemw2i6uR8KyGjwGdXI8f32QZZ1cKGucBN6ekJvpUpHy6XAuQnTv0mPA== + dependencies: + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + bn.js "^4.11.9" + "@ethersproject/bytes@5.1.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.0", "@ethersproject/bytes@^5.0.2", "@ethersproject/bytes@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.1.0.tgz#55dfa9c4c21df1b1b538be3accb50fb76d5facfd" @@ -777,6 +797,13 @@ dependencies: "@ethersproject/logger" "^5.1.0" +"@ethersproject/bytes@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.3.0.tgz#473e0da7f831d535b2002be05e6f4ca3729a1bc9" + integrity sha512-rqLJjdVqCcn7glPer7Fxh87PRqlnRScVAoxcIP3PmOUNApMWJ6yRdOFfo2KvPAdO7Le3yEI1o0YW+Yvr7XCYvw== + dependencies: + "@ethersproject/logger" "^5.3.0" + "@ethersproject/constants@5.1.0", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.0", "@ethersproject/constants@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.1.0.tgz#4e7da6367ea0e9be87585d8b09f3fccf384b1452" @@ -784,6 +811,13 @@ dependencies: "@ethersproject/bignumber" "^5.1.0" +"@ethersproject/constants@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.3.0.tgz#a5d6d86c0eec2c64c3024479609493b9afb3fc77" + integrity sha512-4y1feNOwEpgjAfiCFWOHznvv6qUF/H6uI0UKp8xdhftb+H+FbKflXg1pOgH5qs4Sr7EYBL+zPyPb+YD5g1aEyw== + dependencies: + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/contracts@5.1.0", "@ethersproject/contracts@^5.0.0", "@ethersproject/contracts@^5.0.2", "@ethersproject/contracts@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.1.0.tgz#f7c3451f1af77e029005733ccab3419d07d23f6b" @@ -871,11 +905,24 @@ "@ethersproject/bytes" "^5.1.0" js-sha3 "0.5.7" +"@ethersproject/keccak256@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.3.0.tgz#fb5cd36bdfd6fa02e2ea84964078a9fc6bd731be" + integrity sha512-Gv2YqgIUmRbYVNIibafT0qGaeGYLIA/EdWHJ7JcVxVSs2vyxafGxOJ5VpSBHWeOIsE6OOaCelYowhuuTicgdFQ== + dependencies: + "@ethersproject/bytes" "^5.3.0" + js-sha3 "0.5.7" + "@ethersproject/logger@5.1.0", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.0", "@ethersproject/logger@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.1.0.tgz#4cdeeefac029373349d5818f39c31b82cc6d9bbf" integrity sha512-wtUaD1lBX10HBXjjKV9VHCBnTdUaKQnQ2XSET1ezglqLdPdllNOIlLfhyCRqXm5xwcjExVI5ETokOYfjPtaAlw== +"@ethersproject/logger@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.3.0.tgz#7a69fa1d4ca0d4b7138da1627eb152f763d84dd0" + integrity sha512-8bwJ2gxJGkZZnpQSq5uSiZSJjyVTWmlGft4oH8vxHdvO1Asy4TwVepAhPgxIQIMxXZFUNMych1YjIV4oQ4I7dA== + "@ethersproject/networks@5.1.0", "@ethersproject/networks@^5.0.0", "@ethersproject/networks@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.1.0.tgz#f537290cb05aa6dc5e81e910926c04cfd5814bca" @@ -898,6 +945,13 @@ dependencies: "@ethersproject/logger" "^5.1.0" +"@ethersproject/properties@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.3.0.tgz#feef4c4babeb7c10a6b3449575016f4ad2c092b2" + integrity sha512-PaHxJyM5/bfusk6vr3yP//JMnm4UEojpzuWGTmtL5X4uNhNnFNvlYilZLyDr4I9cTkIbipCMsAuIcXWsmdRnEw== + dependencies: + "@ethersproject/logger" "^5.3.0" + "@ethersproject/providers@5.1.0", "@ethersproject/providers@^5.0.0", "@ethersproject/providers@^5.0.14", "@ethersproject/providers@^5.0.21", "@ethersproject/providers@^5.0.24", "@ethersproject/providers@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.1.0.tgz#27695a02cfafa370428cde1c7a4abab13afb6a35" @@ -939,6 +993,14 @@ "@ethersproject/bytes" "^5.1.0" "@ethersproject/logger" "^5.1.0" +"@ethersproject/rlp@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.3.0.tgz#7cb93a7b5dfa69163894153c9d4b0d936f333188" + integrity sha512-oI0joYpsRanl9guDubaW+1NbcpK0vJ3F/6Wpcanzcnqq+oaW9O5E98liwkEDPcb16BUTLIJ+ZF8GPIHYxJ/5Pw== + dependencies: + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/sha2@5.1.0", "@ethersproject/sha2@^5.0.0", "@ethersproject/sha2@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.1.0.tgz#6ca42d1a26884b3e32ffa943fe6494af7211506c" @@ -959,6 +1021,18 @@ bn.js "^4.4.0" elliptic "6.5.4" +"@ethersproject/signing-key@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.3.0.tgz#a96c88f8173e1abedfa35de32d3e5db7c48e5259" + integrity sha512-+DX/GwHAd0ok1bgedV1cKO0zfK7P/9aEyNoaYiRsGHpCecN7mhLqcdoUiUzE7Uz86LBsxm5ssK0qA1kBB47fbQ== + dependencies: + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + bn.js "^4.11.9" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/solidity@5.1.0", "@ethersproject/solidity@^5.0.0", "@ethersproject/solidity@^5.0.2": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.1.0.tgz#095a9c75244edccb26c452c155736d363399b954" @@ -1009,6 +1083,21 @@ "@ethersproject/rlp" "^5.1.0" "@ethersproject/signing-key" "^5.1.0" +"@ethersproject/transactions@^5.0.31": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.3.0.tgz#49b86f2bafa4d0bdf8e596578fc795ee47c50458" + integrity sha512-cdfK8VVyW2oEBCXhURG0WQ6AICL/r6Gmjh0e4Bvbv6MCn/GBd8FeBH3rtl7ho+AW50csMKeGv3m3K1HSHB2jMQ== + dependencies: + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/rlp" "^5.3.0" + "@ethersproject/signing-key" "^5.3.0" + "@ethersproject/units@5.1.0", "@ethersproject/units@^5.0.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.1.0.tgz#b6ab3430ebc22adc3cb4839516496f167bee3ad5" @@ -8069,7 +8158,7 @@ hash.js@1.1.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -8213,6 +8302,11 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" + integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"