diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index b629106972e..447018eb62e 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - product: [vrf, automation, llo-feeds, functions, shared] + product: [vrf, automation, llo-feeds, l2ep, functions, shared] needs: [changes] name: Foundry Tests ${{ matrix.product }} # See https://github.com/foundry-rs/foundry/issues/3827 diff --git a/contracts/GNUmakefile b/contracts/GNUmakefile index 8ec536d520f..f666014c48f 100644 --- a/contracts/GNUmakefile +++ b/contracts/GNUmakefile @@ -1,6 +1,6 @@ # ALL_FOUNDRY_PRODUCTS contains a list of all products that have a foundry # profile defined and use the Foundry snapshots. -ALL_FOUNDRY_PRODUCTS = llo-feeds functions shared +ALL_FOUNDRY_PRODUCTS = l2ep llo-feeds functions shared # To make a snapshot for a specific product, either set the `FOUNDRY_PROFILE` env var # or call the target with `FOUNDRY_PROFILE=product` diff --git a/contracts/gas-snapshots/l2ep.gas-snapshot b/contracts/gas-snapshots/l2ep.gas-snapshot new file mode 100644 index 00000000000..569af2fe661 --- /dev/null +++ b/contracts/gas-snapshots/l2ep.gas-snapshot @@ -0,0 +1,146 @@ +ArbitrumCrossDomainForwarder_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 37312) +ArbitrumCrossDomainForwarder_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 12963) +ArbitrumCrossDomainForwarder_Constructor:test_InitialState() (gas: 18431) +ArbitrumCrossDomainForwarder_Forward:test_Forward() (gas: 47601) +ArbitrumCrossDomainForwarder_Forward:test_ForwardRevert() (gas: 22151) +ArbitrumCrossDomainForwarder_Forward:test_NotCallableByUnknownAddress() (gas: 16048) +ArbitrumCrossDomainForwarder_TransferL1Ownership:test_CallableByL1Owner() (gas: 41408) +ArbitrumCrossDomainForwarder_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 19312) +ArbitrumCrossDomainForwarder_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 18323) +ArbitrumCrossDomainForwarder_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 13200) +ArbitrumCrossDomainGovernor_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 37312) +ArbitrumCrossDomainGovernor_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 12963) +ArbitrumCrossDomainGovernor_Constructor:test_InitialState() (gas: 18454) +ArbitrumCrossDomainGovernor_Forward:test_CallableByL2Owner() (gas: 49720) +ArbitrumCrossDomainGovernor_Forward:test_Forward() (gas: 47658) +ArbitrumCrossDomainGovernor_Forward:test_ForwardRevert() (gas: 24348) +ArbitrumCrossDomainGovernor_Forward:test_NotCallableByUnknownAddress() (gas: 18247) +ArbitrumCrossDomainGovernor_ForwardDelegate:test_BubbleUpRevert() (gas: 19386) +ArbitrumCrossDomainGovernor_ForwardDelegate:test_CallableByCrossDomainMessengerAddressOrL1Owner() (gas: 60617) +ArbitrumCrossDomainGovernor_ForwardDelegate:test_CallableByL2Owner() (gas: 62723) +ArbitrumCrossDomainGovernor_ForwardDelegate:test_NotCallableByUnknownAddress() (gas: 18237) +ArbitrumCrossDomainGovernor_ForwardDelegate:test_RevertsBatchWhenOneCallFails() (gas: 64110) +ArbitrumCrossDomainGovernor_TransferL1Ownership:test_CallableByL1Owner() (gas: 41408) +ArbitrumCrossDomainGovernor_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 19312) +ArbitrumCrossDomainGovernor_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 18323) +ArbitrumCrossDomainGovernor_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 13200) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetAnswer() (gas: 92118) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetRoundData() (gas: 92673) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetTimestamp() (gas: 92039) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestAnswer() (gas: 89813) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRound() (gas: 89705) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRoundData() (gas: 90246) +ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestTimestamp() (gas: 89690) +ArbitrumSequencerUptimeFeed_AggregatorV3Interface:test_AggregatorV3Interface() (gas: 98825) +ArbitrumSequencerUptimeFeed_AggregatorV3Interface:test_Return0WhenRoundDoesNotExistYet() (gas: 18309) +ArbitrumSequencerUptimeFeed_Constants:test_InitialState() (gas: 5684) +ArbitrumSequencerUptimeFeed_GasCosts:test_GasCosts() (gas: 97495) +ArbitrumSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() (gas: 602711) +ArbitrumSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() (gas: 573802) +ArbitrumSequencerUptimeFeed_UpdateStatus:test_IgnoreOutOfOrderUpdates() (gas: 98976) +ArbitrumSequencerUptimeFeed_UpdateStatus:test_RevertIfNotL2CrossDomainMessengerAddr() (gas: 15776) +ArbitrumSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndNoTimeChange() (gas: 113269) +ArbitrumSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndTimeChange() (gas: 113329) +ArbitrumValidator_Validate:test_PostSequencerOffline() (gas: 69068) +OptimismCrossDomainForwarder_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 46888) +OptimismCrossDomainForwarder_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 22155) +OptimismCrossDomainForwarder_Constructor:test_InitialState() (gas: 18266) +OptimismCrossDomainForwarder_Forward:test_Forward() (gas: 58025) +OptimismCrossDomainForwarder_Forward:test_ForwardRevert() (gas: 32546) +OptimismCrossDomainForwarder_Forward:test_NotCallableByUnknownAddress() (gas: 13859) +OptimismCrossDomainForwarder_TransferL1Ownership:test_CallableByL1Owner() (gas: 48886) +OptimismCrossDomainForwarder_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 28767) +OptimismCrossDomainForwarder_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 16134) +OptimismCrossDomainForwarder_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 11011) +OptimismCrossDomainGovernor_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 46888) +OptimismCrossDomainGovernor_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 22155) +OptimismCrossDomainGovernor_Constructor:test_InitialState() (gas: 18289) +OptimismCrossDomainGovernor_Forward:test_CallableByL2Owner() (gas: 47557) +OptimismCrossDomainGovernor_Forward:test_Forward() (gas: 58096) +OptimismCrossDomainGovernor_Forward:test_ForwardRevert() (gas: 32627) +OptimismCrossDomainGovernor_Forward:test_NotCallableByUnknownAddress() (gas: 16061) +OptimismCrossDomainGovernor_ForwardDelegate:test_BubbleUpRevert() (gas: 29181) +OptimismCrossDomainGovernor_ForwardDelegate:test_CallableByCrossDomainMessengerAddressOrL1Owner() (gas: 72695) +OptimismCrossDomainGovernor_ForwardDelegate:test_CallableByL2Owner() (gas: 72685) +OptimismCrossDomainGovernor_ForwardDelegate:test_NotCallableByUnknownAddress() (gas: 16051) +OptimismCrossDomainGovernor_ForwardDelegate:test_RevertsBatchWhenOneCallFails() (gas: 75908) +OptimismCrossDomainGovernor_TransferL1Ownership:test_CallableByL1Owner() (gas: 48886) +OptimismCrossDomainGovernor_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 28767) +OptimismCrossDomainGovernor_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 16134) +OptimismCrossDomainGovernor_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 11011) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetAnswer() (gas: 59095) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetRoundData() (gas: 59635) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetTimestamp() (gas: 58950) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestAnswer() (gas: 56887) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRound() (gas: 56773) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRoundData() (gas: 57309) +OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestTimestamp() (gas: 56740) +OptimismSequencerUptimeFeed_AggregatorV3Interface:test_AggregatorV3Interface() (gas: 65617) +OptimismSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetAnswerWhenRoundDoesNotExistYet() (gas: 18039) +OptimismSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetRoundDataWhenRoundDoesNotExistYet() (gas: 18257) +OptimismSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetTimestampWhenRoundDoesNotExistYet() (gas: 17963) +OptimismSequencerUptimeFeed_Constructor:test_InitialState() (gas: 21078) +OptimismSequencerUptimeFeed_GasCosts:test_GasCosts() (gas: 67197) +OptimismSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() (gas: 597640) +OptimismSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() (gas: 573807) +OptimismSequencerUptimeFeed_UpdateStatus:test_IgnoreOutOfOrderUpdates() (gas: 66532) +OptimismSequencerUptimeFeed_UpdateStatus:test_RevertIfNotL2CrossDomainMessengerAddr() (gas: 13560) +OptimismSequencerUptimeFeed_UpdateStatus:test_RevertIfNotL2CrossDomainMessengerAddrAndNotL1SenderAddr() (gas: 23967) +OptimismSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenNoChange() (gas: 74035) +OptimismSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndNoTimeChange() (gas: 96155) +OptimismSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndTimeChange() (gas: 96215) +OptimismValidator_SetGasLimit:test_CorrectlyUpdatesTheGasLimit() (gas: 15503) +OptimismValidator_Validate:test_PostSequencerOffline() (gas: 74813) +OptimismValidator_Validate:test_PostSequencerStatusWhenThereIsNotStatusChange() (gas: 74869) +OptimismValidator_Validate:test_RevertsIfCalledByAnAccountWithNoAccess() (gas: 15563) +ScrollCrossDomainForwarder_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 46988) +ScrollCrossDomainForwarder_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 22207) +ScrollCrossDomainForwarder_Constructor:test_InitialState() (gas: 17930) +ScrollCrossDomainForwarder_Forward:test_Forward() (gas: 58092) +ScrollCrossDomainForwarder_Forward:test_ForwardRevert() (gas: 32619) +ScrollCrossDomainForwarder_Forward:test_NotCallableByUnknownAddress() (gas: 13859) +ScrollCrossDomainForwarder_TransferL1Ownership:test_CallableByL1Owner() (gas: 48952) +ScrollCrossDomainForwarder_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 28833) +ScrollCrossDomainForwarder_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 16134) +ScrollCrossDomainForwarder_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 11011) +ScrollCrossDomainGovernor_AcceptL1Ownership:test_CallableByPendingL1Owner() (gas: 46988) +ScrollCrossDomainGovernor_AcceptL1Ownership:test_NotCallableByNonPendingOwners() (gas: 22207) +ScrollCrossDomainGovernor_Constructor:test_InitialState() (gas: 17953) +ScrollCrossDomainGovernor_Forward:test_CallableByL2Owner() (gas: 47552) +ScrollCrossDomainGovernor_Forward:test_Forward() (gas: 58158) +ScrollCrossDomainGovernor_Forward:test_ForwardRevert() (gas: 32697) +ScrollCrossDomainGovernor_Forward:test_NotCallableByUnknownAddress() (gas: 16058) +ScrollCrossDomainGovernor_ForwardDelegate:test_BubbleUpRevert() (gas: 29248) +ScrollCrossDomainGovernor_ForwardDelegate:test_CallableByCrossDomainMessengerAddressOrL1Owner() (gas: 72756) +ScrollCrossDomainGovernor_ForwardDelegate:test_CallableByL2Owner() (gas: 72746) +ScrollCrossDomainGovernor_ForwardDelegate:test_NotCallableByUnknownAddress() (gas: 16048) +ScrollCrossDomainGovernor_ForwardDelegate:test_RevertsBatchWhenOneCallFails() (gas: 75970) +ScrollCrossDomainGovernor_TransferL1Ownership:test_CallableByL1Owner() (gas: 48952) +ScrollCrossDomainGovernor_TransferL1Ownership:test_CallableByL1OwnerOrZeroAddress() (gas: 28833) +ScrollCrossDomainGovernor_TransferL1Ownership:test_NotCallableByL2Owner() (gas: 16134) +ScrollCrossDomainGovernor_TransferL1Ownership:test_NotCallableByNonOwners() (gas: 11011) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetAnswer() (gas: 57250) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetRoundData() (gas: 57780) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForGetTimestamp() (gas: 57105) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestAnswer() (gas: 54888) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRound() (gas: 54768) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestRoundData() (gas: 55473) +ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts:test_GasUsageForLatestTimestamp() (gas: 54758) +ScrollSequencerUptimeFeed_AggregatorV3Interface:test_AggregatorV3Interface() (gas: 63903) +ScrollSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetAnswerWhenRoundDoesNotExistYet() (gas: 18035) +ScrollSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetRoundDataWhenRoundDoesNotExistYet() (gas: 18253) +ScrollSequencerUptimeFeed_AggregatorV3Interface:test_RevertGetTimestampWhenRoundDoesNotExistYet() (gas: 17959) +ScrollSequencerUptimeFeed_Constructor:test_InitialState() (gas: 21085) +ScrollSequencerUptimeFeed_GasCosts:test_GasCosts() (gas: 64888) +ScrollSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() (gas: 597491) +ScrollSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions:test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() (gas: 573807) +ScrollSequencerUptimeFeed_UpdateStatus:test_IgnoreOutOfOrderUpdates() (gas: 64417) +ScrollSequencerUptimeFeed_UpdateStatus:test_RevertIfNotL2CrossDomainMessengerAddr() (gas: 13560) +ScrollSequencerUptimeFeed_UpdateStatus:test_RevertIfNotL2CrossDomainMessengerAddrAndNotL1SenderAddr() (gas: 23967) +ScrollSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenNoChange() (gas: 71618) +ScrollSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndNoTimeChange() (gas: 92018) +ScrollSequencerUptimeFeed_UpdateStatus:test_UpdateStatusWhenStatusChangeAndTimeChange() (gas: 92078) +ScrollValidator_SetGasLimit:test_CorrectlyUpdatesTheGasLimit() (gas: 15503) +ScrollValidator_Validate:test_PostSequencerOffline() (gas: 75094) +ScrollValidator_Validate:test_PostSequencerStatusWhenThereIsNotStatusChange() (gas: 75156) +ScrollValidator_Validate:test_RevertsIfCalledByAnAccountWithNoAccess() (gas: 15563) \ No newline at end of file diff --git a/contracts/remappings.txt b/contracts/remappings.txt index f0ac4993c2c..a9d24257659 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,6 +1,7 @@ -ds-test/=foundry-lib/forge-std/lib/ds-test/src -forge-std/=foundry-lib/forge-std/src +ds-test/=foundry-lib/forge-std/lib/ds-test/src/ +forge-std/=foundry-lib/forge-std/src/ @openzeppelin/=node_modules/@openzeppelin/ hardhat/=node_modules/hardhat/ -@eth-optimism/=node_modules/@eth-optimism +@eth-optimism/=node_modules/@eth-optimism/ +@scroll-tech/=node_modules/@scroll-tech/ diff --git a/contracts/scripts/native_solc_compile_all_l2ep b/contracts/scripts/native_solc_compile_all_l2ep new file mode 100755 index 00000000000..1b9f5fb611d --- /dev/null +++ b/contracts/scripts/native_solc_compile_all_l2ep @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +########### +# Logging # +########### + +set -e + +echo " ┌──────────────────────────────────────────────┐" +echo " │ Compiling L2EP contracts... │" +echo " └──────────────────────────────────────────────┘" + +###################### +# Helper Variable(s) # +###################### + +export SOLC_VERSION="0.8.19" + +SCRIPTPATH="$( + cd "$(dirname "$0")" >/dev/null 2>&1 + pwd -P +)" + +ROOT="$( + cd "$(dirname "$0")" >/dev/null 2>&1 + cd ../ && pwd -P +)" + +###################### +# Helper Function(s) # +###################### + +compileContract() { + local optimize_runs=1000000 + local version="$1" + local srcpath="$2" + solc \ + @openzeppelin/=$ROOT/node_modules/@openzeppelin/ \ + @eth-optimism/=$ROOT/node_modules/@eth-optimism/ \ + @scroll-tech/=$ROOT/node_modules/@scroll-tech/ \ + --overwrite --optimize --optimize-runs $optimize_runs --metadata-hash none \ + -o $ROOT/solc/v$SOLC_VERSION/l2ep/"$version" \ + --abi --bin \ + --allow-paths $ROOT/src/v0.8,$ROOT/node_modules \ + $ROOT/src/v0.8/l2ep/"$srcpath" +} + +################# +# Version 1.0.0 # +################# + +python3 -m pip install --require-hashes -r $SCRIPTPATH/requirements.txt + +solc-select install $SOLC_VERSION +solc-select use $SOLC_VERSION + +compileContract v1_0_0 dev/arbitrum/ArbitrumValidator.sol +compileContract v1_0_0 dev/arbitrum/ArbitrumSequencerUptimeFeed.sol +compileContract v1_0_0 dev/arbitrum/ArbitrumCrossDomainForwarder.sol +compileContract v1_0_0 dev/arbitrum/ArbitrumCrossDomainGovernor.sol + +compileContract v1_0_0 dev/optimism/OptimismValidator.sol +compileContract v1_0_0 dev/optimism/OptimismSequencerUptimeFeed.sol +compileContract v1_0_0 dev/optimism/OptimismCrossDomainForwarder.sol +compileContract v1_0_0 dev/optimism/OptimismCrossDomainGovernor.sol + +compileContract v1_0_0 dev/scroll/ScrollValidator.sol +compileContract v1_0_0 dev/scroll/ScrollSequencerUptimeFeed.sol +compileContract v1_0_0 dev/scroll/ScrollCrossDomainForwarder.sol +compileContract v1_0_0 dev/scroll/ScrollCrossDomainGovernor.sol diff --git a/contracts/src/v0.8/l2ep/README.md b/contracts/src/v0.8/l2ep/README.md new file mode 100644 index 00000000000..21a537c4fe7 --- /dev/null +++ b/contracts/src/v0.8/l2ep/README.md @@ -0,0 +1,148 @@ +# Overview + +This folder contains the source code and tests for the Layer 2 +Emergency Protocol (L2EP) contracts. It is organized as follows: + +```text +. +├─/dev (stores the latest source code for L2EP) +├─/test (stores the Foundry tests for L2EP) +``` + +## The `/dev` Folder + +The `/dev` folder contains subfolders for each chain that +has an L2EP solution implemented for it (e.g. `/scroll`, `/arbitrum`, +`/optimism`). It also contains a subfolder named `/interfaces`, +which stores shared interface types between all the supported +contracts. The top-level contracts (e.g. `CrossDomainOwnable.sol`) +serve as either abstract or parent contracts that are meant +to be reused for each indiviudal chain. + +## The `/test` Folder + +This folder is arranged as follows: + +- `/mocks`: used for both Foundry test cases and Hardhat test cases (NOTE: +Hardhat test cases should be considered deprecated at this point) + +- `/[version]`: test cases for a specific version of the L2EP contracts + +### Testing Conventions and Methodology + +By convention, each testing file should end in `.t.sol` (this is a standard +that other projects have also adopted). Each testing file in this folder +follows a similar structure. + +```text +TestFile.t.sol + | + |--- Base Contract (inherits L2EPTest contract) + | + |--- Child Contract 1 (inherits base contract) + | | + | |--- Test Function + | | + | |--- ... + | + | + |--- Child Contract 2 (inherits base contract) + | | + | |--- Test Function + | | + | |--- ... + | + | + ... +``` + +All test files contain a base contract defined at the top of the file. This +base contract inherits from a contract called `L2EPTest`. The `L2EPTest` +contract and base contracts have no test cases. Instead, the `L2EPTest` +contract is meant to store data/functions that will be reused among all +the base contracts. Similarly, the base contract is meant to store data +and/or functions that will be reused by any contracts that inherit it. +As such, each test file will define separate child contracts, and each +will inherit from the base contract + define its own set of tests. + +The base contract defines a `setUp` function which is automatically called +exactly once before ***each*** of the tests are run in an inheriting contract. +The `setUp` function typically deploys a fresh set of test contracts so that +tests can run independently of each other. Alongside the `setUp` function, +the base contract can also define variables, constants, events, etc. that +are meant to be reused per test. + +The name of the base contract follows the following convention: + +```text +Test +``` + +The child contract names follow a similar convention: + +```text +_ +``` + +Each test function within the child contract complies +with the following naming pattern: + +```text +test_ +``` + +### Running Foundry Tests + +#### Usage + +First make sure you are in the contracts directory: + +```sh +# Assuming you are currently in the /chainlink directory +cd ./contracts +``` + +If you already have foundry installed, you can use the following command +to run all L2EP tests: + +```sh +FOUNDRY_PROFILE=l2ep forge test -vvv +``` + +To run a specific L2EP test, you can use a variation of the following command: + +```sh +FOUNDRY_PROFILE=l2ep forge test -vvv --match-path ./src/v0.8/l2ep/test/v1_0_0/scroll/ScrollSequencerUptimeFeed.t.sol +``` + +Or alternatively: + +```sh +FOUNDRY_PROFILE=l2ep forge test -vvv --match-contract ScrollSequencerUptimeFeed +``` + +If you prefer, you can also export `FOUNDRY_PROFILE` so that it doesn't need +to be provided before every command: + +```sh +# Export foundry profile +export FOUNDRY_PROFILE=l2ep + +# Run all tests +forge test -vvv + +# Run all tests and generate a gas snapshot +make snapshot +``` + +A full list of flags for `forge test` can be found [here](https://book.getfoundry.sh/reference/forge/forge-test). + +#### Coverage + +First ensure that the correct files are being evaluated. For example, if only +v1 contracts are, being evaluated then temporarily change the L2EP profile in +`./foundry.toml`. + +```sh +forge coverage +``` diff --git a/contracts/src/v0.8/l2ep/test/mocks/MockAggregatorV2V3.sol b/contracts/src/v0.8/l2ep/test/mocks/MockAggregatorV2V3.sol new file mode 100644 index 00000000000..c4e2f710300 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/mocks/MockAggregatorV2V3.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {AggregatorV2V3Interface} from "../../../shared/interfaces/AggregatorV2V3Interface.sol"; + +contract MockAggregatorV2V3 is AggregatorV2V3Interface { + function latestAnswer() external pure returns (int256) { + return 0; + } + + function latestTimestamp() external pure returns (uint256) { + return 0; + } + + function latestRound() external pure returns (uint256) { + return 0; + } + + function getAnswer(uint256) external pure returns (int256) { + return 0; + } + + function getTimestamp(uint256 roundId) external pure returns (uint256) { + return roundId; + } + + function decimals() external pure returns (uint8) { + return 0; + } + + function description() external pure returns (string memory) { + return ""; + } + + function version() external pure returns (uint256) { + return 0; + } + + function getRoundData( + uint80 + ) + external + pure + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return (0, 0, 0, 0, 0); + } + + function latestRoundData() + external + pure + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return (73786976294838220258, 96800000000, 163826896, 1638268960, 73786976294838220258); + } +} diff --git a/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol b/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol new file mode 100644 index 00000000000..3a45cba347a --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/mocks/optimism/MockOVMCrossDomainMessenger.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.6 <0.9.0; + +import {iOVM_CrossDomainMessenger} from "../../../../vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol"; + +import {Address} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/Address.sol"; + +contract MockOVMCrossDomainMessenger is iOVM_CrossDomainMessenger { + address internal s_mockMessageSender; + + constructor(address sender) { + s_mockMessageSender = sender; + } + + function xDomainMessageSender() external view override returns (address) { + return s_mockMessageSender; + } + + function _setMockMessageSender(address sender) external { + s_mockMessageSender = sender; + } + + /** + * Sends a cross domain message to the target messenger. + * @param _target Target contract address. + * @param _message Message to send to the target. + */ + function sendMessage(address _target, bytes calldata _message, uint32) external override { + Address.functionCall(_target, _message, "sendMessage reverted"); + } +} diff --git a/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollCrossDomainMessenger.sol b/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollCrossDomainMessenger.sol new file mode 100644 index 00000000000..37244910b81 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollCrossDomainMessenger.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {IScrollMessenger} from "@scroll-tech/contracts/libraries/IScrollMessenger.sol"; + +import {Address} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/Address.sol"; + +contract MockScrollCrossDomainMessenger is IScrollMessenger { + address internal s_mockMessageSender; + + constructor(address sender) { + s_mockMessageSender = sender; + } + + function xDomainMessageSender() external view override returns (address) { + return s_mockMessageSender; + } + + function _setMockMessageSender(address sender) external { + s_mockMessageSender = sender; + } + + /// @notice Send cross chain message from L1 to L2 or L2 to L1. + /// @param _target The address of account who receive the message. + /// @param _message The content of the message. + function sendMessage(address _target, uint256, bytes calldata _message, uint256) external payable override { + Address.functionCall(_target, _message, "sendMessage reverted"); + } + + /// @notice Send cross chain message from L1 to L2 or L2 to L1. + /// @param _target The address of account who receive the message. + /// @param _message The content of the message. + function sendMessage(address _target, uint256, bytes calldata _message, uint256, address) external payable override { + Address.functionCall(_target, _message, "sendMessage reverted"); + } +} diff --git a/contracts/src/v0.8/l2ep/test/mocks/MockScrollL1CrossDomainMessenger.sol b/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollL1CrossDomainMessenger.sol similarity index 100% rename from contracts/src/v0.8/l2ep/test/mocks/MockScrollL1CrossDomainMessenger.sol rename to contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollL1CrossDomainMessenger.sol diff --git a/contracts/src/v0.8/l2ep/test/mocks/MockScrollL2CrossDomainMessenger.sol b/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollL2CrossDomainMessenger.sol similarity index 90% rename from contracts/src/v0.8/l2ep/test/mocks/MockScrollL2CrossDomainMessenger.sol rename to contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollL2CrossDomainMessenger.sol index f63faa35179..66400b7d305 100644 --- a/contracts/src/v0.8/l2ep/test/mocks/MockScrollL2CrossDomainMessenger.sol +++ b/contracts/src/v0.8/l2ep/test/mocks/scroll/MockScrollL2CrossDomainMessenger.sol @@ -40,11 +40,11 @@ contract MockScrollL2CrossDomainMessenger is IL2ScrollMessenger { bytes calldata message ) external override {} - /// Needed for testing + /// Needed for backwards compatibility in Hardhat tests function setSender(address newSender) external { s_sender = newSender; } - /// Needed for testing + /// Needed for backwards compatibility in Hardhat tests receive() external payable {} } diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/L2EPTest.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/L2EPTest.t.sol new file mode 100644 index 00000000000..561e32be1a2 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/L2EPTest.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Greeter} from "../../../tests/Greeter.sol"; + +import {MultiSend} from "../../../vendor/MultiSend.sol"; +import {Test} from "forge-std/Test.sol"; + +contract L2EPTest is Test { + /// Helper variable(s) + address internal s_strangerAddr = vm.addr(0x1); + address internal s_l1OwnerAddr = vm.addr(0x2); + address internal s_eoaValidator = vm.addr(0x3); + address internal s_deployerAddr = vm.addr(0x4); + + /// @param expectedGasUsage - the expected gas usage + /// @param startGasUsage - the gas usage before the code of interest is run + /// @param finalGasUsage - the gas usage after the code of interest is run + /// @param deviation - the amount of gas that the actual usage is allowed to deviate by (e.g. (expectedGas - deviation) <= actualGasUsage <= (expectedGas + deviation)) + function assertGasUsageIsCloseTo( + uint256 expectedGasUsage, + uint256 startGasUsage, + uint256 finalGasUsage, + uint256 deviation + ) public { + uint256 gasUsed = (startGasUsage - finalGasUsage) * tx.gasprice; + assertLe(gasUsed, expectedGasUsage + deviation); + assertGe(gasUsed, expectedGasUsage - deviation); + } + + /// @param selector - the function selector + /// @param greeterAddr - the address of the Greeter contract + /// @param message - the new greeting message, which will be passed as an argument to Greeter#setGreeting + /// @return a 2-layer encoding such that decoding the first layer provides the CrossDomainForwarder#forward + /// function selector and the corresponding arguments to the forward function, and decoding the + /// second layer provides the Greeter#setGreeting function selector and the corresponding + /// arguments to the set greeting function (which in this case is the input message) + function encodeCrossDomainSetGreetingMsg( + bytes4 selector, + address greeterAddr, + string memory message + ) public pure returns (bytes memory) { + return abi.encodeWithSelector(selector, greeterAddr, abi.encodeWithSelector(Greeter.setGreeting.selector, message)); + } + + /// @param selector - the function selector + /// @param multiSendAddr - the address of the MultiSend contract + /// @param encodedTxs - an encoded list of transactions (e.g. abi.encodePacked(encodeMultiSendTx("some data"), ...)) + /// @return a 2-layer encoding such that decoding the first layer provides the CrossDomainGoverner#forwardDelegate + /// function selector and the corresponding arguments to the forwardDelegate function, and decoding the + /// second layer provides the MultiSend#multiSend function selector and the corresponding + /// arguments to the multiSend function (which in this case is the input encodedTxs) + function encodeCrossDomainMultiSendMsg( + bytes4 selector, + address multiSendAddr, + bytes memory encodedTxs + ) public pure returns (bytes memory) { + return + abi.encodeWithSelector(selector, multiSendAddr, abi.encodeWithSelector(MultiSend.multiSend.selector, encodedTxs)); + } + + /// @param greeterAddr - the address of the greeter contract + /// @param data - the transaction data string + /// @return an encoded transaction structured as specified in the MultiSend#multiSend comments + function encodeMultiSendTx(address greeterAddr, bytes memory data) public pure returns (bytes memory) { + bytes memory txData = abi.encodeWithSelector(Greeter.setGreeting.selector, data); + return + abi.encodePacked( + uint8(0), // operation + greeterAddr, // to + uint256(0), // value + uint256(txData.length), // data length + txData // data as bytes + ); + } + + /// @param l1Address - Address on L1 + /// @return an Arbitrum L2 address + function toArbitrumL2AliasAddress(address l1Address) public pure returns (address) { + return address(uint160(l1Address) + uint160(0x1111000000000000000000000000000000001111)); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainForwarder.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainForwarder.t.sol new file mode 100644 index 00000000000..be3851c5b5d --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainForwarder.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ArbitrumCrossDomainForwarder} from "../../../dev/arbitrum/ArbitrumCrossDomainForwarder.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ArbitrumCrossDomainForwarderTest is L2EPTest { + /// Helper variable(s) + address internal s_crossDomainMessengerAddr = toArbitrumL2AliasAddress(s_l1OwnerAddr); + address internal s_newOwnerCrossDomainMessengerAddr = toArbitrumL2AliasAddress(s_strangerAddr); + + /// Contracts + ArbitrumCrossDomainForwarder internal s_arbitrumCrossDomainForwarder; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_arbitrumCrossDomainForwarder = new ArbitrumCrossDomainForwarder(s_l1OwnerAddr); + s_greeter = new Greeter(address(s_arbitrumCrossDomainForwarder)); + vm.stopPrank(); + } +} + +contract ArbitrumCrossDomainForwarder_Constructor is ArbitrumCrossDomainForwarderTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_arbitrumCrossDomainForwarder.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_arbitrumCrossDomainForwarder.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_arbitrumCrossDomainForwarder.crossDomainMessenger(), s_crossDomainMessengerAddr); + + // it should set the typeAndVersion correctly + assertEq(s_arbitrumCrossDomainForwarder.typeAndVersion(), "ArbitrumCrossDomainForwarder 1.0.0"); + } +} + +contract ArbitrumCrossDomainForwarder_Forward is ArbitrumCrossDomainForwarderTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_arbitrumCrossDomainForwarder.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_arbitrumCrossDomainForwarder.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, greeting) + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_arbitrumCrossDomainForwarder.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, "") + ); + } +} + +contract ArbitrumCrossDomainForwarder_TransferL1Ownership is ArbitrumCrossDomainForwarderTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_arbitrumCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_arbitrumCrossDomainForwarder.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_arbitrumCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_arbitrumCrossDomainForwarder.l1Owner(), s_strangerAddr); + + // Sends the message + s_arbitrumCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_arbitrumCrossDomainForwarder.l1Owner(), address(0)); + + // Sends the message + s_arbitrumCrossDomainForwarder.transferL1Ownership(address(0)); + } +} + +contract ArbitrumCrossDomainForwarder_AcceptL1Ownership is ArbitrumCrossDomainForwarderTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_arbitrumCrossDomainForwarder.acceptL1Ownership(); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Request ownership transfer + vm.startPrank(s_crossDomainMessengerAddr); + s_arbitrumCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + vm.startPrank(s_newOwnerCrossDomainMessengerAddr); + s_arbitrumCrossDomainForwarder.acceptL1Ownership(); + + // Asserts that the ownership was actually transferred + assertEq(s_arbitrumCrossDomainForwarder.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainGovernor.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainGovernor.t.sol new file mode 100644 index 00000000000..c5b8adaf7d2 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumCrossDomainGovernor.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ArbitrumCrossDomainGovernor} from "../../../dev/arbitrum/ArbitrumCrossDomainGovernor.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +import {MultiSend} from "../../../../vendor/MultiSend.sol"; + +contract ArbitrumCrossDomainGovernorTest is L2EPTest { + /// Helper variable(s) + address internal s_crossDomainMessengerAddr = toArbitrumL2AliasAddress(s_l1OwnerAddr); + address internal s_newOwnerCrossDomainMessengerAddr = toArbitrumL2AliasAddress(s_strangerAddr); + + /// Contracts + ArbitrumCrossDomainGovernor internal s_arbitrumCrossDomainGovernor; + MultiSend internal s_multiSend; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_arbitrumCrossDomainGovernor = new ArbitrumCrossDomainGovernor(s_l1OwnerAddr); + s_greeter = new Greeter(address(s_arbitrumCrossDomainGovernor)); + s_multiSend = new MultiSend(); + vm.stopPrank(); + } +} + +contract ArbitrumCrossDomainGovernor_Constructor is ArbitrumCrossDomainGovernorTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_arbitrumCrossDomainGovernor.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_arbitrumCrossDomainGovernor.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_arbitrumCrossDomainGovernor.crossDomainMessenger(), s_crossDomainMessengerAddr); + + // it should set the typeAndVersion correctly + assertEq(s_arbitrumCrossDomainGovernor.typeAndVersion(), "ArbitrumCrossDomainGovernor 1.0.0"); + } +} + +contract ArbitrumCrossDomainGovernor_Forward is ArbitrumCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_arbitrumCrossDomainGovernor.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_arbitrumCrossDomainGovernor.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, greeting) + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_arbitrumCrossDomainGovernor.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, greeting) + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_arbitrumCrossDomainGovernor.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, "") + ); + } +} + +contract ArbitrumCrossDomainGovernor_ForwardDelegate is ArbitrumCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_arbitrumCrossDomainGovernor.forwardDelegate(address(s_multiSend), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_CallableByCrossDomainMessengerAddressOrL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends the message + s_arbitrumCrossDomainGovernor.forwardDelegate( + address(s_multiSend), + abi.encodeWithSelector( + MultiSend.multiSend.selector, + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ) + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Sends the message + s_arbitrumCrossDomainGovernor.forwardDelegate( + address(s_multiSend), + abi.encodeWithSelector( + MultiSend.multiSend.selector, + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ) + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should revert batch when one call fails + function test_RevertsBatchWhenOneCallFails() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Governor delegatecall reverted"); + s_arbitrumCrossDomainGovernor.forwardDelegate( + address(s_multiSend), + abi.encodeWithSelector( + MultiSend.multiSend.selector, + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "")) + ) + ); + + // Checks that the greeter message is unchanged + assertEq(s_greeter.greeting(), ""); + } + + /// @notice it should bubble up revert when contract call reverts + function test_BubbleUpRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Greeter: revert triggered"); + s_arbitrumCrossDomainGovernor.forwardDelegate( + address(s_greeter), + abi.encodeWithSelector(Greeter.triggerRevert.selector) + ); + } +} + +contract ArbitrumCrossDomainGovernor_TransferL1Ownership is ArbitrumCrossDomainGovernorTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_arbitrumCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_arbitrumCrossDomainGovernor.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_arbitrumCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_arbitrumCrossDomainGovernor.l1Owner(), s_strangerAddr); + + // Sends the message + s_arbitrumCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_arbitrumCrossDomainGovernor.l1Owner(), address(0)); + + // Sends the message + s_arbitrumCrossDomainGovernor.transferL1Ownership(address(0)); + } +} + +contract ArbitrumCrossDomainGovernor_AcceptL1Ownership is ArbitrumCrossDomainGovernorTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_crossDomainMessengerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_arbitrumCrossDomainGovernor.acceptL1Ownership(); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Request ownership transfer + vm.startPrank(s_crossDomainMessengerAddr); + s_arbitrumCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + vm.startPrank(s_newOwnerCrossDomainMessengerAddr); + s_arbitrumCrossDomainGovernor.acceptL1Ownership(); + + // Asserts that the ownership was actually transferred + assertEq(s_arbitrumCrossDomainGovernor.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumSequencerUptimeFeed.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumSequencerUptimeFeed.t.sol new file mode 100644 index 00000000000..054c49b1605 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumSequencerUptimeFeed.t.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {SimpleWriteAccessController} from "../../../../shared/access/SimpleWriteAccessController.sol"; +import {ArbitrumSequencerUptimeFeed} from "../../../dev/arbitrum/ArbitrumSequencerUptimeFeed.sol"; +import {MockAggregatorV2V3} from "../../mocks/MockAggregatorV2V3.sol"; +import {FeedConsumer} from "../../../../tests/FeedConsumer.sol"; +import {Flags} from "../../../dev/Flags.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ArbitrumSequencerUptimeFeedTest is L2EPTest { + /// Constants + uint256 internal constant GAS_USED_DEVIATION = 100; + + /// Helper variable(s) + address internal s_l2MessengerAddr = toArbitrumL2AliasAddress(s_l1OwnerAddr); + + /// L2EP contracts + ArbitrumSequencerUptimeFeed internal s_arbitrumSequencerUptimeFeed; + SimpleWriteAccessController internal s_accessController; + MockAggregatorV2V3 internal s_l1GasFeed; + Flags internal s_flags; + + /// Events + event UpdateIgnored(bool latestStatus, uint64 latestTimestamp, bool incomingStatus, uint64 incomingTimestamp); + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); + event RoundUpdated(int256 status, uint64 updatedAt); + event Initialized(); + + /// Setup + function setUp() public { + vm.startPrank(s_deployerAddr, s_deployerAddr); + + s_accessController = new SimpleWriteAccessController(); + s_flags = new Flags(address(s_accessController), address(s_accessController)); + s_arbitrumSequencerUptimeFeed = new ArbitrumSequencerUptimeFeed(address(s_flags), s_l1OwnerAddr); + + s_accessController.addAccess(address(s_arbitrumSequencerUptimeFeed)); + s_accessController.addAccess(address(s_flags)); + s_accessController.addAccess(s_deployerAddr); + s_flags.addAccess(address(s_arbitrumSequencerUptimeFeed)); + + vm.expectEmit(); + emit Initialized(); + s_arbitrumSequencerUptimeFeed.initialize(); + + vm.stopPrank(); + } +} + +contract ArbitrumSequencerUptimeFeed_Constants is ArbitrumSequencerUptimeFeedTest { + /// @notice it should have the correct value for FLAG_L2_SEQ_OFFLINE' + function test_InitialState() public { + assertEq(s_arbitrumSequencerUptimeFeed.FLAG_L2_SEQ_OFFLINE(), 0xa438451D6458044c3c8CD2f6f31c91ac882A6d91); + } +} + +contract ArbitrumSequencerUptimeFeed_UpdateStatus is ArbitrumSequencerUptimeFeedTest { + /// @notice it should revert if called by an address that is not the L2 Cross Domain Messenger + function test_RevertIfNotL2CrossDomainMessengerAddr() public { + // Sets msg.sender and tx.origin to an unauthorized address + vm.startPrank(s_strangerAddr, s_strangerAddr); + + // Tries to update the status from an unauthorized account + vm.expectRevert(abi.encodeWithSelector(ArbitrumSequencerUptimeFeed.InvalidSender.selector)); + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(1)); + } + + /// @notice it should update status when status has changed and incoming timestamp is newer than the latest + function test_UpdateStatusWhenStatusChangeAndTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp(); + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_arbitrumSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, newer timestamp should update + timestamp = timestamp + 200; + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_arbitrumSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_arbitrumSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should update status when status has changed and incoming timestamp is the same as latest + function test_UpdateStatusWhenStatusChangeAndNoTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Fetches the latest timestamp + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp(); + + // Submits a status update + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_arbitrumSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, same timestamp should update + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_arbitrumSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_arbitrumSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should ignore out-of-order updates + function test_IgnoreOutOfOrderUpdates() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 10000; + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_arbitrumSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Update with different status, but stale timestamp, should be ignored + timestamp = timestamp - 1000; + vm.expectEmit(false, false, false, false); + emit UpdateIgnored(true, 0, true, 0); // arguments are dummy values + // TODO: how can we check that an AnswerUpdated event was NOT emitted + s_arbitrumSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + } +} + +contract ArbitrumSequencerUptimeFeed_AggregatorV3Interface is ArbitrumSequencerUptimeFeedTest { + /// @notice it should return valid answer from getRoundData and latestRoundData + function test_AggregatorV3Interface() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables + uint80 roundId; + int256 answer; + uint256 startedAt; + uint256 updatedAt; + uint80 answeredInRound; + + // Checks initial state + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_arbitrumSequencerUptimeFeed.latestRoundData(); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Submits status update with different status and newer timestamp, should update + uint256 timestamp = startedAt + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_arbitrumSequencerUptimeFeed.getRoundData(2); + assertEq(roundId, 2); + assertEq(answer, 1); + assertEq(answeredInRound, roundId); + assertEq(startedAt, timestamp); + assertLe(updatedAt, startedAt); + + // Saves round 2 data + uint80 roundId2 = roundId; + int256 answer2 = answer; + uint256 startedAt2 = startedAt; + uint256 updatedAt2 = updatedAt; + uint80 answeredInRound2 = answeredInRound; + + // Checks that last round is still returning the correct data + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_arbitrumSequencerUptimeFeed.getRoundData(1); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Assert latestRoundData corresponds to latest round id + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_arbitrumSequencerUptimeFeed.latestRoundData(); + assertEq(roundId2, roundId); + assertEq(answer2, answer); + assertEq(startedAt2, startedAt); + assertEq(updatedAt2, updatedAt); + assertEq(answeredInRound2, answeredInRound); + } + + /// @notice it should revert from #getRoundData when round does not yet exist (future roundId) + function test_Return0WhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = s_arbitrumSequencerUptimeFeed.getRoundData(2); + + // Validates round data + assertEq(roundId, 2); + assertEq(answer, 0); + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 2); + } +} + +contract ArbitrumSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions is ArbitrumSequencerUptimeFeedTest { + /// @notice it should disallow reads on AggregatorV2V3Interface functions when consuming contract is not whitelisted + function test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_arbitrumSequencerUptimeFeed)); + + // Sanity - consumer is not whitelisted + assertEq(s_arbitrumSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_arbitrumSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), false); + + // Asserts reads are not possible from consuming contract + vm.expectRevert("No access"); + feedConsumer.latestAnswer(); + vm.expectRevert("No access"); + feedConsumer.latestRoundData(); + } + + /// @notice it should allow reads on AggregatorV2V3Interface functions when consuming contract is whitelisted + function test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_arbitrumSequencerUptimeFeed)); + + // Whitelist consumer + vm.startPrank(s_deployerAddr, s_deployerAddr); + s_arbitrumSequencerUptimeFeed.addAccess(address(feedConsumer)); + + // Sanity - consumer is whitelisted + assertEq(s_arbitrumSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_arbitrumSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), true); + + // Asserts reads are possible from consuming contract + (uint80 roundId, int256 answer, , , ) = feedConsumer.latestRoundData(); + assertEq(feedConsumer.latestAnswer(), 0); + assertEq(roundId, 1); + assertEq(answer, 0); + } +} + +contract ArbitrumSequencerUptimeFeed_GasCosts is ArbitrumSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for updates + function test_GasCosts() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Assert initial conditions + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp(); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 0); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed; + uint256 gasStart; + uint256 gasFinal; + + // measures gas used for no update + expectedGasUsed = 5507; // NOTE: used to be 28300 in hardhat tests + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.updateStatus(false, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 0); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + + // measures gas used for update + expectedGasUsed = 68198; // NOTE: used to be 93015 in hardhat tests + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_arbitrumSequencerUptimeFeed.latestAnswer(), 1); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} + +contract ArbitrumSequencerUptimeFeed_AggregatorInterfaceGasCosts is ArbitrumSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for getRoundData(uint80) + function test_GasUsageForGetRoundData() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 4658; // NOTE: used to be 31157 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.getRoundData(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRoundData() + function test_GasUsageForLatestRoundData() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 2154; // NOTE: used to be 28523 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.latestRoundData(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestAnswer() + function test_GasUsageForLatestAnswer() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1722; // NOTE: used to be 28329 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.latestAnswer(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestTimestamp() + function test_GasUsageForLatestTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1652; // NOTE: used to be 28229 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.latestTimestamp(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRound() + function test_GasUsageForLatestRound() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1632; // NOTE: used to be 28245 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.latestRound(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getAnswer() + function test_GasUsageForGetAnswer() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 4059; // NOTE: used to be 30799 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.getAnswer(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getTimestamp() + function test_GasUsageForGetTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l2MessengerAddr, s_l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 4024; // NOTE: used to be 30753 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_arbitrumSequencerUptimeFeed.latestTimestamp() + 1000; + s_arbitrumSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_arbitrumSequencerUptimeFeed.getTimestamp(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumValidator.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumValidator.t.sol new file mode 100644 index 00000000000..504635540ce --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/arbitrum/ArbitrumValidator.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {AccessControllerInterface} from "../../../../shared/interfaces/AccessControllerInterface.sol"; + +import {SimpleWriteAccessController} from "../../../../shared/access/SimpleWriteAccessController.sol"; +import {ArbitrumSequencerUptimeFeed} from "../../../dev/arbitrum/ArbitrumSequencerUptimeFeed.sol"; +import {ArbitrumValidator} from "../../../dev/arbitrum/ArbitrumValidator.sol"; +import {MockArbitrumInbox} from "../../../../tests/MockArbitrumInbox.sol"; +import {MockAggregatorV2V3} from "../../mocks/MockAggregatorV2V3.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ArbitrumValidatorTest is L2EPTest { + /// Helper constants + address internal constant L2_SEQ_STATUS_RECORDER_ADDRESS = 0x491B1dDA0A8fa069bbC1125133A975BF4e85a91b; + uint256 internal constant GAS_PRICE_BID = 1000000; + uint256 internal constant BASE_FEE = 14000000000; + uint256 internal constant MAX_GAS = 1000000; + + /// L2EP contracts + AccessControllerInterface internal s_accessController; + MockArbitrumInbox internal s_mockArbitrumInbox; + ArbitrumValidator internal s_arbitrumValidator; + MockAggregatorV2V3 internal s_l1GasFeed; + + /// Events + event RetryableTicketNoRefundAliasRewriteCreated( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes data + ); + + /// Setup + function setUp() public { + s_accessController = new SimpleWriteAccessController(); + s_mockArbitrumInbox = new MockArbitrumInbox(); + s_l1GasFeed = new MockAggregatorV2V3(); + s_arbitrumValidator = new ArbitrumValidator( + address(s_mockArbitrumInbox), + L2_SEQ_STATUS_RECORDER_ADDRESS, + address(s_accessController), + MAX_GAS, + GAS_PRICE_BID, + BASE_FEE, + address(s_l1GasFeed), + ArbitrumValidator.PaymentStrategy.L1 + ); + } +} + +contract ArbitrumValidator_Validate is ArbitrumValidatorTest { + /// @notice it post sequencer offline + function test_PostSequencerOffline() public { + // Gives access to the s_eoaValidator + s_arbitrumValidator.addAccess(s_eoaValidator); + + // Gets the ArbitrumValidator L2 address + address arbitrumValidatorL2Addr = toArbitrumL2AliasAddress(address(s_arbitrumValidator)); + + // Sets block.timestamp to a later date, funds the ArbitrumValidator contract, and sets msg.sender and tx.origin + uint256 futureTimestampInSeconds = block.timestamp + 5000; + vm.warp(futureTimestampInSeconds); + vm.deal(address(s_arbitrumValidator), 1 ether); + vm.startPrank(s_eoaValidator); + + // Sets up the expected event data + vm.expectEmit(); + emit RetryableTicketNoRefundAliasRewriteCreated( + L2_SEQ_STATUS_RECORDER_ADDRESS, // destAddr + 0, // arbTxCallValue + 25312000000000, // maxSubmissionCost + arbitrumValidatorL2Addr, // submissionRefundAddress + arbitrumValidatorL2Addr, // valueRefundAddress + MAX_GAS, // maxGas + GAS_PRICE_BID, // gasPriceBid + abi.encodeWithSelector(ArbitrumSequencerUptimeFeed.updateStatus.selector, true, futureTimestampInSeconds) // data + ); + + // Runs the function (which produces the event to test) + s_arbitrumValidator.validate(0, 0, 1, 1); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainForwarder.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainForwarder.t.sol new file mode 100644 index 00000000000..d5c482dce98 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainForwarder.t.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {OptimismCrossDomainForwarder} from "../../../dev/optimism/OptimismCrossDomainForwarder.sol"; +import {MockOVMCrossDomainMessenger} from "../../mocks/optimism/MockOVMCrossDomainMessenger.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract OptimismCrossDomainForwarderTest is L2EPTest { + /// Contracts + MockOVMCrossDomainMessenger internal s_mockOptimismCrossDomainMessenger; + OptimismCrossDomainForwarder internal s_optimismCrossDomainForwarder; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_mockOptimismCrossDomainMessenger = new MockOVMCrossDomainMessenger(s_l1OwnerAddr); + s_optimismCrossDomainForwarder = new OptimismCrossDomainForwarder( + s_mockOptimismCrossDomainMessenger, + s_l1OwnerAddr + ); + s_greeter = new Greeter(address(s_optimismCrossDomainForwarder)); + vm.stopPrank(); + } +} + +contract OptimismCrossDomainForwarder_Constructor is OptimismCrossDomainForwarderTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_optimismCrossDomainForwarder.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_optimismCrossDomainForwarder.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_optimismCrossDomainForwarder.crossDomainMessenger(), address(s_mockOptimismCrossDomainMessenger)); + + // it should set the typeAndVersion correctly + assertEq(s_optimismCrossDomainForwarder.typeAndVersion(), "OptimismCrossDomainForwarder 1.0.0"); + } +} + +contract OptimismCrossDomainForwarder_Forward is OptimismCrossDomainForwarderTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_optimismCrossDomainForwarder.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + encodeCrossDomainSetGreetingMsg(s_optimismCrossDomainForwarder.forward.selector, address(s_greeter), greeting), // message + 0 // gas limit + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + encodeCrossDomainSetGreetingMsg(s_optimismCrossDomainForwarder.forward.selector, address(s_greeter), ""), // message + 0 // gas limit + ); + } +} + +contract OptimismCrossDomainForwarder_TransferL1Ownership is OptimismCrossDomainForwarderTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_optimismCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_optimismCrossDomainForwarder.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_optimismCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_optimismCrossDomainForwarder.l1Owner(), s_strangerAddr); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + abi.encodeWithSelector(s_optimismCrossDomainForwarder.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_optimismCrossDomainForwarder.l1Owner(), address(0)); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + abi.encodeWithSelector(s_optimismCrossDomainForwarder.transferL1Ownership.selector, address(0)), // message + 0 // gas limit + ); + } +} + +contract OptimismCrossDomainForwarder_AcceptL1Ownership is OptimismCrossDomainForwarderTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + abi.encodeWithSelector(s_optimismCrossDomainForwarder.acceptL1Ownership.selector), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Request ownership transfer + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + abi.encodeWithSelector(s_optimismCrossDomainForwarder.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Sets a mock message sender + s_mockOptimismCrossDomainMessenger._setMockMessageSender(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainForwarder), // target + abi.encodeWithSelector(s_optimismCrossDomainForwarder.acceptL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Asserts that the ownership was actually transferred + assertEq(s_optimismCrossDomainForwarder.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainGovernor.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainGovernor.t.sol new file mode 100644 index 00000000000..e1a5aef95a1 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismCrossDomainGovernor.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {OptimismCrossDomainGovernor} from "../../../dev/optimism/OptimismCrossDomainGovernor.sol"; +import {MockOVMCrossDomainMessenger} from "../../mocks/optimism/MockOVMCrossDomainMessenger.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +import {MultiSend} from "../../../../vendor/MultiSend.sol"; + +contract OptimismCrossDomainGovernorTest is L2EPTest { + /// Contracts + MockOVMCrossDomainMessenger internal s_mockOptimismCrossDomainMessenger; + OptimismCrossDomainGovernor internal s_optimismCrossDomainGovernor; + MultiSend internal s_multiSend; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_mockOptimismCrossDomainMessenger = new MockOVMCrossDomainMessenger(s_l1OwnerAddr); + s_optimismCrossDomainGovernor = new OptimismCrossDomainGovernor(s_mockOptimismCrossDomainMessenger, s_l1OwnerAddr); + s_greeter = new Greeter(address(s_optimismCrossDomainGovernor)); + s_multiSend = new MultiSend(); + vm.stopPrank(); + } +} + +contract OptimismCrossDomainGovernor_Constructor is OptimismCrossDomainGovernorTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_optimismCrossDomainGovernor.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_optimismCrossDomainGovernor.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_optimismCrossDomainGovernor.crossDomainMessenger(), address(s_mockOptimismCrossDomainMessenger)); + + // it should set the typeAndVersion correctly + assertEq(s_optimismCrossDomainGovernor.typeAndVersion(), "OptimismCrossDomainGovernor 1.0.0"); + } +} + +contract OptimismCrossDomainGovernor_Forward is OptimismCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_optimismCrossDomainGovernor.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + encodeCrossDomainSetGreetingMsg(s_optimismCrossDomainGovernor.forward.selector, address(s_greeter), greeting), // message + 0 // gas limit + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + encodeCrossDomainSetGreetingMsg(s_optimismCrossDomainGovernor.forward.selector, address(s_greeter), ""), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_optimismCrossDomainGovernor.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, greeting) + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), greeting); + } +} + +contract OptimismCrossDomainGovernor_ForwardDelegate is OptimismCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_optimismCrossDomainGovernor.forwardDelegate(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_CallableByCrossDomainMessengerAddressOrL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + encodeCrossDomainMultiSendMsg( + s_optimismCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + encodeCrossDomainMultiSendMsg( + s_optimismCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should revert batch when one call fails + function test_RevertsBatchWhenOneCallFails() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Governor delegatecall reverted"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + encodeCrossDomainMultiSendMsg( + s_optimismCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message is unchanged + assertEq(s_greeter.greeting(), ""); + } + + /// @notice it should bubble up revert when contract call reverts + function test_BubbleUpRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Greeter: revert triggered"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector( + OptimismCrossDomainGovernor.forwardDelegate.selector, + address(s_greeter), + abi.encodeWithSelector(Greeter.triggerRevert.selector) + ), // message + 0 // gas limit + ); + } +} + +contract OptimismCrossDomainGovernor_TransferL1Ownership is OptimismCrossDomainGovernorTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_optimismCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_optimismCrossDomainGovernor.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_optimismCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_optimismCrossDomainGovernor.l1Owner(), s_strangerAddr); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector(s_optimismCrossDomainGovernor.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_optimismCrossDomainGovernor.l1Owner(), address(0)); + + // Sends the message + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector(s_optimismCrossDomainGovernor.transferL1Ownership.selector, address(0)), // message + 0 // gas limit + ); + } +} + +contract OptimismCrossDomainGovernor_AcceptL1Ownership is OptimismCrossDomainGovernorTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector(s_optimismCrossDomainGovernor.acceptL1Ownership.selector), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Request ownership transfer + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector(s_optimismCrossDomainGovernor.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Sets a mock message sender + s_mockOptimismCrossDomainMessenger._setMockMessageSender(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + s_mockOptimismCrossDomainMessenger.sendMessage( + address(s_optimismCrossDomainGovernor), // target + abi.encodeWithSelector(s_optimismCrossDomainGovernor.acceptL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Asserts that the ownership was actually transferred + assertEq(s_optimismCrossDomainGovernor.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismSequencerUptimeFeed.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismSequencerUptimeFeed.t.sol new file mode 100644 index 00000000000..b4aa32ce69b --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismSequencerUptimeFeed.t.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockOptimismL1CrossDomainMessenger} from "../../../../tests/MockOptimismL1CrossDomainMessenger.sol"; +import {MockOptimismL2CrossDomainMessenger} from "../../../../tests/MockOptimismL2CrossDomainMessenger.sol"; +import {OptimismSequencerUptimeFeed} from "../../../dev/optimism/OptimismSequencerUptimeFeed.sol"; +import {FeedConsumer} from "../../../../tests/FeedConsumer.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract OptimismSequencerUptimeFeedTest is L2EPTest { + /// Constants + uint256 internal constant GAS_USED_DEVIATION = 100; + + /// L2EP contracts + MockOptimismL1CrossDomainMessenger internal s_mockOptimismL1CrossDomainMessenger; + MockOptimismL2CrossDomainMessenger internal s_mockOptimismL2CrossDomainMessenger; + OptimismSequencerUptimeFeed internal s_optimismSequencerUptimeFeed; + + /// Events + event UpdateIgnored(bool latestStatus, uint64 latestTimestamp, bool incomingStatus, uint64 incomingTimestamp); + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); + event RoundUpdated(int256 status, uint64 updatedAt); + + /// Setup + function setUp() public { + // Deploys contracts + s_mockOptimismL1CrossDomainMessenger = new MockOptimismL1CrossDomainMessenger(); + s_mockOptimismL2CrossDomainMessenger = new MockOptimismL2CrossDomainMessenger(); + s_optimismSequencerUptimeFeed = new OptimismSequencerUptimeFeed( + s_l1OwnerAddr, + address(s_mockOptimismL2CrossDomainMessenger), + false + ); + + // Sets mock sender in mock L2 messenger contract + s_mockOptimismL2CrossDomainMessenger.setSender(s_l1OwnerAddr); + } +} + +contract OptimismSequencerUptimeFeed_Constructor is OptimismSequencerUptimeFeedTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Checks L1 sender + address actualL1Addr = s_optimismSequencerUptimeFeed.l1Sender(); + assertEq(actualL1Addr, s_l1OwnerAddr); + + // Checks latest round data + (uint80 roundId, int256 answer, , , ) = s_optimismSequencerUptimeFeed.latestRoundData(); + assertEq(roundId, 1); + assertEq(answer, 0); + } +} + +contract OptimismSequencerUptimeFeed_UpdateStatus is OptimismSequencerUptimeFeedTest { + /// @notice it should revert if called by an address that is not the L2 Cross Domain Messenger + function test_RevertIfNotL2CrossDomainMessengerAddr() public { + // Sets msg.sender and tx.origin to an unauthorized address + vm.startPrank(s_strangerAddr, s_strangerAddr); + + // Tries to update the status from an unauthorized account + vm.expectRevert(abi.encodeWithSelector(OptimismSequencerUptimeFeed.InvalidSender.selector)); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(1)); + } + + /// @notice it should revert if called by an address that is not the L2 Cross Domain Messenger and is not the L1 sender + function test_RevertIfNotL2CrossDomainMessengerAddrAndNotL1SenderAddr() public { + // Sets msg.sender and tx.origin to an unauthorized address + vm.startPrank(s_strangerAddr, s_strangerAddr); + + // Sets mock sender in mock L2 messenger contract + s_mockOptimismL2CrossDomainMessenger.setSender(s_strangerAddr); + + // Tries to update the status from an unauthorized account + vm.expectRevert(abi.encodeWithSelector(OptimismSequencerUptimeFeed.InvalidSender.selector)); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(1)); + } + + /// @notice it should update status when status has not changed and incoming timestamp is the same as latest + function test_UpdateStatusWhenNoChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Fetches the latest timestamp + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp(); + + // Submits a status update + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Stores the current round data before updating it + ( + uint80 roundIdBeforeUpdate, + int256 answerBeforeUpdate, + uint256 startedAtBeforeUpdate, + , + uint80 answeredInRoundBeforeUpdate + ) = s_optimismSequencerUptimeFeed.latestRoundData(); + + // Submit another status update with the same status + vm.expectEmit(); + emit RoundUpdated(1, uint64(block.timestamp)); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp + 200)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Stores the current round data after updating it + ( + uint80 roundIdAfterUpdate, + int256 answerAfterUpdate, + uint256 startedAtAfterUpdate, + uint256 updatedAtAfterUpdate, + uint80 answeredInRoundAfterUpdate + ) = s_optimismSequencerUptimeFeed.latestRoundData(); + + // Verifies the latest round data has been properly updated + assertEq(roundIdAfterUpdate, roundIdBeforeUpdate); + assertEq(answerAfterUpdate, answerBeforeUpdate); + assertEq(startedAtAfterUpdate, startedAtBeforeUpdate); + assertEq(answeredInRoundAfterUpdate, answeredInRoundBeforeUpdate); + assertEq(updatedAtAfterUpdate, block.timestamp); + } + + /// @notice it should update status when status has changed and incoming timestamp is newer than the latest + function test_UpdateStatusWhenStatusChangeAndTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp(); + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, newer timestamp should update + timestamp = timestamp + 200; + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should update status when status has changed and incoming timestamp is the same as latest + function test_UpdateStatusWhenStatusChangeAndNoTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Fetches the latest timestamp + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp(); + + // Submits a status update + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, same timestamp should update + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should ignore out-of-order updates + function test_IgnoreOutOfOrderUpdates() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 10000; + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_optimismSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Update with different status, but stale timestamp, should be ignored + timestamp = timestamp - 1000; + vm.expectEmit(false, false, false, false); + emit UpdateIgnored(true, 0, true, 0); // arguments are dummy values + // TODO: how can we check that an AnswerUpdated event was NOT emitted + s_optimismSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + } +} + +contract OptimismSequencerUptimeFeed_AggregatorV3Interface is OptimismSequencerUptimeFeedTest { + /// @notice it should return valid answer from getRoundData and latestRoundData + function test_AggregatorV3Interface() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables + uint80 roundId; + int256 answer; + uint256 startedAt; + uint256 updatedAt; + uint80 answeredInRound; + + // Checks initial state + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_optimismSequencerUptimeFeed.latestRoundData(); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Submits status update with different status and newer timestamp, should update + uint256 timestamp = startedAt + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_optimismSequencerUptimeFeed.getRoundData(2); + assertEq(roundId, 2); + assertEq(answer, 1); + assertEq(answeredInRound, roundId); + assertEq(startedAt, timestamp); + assertLe(updatedAt, startedAt); + + // Saves round 2 data + uint80 roundId2 = roundId; + int256 answer2 = answer; + uint256 startedAt2 = startedAt; + uint256 updatedAt2 = updatedAt; + uint80 answeredInRound2 = answeredInRound; + + // Checks that last round is still returning the correct data + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_optimismSequencerUptimeFeed.getRoundData(1); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Assert latestRoundData corresponds to latest round id + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_optimismSequencerUptimeFeed.latestRoundData(); + assertEq(roundId2, roundId); + assertEq(answer2, answer); + assertEq(startedAt2, startedAt); + assertEq(updatedAt2, updatedAt); + assertEq(answeredInRound2, answeredInRound); + } + + /// @notice it should revert from #getRoundData when round does not yet exist (future roundId) + function test_RevertGetRoundDataWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(OptimismSequencerUptimeFeed.NoDataPresent.selector)); + s_optimismSequencerUptimeFeed.getRoundData(2); + } + + /// @notice it should revert from #getAnswer when round does not yet exist (future roundId) + function test_RevertGetAnswerWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(OptimismSequencerUptimeFeed.NoDataPresent.selector)); + s_optimismSequencerUptimeFeed.getAnswer(2); + } + + /// @notice it should revert from #getTimestamp when round does not yet exist (future roundId) + function test_RevertGetTimestampWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(OptimismSequencerUptimeFeed.NoDataPresent.selector)); + s_optimismSequencerUptimeFeed.getTimestamp(2); + } +} + +contract OptimismSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions is OptimismSequencerUptimeFeedTest { + /// @notice it should disallow reads on AggregatorV2V3Interface functions when consuming contract is not whitelisted + function test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_optimismSequencerUptimeFeed)); + + // Sanity - consumer is not whitelisted + assertEq(s_optimismSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_optimismSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), false); + + // Asserts reads are not possible from consuming contract + vm.expectRevert("No access"); + feedConsumer.latestAnswer(); + vm.expectRevert("No access"); + feedConsumer.latestRoundData(); + } + + /// @notice it should allow reads on AggregatorV2V3Interface functions when consuming contract is whitelisted + function test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_optimismSequencerUptimeFeed)); + + // Whitelist consumer + s_optimismSequencerUptimeFeed.addAccess(address(feedConsumer)); + + // Sanity - consumer is whitelisted + assertEq(s_optimismSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_optimismSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), true); + + // Asserts reads are possible from consuming contract + (uint80 roundId, int256 answer, , , ) = feedConsumer.latestRoundData(); + assertEq(feedConsumer.latestAnswer(), 0); + assertEq(roundId, 1); + assertEq(answer, 0); + } +} + +contract OptimismSequencerUptimeFeed_GasCosts is OptimismSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for updates + function test_GasCosts() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Assert initial conditions + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp(); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 0); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed; + uint256 gasStart; + uint256 gasFinal; + + // measures gas used for no update + expectedGasUsed = 10197; // NOTE: used to be 38594 in hardhat tests + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.updateStatus(false, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 0); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + + // measures gas used for update + expectedGasUsed = 33348; // NOTE: used to be 60170 in hardhat tests + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_optimismSequencerUptimeFeed.latestAnswer(), 1); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} + +contract OptimismSequencerUptimeFeed_AggregatorInterfaceGasCosts is OptimismSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for getRoundData(uint80) + function test_GasUsageForGetRoundData() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 4504; // NOTE: used to be 30952 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.getRoundData(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRoundData() + function test_GasUsageForLatestRoundData() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 2154; // NOTE: used to be 28523 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.latestRoundData(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestAnswer() + function test_GasUsageForLatestAnswer() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1722; // NOTE: used to be 28329 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.latestAnswer(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestTimestamp() + function test_GasUsageForLatestTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1598; // NOTE: used to be 28229 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.latestTimestamp(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRound() + function test_GasUsageForLatestRound() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1632; // NOTE: used to be 28245 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.latestRound(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getAnswer() + function test_GasUsageForGetAnswer() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 3929; // NOTE: used to be 30682 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.getAnswer(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getTimestamp() + function test_GasUsageForGetTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockOptimismL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 3817; // NOTE: used to be 30570 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_optimismSequencerUptimeFeed.latestTimestamp() + 1000; + s_optimismSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_optimismSequencerUptimeFeed.getTimestamp(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismValidator.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismValidator.t.sol new file mode 100644 index 00000000000..9364396817a --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/optimism/OptimismValidator.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockOptimismL1CrossDomainMessenger} from "../../../../tests/MockOptimismL1CrossDomainMessenger.sol"; +import {MockOptimismL2CrossDomainMessenger} from "../../../../tests/MockOptimismL2CrossDomainMessenger.sol"; +import {OptimismSequencerUptimeFeed} from "../../../dev/optimism/OptimismSequencerUptimeFeed.sol"; +import {OptimismValidator} from "../../../dev/optimism/OptimismValidator.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract OptimismValidatorTest is L2EPTest { + /// Helper constants + address internal constant L2_SEQ_STATUS_RECORDER_ADDRESS = 0x491B1dDA0A8fa069bbC1125133A975BF4e85a91b; + uint32 internal constant INIT_GAS_LIMIT = 1900000; + + /// L2EP contracts + MockOptimismL1CrossDomainMessenger internal s_mockOptimismL1CrossDomainMessenger; + MockOptimismL2CrossDomainMessenger internal s_mockOptimismL2CrossDomainMessenger; + OptimismSequencerUptimeFeed internal s_optimismSequencerUptimeFeed; + OptimismValidator internal s_optimismValidator; + + /// Events + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + /// Setup + function setUp() public { + s_mockOptimismL1CrossDomainMessenger = new MockOptimismL1CrossDomainMessenger(); + s_mockOptimismL2CrossDomainMessenger = new MockOptimismL2CrossDomainMessenger(); + + s_optimismSequencerUptimeFeed = new OptimismSequencerUptimeFeed( + address(s_mockOptimismL1CrossDomainMessenger), + address(s_mockOptimismL2CrossDomainMessenger), + true + ); + + s_optimismValidator = new OptimismValidator( + address(s_mockOptimismL1CrossDomainMessenger), + address(s_optimismSequencerUptimeFeed), + INIT_GAS_LIMIT + ); + } +} + +contract OptimismValidator_SetGasLimit is OptimismValidatorTest { + /// @notice it correctly updates the gas limit + function test_CorrectlyUpdatesTheGasLimit() public { + uint32 newGasLimit = 2000000; + assertEq(s_optimismValidator.getGasLimit(), INIT_GAS_LIMIT); + s_optimismValidator.setGasLimit(newGasLimit); + assertEq(s_optimismValidator.getGasLimit(), newGasLimit); + } +} + +contract OptimismValidator_Validate is OptimismValidatorTest { + /// @notice it reverts if called by account with no access + function test_RevertsIfCalledByAnAccountWithNoAccess() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("No access"); + s_optimismValidator.validate(0, 0, 1, 1); + } + + /// @notice it posts sequencer status when there is not status change + function test_PostSequencerStatusWhenThereIsNotStatusChange() public { + // Gives access to the s_eoaValidator + s_optimismValidator.addAccess(s_eoaValidator); + + // Sets block.timestamp to a later date + uint256 futureTimestampInSeconds = block.timestamp + 5000; + vm.startPrank(s_eoaValidator); + vm.warp(futureTimestampInSeconds); + + // Sets up the expected event data + vm.expectEmit(false, false, false, true); + emit SentMessage( + L2_SEQ_STATUS_RECORDER_ADDRESS, // target + address(s_optimismValidator), // sender + abi.encodeWithSelector(OptimismSequencerUptimeFeed.updateStatus.selector, false, futureTimestampInSeconds), // message + 0, // nonce + INIT_GAS_LIMIT // gas limit + ); + + // Runs the function (which produces the event to test) + s_optimismValidator.validate(0, 0, 0, 0); + } + + /// @notice it post sequencer offline + function test_PostSequencerOffline() public { + // Gives access to the s_eoaValidator + s_optimismValidator.addAccess(s_eoaValidator); + + // Sets block.timestamp to a later date + uint256 futureTimestampInSeconds = block.timestamp + 10000; + vm.startPrank(s_eoaValidator); + vm.warp(futureTimestampInSeconds); + + // Sets up the expected event data + vm.expectEmit(false, false, false, true); + emit SentMessage( + L2_SEQ_STATUS_RECORDER_ADDRESS, // target + address(s_optimismValidator), // sender + abi.encodeWithSelector(OptimismSequencerUptimeFeed.updateStatus.selector, true, futureTimestampInSeconds), // message + 0, // nonce + INIT_GAS_LIMIT // gas limit + ); + + // Runs the function (which produces the event to test) + s_optimismValidator.validate(0, 0, 1, 1); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainForwarder.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainForwarder.t.sol new file mode 100644 index 00000000000..f921fa9242e --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainForwarder.t.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockScrollCrossDomainMessenger} from "../../mocks/scroll/MockScrollCrossDomainMessenger.sol"; +import {ScrollCrossDomainForwarder} from "../../../dev/scroll/ScrollCrossDomainForwarder.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ScrollCrossDomainForwarderTest is L2EPTest { + /// Contracts + MockScrollCrossDomainMessenger internal s_mockScrollCrossDomainMessenger; + ScrollCrossDomainForwarder internal s_scrollCrossDomainForwarder; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_mockScrollCrossDomainMessenger = new MockScrollCrossDomainMessenger(s_l1OwnerAddr); + s_scrollCrossDomainForwarder = new ScrollCrossDomainForwarder(s_mockScrollCrossDomainMessenger, s_l1OwnerAddr); + s_greeter = new Greeter(address(s_scrollCrossDomainForwarder)); + vm.stopPrank(); + } +} + +contract ScrollCrossDomainForwarder_Constructor is ScrollCrossDomainForwarderTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_scrollCrossDomainForwarder.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_scrollCrossDomainForwarder.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_scrollCrossDomainForwarder.crossDomainMessenger(), address(s_mockScrollCrossDomainMessenger)); + + // it should set the typeAndVersion correctly + assertEq(s_scrollCrossDomainForwarder.typeAndVersion(), "ScrollCrossDomainForwarder 1.0.0"); + } +} + +contract ScrollCrossDomainForwarder_Forward is ScrollCrossDomainForwarderTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_scrollCrossDomainForwarder.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + encodeCrossDomainSetGreetingMsg(s_scrollCrossDomainForwarder.forward.selector, address(s_greeter), greeting), // message + 0 // gas limit + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + encodeCrossDomainSetGreetingMsg(s_scrollCrossDomainForwarder.forward.selector, address(s_greeter), ""), // message + 0 // gas limit + ); + } +} + +contract ScrollCrossDomainForwarder_TransferL1Ownership is ScrollCrossDomainForwarderTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_scrollCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_scrollCrossDomainForwarder.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_scrollCrossDomainForwarder.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_scrollCrossDomainForwarder.l1Owner(), s_strangerAddr); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainForwarder.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_scrollCrossDomainForwarder.l1Owner(), address(0)); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainForwarder.transferL1Ownership.selector, address(0)), // message + 0 // gas limit + ); + } +} + +contract ScrollCrossDomainForwarder_AcceptL1Ownership is ScrollCrossDomainForwarderTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainForwarder.acceptL1Ownership.selector), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Request ownership transfer + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainForwarder.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Sets a mock message sender + s_mockScrollCrossDomainMessenger._setMockMessageSender(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainForwarder), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainForwarder.acceptL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Asserts that the ownership was actually transferred + assertEq(s_scrollCrossDomainForwarder.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainGovernor.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainGovernor.t.sol new file mode 100644 index 00000000000..9c444604946 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollCrossDomainGovernor.t.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockScrollCrossDomainMessenger} from "../../mocks/scroll/MockScrollCrossDomainMessenger.sol"; +import {ScrollCrossDomainGovernor} from "../../../dev/scroll/ScrollCrossDomainGovernor.sol"; +import {Greeter} from "../../../../tests/Greeter.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +import {MultiSend} from "../../../../vendor/MultiSend.sol"; + +contract ScrollCrossDomainGovernorTest is L2EPTest { + /// Contracts + MockScrollCrossDomainMessenger internal s_mockScrollCrossDomainMessenger; + ScrollCrossDomainGovernor internal s_scrollCrossDomainGovernor; + MultiSend internal s_multiSend; + Greeter internal s_greeter; + + /// Events + event L1OwnershipTransferRequested(address indexed from, address indexed to); + event L1OwnershipTransferred(address indexed from, address indexed to); + + /// Setup + function setUp() public { + // Deploys contracts + vm.startPrank(s_l1OwnerAddr); + s_mockScrollCrossDomainMessenger = new MockScrollCrossDomainMessenger(s_l1OwnerAddr); + s_scrollCrossDomainGovernor = new ScrollCrossDomainGovernor(s_mockScrollCrossDomainMessenger, s_l1OwnerAddr); + s_greeter = new Greeter(address(s_scrollCrossDomainGovernor)); + s_multiSend = new MultiSend(); + vm.stopPrank(); + } +} + +contract ScrollCrossDomainGovernor_Constructor is ScrollCrossDomainGovernorTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // it should set the owner correctly + assertEq(s_scrollCrossDomainGovernor.owner(), s_l1OwnerAddr); + + // it should set the l1Owner correctly + assertEq(s_scrollCrossDomainGovernor.l1Owner(), s_l1OwnerAddr); + + // it should set the crossdomain messenger correctly + assertEq(s_scrollCrossDomainGovernor.crossDomainMessenger(), address(s_mockScrollCrossDomainMessenger)); + + // it should set the typeAndVersion correctly + assertEq(s_scrollCrossDomainGovernor.typeAndVersion(), "ScrollCrossDomainGovernor 1.0.0"); + } +} + +contract ScrollCrossDomainGovernor_Forward is ScrollCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_scrollCrossDomainGovernor.forward(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_Forward() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + encodeCrossDomainSetGreetingMsg(s_scrollCrossDomainGovernor.forward.selector, address(s_greeter), greeting), // message + 0 // gas limit + ); + + // Checks that the greeter got the message + assertEq(s_greeter.greeting(), greeting); + } + + /// @notice it should revert when contract call reverts + function test_ForwardRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message + vm.expectRevert("Invalid greeting length"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + encodeCrossDomainSetGreetingMsg(s_scrollCrossDomainGovernor.forward.selector, address(s_greeter), ""), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Defines the cross domain message to send + string memory greeting = "hello"; + + // Sends the message + s_scrollCrossDomainGovernor.forward( + address(s_greeter), + abi.encodeWithSelector(s_greeter.setGreeting.selector, greeting) + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), greeting); + } +} + +contract ScrollCrossDomainGovernor_ForwardDelegate is ScrollCrossDomainGovernorTest { + /// @notice it should not be callable by unknown address + function test_NotCallableByUnknownAddress() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger or owner"); + s_scrollCrossDomainGovernor.forwardDelegate(address(s_greeter), abi.encode("")); + } + + /// @notice it should be callable by crossdomain messenger address / L1 owner + function test_CallableByCrossDomainMessengerAddressOrL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + encodeCrossDomainMultiSendMsg( + s_scrollCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should be callable by L2 owner + function test_CallableByL2Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_l1OwnerAddr); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + encodeCrossDomainMultiSendMsg( + s_scrollCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "bar")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message was updated + assertEq(s_greeter.greeting(), "bar"); + } + + /// @notice it should revert batch when one call fails + function test_RevertsBatchWhenOneCallFails() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Governor delegatecall reverted"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + encodeCrossDomainMultiSendMsg( + s_scrollCrossDomainGovernor.forwardDelegate.selector, + address(s_multiSend), + abi.encodePacked(encodeMultiSendTx(address(s_greeter), "foo"), encodeMultiSendTx(address(s_greeter), "")) + ), // message + 0 // gas limit + ); + + // Checks that the greeter message is unchanged + assertEq(s_greeter.greeting(), ""); + } + + /// @notice it should bubble up revert when contract call reverts + function test_BubbleUpRevert() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends an invalid message (empty transaction data is not allowed) + vm.expectRevert("Greeter: revert triggered"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector( + ScrollCrossDomainGovernor.forwardDelegate.selector, + address(s_greeter), + abi.encodeWithSelector(Greeter.triggerRevert.selector) + ), // message + 0 // gas limit + ); + } +} + +contract ScrollCrossDomainGovernor_TransferL1Ownership is ScrollCrossDomainGovernorTest { + /// @notice it should not be callable by non-owners + function test_NotCallableByNonOwners() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_scrollCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should not be callable by L2 owner + function test_NotCallableByL2Owner() public { + vm.startPrank(s_l1OwnerAddr); + assertEq(s_scrollCrossDomainGovernor.owner(), s_l1OwnerAddr); + vm.expectRevert("Sender is not the L2 messenger"); + s_scrollCrossDomainGovernor.transferL1Ownership(s_strangerAddr); + } + + /// @notice it should be callable by current L1 owner + function test_CallableByL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_scrollCrossDomainGovernor.l1Owner(), s_strangerAddr); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainGovernor.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by current L1 owner to zero address + function test_CallableByL1OwnerOrZeroAddress() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Defines the cross domain message to send + vm.expectEmit(); + emit L1OwnershipTransferRequested(s_scrollCrossDomainGovernor.l1Owner(), address(0)); + + // Sends the message + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainGovernor.transferL1Ownership.selector, address(0)), // message + 0 // gas limit + ); + } +} + +contract ScrollCrossDomainGovernor_AcceptL1Ownership is ScrollCrossDomainGovernorTest { + /// @notice it should not be callable by non pending-owners + function test_NotCallableByNonPendingOwners() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Sends the message + vm.expectRevert("Must be proposed L1 owner"); + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainGovernor.acceptL1Ownership.selector), // message + 0 // gas limit + ); + } + + /// @notice it should be callable by pending L1 owner + function test_CallableByPendingL1Owner() public { + // Sets msg.sender and tx.origin + vm.startPrank(s_strangerAddr); + + // Request ownership transfer + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainGovernor.transferL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Sets a mock message sender + s_mockScrollCrossDomainMessenger._setMockMessageSender(s_strangerAddr); + + // Prepares expected event payload + vm.expectEmit(); + emit L1OwnershipTransferred(s_l1OwnerAddr, s_strangerAddr); + + // Accepts ownership transfer request + s_mockScrollCrossDomainMessenger.sendMessage( + address(s_scrollCrossDomainGovernor), // target + 0, // value + abi.encodeWithSelector(s_scrollCrossDomainGovernor.acceptL1Ownership.selector, s_strangerAddr), // message + 0 // gas limit + ); + + // Asserts that the ownership was actually transferred + assertEq(s_scrollCrossDomainGovernor.l1Owner(), s_strangerAddr); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollSequencerUptimeFeed.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollSequencerUptimeFeed.t.sol new file mode 100644 index 00000000000..85195816b90 --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollSequencerUptimeFeed.t.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockScrollL1CrossDomainMessenger} from "../../mocks/scroll/MockScrollL1CrossDomainMessenger.sol"; +import {MockScrollL2CrossDomainMessenger} from "../../mocks/scroll/MockScrollL2CrossDomainMessenger.sol"; +import {ScrollSequencerUptimeFeed} from "../../../dev/scroll/ScrollSequencerUptimeFeed.sol"; +import {FeedConsumer} from "../../../../tests/FeedConsumer.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ScrollSequencerUptimeFeedTest is L2EPTest { + /// Constants + uint256 internal constant GAS_USED_DEVIATION = 100; + + /// L2EP contracts + MockScrollL1CrossDomainMessenger internal s_mockScrollL1CrossDomainMessenger; + MockScrollL2CrossDomainMessenger internal s_mockScrollL2CrossDomainMessenger; + ScrollSequencerUptimeFeed internal s_scrollSequencerUptimeFeed; + + /// Events + event UpdateIgnored(bool latestStatus, uint64 latestTimestamp, bool incomingStatus, uint64 incomingTimestamp); + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); + event RoundUpdated(int256 status, uint64 updatedAt); + + /// Setup + function setUp() public { + // Deploys contracts + s_mockScrollL1CrossDomainMessenger = new MockScrollL1CrossDomainMessenger(); + s_mockScrollL2CrossDomainMessenger = new MockScrollL2CrossDomainMessenger(); + s_scrollSequencerUptimeFeed = new ScrollSequencerUptimeFeed( + s_l1OwnerAddr, + address(s_mockScrollL2CrossDomainMessenger), + false + ); + + // Sets mock sender in mock L2 messenger contract + s_mockScrollL2CrossDomainMessenger.setSender(s_l1OwnerAddr); + } +} + +contract ScrollSequencerUptimeFeed_Constructor is ScrollSequencerUptimeFeedTest { + /// @notice it should have been deployed with the correct initial state + function test_InitialState() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Checks L1 sender + address actualL1Addr = s_scrollSequencerUptimeFeed.l1Sender(); + assertEq(actualL1Addr, s_l1OwnerAddr); + + // Checks latest round data + (uint80 roundId, int256 answer, , , ) = s_scrollSequencerUptimeFeed.latestRoundData(); + assertEq(roundId, 1); + assertEq(answer, 0); + } +} + +contract ScrollSequencerUptimeFeed_UpdateStatus is ScrollSequencerUptimeFeedTest { + /// @notice it should revert if called by an address that is not the L2 Cross Domain Messenger + function test_RevertIfNotL2CrossDomainMessengerAddr() public { + // Sets msg.sender and tx.origin to an unauthorized address + vm.startPrank(s_strangerAddr, s_strangerAddr); + + // Tries to update the status from an unauthorized account + vm.expectRevert(abi.encodeWithSelector(ScrollSequencerUptimeFeed.InvalidSender.selector)); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(1)); + } + + /// @notice it should revert if called by an address that is not the L2 Cross Domain Messenger and is not the L1 sender + function test_RevertIfNotL2CrossDomainMessengerAddrAndNotL1SenderAddr() public { + // Sets msg.sender and tx.origin to an unauthorized address + vm.startPrank(s_strangerAddr, s_strangerAddr); + + // Sets mock sender in mock L2 messenger contract + s_mockScrollL2CrossDomainMessenger.setSender(s_strangerAddr); + + // Tries to update the status from an unauthorized account + vm.expectRevert(abi.encodeWithSelector(ScrollSequencerUptimeFeed.InvalidSender.selector)); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(1)); + } + + /// @notice it should update status when status has not changed and incoming timestamp is the same as latest + function test_UpdateStatusWhenNoChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Fetches the latest timestamp + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp(); + + // Submits a status update + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Stores the current round data before updating it + ( + uint80 roundIdBeforeUpdate, + int256 answerBeforeUpdate, + uint256 startedAtBeforeUpdate, + , + uint80 answeredInRoundBeforeUpdate + ) = s_scrollSequencerUptimeFeed.latestRoundData(); + + // Submit another status update with the same status + vm.expectEmit(); + emit RoundUpdated(1, uint64(block.timestamp)); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp + 200)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Stores the current round data after updating it + ( + uint80 roundIdAfterUpdate, + int256 answerAfterUpdate, + uint256 startedAtAfterUpdate, + uint256 updatedAtAfterUpdate, + uint80 answeredInRoundAfterUpdate + ) = s_scrollSequencerUptimeFeed.latestRoundData(); + + // Verifies the latest round data has been properly updated + assertEq(roundIdAfterUpdate, roundIdBeforeUpdate); + assertEq(answerAfterUpdate, answerBeforeUpdate); + assertEq(startedAtAfterUpdate, startedAtBeforeUpdate); + assertEq(answeredInRoundAfterUpdate, answeredInRoundBeforeUpdate); + assertEq(updatedAtAfterUpdate, block.timestamp); + } + + /// @notice it should update status when status has changed and incoming timestamp is newer than the latest + function test_UpdateStatusWhenStatusChangeAndTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp(); + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, newer timestamp should update + timestamp = timestamp + 200; + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should update status when status has changed and incoming timestamp is the same as latest + function test_UpdateStatusWhenStatusChangeAndNoTimeChange() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Fetches the latest timestamp + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp(); + + // Submits a status update + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Submit another status update, different status, same timestamp should update + vm.expectEmit(); + emit AnswerUpdated(0, 3, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 0); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + } + + /// @notice it should ignore out-of-order updates + function test_IgnoreOutOfOrderUpdates() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Submits a status update + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 10000; + vm.expectEmit(); + emit AnswerUpdated(1, 2, timestamp); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertEq(s_scrollSequencerUptimeFeed.latestTimestamp(), uint64(timestamp)); + + // Update with different status, but stale timestamp, should be ignored + timestamp = timestamp - 1000; + vm.expectEmit(false, false, false, false); + emit UpdateIgnored(true, 0, true, 0); // arguments are dummy values + // TODO: how can we check that an AnswerUpdated event was NOT emitted + s_scrollSequencerUptimeFeed.updateStatus(false, uint64(timestamp)); + } +} + +contract ScrollSequencerUptimeFeed_AggregatorV3Interface is ScrollSequencerUptimeFeedTest { + /// @notice it should return valid answer from getRoundData and latestRoundData + function test_AggregatorV3Interface() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables + uint80 roundId; + int256 answer; + uint256 startedAt; + uint256 updatedAt; + uint80 answeredInRound; + + // Checks initial state + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_scrollSequencerUptimeFeed.latestRoundData(); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Submits status update with different status and newer timestamp, should update + uint256 timestamp = startedAt + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_scrollSequencerUptimeFeed.getRoundData(2); + assertEq(roundId, 2); + assertEq(answer, 1); + assertEq(answeredInRound, roundId); + assertEq(startedAt, timestamp); + assertLe(updatedAt, startedAt); + + // Saves round 2 data + uint80 roundId2 = roundId; + int256 answer2 = answer; + uint256 startedAt2 = startedAt; + uint256 updatedAt2 = updatedAt; + uint80 answeredInRound2 = answeredInRound; + + // Checks that last round is still returning the correct data + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_scrollSequencerUptimeFeed.getRoundData(1); + assertEq(roundId, 1); + assertEq(answer, 0); + assertEq(answeredInRound, roundId); + assertEq(startedAt, updatedAt); + + // Assert latestRoundData corresponds to latest round id + (roundId, answer, startedAt, updatedAt, answeredInRound) = s_scrollSequencerUptimeFeed.latestRoundData(); + assertEq(roundId2, roundId); + assertEq(answer2, answer); + assertEq(startedAt2, startedAt); + assertEq(updatedAt2, updatedAt); + assertEq(answeredInRound2, answeredInRound); + } + + /// @notice it should revert from #getRoundData when round does not yet exist (future roundId) + function test_RevertGetRoundDataWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(ScrollSequencerUptimeFeed.NoDataPresent.selector)); + s_scrollSequencerUptimeFeed.getRoundData(2); + } + + /// @notice it should revert from #getAnswer when round does not yet exist (future roundId) + function test_RevertGetAnswerWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(ScrollSequencerUptimeFeed.NoDataPresent.selector)); + s_scrollSequencerUptimeFeed.getAnswer(2); + } + + /// @notice it should revert from #getTimestamp when round does not yet exist (future roundId) + function test_RevertGetTimestampWhenRoundDoesNotExistYet() public { + // Sets msg.sender and tx.origin to a valid address + vm.startPrank(s_l1OwnerAddr, s_l1OwnerAddr); + + // Gets data from a round that has not happened yet + vm.expectRevert(abi.encodeWithSelector(ScrollSequencerUptimeFeed.NoDataPresent.selector)); + s_scrollSequencerUptimeFeed.getTimestamp(2); + } +} + +contract ScrollSequencerUptimeFeed_ProtectReadsOnAggregatorV2V3InterfaceFunctions is ScrollSequencerUptimeFeedTest { + /// @notice it should disallow reads on AggregatorV2V3Interface functions when consuming contract is not whitelisted + function test_AggregatorV2V3InterfaceDisallowReadsIfConsumingContractIsNotWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_scrollSequencerUptimeFeed)); + + // Sanity - consumer is not whitelisted + assertEq(s_scrollSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_scrollSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), false); + + // Asserts reads are not possible from consuming contract + vm.expectRevert("No access"); + feedConsumer.latestAnswer(); + vm.expectRevert("No access"); + feedConsumer.latestRoundData(); + } + + /// @notice it should allow reads on AggregatorV2V3Interface functions when consuming contract is whitelisted + function test_AggregatorV2V3InterfaceAllowReadsIfConsumingContractIsWhitelisted() public { + // Deploys a FeedConsumer contract + FeedConsumer feedConsumer = new FeedConsumer(address(s_scrollSequencerUptimeFeed)); + + // Whitelist consumer + s_scrollSequencerUptimeFeed.addAccess(address(feedConsumer)); + + // Sanity - consumer is whitelisted + assertEq(s_scrollSequencerUptimeFeed.checkEnabled(), true); + assertEq(s_scrollSequencerUptimeFeed.hasAccess(address(feedConsumer), abi.encode("")), true); + + // Asserts reads are possible from consuming contract + (uint80 roundId, int256 answer, , , ) = feedConsumer.latestRoundData(); + assertEq(feedConsumer.latestAnswer(), 0); + assertEq(roundId, 1); + assertEq(answer, 0); + } +} + +contract ScrollSequencerUptimeFeed_GasCosts is ScrollSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for updates + function test_GasCosts() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Assert initial conditions + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp(); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 0); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed; + uint256 gasStart; + uint256 gasFinal; + + // measures gas used for no update + expectedGasUsed = 10197; // NOTE: used to be 38594 in hardhat tests + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.updateStatus(false, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 0); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + + // measures gas used for update + expectedGasUsed = 31644; // NOTE: used to be 58458 in hardhat tests + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp + 1000)); + gasFinal = gasleft(); + assertEq(s_scrollSequencerUptimeFeed.latestAnswer(), 1); + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} + +contract ScrollSequencerUptimeFeed_AggregatorInterfaceGasCosts is ScrollSequencerUptimeFeedTest { + /// @notice it should consume a known amount of gas for getRoundData(uint80) + function test_GasUsageForGetRoundData() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 4504; // NOTE: used to be 30952 in hardhat tesst + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.getRoundData(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRoundData() + function test_GasUsageForLatestRoundData() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 2154; // NOTE: used to be 28523 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.latestRoundData(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestAnswer() + function test_GasUsageForLatestAnswer() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1566; // NOTE: used to be 28229 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.latestAnswer(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestTimestamp() + function test_GasUsageForLatestTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1459; // NOTE: used to be 28129 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.latestTimestamp(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for latestRound() + function test_GasUsageForLatestRound() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 1470; // NOTE: used to be 28145 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.latestRound(); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getAnswer() + function test_GasUsageForGetAnswer() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 3929; // NOTE: used to be 30682 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.getAnswer(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } + + /// @notice it should consume a known amount of gas for getTimestamp() + function test_GasUsageForGetTimestamp() public { + // Sets msg.sender and tx.origin to a valid address + address l2MessengerAddr = address(s_mockScrollL2CrossDomainMessenger); + vm.startPrank(l2MessengerAddr, l2MessengerAddr); + + // Defines helper variables for measuring gas usage + uint256 expectedGasUsed = 3817; // NOTE: used to be 30570 in hardhat tests + uint256 gasStart; + uint256 gasFinal; + + // Initializes a round + uint256 timestamp = s_scrollSequencerUptimeFeed.latestTimestamp() + 1000; + s_scrollSequencerUptimeFeed.updateStatus(true, uint64(timestamp)); + + // Measures gas usage + gasStart = gasleft(); + s_scrollSequencerUptimeFeed.getTimestamp(1); + gasFinal = gasleft(); + + // Checks that gas usage is within expected range + assertGasUsageIsCloseTo(expectedGasUsed, gasStart, gasFinal, GAS_USED_DEVIATION); + } +} diff --git a/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollValidator.t.sol b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollValidator.t.sol new file mode 100644 index 00000000000..969c78c72ef --- /dev/null +++ b/contracts/src/v0.8/l2ep/test/v1_0_0/scroll/ScrollValidator.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {MockScrollL1CrossDomainMessenger} from "../../mocks/scroll/MockScrollL1CrossDomainMessenger.sol"; +import {MockScrollL2CrossDomainMessenger} from "../../mocks/scroll/MockScrollL2CrossDomainMessenger.sol"; +import {ScrollSequencerUptimeFeed} from "../../../dev/scroll/ScrollSequencerUptimeFeed.sol"; +import {ScrollValidator} from "../../../dev/scroll/ScrollValidator.sol"; +import {L2EPTest} from "../L2EPTest.t.sol"; + +contract ScrollValidatorTest is L2EPTest { + /// Helper constants + address internal constant L2_SEQ_STATUS_RECORDER_ADDRESS = 0x491B1dDA0A8fa069bbC1125133A975BF4e85a91b; + uint32 internal constant INIT_GAS_LIMIT = 1900000; + + /// L2EP contracts + MockScrollL1CrossDomainMessenger internal s_mockScrollL1CrossDomainMessenger; + MockScrollL2CrossDomainMessenger internal s_mockScrollL2CrossDomainMessenger; + ScrollSequencerUptimeFeed internal s_scrollSequencerUptimeFeed; + ScrollValidator internal s_scrollValidator; + + /// https://github.com/scroll-tech/scroll/blob/03089eaeee1193ff44c532c7038611ae123e7ef3/contracts/src/libraries/IScrollMessenger.sol#L22 + event SentMessage( + address indexed sender, + address indexed target, + uint256 value, + uint256 messageNonce, + uint256 gasLimit, + bytes message + ); + + /// Setup + function setUp() public { + s_mockScrollL1CrossDomainMessenger = new MockScrollL1CrossDomainMessenger(); + s_mockScrollL2CrossDomainMessenger = new MockScrollL2CrossDomainMessenger(); + + s_scrollSequencerUptimeFeed = new ScrollSequencerUptimeFeed( + address(s_mockScrollL1CrossDomainMessenger), + address(s_mockScrollL2CrossDomainMessenger), + true + ); + + s_scrollValidator = new ScrollValidator( + address(s_mockScrollL1CrossDomainMessenger), + address(s_scrollSequencerUptimeFeed), + INIT_GAS_LIMIT + ); + } +} + +contract ScrollValidator_SetGasLimit is ScrollValidatorTest { + /// @notice it correctly updates the gas limit + function test_CorrectlyUpdatesTheGasLimit() public { + uint32 newGasLimit = 2000000; + assertEq(s_scrollValidator.getGasLimit(), INIT_GAS_LIMIT); + s_scrollValidator.setGasLimit(newGasLimit); + assertEq(s_scrollValidator.getGasLimit(), newGasLimit); + } +} + +contract ScrollValidator_Validate is ScrollValidatorTest { + /// @notice it reverts if called by account with no access + function test_RevertsIfCalledByAnAccountWithNoAccess() public { + vm.startPrank(s_strangerAddr); + vm.expectRevert("No access"); + s_scrollValidator.validate(0, 0, 1, 1); + } + + /// @notice it posts sequencer status when there is not status change + function test_PostSequencerStatusWhenThereIsNotStatusChange() public { + // Gives access to the s_eoaValidator + s_scrollValidator.addAccess(s_eoaValidator); + + // Sets block.timestamp to a later date + uint256 futureTimestampInSeconds = block.timestamp + 5000; + vm.startPrank(s_eoaValidator); + vm.warp(futureTimestampInSeconds); + + // Sets up the expected event data + vm.expectEmit(false, false, false, true); + emit SentMessage( + address(s_scrollValidator), // sender + L2_SEQ_STATUS_RECORDER_ADDRESS, // target + 0, // value + 0, // nonce + INIT_GAS_LIMIT, // gas limit + abi.encodeWithSelector(ScrollSequencerUptimeFeed.updateStatus.selector, false, futureTimestampInSeconds) // message + ); + + // Runs the function (which produces the event to test) + s_scrollValidator.validate(0, 0, 0, 0); + } + + /// @notice it post sequencer offline + function test_PostSequencerOffline() public { + // Gives access to the s_eoaValidator + s_scrollValidator.addAccess(s_eoaValidator); + + // Sets block.timestamp to a later date + uint256 futureTimestampInSeconds = block.timestamp + 10000; + vm.startPrank(s_eoaValidator); + vm.warp(futureTimestampInSeconds); + + // Sets up the expected event data + vm.expectEmit(false, false, false, true); + emit SentMessage( + address(s_scrollValidator), // sender + L2_SEQ_STATUS_RECORDER_ADDRESS, // target + 0, // value + 0, // nonce + INIT_GAS_LIMIT, // gas limit + abi.encodeWithSelector(ScrollSequencerUptimeFeed.updateStatus.selector, true, futureTimestampInSeconds) // message + ); + + // Runs the function (which produces the event to test) + s_scrollValidator.validate(0, 0, 1, 1); + } +} diff --git a/contracts/src/v0.8/tests/MockArbitrumInbox.sol b/contracts/src/v0.8/tests/MockArbitrumInbox.sol index cd85ed4d6ea..445a361b309 100644 --- a/contracts/src/v0.8/tests/MockArbitrumInbox.sol +++ b/contracts/src/v0.8/tests/MockArbitrumInbox.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + import {IInbox} from "../vendor/arb-bridge-eth/v0.8.0-custom/contracts/bridge/interfaces/IInbox.sol"; import {IBridge} from "../vendor/arb-bridge-eth/v0.8.0-custom/contracts/bridge/interfaces/IBridge.sol"; @@ -13,46 +16,46 @@ contract MockArbitrumInbox is IInbox { bytes data ); - function sendL2Message(bytes calldata messageData) external override returns (uint256) { + function sendL2Message(bytes calldata /* messageData */) external pure override returns (uint256) { return 0; } function sendUnsignedTransaction( - uint256 maxGas, - uint256 gasPriceBid, - uint256 nonce, - address destAddr, - uint256 amount, - bytes calldata data - ) external override returns (uint256) { + uint256 /* maxGas */, + uint256 /* gasPriceBid */, + uint256 /* nonce */, + address /* destAddr */, + uint256 /* amount */, + bytes calldata /* data */ + ) external pure override returns (uint256) { return 0; } function sendContractTransaction( - uint256 maxGas, - uint256 gasPriceBid, - address destAddr, - uint256 amount, - bytes calldata data - ) external override returns (uint256) { + uint256 /* maxGas */, + uint256 /* gasPriceBid */, + address /* destAddr */, + uint256 /* amount */, + bytes calldata /* data */ + ) external pure override returns (uint256) { return 0; } function sendL1FundedUnsignedTransaction( - uint256 maxGas, - uint256 gasPriceBid, - uint256 nonce, - address destAddr, - bytes calldata data + uint256 /* maxGas */, + uint256 /* gasPriceBid */, + uint256 /* nonce */, + address /* destAddr */, + bytes calldata /* data */ ) external payable override returns (uint256) { return 0; } function sendL1FundedContractTransaction( - uint256 maxGas, - uint256 gasPriceBid, - address destAddr, - bytes calldata data + uint256 /* maxGas */, + uint256 /* gasPriceBid */, + address /* destAddr */, + bytes calldata /* data */ ) external payable override returns (uint256) { return 0; } @@ -81,32 +84,32 @@ contract MockArbitrumInbox is IInbox { } function createRetryableTicket( - address destAddr, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data + address /* destAddr */, + uint256 /* arbTxCallValue */, + uint256 /* maxSubmissionCost */, + address /* submissionRefundAddress */, + address /* valueRefundAddress */, + uint256 /* maxGas */, + uint256 /* gasPriceBid */, + bytes calldata /* data */ ) external payable override returns (uint256) { return 0; } - function depositEth(address destAddr) external payable override returns (uint256) { + function depositEth(address /* destAddr */) external payable override returns (uint256) { return 0; } function depositEthRetryable( - address destAddr, - uint256 maxSubmissionCost, - uint256 maxGas, - uint256 maxGasPrice + address /* destAddr */, + uint256 /* maxSubmissionCost */, + uint256 /* maxGas */, + uint256 /* maxGasPrice */ ) external payable override returns (uint256) { return 0; } - function bridge() external view override returns (IBridge) { + function bridge() external pure override returns (IBridge) { return IBridge(address(0)); } diff --git a/contracts/src/v0.8/tests/MockOptimismL1CrossDomainMessenger.sol b/contracts/src/v0.8/tests/MockOptimismL1CrossDomainMessenger.sol index 3184e7bb4a5..a92ff8fb556 100644 --- a/contracts/src/v0.8/tests/MockOptimismL1CrossDomainMessenger.sol +++ b/contracts/src/v0.8/tests/MockOptimismL1CrossDomainMessenger.sol @@ -8,7 +8,7 @@ contract MockOptimismL1CrossDomainMessenger is IL1CrossDomainMessenger { uint256 private s_nonce; // slither-disable-next-line external-function - function xDomainMessageSender() public view returns (address) { + function xDomainMessageSender() public pure returns (address) { return address(0); } diff --git a/contracts/src/v0.8/vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol index 6d74788e352..8b5aad82e5d 100644 --- a/contracts/src/v0.8/vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol +++ b/contracts/src/v0.8/vendor/@eth-optimism/contracts/v0.4.7/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity >=0.7.6 <0.9.0; /** @@ -28,9 +29,5 @@ interface iOVM_CrossDomainMessenger { * @param _message Message to send to the target. * @param _gasLimit Gas limit for the provided message. */ - function sendMessage( - address _target, - bytes calldata _message, - uint32 _gasLimit - ) external; + function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external; } diff --git a/contracts/test/v0.8/dev/ScrollSequencerUptimeFeed.test.ts b/contracts/test/v0.8/dev/ScrollSequencerUptimeFeed.test.ts index b294032e73d..1d93497b9fa 100644 --- a/contracts/test/v0.8/dev/ScrollSequencerUptimeFeed.test.ts +++ b/contracts/test/v0.8/dev/ScrollSequencerUptimeFeed.test.ts @@ -21,7 +21,7 @@ describe('ScrollSequencerUptimeFeed', () => { dummy = accounts[3] const l2CrossDomainMessengerFactory = await ethers.getContractFactory( - 'src/v0.8/l2ep/test/mocks/MockScrollL2CrossDomainMessenger.sol:MockScrollL2CrossDomainMessenger', + 'src/v0.8/l2ep/test/mocks/scroll/MockScrollL2CrossDomainMessenger.sol:MockScrollL2CrossDomainMessenger', deployer, ) diff --git a/contracts/test/v0.8/dev/ScrollValidator.test.ts b/contracts/test/v0.8/dev/ScrollValidator.test.ts index 866d52b202f..c5ec59c5c99 100644 --- a/contracts/test/v0.8/dev/ScrollValidator.test.ts +++ b/contracts/test/v0.8/dev/ScrollValidator.test.ts @@ -30,7 +30,7 @@ describe('ScrollValidator', () => { // Scroll Messenger contract on L1 const mockScrollL1CrossDomainMessengerFactory = await ethers.getContractFactory( - 'src/v0.8/l2ep/test/mocks/MockScrollL1CrossDomainMessenger.sol:MockScrollL1CrossDomainMessenger', + 'src/v0.8/l2ep/test/mocks/scroll/MockScrollL1CrossDomainMessenger.sol:MockScrollL1CrossDomainMessenger', ) mockScrollL1CrossDomainMessenger = await mockScrollL1CrossDomainMessengerFactory.deploy()