diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f15fee4c2..e1df41ad5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 with: - version: stable + version: v1.5.1 - name: Install Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000..709c65daa --- /dev/null +++ b/.mise.toml @@ -0,0 +1,15 @@ +# mise tool versions — https://mise.jdx.dev +# +# This file pins the toolchain versions used across the contracts repo. +# Contributors should install mise and run `mise install` at the repo root so +# that everyone executes builds, tests, and snapshot generation with +# byte-identical tooling. +# +# When bumping any version here, also update the matching pin in +# `.github/workflows/test.yml` (or anywhere else CI installs the same tool) so +# that local and CI runs stay aligned. + +[tools] +# Foundry (forge/cast/anvil/chisel) — pinned for deterministic build +# artifacts, ABI/storage snapshots, and semver-lock hashes. +foundry = "1.5.1" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c4c68cb46 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# AGENTS.md — base/contracts + +Working conventions for agents on this repo, distilled from prior sessions. +These are defaults for new or touched work, not permission to churn unrelated +code. When this file conflicts with the current tree, nearby code and enforced +tests win. + +## Tooling + +- `just snapshots` — regenerates ABI, storage layout, and semver-lock in one shot. Run after source changes that affect API, storage, or semver snapshots. +- `just semver-lock` — full source build + semver hash. Run before pushing semver-tagged `src/` changes or when `snapshots/semver-lock.json` may change. Do not use `just semver-lock-no-build` after source edits; it reads existing artifacts and can write stale hashes. +- `just semver-lock-no-build` — hash only, from existing artifacts. +- CI regenerates `snapshots/semver-lock.json` and diffs it against the committed file. ABI/storage snapshots are generated by `scripts/autogen`, but check the current workflow before assuming CI diffs every snapshot type. +- Snapshot generation is handled by Go scripts under `scripts/autogen`. +- Tests: `just test` (full suite), `just test --match-path ` (single file), `just test --match-test ` (single test). +- mise is used to keep local and CI Foundry versions in sync. Run `mise install` once after cloning. + +## Contract structure conventions + +### Style-guide ordering inside a contract + +For new or touched contract bodies, prefer: + +Type declarations → Constants → Immutables → State variables → Events → Errors → Constructor → external → external view → external pure → public → public view → public pure → internal → internal view → internal pure → private → private view → private pure. + +### Visibility + +- A concrete implementation **not meant to be inherited** should mark implementation internals `private`, not `internal`. +- Inline single-caller and single-line helpers by default; extract only when logic is genuinely shared across multiple callers. + +### Interface / implementation split + +- Deploy-script-backed protocol implementations generally inherit base contracts and shared mixins (proxy-owned base, initializable, reinitializable, semver), not their own `I` interface. +- When a contract has a paired interface, keep ABI-relevant declarations in sync with that interface. Do not invent a new interface only to satisfy this note. +- The `__constructor__() external` declaration in interfaces is a tooling artifact for ABI/snapshot generation, not callable. Do not remove it. +- For new semver functions in recent protocol work, prefer `function version() public pure virtual returns (string memory)`. + +### API / event surface + +- Use explicit `return x;`, not named returns. +- Trim events: drop params already carried in log metadata (e.g. block number). +- Add indexed query getters when arbitrary entries need querying, not just tail reads. + +### Naming + +- Errors: prefer `ContractName_ErrorName` for new protocol contracts. +- Events: no prefix (e.g. `SomethingRegistered`, `TimestampSet`). +- Test contracts: `ContractName_FunctionName_Test` for standard contract behavior suites; standalone unit tests may use `NameTest`. +- Test functions: prefer `test_functionName_scenario_succeeds` / `_reverts` for new tests. +- Commits: conventional — `type(scope): description` (e.g. `refactor(L1):`, `fix(L1):`). +- Branding: prefer `offchain` / `onchain` in new proof/protocol wording. + +## Access control + +- The proxy-owned base mixin provides `_assertOnlyProxyAdminOwner()` and `_assertOnlyProxyAdminOrProxyAdminOwner()`. +- For `ProxyAdminOwnedBase`, owner = `proxyAdmin().owner()`, read from the proxy-owner storage slot. +- Secondary roles in proxy-owned protocol contracts are plain `address` fields with inline `msg.sender` checks. Inline one-line local role checks. + +## Proxy deployment + +- For fresh deployment of an initialized proxy, use `upgradeToAndCall` (atomic upgrade + init in one tx). Separate `upgradeTo` then `initialize` opens a window of uninitialized state; mutations in that window corrupt persistent state permanently. Separate upgrade-only paths can be valid for admin migrations, tests, or proxies that are not being initialized. +- Upgradeable implementation constructors call `_disableInitializers()`. +- Contracts that inherit `ReinitializableBase` initialize with `reinitializer(initVersion())`. The `reinitializer` modifier fires **before** the function body, so the "already initialized" revert precedes any owner/admin assert — any caller triggers it. Contracts that use plain OpenZeppelin `initializer` are valid exceptions. + +## Adding a new `Initializable` contract + +Standard deploy-script-backed `src/` contracts with `initialize()` must be accounted for in the repo-wide reinitialization test that scans `src/` via FFI. The test has explicit exclusions for categories such as L2/predeploys, periphery, and contracts with custom initialization state. + +- **Deployed via the standard deploy script**: add both `Impl` and `Proxy` entries. Proxy address comes from the shared test setup; impl address via the EIP-1967 implementation-slot helper on the proxy. +- **Not yet in the deploy script**: add the source path to the excludes list (fixed capacity — mind the array size) with a comment. +- Proxied-contract detection uses the `@custom:proxied` devdoc NatSpec tag — a proxied contract missing that tag won't get its proxy entry checked. +- **Always run `forge test --match-test test_cannotReinitialize_succeeds`** when adding a standard initialized contract, in addition to the contract's own tests. A narrow `--match-path` run misses it. +- New production contracts must be wired into the standard deploy script, not permanently excluded from the tracking test. + +## Test conventions + +- Tests for standard deploy-script-deployed system contracts should inherit the shared `CommonTest` base and use the deploy-script-deployed instances it exposes (via the shared `Setup`) — **not** `new Contract()` + a hand-rolled proxy. This makes the deploy script a real dependency of those tests. +- Mirror an existing contract test of the same type: call `super.setUp()`, read parameters from the deploy config, and get the implementation via the EIP-1967 implementation-slot helper. +- If an initializer edge-case test for a deploy-script-backed contract truly needs a fresh uninitialized proxy, deploy **only** a proxy and point it at the already-deployed implementation (via the implementation-slot helper). Do not `new Contract()` in that test file; that dodges the deploy-script dependency. +- Re-initialize / initializer edge-case tests: reset the initializable slot with `vm.store(addr, bytes32(0), bytes32(0))` behind a small helper, then prank the proxy admin. Skip on forks with `skipIfForkTest(...)`. +- Run the **full** suite after adding a contract, not just its own file (cross-cutting registration tests live outside the contract's file). +- Kill redundant/duplicate tests: if an exact-value assertion subsumes a weaker one, keep one. diff --git a/README.md b/README.md index 9f9ae270a..b1aa36e80 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ # contracts -This repo contains contracts and scripts for Base. -Note that Base primarily utilizes Optimism's bedrock contracts located in Optimism's repo [here](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock). +This repo contains the contracts and scripts for Base. For contract deployment artifacts, see [base-org/contract-deployments](https://github.com/base-org/contract-deployments). diff --git a/audits/README.md b/audits/README.md new file mode 100644 index 000000000..17f39758c --- /dev/null +++ b/audits/README.md @@ -0,0 +1,11 @@ +# Audits + +Audits of the formerly shared OP Stack components can be found in [Optimism’s repository](https://github.com/ethereum-optimism/optimism/tree/develop/docs/security-reviews). The audits of the OP Stack components through Upgrade 18 are relevant for Base. Going forward, audits of the Base Stack smart contracts and protocol can be found here. + +| Date | Reviewer | Focus and Scope | Report Link | Commit Hash | +| :---- | :---- | :---- | :---- | :---- | +| 03/17/26 | [Cantina](https://cantina.xyz/portfolio/25ba64ea-d6f3-411e-8338-419ffc385ba6) | AggregateVerifier Multiproof Contracts | [Report](./cantina_coinbase_multiproof_mar2026.pdf) | [b6c4689b8f814bf23fb915ac1d68a537d707ae4e](https://github.com/base/contracts/tree/b6c4689b8f814bf23fb915ac1d68a537d707ae4e/) | +| 03/19/26 | [Cantina](https://cantina.xyz/portfolio/423a9f33-b710-4445-a125-950b0a7771d7) | TEE Multiproof Contracts | [Report](./cantina_coinbase_nitro_enclave_mar2026.pdf) | [2421afdd332a98e9b45c6caf0a2e26b896b17e0d](https://github.com/base/contracts/tree/2421afdd332a98e9b45c6caf0a2e26b896b17e0d) | +| 04/10/26 | [Cantina](https://cantina.xyz/portfolio/a4f952cf-1c5b-4e3c-8153-c3adff899613) | TEE Multiproof Contracts 2 | [Report](./cantina_coinbase_nitro_enclave_apr2026.pdf) | [fe2af8cbffefa44bbb1a3917507f8bd8ebec7a2](https://github.com/base/contracts/tree/ffe2af8cbffefa44bbb1a3917507f8bd8ebec7a2) | +| 04/10/26 | [Cantina](https://cantina.xyz/portfolio/b72c7078-f6da-4074-a3bd-4f938f469fb7) | AggregateVerifier Multiproof Contracts 2 | [Report](./cantina_coinbase_aggregateverifier_apr2026.pdf) | [ffe2af8cbffefa44bbb1a3917507f8bd8ebec7a2](https://github.com/base/contracts/tree/ffe2af8cbffefa44bbb1a3917507f8bd8ebec7a2) | +| 06/04/26 | [Cantina](https://cantina.xyz/portfolio/6ce647dc-3b2c-448c-9421-426087341ce8) | Proof Contracts Update | [Report](./cantina_coinbase_proof_contracts_update_jun2026.pdf) | [e225648a7ed538e7e28c041d44f3b7a606ba7743](https://github.com/base/contracts/tree/e225648a7ed538e7e28c041d44f3b7a606ba7743) | \ No newline at end of file diff --git a/audits/cantina_coinbase_aggregateverifier_apr2026.pdf b/audits/cantina_coinbase_aggregateverifier_apr2026.pdf new file mode 100644 index 000000000..db2acee5b Binary files /dev/null and b/audits/cantina_coinbase_aggregateverifier_apr2026.pdf differ diff --git a/audits/cantina_coinbase_multiproof_mar2026.pdf b/audits/cantina_coinbase_multiproof_mar2026.pdf new file mode 100644 index 000000000..5debd6359 Binary files /dev/null and b/audits/cantina_coinbase_multiproof_mar2026.pdf differ diff --git a/audits/cantina_coinbase_nitro_enclave_apr2026.pdf b/audits/cantina_coinbase_nitro_enclave_apr2026.pdf new file mode 100644 index 000000000..010b49cd2 Binary files /dev/null and b/audits/cantina_coinbase_nitro_enclave_apr2026.pdf differ diff --git a/audits/cantina_coinbase_nitro_enclave_mar2026.pdf b/audits/cantina_coinbase_nitro_enclave_mar2026.pdf new file mode 100644 index 000000000..fe676717e Binary files /dev/null and b/audits/cantina_coinbase_nitro_enclave_mar2026.pdf differ diff --git a/audits/cantina_coinbase_proof_contracts_update_jun2026.pdf b/audits/cantina_coinbase_proof_contracts_update_jun2026.pdf new file mode 100644 index 000000000..1bc31958f Binary files /dev/null and b/audits/cantina_coinbase_proof_contracts_update_jun2026.pdf differ diff --git a/deploy-config/local-tee.json b/deploy-config/local-tee.json index 53dc3efde..9e223905d 100644 --- a/deploy-config/local-tee.json +++ b/deploy-config/local-tee.json @@ -23,6 +23,7 @@ "multiproofGameType": 621, "multiproofGenesisBlockNumber": 0, "multiproofIntermediateBlockInterval": 10, + "multiproofMaxUpgradeId": 12, "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "nitroEnclaveVerifier": "0x0000000000000000000000000000000000000000", "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", diff --git a/deploy-config/local.json b/deploy-config/local.json index 738f0f0ee..1f4a681b0 100644 --- a/deploy-config/local.json +++ b/deploy-config/local.json @@ -22,6 +22,7 @@ "multiproofGameType": 621, "multiproofGenesisBlockNumber": 0, "multiproofIntermediateBlockInterval": 10, + "multiproofMaxUpgradeId": 12, "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "nitroEnclaveVerifier": "0x0000000000000000000000000000000000000000", "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", diff --git a/deploy-config/mainnet.json b/deploy-config/mainnet.json index ecdde63b3..f6d851c86 100644 --- a/deploy-config/mainnet.json +++ b/deploy-config/mainnet.json @@ -12,8 +12,11 @@ "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", "l1FeeVaultWithdrawalNetwork": 0, + "l2BlockTime": 2, "l2ChainId": 10, "l2GenesisBlockGasLimit": "0x1c9c380", + "l2GenesisBlockNumber": 0, + "l2GenesisTimestamp": 1686789347, "l2OutputOracleStartingBlockNumber": 105235063, "l2OutputOracleStartingTimestamp": 1686068903, "multiproofBlockInterval": 600, @@ -22,6 +25,7 @@ "multiproofGenesisBlockNumber": 0, "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "multiproofIntermediateBlockInterval": 30, + "multiproofMaxUpgradeId": 12, "nitroEnclaveVerifier": "0x0000000000000000000000000000000000000000", "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "operatorFeeVaultRecipient": "0xa3d596EAfaB6B13Ab18D40FaE1A962700C84ADEa", diff --git a/deploy-config/sepolia.json b/deploy-config/sepolia.json index cfcea5d2d..eebc3ccd9 100644 --- a/deploy-config/sepolia.json +++ b/deploy-config/sepolia.json @@ -12,8 +12,11 @@ "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "l1FeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", "l1FeeVaultWithdrawalNetwork": 0, + "l2BlockTime": 2, "l2ChainId": 11155420, "l2GenesisBlockGasLimit": "0x1c9c380", + "l2GenesisBlockNumber": 0, + "l2GenesisTimestamp": 1691802540, "l2OutputOracleStartingBlockNumber": 0, "l2OutputOracleStartingTimestamp": 1690493568, "multiproofBlockInterval": 600, @@ -22,6 +25,7 @@ "multiproofGenesisBlockNumber": 37223829, "multiproofGenesisOutputRoot": "0xbc273d5876d1858ecd5aaf4ce4eaf16c73f0187ca4271b774ed5da7d2254ba79", "multiproofIntermediateBlockInterval": 30, + "multiproofMaxUpgradeId": 12, "nitroEnclaveVerifier": "0x77461a6434fFE3435206B19658F33274f3104e07", "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", "operatorFeeVaultRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", diff --git a/interfaces/L1/IETHLockbox.sol b/interfaces/L1/IETHLockbox.sol deleted file mode 100644 index c5ee53d44..000000000 --- a/interfaces/L1/IETHLockbox.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { ISemver } from "interfaces/universal/ISemver.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; - -interface IETHLockbox is IProxyAdminOwnedBase, ISemver, IReinitializableBase { - error ETHLockbox_Unauthorized(); - error ETHLockbox_Paused(); - error ETHLockbox_InsufficientBalance(); - error ETHLockbox_NoWithdrawalTransactions(); - error ETHLockbox_DifferentSuperchainConfig(); - - event Initialized(uint8 version); - event ETHLocked(IOptimismPortal2 indexed portal, uint256 amount); - event ETHUnlocked(IOptimismPortal2 indexed portal, uint256 amount); - event PortalAuthorized(IOptimismPortal2 indexed portal); - event LockboxAuthorized(IETHLockbox indexed lockbox); - event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); - event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); - - function initialize(ISystemConfig _systemConfig, IOptimismPortal2[] calldata _portals) external; - function systemConfig() external view returns (ISystemConfig); - function paused() external view returns (bool); - function authorizedPortals(IOptimismPortal2) external view returns (bool); - function authorizedLockboxes(IETHLockbox) external view returns (bool); - function receiveLiquidity() external payable; - function lockETH() external payable; - function unlockETH(uint256 _value) external; - function authorizePortal(IOptimismPortal2 _portal) external; - function authorizeLockbox(IETHLockbox _lockbox) external; - function migrateLiquidity(IETHLockbox _lockbox) external; - function superchainConfig() external view returns (ISuperchainConfig); - - function __constructor__() external; -} diff --git a/interfaces/L1/IOptimismPortal2.sol b/interfaces/L1/IOptimismPortal2.sol index 210532d3a..9a5d8462a 100644 --- a/interfaces/L1/IOptimismPortal2.sol +++ b/interfaces/L1/IOptimismPortal2.sol @@ -9,7 +9,6 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; interface IOptimismPortal2 is IProxyAdminOwnedBase { error ContentLengthMismatch(); @@ -34,7 +33,6 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { error OptimismPortal_ProofNotOldEnough(); error OptimismPortal_Unproven(); error OptimismPortal_ImmediateFinalityNotEnabled(); - error OptimismPortal_InvalidLockboxState(); error OutOfGas(); error UnexpectedList(); error UnexpectedString(); @@ -48,7 +46,6 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { receive() external payable; function anchorStateRegistry() external view returns (IAnchorStateRegistry); - function ethLockbox() external view returns (IETHLockbox); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol new file mode 100644 index 000000000..0d4bf1b79 --- /dev/null +++ b/interfaces/L1/IProtocolVersions.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; + +/// @title IProtocolVersions +/// @notice Interface for the ProtocolVersions upgrade schedule contract. +interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBase { + event UpgradeRegistered(uint256 indexed id); + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); + event TimestampSet(uint256 indexed id, uint256 timestamp); + event ScheduleIdUpdated(bytes32 indexed newScheduleId); + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); + event Initialized(uint8 version); + + error ProtocolVersions_UnknownUpgrade(uint256 id); + error ProtocolVersions_InvalidProtocolVersion(); + error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); + error ProtocolVersions_NotIncidentResponder(); + error ProtocolVersions_NotScheduled(uint256 id); + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + error ProtocolVersions_StaticScheduleHole(uint256 id, uint256 nextScheduledId); + error ProtocolVersions_TimestampNotAfterPrevious( + uint256 id, uint256 previousId, uint64 previousTimestamp, uint64 timestamp + ); + error ProtocolVersions_TimestampNotBeforeNext(uint256 id, uint256 nextId, uint64 timestamp, uint64 nextTimestamp); + error ProtocolVersions_NotInitialized(); + error ProtocolVersions_InsufficientNotice(uint64 timestamp); + + function initialize(address _incidentResponder) external; + function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256); + function setMinimumProtocolVersion(uint256 protocolVersion) external; + function setTimestamp(uint256 id, uint64 timestamp) external; + function setIncidentResponder(address newIncidentResponder) external; + function delayTimestamp(uint256 id, uint64 newTimestamp) external; + + function MIN_NOTICE() external view returns (uint64); + function minimumProtocolVersion() external view returns (uint256); + function incidentResponder() external view returns (address); + function scheduleId() external view returns (bytes32); + function scheduleId(uint256 id) external view returns (bytes32); + function activatedScheduleId(uint64 l2Timestamp) external view returns (bytes32); + function getSchedule() external view returns (uint64[] memory); + + function __constructor__() external; +} diff --git a/interfaces/L1/proofs/IAggregateVerifier.sol b/interfaces/L1/proofs/IAggregateVerifier.sol index dd588e4ec..639743399 100644 --- a/interfaces/L1/proofs/IAggregateVerifier.sol +++ b/interfaces/L1/proofs/IAggregateVerifier.sol @@ -5,6 +5,7 @@ import { IDisputeGame } from "./IDisputeGame.sol"; import { IDisputeGameFactory } from "./IDisputeGameFactory.sol"; import { IDelayedWETH } from "./IDelayedWETH.sol"; import { IVerifier } from "./IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { Proposal, Hash } from "src/libraries/bridge/Types.sol"; import { Timestamp } from "src/libraries/bridge/LibUDT.sol"; @@ -23,6 +24,8 @@ interface IAggregateVerifier is IDisputeGame { function ZK_RANGE_HASH() external view returns (bytes32); function ZK_AGGREGATE_HASH() external view returns (bytes32); function CONFIG_HASH() external view returns (bytes32); + function PROTOCOL_VERSIONS() external view returns (IProtocolVersions); + function MAX_UPGRADE_ID() external view returns (uint256); function L2_CHAIN_ID() external view returns (uint256); function BLOCK_INTERVAL() external view returns (uint256); function INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); @@ -35,6 +38,7 @@ interface IAggregateVerifier is IDisputeGame { function counteredByIntermediateRootIndexPlusOne() external view returns (uint256); function expectedResolution() external view returns (Timestamp); function proofCount() external view returns (uint8); + function scheduleId() external view returns (bytes32); function initializeWithInitData(bytes calldata proof) external payable; function verifyProposalProof(bytes calldata proofBytes) external; diff --git a/interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol b/interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol index cffe9388f..a17b715f9 100644 --- a/interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol +++ b/interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol @@ -9,10 +9,8 @@ pragma solidity ^0.8.0; /// - Errors and events moved to the implementation contract /// - Adds new admin functions -/** - * @dev Enumeration of supported zero-knowledge proof coprocessor types - * Used to specify which proving system to use for attestation verification - */ +/// @dev Enumeration of supported zero-knowledge proof coprocessor types +/// Used to specify which proving system to use for attestation verification enum ZkCoProcessorType { Unknown, // RISC Zero zkVM proving system @@ -21,10 +19,8 @@ enum ZkCoProcessorType { Succinct } -/** - * @dev Configuration parameters for a specific zero-knowledge coprocessor - * Contains all necessary identifiers and addresses for ZK proof verification - */ +/// @dev Configuration parameters for a specific zero-knowledge coprocessor +/// Contains all necessary identifiers and addresses for ZK proof verification struct ZkCoProcessorConfig { // Latest program ID for single attestation verification bytes32 verifierId; @@ -34,10 +30,8 @@ struct ZkCoProcessorConfig { address zkVerifier; } -/** - * @dev Input structure for attestation report verification - * Contains the raw attestation data and trusted certificate chain length - */ +/// @dev Input structure for attestation report verification +/// Contains the raw attestation data and trusted certificate chain length struct VerifierInput { // Number of trusted certificates in the chain uint8 trustedCertsPrefixLen; @@ -45,10 +39,8 @@ struct VerifierInput { bytes attestationReport; } -/** - * @dev Output structure containing verified attestation data and metadata - * This represents the journal/output from zero-knowledge proof verification - */ +/// @dev Output structure containing verified attestation data and metadata +/// This represents the journal/output from zero-knowledge proof verification struct VerifierJournal { // Overall verification result status VerificationResult result; @@ -72,10 +64,8 @@ struct VerifierJournal { string moduleId; } -/** - * @dev Public value (journal) structure for batch verification operations - * Contains the aggregated results of multiple attestation verifications - */ +/// @dev Public value (journal) structure for batch verification operations +/// Contains the aggregated results of multiple attestation verifications struct BatchVerifierJournal { // Verification key that was used for batch verification bytes32 verifierVk; @@ -83,19 +73,15 @@ struct BatchVerifierJournal { VerifierJournal[] outputs; } -/** - * @dev 48-byte data structure for storing PCR values - * Split into two parts due to Solidity's 32-byte word limitation - */ +/// @dev 48-byte data structure for storing PCR values +/// Split into two parts due to Solidity's 32-byte word limitation struct Bytes48 { bytes32 first; bytes16 second; } -/** - * @dev Platform Configuration Register (PCR) entry - * PCRs contain cryptographic measurements of the enclave's runtime state - */ +/// @dev Platform Configuration Register (PCR) entry +/// PCRs contain cryptographic measurements of the enclave's runtime state struct Pcr { // PCR index number (0-23 for AWS Nitro Enclaves) uint64 index; @@ -103,13 +89,11 @@ struct Pcr { Bytes48 value; } -/** - * @dev Enumeration of possible attestation verification results - * Indicates the outcome of the verification process - * - * Note: Unknown is intentionally placed at index 0 so that uninitialized enum - * variables default to a failure state rather than Success (fail-closed). - */ +/// @dev Enumeration of possible attestation verification results +/// Indicates the outcome of the verification process +/// +/// Note: Unknown is intentionally placed at index 0 so that uninitialized enum +/// variables default to a failure state rather than Success (fail-closed). enum VerificationResult { // Default/uninitialized value — treated as a verification failure Unknown, @@ -123,156 +107,124 @@ enum VerificationResult { InvalidTimestamp } -/** - * @title INitroEnclaveVerifier - * @dev Interface for AWS Nitro Enclave attestation verification using zero-knowledge proofs - * - * This interface defines the contract for verifying AWS Nitro Enclave attestation reports - * on-chain using zero-knowledge proof systems (RISC Zero or Succinct SP1). The verifier - * validates the cryptographic integrity of attestation reports while maintaining privacy - * and reducing gas costs through ZK proofs. - * - * Key features: - * - Single and batch attestation verification - * - Support for multiple ZK proving systems - * - Route-based verifier configuration - * - Certificate chain management and revocation - * - Timestamp validation with configurable tolerance - * - Platform Configuration Register (PCR) verification - */ +/// @title INitroEnclaveVerifier +/// @dev Interface for AWS Nitro Enclave attestation verification using zero-knowledge proofs +/// +/// This interface defines the contract for verifying AWS Nitro Enclave attestation reports +/// onchain using zero-knowledge proof systems (RISC Zero or Succinct SP1). The verifier +/// validates the cryptographic integrity of attestation reports while maintaining privacy +/// and reducing gas costs through ZK proofs. +/// +/// Key features: +/// - Single and batch attestation verification +/// - Support for multiple ZK proving systems +/// - Route-based verifier configuration +/// - Certificate chain management and revocation +/// - Timestamp validation with configurable tolerance +/// - Platform Configuration Register (PCR) verification interface INitroEnclaveVerifier { // ============ Query Functions ============ - /** - * @dev Returns the maximum allowed time difference for attestation timestamp validation - * @return Maximum time difference in seconds between attestation time and current block time - */ + /// @dev Returns the maximum allowed time difference for attestation timestamp validation + /// @return Maximum time difference in seconds between attestation time and current block time function maxTimeDiff() external view returns (uint64); - /** - * @dev Returns the hash of the trusted root certificate - * @return Hash of the AWS Nitro Enclave root certificate - */ + /// @dev Returns the hash of the trusted root certificate + /// @return Hash of the AWS Nitro Enclave root certificate function rootCert() external view returns (bytes32); - /** - * @dev Returns the address of the proof submitter - * @return Address of the proof submitter - */ + /// @dev Returns the address of the proof submitter + /// @return Address of the proof submitter function proofSubmitter() external view returns (address); - /** - * @dev Returns the address authorized to revoke intermediate certificates - * @return Address of the revoker (address(0) if disabled) - */ + /// @dev Returns the address authorized to revoke intermediate certificates + /// @return Address of the revoker (address(0) if disabled) function revoker() external view returns (address); - /** - * @dev Returns whether the given intermediate certificate hash has been revoked. - * @param _certHash Hash of the certificate - * @return `true` if the certificate is currently marked as revoked - * - * The revocation sentinel is persistent across `_cacheNewCert` overwrites and - * blocks both verification (via `_verifyJournal`) and the off-chain - * `checkTrustedIntermediateCerts` helper from re-trusting the hash. Re-trust - * requires an explicit `unrevokeCert` call. - */ + /// @dev Returns whether the given intermediate certificate hash has been revoked. + /// @param _certHash Hash of the certificate + /// @return `true` if the certificate is currently marked as revoked + /// + /// The revocation sentinel is persistent across `_cacheNewCert` overwrites and + /// blocks both verification (via `_verifyJournal`) and the offchain + /// `checkTrustedIntermediateCerts` helper from re-trusting the hash. Re-trust + /// requires an explicit `unrevokeCert` call. function revokedCerts(bytes32 _certHash) external view returns (bool); - /** - * @dev Returns the cached `notAfter` timestamp (seconds) for an intermediate certificate. - * @param _certHash Hash of the certificate - * @return Cached expiry timestamp; `0` indicates the certificate is not currently - * cached (either never seen, expired-and-evicted, or revoked). - */ + /// @dev Returns the cached `notAfter` timestamp (seconds) for an intermediate certificate. + /// @param _certHash Hash of the certificate + /// @return Cached expiry timestamp; `0` indicates the certificate is not currently + /// cached (either never seen, expired-and-evicted, or revoked). function trustedIntermediateCerts(bytes32 _certHash) external view returns (uint64); - /** - * @dev Retrieves the configuration for a specific coprocessor - * @param _zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) - * @return ZkCoProcessorConfig Configuration parameters including program IDs and verifier address - */ + /// @dev Retrieves the configuration for a specific coprocessor + /// @param _zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) + /// @return ZkCoProcessorConfig Configuration parameters including program IDs and verifier address function getZkConfig(ZkCoProcessorType _zkCoProcessor) external view returns (ZkCoProcessorConfig memory); - /** - * @dev Gets the verifier address for a specific route - * @param _zkCoProcessor Type of ZK coprocessor - * @param _selector Proof selector - * @return Verifier address (route-specific or default fallback) - * - * Note: Reverts if the route is frozen - */ + /// @dev Gets the verifier address for a specific route + /// @param _zkCoProcessor Type of ZK coprocessor + /// @param _selector Proof selector + /// @return Verifier address (route-specific or default fallback) + /// + /// Note: Reverts if the route is frozen function getZkVerifier(ZkCoProcessorType _zkCoProcessor, bytes4 _selector) external view returns (address); - /** - * @dev Returns the verifierProofId for a given ZkCoProcessorType - * @param _zkCoProcessor Type of ZK coprocessor - * @return The corresponding verifierProofId - */ + /// @dev Returns the verifierProofId for a given ZkCoProcessorType + /// @param _zkCoProcessor Type of ZK coprocessor + /// @return The corresponding verifierProofId function getVerifierProofId(ZkCoProcessorType _zkCoProcessor) external view returns (bytes32); - /** - * @dev Checks how many certificates in each report are trusted - * @param _report_certs Array of certificate chains, each containing certificate hashes - * @return Array indicating the number of trusted certificates in each chain - * - * For each certificate chain: - * - Validates that the first certificate matches the root certificate - * - Counts consecutive trusted certificates starting from the root - * - Returns the count of trusted certificates for each chain - */ + /// @dev Checks how many certificates in each report are trusted + /// @param _report_certs Array of certificate chains, each containing certificate hashes + /// @return Array indicating the number of trusted certificates in each chain + /// + /// For each certificate chain: + /// - Validates that the first certificate matches the root certificate + /// - Counts consecutive trusted certificates starting from the root + /// - Returns the count of trusted certificates for each chain function checkTrustedIntermediateCerts(bytes32[][] calldata _report_certs) external view returns (uint8[] memory); // ============ Admin Functions ============ - /** - * @dev Sets the trusted root certificate hash - * @param _rootCert Hash of the new root certificate - * - * Requirements: - * - Only callable by contract owner - */ + /// @dev Sets the trusted root certificate hash + /// @param _rootCert Hash of the new root certificate + /// + /// Requirements: + /// - Only callable by contract owner function setRootCert(bytes32 _rootCert) external; - /** - * @dev Updates the maximum allowed time difference for attestation timestamp validation - * @param _maxTimeDiff New maximum time difference in seconds - * - * Requirements: - * - Only callable by contract owner - * - Must be greater than zero - */ + /// @dev Updates the maximum allowed time difference for attestation timestamp validation + /// @param _maxTimeDiff New maximum time difference in seconds + /// + /// Requirements: + /// - Only callable by contract owner + /// - Must be greater than zero function setMaxTimeDiff(uint64 _maxTimeDiff) external; - /** - * @dev Sets the proof submitter address - * @param _proofSubmitter The address of the proof submitter - * - * Requirements: - * - Only callable by contract owner - * - Address must not be zero - */ + /// @dev Sets the proof submitter address + /// @param _proofSubmitter The address of the proof submitter + /// + /// Requirements: + /// - Only callable by contract owner + /// - Address must not be zero function setProofSubmitter(address _proofSubmitter) external; - /** - * @dev Updates the revoker address - * @param _newRevoker New revoker address (can be address(0) to disable the revoker role) - * - * Requirements: - * - Only callable by contract owner - */ + /// @dev Updates the revoker address + /// @param _newRevoker New revoker address (can be address(0) to disable the revoker role) + /// + /// Requirements: + /// - Only callable by contract owner function setRevoker(address _newRevoker) external; - /** - * @dev Configures the zero-knowledge verification parameters for a specific coprocessor - * @param _zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) - * @param _config Configuration parameters including program IDs and verifier address - * @param _verifierProofId The verifierProofId corresponding to the verifierId in config - * - * Requirements: - * - Only callable by contract owner - * - Must specify valid coprocessor type and configuration - */ + /// @dev Configures the zero-knowledge verification parameters for a specific coprocessor + /// @param _zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) + /// @param _config Configuration parameters including program IDs and verifier address + /// @param _verifierProofId The verifierProofId corresponding to the verifierId in config + /// + /// Requirements: + /// - Only callable by contract owner + /// - Must specify valid coprocessor type and configuration function setZkConfiguration( ZkCoProcessorType _zkCoProcessor, ZkCoProcessorConfig memory _config, @@ -280,47 +232,40 @@ interface INitroEnclaveVerifier { ) external; - /** - * @dev Revokes a trusted intermediate certificate. - * @param _certHash Hash of the certificate to revoke - * - * Requirements: - * - Only callable by contract owner or revoker - * - Certificate must exist in the trusted set - * - * In addition to clearing the cached entry, this flips a persistent - * revocation sentinel that survives later cache writes. Subsequent - * verifications whose chain traverses the revoked hash are rejected - * regardless of the journal-supplied `trustedCertsPrefixLen`. Re-trust - * requires an explicit `unrevokeCert` call. - */ + /// @dev Revokes an intermediate certificate, whether or not it has been cached as trusted. + /// @param _certHash Hash of the certificate to revoke + /// + /// Requirements: + /// - Only callable by contract owner or revoker + /// + /// In addition to clearing any cached entry, this flips a persistent revocation + /// sentinel that survives later cache writes. Certificates never seen onchain can + /// be revoked preemptively. Subsequent verifications whose chain traverses the + /// revoked hash are rejected regardless of the journal-supplied + /// `trustedCertsPrefixLen`. Re-trust requires an explicit `unrevokeCert` call. function revokeCert(bytes32 _certHash) external; - /** - * @dev Explicitly re-trusts a previously revoked intermediate certificate. - * @param _certHash Hash of the certificate to un-revoke - * - * Requirements: - * - Only callable by contract owner - * - Certificate must currently be marked as revoked - * - * Clears the persistent revocation sentinel. The cached expiry is not - * restored here; the next successful verification whose chain traverses - * `_certHash` will re-cache it via `_cacheNewCert` with the journal-supplied - * `notAfter` timestamp. - */ + /// @dev Explicitly re-trusts a previously revoked intermediate certificate. + /// @param _certHash Hash of the certificate to un-revoke + /// + /// Requirements: + /// - Only callable by contract owner + /// - Certificate must currently be marked as revoked + /// + /// Clears the persistent revocation sentinel. The cached expiry is not + /// restored here; the next successful verification whose chain traverses + /// `_certHash` will re-cache it via `_cacheNewCert` with the journal-supplied + /// `notAfter` timestamp. function unrevokeCert(bytes32 _certHash) external; - /** - * @dev Updates the verifier program ID, adding the new version to the supported set - * @param _zkCoProcessor Type of ZK coprocessor - * @param _newVerifierId New verifier program ID to set as latest - * @param _newVerifierProofId New verifier proof ID (used in batch verification) - * - * Requirements: - * - Only callable by contract owner - * - New ID must be different from current latest - */ + /// @dev Updates the verifier program ID, adding the new version to the supported set + /// @param _zkCoProcessor Type of ZK coprocessor + /// @param _newVerifierId New verifier program ID to set as latest + /// @param _newVerifierProofId New verifier proof ID (used in batch verification) + /// + /// Requirements: + /// - Only callable by contract owner + /// - New ID must be different from current latest function updateVerifierId( ZkCoProcessorType _zkCoProcessor, bytes32 _newVerifierId, @@ -328,60 +273,52 @@ interface INitroEnclaveVerifier { ) external; - /** - * @dev Updates the aggregator program ID, adding the new version to the supported set - * @param _zkCoProcessor Type of ZK coprocessor - * @param _newAggregatorId New aggregator program ID to set as latest - * - * Requirements: - * - Only callable by contract owner - * - New ID must be different from current latest - */ + /// @dev Updates the aggregator program ID, adding the new version to the supported set + /// @param _zkCoProcessor Type of ZK coprocessor + /// @param _newAggregatorId New aggregator program ID to set as latest + /// + /// Requirements: + /// - Only callable by contract owner + /// - New ID must be different from current latest function updateAggregatorId(ZkCoProcessorType _zkCoProcessor, bytes32 _newAggregatorId) external; - /** - * @dev Adds a route-specific verifier override - * @param _zkCoProcessor Type of ZK coprocessor - * @param _selector Proof selector (first 4 bytes of proof data) - * @param _verifier Address of the verifier contract for this route - * - * Requirements: - * - Only callable by contract owner - * - Route must not be frozen - * - Verifier address must not be zero - */ + /// @dev Adds a route-specific verifier override + /// @param _zkCoProcessor Type of ZK coprocessor + /// @param _selector Proof selector (first 4 bytes of proof data) + /// @param _verifier Address of the verifier contract for this route + /// + /// Requirements: + /// - Only callable by contract owner + /// - Route must not be frozen + /// - Verifier address must not be zero function addVerifyRoute(ZkCoProcessorType _zkCoProcessor, bytes4 _selector, address _verifier) external; - /** - * @dev Permanently freezes a verification route - * @param _zkCoProcessor Type of ZK coprocessor - * @param _selector Proof selector to freeze - * - * Requirements: - * - Only callable by contract owner - * - Route must not already be frozen - * - * WARNING: This action is IRREVERSIBLE - */ + /// @dev Permanently freezes a verification route + /// @param _zkCoProcessor Type of ZK coprocessor + /// @param _selector Proof selector to freeze + /// + /// Requirements: + /// - Only callable by contract owner + /// - Route must not already be frozen + /// + /// WARNING: This action is IRREVERSIBLE function freezeVerifyRoute(ZkCoProcessorType _zkCoProcessor, bytes4 _selector) external; // ============ Verification Functions ============ - /** - * @dev Verifies a single attestation report using zero-knowledge proof - * @param output Encoded VerifierJournal containing the verification result - * @param zkCoprocessor Type of ZK coprocessor used to generate the proof - * @param proofBytes Zero-knowledge proof data for the attestation - * @return VerifierJournal containing the verification result and extracted data - * - * This function: - * 1. Verifies the ZK proof using the specified coprocessor - * 2. Decodes the verification result - * 3. Validates the certificate chain against trusted certificates - * 4. Checks timestamp validity within the allowed time difference - * 5. Caches newly discovered trusted certificates - * 6. Returns the complete verification result - */ + /// @dev Verifies a single attestation report using zero-knowledge proof + /// @param output Encoded VerifierJournal containing the verification result + /// @param zkCoprocessor Type of ZK coprocessor used to generate the proof + /// @param proofBytes Zero-knowledge proof data for the attestation + /// @return VerifierJournal containing the verification result and extracted data + /// + /// This function: + /// 1. Verifies the ZK proof using the specified coprocessor + /// 2. Decodes the verification result + /// 3. Validates the certificate chain against trusted certificates + /// 4. Checks timestamp validity within the allowed time difference + /// 5. Caches newly discovered trusted certificates + /// 6. Returns the complete verification result function verify( bytes calldata output, ZkCoProcessorType zkCoprocessor, @@ -390,20 +327,18 @@ interface INitroEnclaveVerifier { external returns (VerifierJournal memory); - /** - * @dev Verifies multiple attestation reports in a single batch operation - * @param output Encoded BatchVerifierJournal containing aggregated verification results - * @param zkCoprocessor Type of ZK coprocessor used to generate the proof - * @param proofBytes Zero-knowledge proof data for batch verification - * @return Array of VerifierJournal results, one for each attestation in the batch - * - * This function: - * 1. Verifies the ZK proof using the specified coprocessor - * 2. Decodes the batch verification results - * 3. Validates each attestation's certificate chain and timestamp - * 4. Caches newly discovered trusted certificates - * 5. Returns the verification results for all attestations - */ + /// @dev Verifies multiple attestation reports in a single batch operation + /// @param output Encoded BatchVerifierJournal containing aggregated verification results + /// @param zkCoprocessor Type of ZK coprocessor used to generate the proof + /// @param proofBytes Zero-knowledge proof data for batch verification + /// @return Array of VerifierJournal results, one for each attestation in the batch + /// + /// This function: + /// 1. Verifies the ZK proof using the specified coprocessor + /// 2. Decodes the batch verification results + /// 3. Validates each attestation's certificate chain and timestamp + /// 4. Caches newly discovered trusted certificates + /// 5. Returns the verification results for all attestations function batchVerify( bytes calldata output, ZkCoProcessorType zkCoprocessor, diff --git a/interfaces/L2/IBaseFeeVault.sol b/interfaces/L2/IBaseFeeVault.sol index 2c6c51ea6..7cb556cc8 100644 --- a/interfaces/L2/IBaseFeeVault.sol +++ b/interfaces/L2/IBaseFeeVault.sol @@ -1,42 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Types } from "src/libraries/Types.sol"; - -interface IBaseFeeVault { - error FeeVault_OnlyProxyAdminOwner(); - - error InvalidInitialization(); - error NotInitializing(); - - event Initialized(uint64 version); - event Withdrawal(uint256 value, address to, address from); - event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); - event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); - event RecipientUpdated(address oldRecipient, address newRecipient); - event WithdrawalNetworkUpdated( - Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork - ); - - receive() external payable; - - function initialize( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; - function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); - function RECIPIENT() external view returns (address); - function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256); - function recipient() external view returns (address); - function totalProcessed() external view returns (uint256); - function withdraw() external returns (uint256 value_); - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); - function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; - function setRecipient(address _newRecipient) external; - function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +interface IBaseFeeVault is IFeeVault { function version() external view returns (string memory); } diff --git a/interfaces/L2/IBaseTime.sol b/interfaces/L2/IBaseTime.sol new file mode 100644 index 000000000..6a8d18a74 --- /dev/null +++ b/interfaces/L2/IBaseTime.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @title IBaseTime +/// @notice Interface for the BaseTime predeploy. +interface IBaseTime is ISemver { + /// @notice Thrown when a caller other than the protocol depositor attempts to update BaseTime. + error BaseTime_NotDepositor(); + + /// @notice Thrown when the millisecond component is not aligned to a 200 millisecond interval. + error BaseTime_InvalidTimestampMillisPart(); + + /// @notice Returns the millisecond component of the current L2 block timestamp. + function timestampMillisPart() external view returns (uint16); + + /// @notice Returns the current L2 block timestamp in milliseconds. + function timestampMs() external view returns (uint64 timestampMs_); + + /// @notice Updates the millisecond component of the current L2 block timestamp. + function setTimestampMillisPart(uint16 _timestampMillisPart) external; +} diff --git a/interfaces/L2/IFeeVault.sol b/interfaces/L2/IFeeVault.sol index 96f8a6687..ee6fdba41 100644 --- a/interfaces/L2/IFeeVault.sol +++ b/interfaces/L2/IFeeVault.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { Types } from "src/libraries/Types.sol"; -interface IFeeVault { - error FeeVault_OnlyProxyAdminOwner(); +interface IFeeVault is IProxyAdminOwnedBase { error InvalidInitialization(); error NotInitializing(); diff --git a/interfaces/L2/IL1FeeVault.sol b/interfaces/L2/IL1FeeVault.sol index 6da52552c..6d2614476 100644 --- a/interfaces/L2/IL1FeeVault.sol +++ b/interfaces/L2/IL1FeeVault.sol @@ -1,42 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Types } from "src/libraries/Types.sol"; - -interface IL1FeeVault { - error FeeVault_OnlyProxyAdminOwner(); - - error InvalidInitialization(); - error NotInitializing(); - - event Initialized(uint64 version); - event Withdrawal(uint256 value, address to, address from); - event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); - event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); - event RecipientUpdated(address oldRecipient, address newRecipient); - event WithdrawalNetworkUpdated( - Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork - ); - - receive() external payable; - - function initialize( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; - function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); - function RECIPIENT() external view returns (address); - function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256); - function recipient() external view returns (address); - function totalProcessed() external view returns (uint256); - function withdraw() external returns (uint256 value_); - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); - function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; - function setRecipient(address _newRecipient) external; - function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +interface IL1FeeVault is IFeeVault { function version() external view returns (string memory); } diff --git a/interfaces/L2/IOperatorFeeVault.sol b/interfaces/L2/IOperatorFeeVault.sol index fe2789ca9..1f557e648 100644 --- a/interfaces/L2/IOperatorFeeVault.sol +++ b/interfaces/L2/IOperatorFeeVault.sol @@ -1,42 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Types } from "src/libraries/Types.sol"; - -interface IOperatorFeeVault { - error FeeVault_OnlyProxyAdminOwner(); - - error InvalidInitialization(); - error NotInitializing(); - - event Initialized(uint64 version); - event Withdrawal(uint256 value, address to, address from); - event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); - event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); - event RecipientUpdated(address oldRecipient, address newRecipient); - event WithdrawalNetworkUpdated( - Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork - ); - - receive() external payable; - - function initialize( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; - function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); - function RECIPIENT() external view returns (address); - function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256); - function recipient() external view returns (address); - function totalProcessed() external view returns (uint256); - function withdraw() external returns (uint256 value_); - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); - function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; - function setRecipient(address _newRecipient) external; - function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +interface IOperatorFeeVault is IFeeVault { function version() external view returns (string memory); } diff --git a/interfaces/L2/ISequencerFeeVault.sol b/interfaces/L2/ISequencerFeeVault.sol index 1770b5126..7f9b5c440 100644 --- a/interfaces/L2/ISequencerFeeVault.sol +++ b/interfaces/L2/ISequencerFeeVault.sol @@ -1,43 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Types } from "src/libraries/Types.sol"; - -interface ISequencerFeeVault { - error FeeVault_OnlyProxyAdminOwner(); - - error InvalidInitialization(); - error NotInitializing(); - - event Initialized(uint64 version); - event Withdrawal(uint256 value, address to, address from); - event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); - event MinWithdrawalAmountUpdated(uint256 oldWithdrawalAmount, uint256 newWithdrawalAmount); - event RecipientUpdated(address oldRecipient, address newRecipient); - event WithdrawalNetworkUpdated( - Types.WithdrawalNetwork oldWithdrawalNetwork, Types.WithdrawalNetwork newWithdrawalNetwork - ); - - receive() external payable; - - function initialize( - address _recipient, - uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork - ) - external; - function MIN_WITHDRAWAL_AMOUNT() external view returns (uint256); - function RECIPIENT() external view returns (address); - function WITHDRAWAL_NETWORK() external view returns (Types.WithdrawalNetwork); - function minWithdrawalAmount() external view returns (uint256); - function recipient() external view returns (address); - function totalProcessed() external view returns (uint256); - function withdraw() external returns (uint256 value_); - function withdrawalNetwork() external view returns (Types.WithdrawalNetwork); - function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external; - function setRecipient(address _newRecipient) external; - function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; +interface ISequencerFeeVault is IFeeVault { function version() external view returns (string memory); function l1FeeWallet() external view returns (address); } diff --git a/scripts/Artifacts.s.sol b/scripts/Artifacts.s.sol index 24abd748a..745737061 100644 --- a/scripts/Artifacts.s.sol +++ b/scripts/Artifacts.s.sol @@ -48,6 +48,7 @@ contract Artifacts { _predeploys[keccak256("OperatorFeeVault")] = payable(Predeploys.OPERATOR_FEE_VAULT); _predeploys[keccak256("SchemaRegistry")] = payable(Predeploys.SCHEMA_REGISTRY); _predeploys[keccak256("EAS")] = payable(Predeploys.EAS); + _predeploys[keccak256("BaseTime")] = payable(Predeploys.BASE_TIME); } /// @notice Loads previously-saved deployments from the outfile back into memory. diff --git a/scripts/L2Genesis.s.sol b/scripts/L2Genesis.s.sol index d002645af..46bb72bf7 100644 --- a/scripts/L2Genesis.s.sol +++ b/scripts/L2Genesis.s.sol @@ -168,19 +168,22 @@ contract L2Genesis is Script { // 8,9,A,B,C,D,E: legacy, not used in OP-Stack. setGasPriceOracle(); // f setL2StandardBridge(_input.l1StandardBridgeProxy); // 10 + // ProxyAdmin must be configured before fee vault initialization, which requires + // the ProxyAdmin owner via ProxyAdminOwnedBase. + setProxyAdmin(_input); // 18 setSequencerFeeVault(_input); // 11 setOptimismMintableERC20Factory(); // 12 setL2ERC721Bridge(_input.l1ERC721BridgeProxy); // 14 setL1Block(); // 15 setL2ToL1MessagePasser(); // 16 setOptimismMintableERC721Factory(_input); // 17 - setProxyAdmin(_input); // 18 setBaseFeeVault(_input); // 19 setL1FeeVault(_input); // 1A setOperatorFeeVault(_input); // 1B // 1C,1D,1E,1F: not used. setSchemaRegistry(); // 20 setEAS(); // 21 + setBaseTime(); // 30 } function setProxyAdmin(Input memory _input) internal { @@ -232,7 +235,8 @@ contract L2Genesis is Script { _vaultAddr: Predeploys.SEQUENCER_FEE_WALLET, _recipient: _input.sequencerFeeVaultRecipient, _minWithdrawalAmount: _input.sequencerFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) + _withdrawalNetwork: Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork), + _proxyAdminOwner: _input.opChainProxyAdminOwner }); } @@ -283,7 +287,8 @@ contract L2Genesis is Script { _vaultAddr: Predeploys.BASE_FEE_VAULT, _recipient: _input.baseFeeVaultRecipient, _minWithdrawalAmount: _input.baseFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) + _withdrawalNetwork: Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork), + _proxyAdminOwner: _input.opChainProxyAdminOwner }); } @@ -293,7 +298,8 @@ contract L2Genesis is Script { _vaultAddr: Predeploys.L1_FEE_VAULT, _recipient: _input.l1FeeVaultRecipient, _minWithdrawalAmount: _input.l1FeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) + _withdrawalNetwork: Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork), + _proxyAdminOwner: _input.opChainProxyAdminOwner }); } @@ -303,10 +309,16 @@ contract L2Genesis is Script { _vaultAddr: Predeploys.OPERATOR_FEE_VAULT, _recipient: _input.operatorFeeVaultRecipient, _minWithdrawalAmount: _input.operatorFeeVaultMinimumWithdrawalAmount, - _withdrawalNetwork: Types.WithdrawalNetwork(_input.operatorFeeVaultWithdrawalNetwork) + _withdrawalNetwork: Types.WithdrawalNetwork(_input.operatorFeeVaultWithdrawalNetwork), + _proxyAdminOwner: _input.opChainProxyAdminOwner }); } + /// @notice This predeploy is following the safety invariant #1. + function setBaseTime() internal { + _setImplementationCode(Predeploys.BASE_TIME); + } + /// @notice This predeploy is following the safety invariant #1. function setSchemaRegistry() internal { _setImplementationCode(Predeploys.SCHEMA_REGISTRY); @@ -371,12 +383,17 @@ contract L2Genesis is Script { address _vaultAddr, address _recipient, uint256 _minWithdrawalAmount, - Types.WithdrawalNetwork _withdrawalNetwork + Types.WithdrawalNetwork _withdrawalNetwork, + address _proxyAdminOwner ) internal { address impl = _setImplementationCode(_vaultAddr); + // Allow ProxyAdminOwnedBase access checks during implementation initialization. + EIP1967Helper.setAdmin(impl, Predeploys.PROXY_ADMIN); + + vm.startPrank(_proxyAdminOwner); /// Initialize the implementation using max value for min withdrawal amount to make it unusable IFeeVault(payable(impl)).initialize(address(0), type(uint256).max, Types.WithdrawalNetwork.L1); // Initialize the predeploy @@ -386,6 +403,7 @@ contract L2Genesis is Script { _minWithdrawalAmount: _minWithdrawalAmount, _withdrawalNetwork: _withdrawalNetwork }); + vm.stopPrank(); } /// @notice Funds the default dev accounts with ether diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index 285c533f5..a4cb2971b 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -47,8 +47,11 @@ contract DeployConfig is Script { uint256 public l1ChainId; uint256 public l1FeeVaultMinimumWithdrawalAmount; uint256 public l1FeeVaultWithdrawalNetwork; + uint256 public l2BlockTime; uint256 public l2ChainId; uint256 public l2GenesisBlockGasLimit; + uint256 public l2GenesisBlockNumber; + uint256 public l2GenesisTimestamp; uint256 public l2OutputOracleStartingBlockNumber; uint256 public l2OutputOracleStartingTimestamp; uint256 public multiproofBlockInterval; @@ -57,6 +60,7 @@ contract DeployConfig is Script { uint256 public multiproofIntermediateBlockInterval; uint64 public slowFinalizationDelay; uint64 public fastFinalizationDelay; + uint256 public multiproofMaxUpgradeId; uint256 public operatorFeeVaultMinimumWithdrawalAmount; uint256 public operatorFeeVaultWithdrawalNetwork; uint256 public proofMaturityDelaySeconds; @@ -103,16 +107,21 @@ contract DeployConfig is Script { l1ChainId = _json.readUint("$.l1ChainId"); l1FeeVaultMinimumWithdrawalAmount = _json.readUint("$.l1FeeVaultMinimumWithdrawalAmount"); l1FeeVaultWithdrawalNetwork = _json.readUint("$.l1FeeVaultWithdrawalNetwork"); + l2BlockTime = _json.readUintOr("$.l2BlockTime", 0); l2ChainId = _json.readUint("$.l2ChainId"); l2GenesisBlockGasLimit = _json.readUint("$.l2GenesisBlockGasLimit"); l2OutputOracleStartingBlockNumber = _json.readUint("$.l2OutputOracleStartingBlockNumber"); l2OutputOracleStartingTimestamp = _json.readUint("$.l2OutputOracleStartingTimestamp"); + l2GenesisBlockNumber = _json.readUintOr("$.l2GenesisBlockNumber", 0); + l2GenesisTimestamp = _json.readUintOr("$.l2GenesisTimestamp", 0); multiproofBlockInterval = _json.readUintOr("$.multiproofBlockInterval", 100); multiproofGameType = _json.readUintOr("$.multiproofGameType", 621); multiproofGenesisBlockNumber = _json.readUintOr("$.multiproofGenesisBlockNumber", 0); multiproofIntermediateBlockInterval = _json.readUintOr("$.multiproofIntermediateBlockInterval", 10); slowFinalizationDelay = uint64(_json.readUintOr("$.slowFinalizationDelay", 5 days)); fastFinalizationDelay = uint64(_json.readUintOr("$.fastFinalizationDelay", 1 days)); + // Mandatory: must match the highest upgrade id in the prover image (BaseUpgrade::CONTRACT_VARIANTS). + multiproofMaxUpgradeId = _json.readUint("$.multiproofMaxUpgradeId"); operatorFeeVaultMinimumWithdrawalAmount = _json.readUint("$.operatorFeeVaultMinimumWithdrawalAmount"); operatorFeeVaultWithdrawalNetwork = _json.readUint("$.operatorFeeVaultWithdrawalNetwork"); proofMaturityDelaySeconds = _json.readUintOr("$.proofMaturityDelaySeconds", 0); diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 0083ad94a..d458d2835 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -12,11 +12,11 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { StateDiff } from "scripts/libraries/StateDiff.sol"; import { Types } from "scripts/libraries/Types.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; @@ -81,6 +81,7 @@ contract SystemDeploy is Script { address nitroEnclaveVerifier; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + uint256 multiproofMaxUpgradeId; ISP1Verifier sp1Verifier; address teeProposer; address teeChallenger; @@ -111,6 +112,7 @@ contract SystemDeploy is Script { ISuperchainConfig superchainConfigProxy; Types.Implementations implementations; ISystemConfig systemConfigProxy; + IProtocolVersions protocolVersionsProxy; } struct UpgradeOutput { @@ -131,6 +133,8 @@ contract SystemDeploy is Script { uint256 l2ChainId; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + uint256 multiproofMaxUpgradeId; + IProtocolVersions protocolVersions; uint64 slowFinalizationDelay; uint64 fastFinalizationDelay; } @@ -216,7 +220,6 @@ contract SystemDeploy is Script { superchainConfigImpl: artifacts.mustGetAddress("SuperchainConfigImpl"), l1ERC721BridgeImpl: artifacts.mustGetAddress("L1ERC721BridgeImpl"), optimismPortalImpl: artifacts.mustGetAddress("OptimismPortalImpl"), - ethLockboxImpl: artifacts.mustGetAddress("ETHLockboxImpl"), systemConfigImpl: artifacts.mustGetAddress("SystemConfigImpl"), optimismMintableERC20FactoryImpl: artifacts.mustGetAddress("OptimismMintableERC20FactoryImpl"), l1CrossDomainMessengerImpl: artifacts.mustGetAddress("L1CrossDomainMessengerImpl"), @@ -224,6 +227,7 @@ contract SystemDeploy is Script { disputeGameFactoryImpl: artifacts.mustGetAddress("DisputeGameFactoryImpl"), anchorStateRegistryImpl: artifacts.mustGetAddress("AnchorStateRegistryImpl"), delayedWETHImpl: artifacts.mustGetAddress("DelayedWETHImpl"), + protocolVersionsImpl: artifacts.mustGetAddress("ProtocolVersionsImpl"), aggregateVerifierImpl: artifacts.getAddress("AggregateVerifier"), teeProverRegistryImpl: artifacts.getAddress("TEEProverRegistryImpl"), teeVerifierImpl: artifacts.getAddress("TEEVerifier"), @@ -300,6 +304,8 @@ contract SystemDeploy is Script { l2ChainId: cfg.l2ChainId(), multiproofBlockInterval: cfg.multiproofBlockInterval(), multiproofIntermediateBlockInterval: cfg.multiproofIntermediateBlockInterval(), + multiproofMaxUpgradeId: cfg.multiproofMaxUpgradeId(), + protocolVersions: IProtocolVersions(artifacts.mustGetAddress("ProtocolVersionsProxy")), slowFinalizationDelay: cfg.slowFinalizationDelay(), fastFinalizationDelay: cfg.fastFinalizationDelay() }) @@ -361,6 +367,7 @@ contract SystemDeploy is Script { nitroEnclaveVerifier: cfg.nitroEnclaveVerifier(), multiproofBlockInterval: cfg.multiproofBlockInterval(), multiproofIntermediateBlockInterval: cfg.multiproofIntermediateBlockInterval(), + multiproofMaxUpgradeId: cfg.multiproofMaxUpgradeId(), sp1Verifier: ISP1Verifier(cfg.sp1Verifier()), teeProposer: cfg.teeProposer(), teeChallenger: cfg.teeChallenger(), @@ -378,7 +385,8 @@ contract SystemDeploy is Script { opChainProxyAdminOwner: cfg.finalSystemOwner(), systemConfigOwner: cfg.finalSystemOwner(), batcher: cfg.batchSenderAddress(), - unsafeBlockSigner: cfg.p2pSequencerAddress() + unsafeBlockSigner: cfg.p2pSequencerAddress(), + incidentResponder: cfg.superchainConfigIncidentResponder() }), basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), @@ -435,7 +443,12 @@ contract SystemDeploy is Script { revert SuperchainConfigNeedsUpgrade(); } - _upgradeOPChain(systemConfigProxy, _input.implementations); + IProtocolVersions protocolVersionsProxy = _input.protocolVersionsProxy; + if (address(protocolVersionsProxy) == address(0) && address(artifacts).code.length != 0) { + protocolVersionsProxy = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); + } + + _upgradeOPChain(systemConfigProxy, _input.implementations, protocolVersionsProxy); output_.chainUpgraded = true; } @@ -542,10 +555,10 @@ contract SystemDeploy is Script { output_.l1StandardBridgeImpl = address(_deployL1StandardBridgeImpl()); output_.optimismMintableERC20FactoryImpl = address(_deployOptimismMintableERC20FactoryImpl()); output_.optimismPortalImpl = address(_deployOptimismPortalImpl(_input)); - output_.ethLockboxImpl = address(_deployETHLockboxImpl()); output_.delayedWETHImpl = address(_deployDelayedWETHImpl(_input)); output_.disputeGameFactoryImpl = address(_deployDisputeGameFactoryImpl()); output_.anchorStateRegistryImpl = address(_deployAnchorStateRegistryImpl(_input)); + output_.protocolVersionsImpl = address(_deployProtocolVersionsImpl()); } function _deployOPChain( @@ -576,7 +589,6 @@ contract SystemDeploy is Script { output_.l1ERC721BridgeProxy = IL1ERC721Bridge(_deployProxy(_input, output_.opChainProxyAdmin, "L1ERC721Bridge")); output_.optimismPortalProxy = IOptimismPortal(payable(_deployProxy(_input, output_.opChainProxyAdmin, "OptimismPortal"))); - output_.ethLockboxProxy = IETHLockbox(_deployProxy(_input, output_.opChainProxyAdmin, "ETHLockbox")); output_.systemConfigProxy = ISystemConfig(_deployProxy(_input, output_.opChainProxyAdmin, "SystemConfig")); output_.optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory( _deployProxy(_input, output_.opChainProxyAdmin, "OptimismMintableERC20Factory") @@ -586,6 +598,8 @@ contract SystemDeploy is Script { output_.anchorStateRegistryProxy = IAnchorStateRegistry(_deployProxy(_input, output_.opChainProxyAdmin, "AnchorStateRegistry")); output_.delayedWETHProxy = IDelayedWETH(payable(_deployProxy(_input, output_.opChainProxyAdmin, "DelayedWETH"))); + output_.protocolVersionsProxy = + IProtocolVersions(_deployProxy(_input, output_.opChainProxyAdmin, "ProtocolVersions")); output_.l1StandardBridgeProxy = IL1StandardBridge( payable(_createDeterministic( @@ -674,15 +688,6 @@ contract SystemDeploy is Script { abi.encodeCall(IOptimismPortal.initialize, (_output.systemConfigProxy, _output.anchorStateRegistryProxy)) ); - IOptimismPortal[] memory portals = new IOptimismPortal[](1); - portals[0] = _output.optimismPortalProxy; - _upgradeToAndCall( - _output.opChainProxyAdmin, - address(_output.ethLockboxProxy), - _impls.ethLockboxImpl, - abi.encodeCall(IETHLockbox.initialize, (_output.systemConfigProxy, portals)) - ); - _upgradeToAndCall( _output.opChainProxyAdmin, address(_output.optimismMintableERC20FactoryProxy), @@ -730,6 +735,13 @@ contract SystemDeploy is Script { _encodeAnchorStateRegistryInitializer(_input, _output) ); } + + _upgradeToAndCall( + _output.opChainProxyAdmin, + address(_output.protocolVersionsProxy), + _impls.protocolVersionsImpl, + abi.encodeCall(IProtocolVersions.initialize, (_input.roles.incidentResponder)) + ); } function _upgradeSuperchainConfigIfNeeded( @@ -748,7 +760,13 @@ contract SystemDeploy is Script { upgraded_ = true; } - function _upgradeOPChain(ISystemConfig _systemConfigProxy, Types.Implementations memory _impls) internal { + function _upgradeOPChain( + ISystemConfig _systemConfigProxy, + Types.Implementations memory _impls, + IProtocolVersions _protocolVersionsProxy + ) + internal + { IProxyAdmin proxyAdmin = _systemConfigProxy.proxyAdmin(); uint256 l2ChainId = _systemConfigProxy.l2ChainId(); @@ -773,6 +791,10 @@ contract SystemDeploy is Script { _upgradeTo(proxyAdmin, opChainAddrs.delayedWETH, _impls.delayedWETHImpl); } + if (address(_protocolVersionsProxy) != address(0)) { + _upgradeTo(proxyAdmin, address(_protocolVersionsProxy), _impls.protocolVersionsImpl); + } + emit Upgraded(l2ChainId, _systemConfigProxy, msg.sender); } @@ -1014,16 +1036,6 @@ contract SystemDeploy is Script { ); } - function _deployETHLockboxImpl() internal returns (IETHLockbox) { - return IETHLockbox( - DeployUtils.createDeterministic({ - _name: "ETHLockbox", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())), - _salt: DeployUtils.DEFAULT_SALT - }) - ); - } - function _deployDelayedWETHImpl(ImplementationInput memory _input) internal returns (IDelayedWETH) { return IDelayedWETH( DeployUtils.createDeterministic({ @@ -1058,6 +1070,16 @@ contract SystemDeploy is Script { ); } + function _deployProtocolVersionsImpl() internal returns (IProtocolVersions) { + return IProtocolVersions( + DeployUtils.createDeterministic({ + _name: "ProtocolVersions", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProtocolVersions.__constructor__, ())), + _salt: DeployUtils.DEFAULT_SALT + }) + ); + } + function _deployMultiproofContracts( Types.DeployInput memory _opChainInput, ImplementationInput memory _input, @@ -1125,6 +1147,14 @@ contract SystemDeploy is Script { IVerifier(address(new ZKVerifier(_input.sp1Verifier, _output.anchorStateRegistryProxy))); } + // Seed unscheduled upgrades through multiproofMaxUpgradeId so the AggregateVerifier + // constructor check passes; governance schedules activations later. + uint256 registered = _output.protocolVersionsProxy.getSchedule().length; + for (uint256 i = registered; i <= _input.multiproofMaxUpgradeId; i++) { + vm.broadcast(msg.sender); + _output.protocolVersionsProxy.registerUpgrade(0, 0); + } + if (_deferAggregateVerifierRegistration(_input)) { // The multiproof config_hash commits to the L2 genesis block hash, which is only known // after the L2 execution client initializes from the generated genesis. Defer @@ -1146,6 +1176,8 @@ contract SystemDeploy is Script { l2ChainId: _opChainInput.l2ChainId, multiproofBlockInterval: _input.multiproofBlockInterval, multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval, + multiproofMaxUpgradeId: _input.multiproofMaxUpgradeId, + protocolVersions: _output.protocolVersionsProxy, slowFinalizationDelay: _input.slowFinalizationDelay, fastFinalizationDelay: _input.fastFinalizationDelay }) @@ -1164,6 +1196,15 @@ contract SystemDeploy is Script { } function _newAggregateVerifier(AggregateVerifierInput memory _input) internal returns (IVerifier) { + AggregateVerifier.GameConfig memory gameConfig = AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays( + _input.slowFinalizationDelay, _input.fastFinalizationDelay + ), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: _input.protocolVersions, maxUpgradeId: _input.multiproofMaxUpgradeId + }) + }); + vm.broadcast(msg.sender); return IVerifier( address( @@ -1179,7 +1220,7 @@ contract SystemDeploy is Script { _input.l2ChainId, _input.multiproofBlockInterval, _input.multiproofIntermediateBlockInterval, - AggregateVerifier.FinalizationDelays(_input.slowFinalizationDelay, _input.fastFinalizationDelay) + gameConfig ) ) ); @@ -1272,7 +1313,6 @@ contract SystemDeploy is Script { DeployUtils.assertValidContractAddress(_impls.superchainConfigImpl); DeployUtils.assertValidContractAddress(_impls.l1ERC721BridgeImpl); DeployUtils.assertValidContractAddress(_impls.optimismPortalImpl); - DeployUtils.assertValidContractAddress(_impls.ethLockboxImpl); DeployUtils.assertValidContractAddress(_impls.systemConfigImpl); DeployUtils.assertValidContractAddress(_impls.optimismMintableERC20FactoryImpl); DeployUtils.assertValidContractAddress(_impls.l1CrossDomainMessengerImpl); @@ -1280,6 +1320,7 @@ contract SystemDeploy is Script { DeployUtils.assertValidContractAddress(_impls.disputeGameFactoryImpl); DeployUtils.assertValidContractAddress(_impls.anchorStateRegistryImpl); DeployUtils.assertValidContractAddress(_impls.delayedWETHImpl); + DeployUtils.assertValidContractAddress(_impls.protocolVersionsImpl); } function _implementationsEmpty(Types.Implementations memory _impls) internal pure returns (bool) { @@ -1301,10 +1342,10 @@ contract SystemDeploy is Script { artifacts.save("OptimismMintableERC20FactoryProxy", address(chain.optimismMintableERC20FactoryProxy)); artifacts.save("L1StandardBridgeProxy", address(chain.l1StandardBridgeProxy)); artifacts.save("L1CrossDomainMessengerProxy", address(chain.l1CrossDomainMessengerProxy)); - artifacts.save("ETHLockboxProxy", address(chain.ethLockboxProxy)); artifacts.save("DisputeGameFactoryProxy", address(chain.disputeGameFactoryProxy)); artifacts.save("DelayedWETHProxy", address(chain.delayedWETHProxy)); artifacts.save("AnchorStateRegistryProxy", address(chain.anchorStateRegistryProxy)); + artifacts.save("ProtocolVersionsProxy", address(chain.protocolVersionsProxy)); artifacts.save("OptimismPortalProxy", address(chain.optimismPortalProxy)); artifacts.save("OptimismPortal2Proxy", address(chain.optimismPortalProxy)); _saveIfSet("TEEProverRegistryProxy", address(chain.teeProverRegistryProxy)); @@ -1323,7 +1364,6 @@ contract SystemDeploy is Script { artifacts.save("SuperchainConfigImpl", _impls.superchainConfigImpl); artifacts.save("L1ERC721BridgeImpl", _impls.l1ERC721BridgeImpl); artifacts.save("OptimismPortalImpl", _impls.optimismPortalImpl); - artifacts.save("ETHLockboxImpl", _impls.ethLockboxImpl); artifacts.save("SystemConfigImpl", _impls.systemConfigImpl); artifacts.save("OptimismMintableERC20FactoryImpl", _impls.optimismMintableERC20FactoryImpl); artifacts.save("L1CrossDomainMessengerImpl", _impls.l1CrossDomainMessengerImpl); @@ -1331,6 +1371,7 @@ contract SystemDeploy is Script { artifacts.save("DisputeGameFactoryImpl", _impls.disputeGameFactoryImpl); artifacts.save("AnchorStateRegistryImpl", _impls.anchorStateRegistryImpl); artifacts.save("DelayedWETHImpl", _impls.delayedWETHImpl); + artifacts.save("ProtocolVersionsImpl", _impls.protocolVersionsImpl); _saveIfSet("AggregateVerifier", _impls.aggregateVerifierImpl); _saveIfSet("TEEProverRegistryImpl", _impls.teeProverRegistryImpl); _saveIfSet("TEEVerifier", _impls.teeVerifierImpl); diff --git a/scripts/libraries/Types.sol b/scripts/libraries/Types.sol index 2431d705c..941784952 100644 --- a/scripts/libraries/Types.sol +++ b/scripts/libraries/Types.sol @@ -16,7 +16,7 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { Proposal } from "src/libraries/bridge/Types.sol"; import { Claim } from "src/libraries/bridge/LibUDT.sol"; @@ -28,6 +28,7 @@ library Types { address systemConfigOwner; address batcher; address unsafeBlockSigner; + address incidentResponder; } /// @notice The full set of inputs to deploy a new OP Stack chain. @@ -50,11 +51,11 @@ library Types { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; - IETHLockbox ethLockboxProxy; IOptimismPortal2 optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IDelayedWETH delayedWETHProxy; + IProtocolVersions protocolVersionsProxy; IVerifier aggregateVerifier; ITEEProverRegistry teeProverRegistryProxy; IVerifier teeVerifier; @@ -68,7 +69,6 @@ library Types { address superchainConfigImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; - address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; @@ -76,6 +76,7 @@ library Types { address disputeGameFactoryImpl; address anchorStateRegistryImpl; address delayedWETHImpl; + address protocolVersionsImpl; address aggregateVerifierImpl; address teeProverRegistryImpl; address teeVerifierImpl; diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index 6fba71121..1e64d5617 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -16,6 +16,8 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; @@ -34,6 +36,7 @@ abstract contract DeployDevBase is Script { IAnchorStateRegistry public mockAnchorRegistry; address public mockDelayedWETH; address public aggregateVerifier; + MinimalProxyAdmin internal proxyAdmin; function setUp() public { DeployUtils.etchLabelAndAllowCheatcodes({ _etchTo: address(cfg), _cname: "DeployConfig" }); @@ -61,7 +64,7 @@ abstract contract DeployDevBase is Script { function _deployInfrastructure(GameType gameType) internal { address owner = cfg.finalSystemOwner(); address factoryImpl = address(new DisputeGameFactory()); - MinimalProxyAdmin proxyAdmin = new MinimalProxyAdmin(owner); + proxyAdmin = new MinimalProxyAdmin(owner); Proxy proxy = new Proxy(msg.sender); proxy.upgradeTo(factoryImpl); @@ -104,6 +107,28 @@ abstract contract DeployDevBase is Script { AggregateVerifier.ZkHashes memory zkHashes = AggregateVerifier.ZkHashes({ rangeHash: cfg.zkRangeHash(), aggregateHash: cfg.zkAggregationHash() }); + Proxy protocolVersionsProxy = new Proxy(msg.sender); + protocolVersionsProxy.upgradeToAndCall( + address(new ProtocolVersions()), abi.encodeCall(IProtocolVersions.initialize, (address(0))) + ); + protocolVersionsProxy.changeAdmin(address(proxyAdmin)); + + // Seed unscheduled upgrades through multiproofMaxUpgradeId so the AggregateVerifier + // constructor check passes; requires finalSystemOwner to be the broadcasting deployer. + uint256 maxUpgradeId = cfg.multiproofMaxUpgradeId(); + for (uint256 i = 0; i <= maxUpgradeId; i++) { + IProtocolVersions(address(protocolVersionsProxy)).registerUpgrade(0, 0); + } + + AggregateVerifier.GameConfig memory gameConfig = AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays( + cfg.slowFinalizationDelay(), cfg.fastFinalizationDelay() + ), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: IProtocolVersions(address(protocolVersionsProxy)), maxUpgradeId: maxUpgradeId + }) + }); + aggregateVerifier = address( new AggregateVerifier( gameType, @@ -117,7 +142,7 @@ abstract contract DeployDevBase is Script { cfg.l2ChainId(), _blockInterval(), _intermediateBlockInterval(), - AggregateVerifier.FinalizationDelays(cfg.slowFinalizationDelay(), cfg.fastFinalizationDelay()) + gameConfig ) ); @@ -149,6 +174,10 @@ abstract contract DeployDevBase is Script { function _logHeader() internal view virtual; function _printSummary() internal view virtual; - function _preflight() internal virtual { } + function _preflight() internal virtual { + require(cfg.l2BlockTime() != 0, "l2BlockTime must be set in config"); + require(cfg.l2GenesisTimestamp() != 0, "l2GenesisTimestamp must be set in config"); + } + function _serializeExtra(string memory key) internal virtual { } } diff --git a/scripts/multiproof/DeployDevWithNitro.s.sol b/scripts/multiproof/DeployDevWithNitro.s.sol index 0282a504f..6fa19f0a6 100644 --- a/scripts/multiproof/DeployDevWithNitro.s.sol +++ b/scripts/multiproof/DeployDevWithNitro.s.sol @@ -42,6 +42,7 @@ contract DeployDevWithNitro is DeployDevBase { } function _preflight() internal override { + super._preflight(); nitroEnclaveVerifierAddr = cfg.nitroEnclaveVerifier(); require( nitroEnclaveVerifierAddr != address(0), diff --git a/snapshots/abi/AggregateVerifier.json b/snapshots/abi/AggregateVerifier.json index d8ee465e9..6f97ddd38 100644 --- a/snapshots/abi/AggregateVerifier.json +++ b/snapshots/abi/AggregateVerifier.json @@ -67,6 +67,47 @@ "internalType": "uint256", "name": "intermediateBlockInterval", "type": "uint256" + }, + { + "components": [ + { + "components": [ + { + "internalType": "uint64", + "name": "slow", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "fast", + "type": "uint64" + } + ], + "internalType": "struct AggregateVerifier.FinalizationDelays", + "name": "finalizationDelays", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "contract IProtocolVersions", + "name": "protocolVersions", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxUpgradeId", + "type": "uint256" + } + ], + "internalType": "struct AggregateVerifier.ScheduleConfig", + "name": "schedule", + "type": "tuple" + } + ], + "internalType": "struct AggregateVerifier.GameConfig", + "name": "gameConfig", + "type": "tuple" } ], "stateMutability": "nonpayable", @@ -202,6 +243,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "MAX_UPGRADE_ID", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "PROOF_THRESHOLD", @@ -215,6 +269,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "PROTOCOL_VERSIONS", + "outputs": [ + { + "internalType": "contract IProtocolVersions", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "SLOW_FINALIZATION_DELAY", @@ -681,6 +748,19 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "startingBlockNumber", diff --git a/snapshots/abi/BaseFeeVault.json b/snapshots/abi/BaseFeeVault.json index 63f770b27..abf0d79c0 100644 --- a/snapshots/abi/BaseFeeVault.json +++ b/snapshots/abi/BaseFeeVault.json @@ -78,6 +78,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "recipient", @@ -310,17 +336,42 @@ }, { "inputs": [], - "name": "FeeVault_OnlyProxyAdminOwner", + "name": "InvalidInitialization", "type": "error" }, { "inputs": [], - "name": "InvalidInitialization", + "name": "NotInitializing", "type": "error" }, { "inputs": [], - "name": "NotInitializing", + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/BaseTime.json b/snapshots/abi/BaseTime.json new file mode 100644 index 000000000..e37d75c6f --- /dev/null +++ b/snapshots/abi/BaseTime.json @@ -0,0 +1,64 @@ +[ + { + "inputs": [ + { + "internalType": "uint16", + "name": "_timestampMillisPart", + "type": "uint16" + } + ], + "name": "setTimestampMillisPart", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "timestampMillisPart", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "timestampMs", + "outputs": [ + { + "internalType": "uint64", + "name": "timestampMs_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "BaseTime_InvalidTimestampMillisPart", + "type": "error" + }, + { + "inputs": [], + "name": "BaseTime_NotDepositor", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/abi/ETHLockbox.json b/snapshots/abi/ETHLockbox.json deleted file mode 100644 index 2f09a99da..000000000 --- a/snapshots/abi/ETHLockbox.json +++ /dev/null @@ -1,395 +0,0 @@ -[ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "contract IETHLockbox", - "name": "_lockbox", - "type": "address" - } - ], - "name": "authorizeLockbox", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", - "type": "address" - } - ], - "name": "authorizePortal", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IETHLockbox", - "name": "", - "type": "address" - } - ], - "name": "authorizedLockboxes", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IOptimismPortal2", - "name": "", - "type": "address" - } - ], - "name": "authorizedPortals", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "initVersion", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract ISystemConfig", - "name": "_systemConfig", - "type": "address" - }, - { - "internalType": "contract IOptimismPortal2[]", - "name": "_portals", - "type": "address[]" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "lockETH", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IETHLockbox", - "name": "_lockbox", - "type": "address" - } - ], - "name": "migrateLiquidity", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proxyAdmin", - "outputs": [ - { - "internalType": "contract IProxyAdmin", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proxyAdminOwner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "receiveLiquidity", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "superchainConfig", - "outputs": [ - { - "internalType": "contract ISuperchainConfig", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "systemConfig", - "outputs": [ - { - "internalType": "contract ISystemConfig", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_value", - "type": "uint256" - } - ], - "name": "unlockETH", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IOptimismPortal2", - "name": "portal", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "ETHLocked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IOptimismPortal2", - "name": "portal", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "ETHUnlocked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IETHLockbox", - "name": "lockbox", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "LiquidityMigrated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IETHLockbox", - "name": "lockbox", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "LiquidityReceived", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IETHLockbox", - "name": "lockbox", - "type": "address" - } - ], - "name": "LockboxAuthorized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IOptimismPortal2", - "name": "portal", - "type": "address" - } - ], - "name": "PortalAuthorized", - "type": "event" - }, - { - "inputs": [], - "name": "ETHLockbox_DifferentSuperchainConfig", - "type": "error" - }, - { - "inputs": [], - "name": "ETHLockbox_InsufficientBalance", - "type": "error" - }, - { - "inputs": [], - "name": "ETHLockbox_NoWithdrawalTransactions", - "type": "error" - }, - { - "inputs": [], - "name": "ETHLockbox_Paused", - "type": "error" - }, - { - "inputs": [], - "name": "ETHLockbox_Unauthorized", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_NotProxyAdmin", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", - "type": "error" - }, - { - "inputs": [], - "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", - "type": "error" - }, - { - "inputs": [], - "name": "ReinitializableBase_ZeroInitVersion", - "type": "error" - } -] \ No newline at end of file diff --git a/snapshots/abi/FeeDisburser.json b/snapshots/abi/FeeDisburser.json index 4dfa787a5..c06b7fb5a 100644 --- a/snapshots/abi/FeeDisburser.json +++ b/snapshots/abi/FeeDisburser.json @@ -45,6 +45,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "MAX_SYSTEM_ADDRESS_COUNT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "WITHDRAWAL_MIN_GAS", @@ -65,6 +78,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address payable[]", + "name": "systemAddresses_", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "targetBalances_", + "type": "uint256[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "lastDisbursementTime", @@ -91,6 +122,70 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "systemAddresses", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "targetBalances", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "version", @@ -148,12 +243,61 @@ "name": "FeesReceived", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [], "name": "NoFeesCollected", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "systemAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceNeeded", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceSent", + "type": "uint256" + } + ], + "name": "ProcessedFunds", + "type": "event" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, { "inputs": [], "name": "FeeVaultMustWithdrawToFeeDisburser", @@ -174,9 +318,64 @@ "name": "IntervalTooLow", "type": "error" }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "TooManySystemAddresses", + "type": "error" + }, { "inputs": [], "name": "ZeroAddress", "type": "error" + }, + { + "inputs": [], + "name": "ZeroTargetBalance", + "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/L1FeeVault.json b/snapshots/abi/L1FeeVault.json index 63f770b27..abf0d79c0 100644 --- a/snapshots/abi/L1FeeVault.json +++ b/snapshots/abi/L1FeeVault.json @@ -78,6 +78,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "recipient", @@ -310,17 +336,42 @@ }, { "inputs": [], - "name": "FeeVault_OnlyProxyAdminOwner", + "name": "InvalidInitialization", "type": "error" }, { "inputs": [], - "name": "InvalidInitialization", + "name": "NotInitializing", "type": "error" }, { "inputs": [], - "name": "NotInitializing", + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/NitroEnclaveVerifier.json b/snapshots/abi/NitroEnclaveVerifier.json index 830264985..2d59ea314 100644 --- a/snapshots/abi/NitroEnclaveVerifier.json +++ b/snapshots/abi/NitroEnclaveVerifier.json @@ -1125,17 +1125,6 @@ "name": "CertExpiriesLengthMismatch", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "certHash", - "type": "bytes32" - } - ], - "name": "CertificateNotFound", - "type": "error" - }, { "inputs": [ { diff --git a/snapshots/abi/OperatorFeeVault.json b/snapshots/abi/OperatorFeeVault.json index 63f770b27..abf0d79c0 100644 --- a/snapshots/abi/OperatorFeeVault.json +++ b/snapshots/abi/OperatorFeeVault.json @@ -78,6 +78,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "recipient", @@ -310,17 +336,42 @@ }, { "inputs": [], - "name": "FeeVault_OnlyProxyAdminOwner", + "name": "InvalidInitialization", "type": "error" }, { "inputs": [], - "name": "InvalidInitialization", + "name": "NotInitializing", "type": "error" }, { "inputs": [], - "name": "NotInitializing", + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/OptimismPortal2.json b/snapshots/abi/OptimismPortal2.json index 9d19d18db..bab79a516 100644 --- a/snapshots/abi/OptimismPortal2.json +++ b/snapshots/abi/OptimismPortal2.json @@ -27,6 +27,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_withdrawalHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_disputeGameIndex", + "type": "uint256" + } + ], + "name": "canProveAndFinalize", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -130,19 +154,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [], - "name": "ethLockbox", - "outputs": [ - { - "internalType": "contract IETHLockbox", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -425,6 +436,88 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_disputeGameIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveAndFinalizeWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -786,17 +879,17 @@ }, { "inputs": [], - "name": "OptimismPortal_ImproperDisputeGame", + "name": "OptimismPortal_ImmediateFinalityNotEnabled", "type": "error" }, { "inputs": [], - "name": "OptimismPortal_InvalidDisputeGame", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "OptimismPortal_InvalidLockboxState", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json new file mode 100644 index 000000000..22a95612e --- /dev/null +++ b/snapshots/abi/ProtocolVersions.json @@ -0,0 +1,534 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "MIN_NOTICE", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "l2Timestamp", + "type": "uint64" + } + ], + "name": "activatedScheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "newTimestamp", + "type": "uint64" + } + ], + "name": "delayTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getSchedule", + "outputs": [ + { + "internalType": "uint64[]", + "name": "", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "incidentResponder", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_incidentResponder", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "minimumProtocolVersion", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "minProtocolVersion", + "type": "uint256" + } + ], + "name": "registerUpgrade", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newIncidentResponder", + "type": "address" + } + ], + "name": "setIncidentResponder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "setMinimumProtocolVersion", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "setTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousIncidentResponder", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newIncidentResponder", + "type": "address" + } + ], + "name": "IncidentResponderUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "MinimumProtocolVersionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "newScheduleId", + "type": "bytes32" + } + ], + "name": "ScheduleIdUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "TimestampSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "UpgradeRegistered", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "activationTimestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_ActivationAlreadyPassed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "currentTimestamp", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "newTimestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_DelayMustBeLater", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_InsufficientNotice", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_InvalidProtocolVersion", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_NotIncidentResponder", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_NotInitialized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ProtocolVersions_NotScheduled", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nextScheduledId", + "type": "uint256" + } + ], + "name": "ProtocolVersions_StaticScheduleHole", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "previousId", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "previousTimestamp", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_TimestampNotAfterPrevious", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nextId", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "nextTimestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_TimestampNotBeforeNext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ProtocolVersions_UnknownUpgrade", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/abi/SequencerFeeVault.json b/snapshots/abi/SequencerFeeVault.json index 0b3563dcc..13d85d3a0 100644 --- a/snapshots/abi/SequencerFeeVault.json +++ b/snapshots/abi/SequencerFeeVault.json @@ -91,6 +91,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "recipient", @@ -323,17 +349,42 @@ }, { "inputs": [], - "name": "FeeVault_OnlyProxyAdminOwner", + "name": "InvalidInitialization", "type": "error" }, { "inputs": [], - "name": "InvalidInitialization", + "name": "NotInitializing", "type": "error" }, { "inputs": [], - "name": "NotInitializing", + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", "type": "error" } ] \ No newline at end of file diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 38db91bb7..b69767ad8 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -1,8 +1,4 @@ { - "src/L1/ETHLockbox.sol:ETHLockbox": { - "initCodeHash": "0x65db3aa3c2e3221065752f66016fa02b66688a01cc5c3066533b27fe620619c8", - "sourceCodeHash": "0x519c4606618c1d5fa489dc99437c895171b16179cbb74f602011ffe0b5902788" - }, "src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger": { "initCodeHash": "0x3dc659aafb03bd357f92abfc6794af89ee0ddd5212364551637422bf8d0b00f9", "sourceCodeHash": "0xb1467f925aaf0a834bc98cdfaffe8832bdf57d57a0e22087179d1c3b4bee2c53" @@ -16,20 +12,24 @@ "sourceCodeHash": "0xd9f576a79e97bc541b3d7a2ee928f34223edaaecb074eeb9e6e2ecee857ce6a0" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0x0d7c9a18e0f23dd9e27f71919665d36316d55a1f69401bd4a02249bbde8ffa85", - "sourceCodeHash": "0x6e4e122b90d681154beb09cd7c1661560d89ce5a3e103fd85235c50d97e3ef71" + "initCodeHash": "0xdaeac3fae27dc1c5924100d06eb337c60010d88b638d6703c5ec25d750810495", + "sourceCodeHash": "0x15cef97e2598ac2ed83fd8662c2f31e61a33e89d9f618b97fdeaa142cf6f9262" + }, + "src/L1/ProtocolVersions.sol:ProtocolVersions": { + "initCodeHash": "0xd762af325410baea14f927f4292ef9ee2bb13b52d72034d6d129eb1e518dfedd", + "sourceCodeHash": "0x5a06b3ae5f442f3e3f3dba6f78b9c3e3ce5abd7218ccb13d477c067f61187dc5" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", "sourceCodeHash": "0x79f0c771fc5f6d222d89b05addedc08ce40a7a42423fbd66f7cbb6cde3c2e74f" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x3e8e52d96398a6de91d8922769cc5d0bc7acb2a692689ceb70f1de816e8d6b14", - "sourceCodeHash": "0xf122a50487efe9bd5a620262ba20ef4adbca14eeec2af7fd32e6e16739001596" + "initCodeHash": "0xb91123c7a4c051f2c9af220a5981301499d43c7740892db4ea8abce183437a8c", + "sourceCodeHash": "0x780ff372493ba9010bc0d13100ac896f2bf75730a9b17d4bb63aaf694dc3c634" }, "src/L1/proofs/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x431d51aacc2c107476e9ff8502078acc138289d6544a6f92ecb78609549d0a09", - "sourceCodeHash": "0xbb9919fac076799e886762939c25518ca5b3978b974735685fb8b3026a1abfa6" + "initCodeHash": "0x6e473cd97acfccf0d7d7fcf6fb7da25e96ae6b89119611ab5cdbcc7eeae7a35d", + "sourceCodeHash": "0x14c777a70256c4001ffe1ac1a135b82902d6555b8c70cff2ab0ead49774d4a9d" }, "src/L1/proofs/AnchorStateRegistry.sol:AnchorStateRegistry": { "initCodeHash": "0x6f3afd2d0ef97a82ca3111976322b99343a270e54cd4a405028f2f29c75f7fb1", @@ -44,8 +44,8 @@ "sourceCodeHash": "0x62ff0209cffa08e5aaaaac47e0f22d89ea1fa6a0ea8773a162ab7230e50806ee" }, "src/L1/proofs/tee/NitroEnclaveVerifier.sol:NitroEnclaveVerifier": { - "initCodeHash": "0x1be3b9418c022094fb60ea68d025910c69e95c7a2314bf1b57c740f601b7cb0e", - "sourceCodeHash": "0xa0bb07f71960cda20c3b3a46c8cc24842f991c00469a9e90ce8d9a1c26e7a8d6" + "initCodeHash": "0xf79cd59d23f6a5ad78960b664e20779592d73bfcf52527c3e03adda04edd9645", + "sourceCodeHash": "0x03c164216b27f82ee13064ace6079d5e24e187d888f41bd01c8daffa60c575d6" }, "src/L1/proofs/tee/TEEProverRegistry.sol:TEEProverRegistry": { "initCodeHash": "0xfd1942e1c2f59b0aa72b33d698a948a53b6e4cf1040106f173fb5d89f63f57b0", @@ -60,12 +60,16 @@ "sourceCodeHash": "0xf90e23a22c2e31d6bb11f9a8a9f7cb1c4eb0f20600d2d9aebf87023d1779972c" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { - "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", + "initCodeHash": "0x812f8ec3945f0b2e2a9615cd7c5cdd60dbe8927f41e606cb2bd488c5e6dec02a", "sourceCodeHash": "0xcb329746df0baddd3dc03c6c88da5d6bdc0f0a96d30e6dc78d0891bb1e935032" }, + "src/L2/BaseTime.sol:BaseTime": { + "initCodeHash": "0xfcd48af0cfd907d0ce91f61ea437c81c9a98269ca2071d7d27291f20c483ccc0", + "sourceCodeHash": "0x98e246b6ff058ecb8aaeacaa33ce70674f4d3d4a8dcabe91df6f772c6d237667" + }, "src/L2/FeeDisburser.sol:FeeDisburser": { - "initCodeHash": "0x1278027e3756e2989e80c0a7b513e221a5fe0d3dbd9ded108375a29b2c1f3d57", - "sourceCodeHash": "0xac49a0ecf22b8a7bb3ebef830a2d27b19050f9b08941186e8563d5113cf0ce9c" + "initCodeHash": "0xe495e0502ee0ab7a39605c2c3bdb323908a81e7e6fe6f7f2fbcd5943bff04bf0", + "sourceCodeHash": "0x0aebe8fc1030935340fbfab9cd7b86ef94be196377692d180f7df6058b688c17" }, "src/L2/GasPriceOracle.sol:GasPriceOracle": { "initCodeHash": "0xf72c23d9c3775afd7b645fde429d09800622d329116feb5ff9829634655123ca", @@ -76,7 +80,7 @@ "sourceCodeHash": "0x6551be49dcb0e2a80e9c1042e7964dc41f70bcb08f9ceefd0c0156de9c14cf2d" }, "src/L2/L1FeeVault.sol:L1FeeVault": { - "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", + "initCodeHash": "0x812f8ec3945f0b2e2a9615cd7c5cdd60dbe8927f41e606cb2bd488c5e6dec02a", "sourceCodeHash": "0x34186bcab29963237b4e0d7575b0a1cff7caf42ccdb55d4b2b2c767db3279189" }, "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": { @@ -96,7 +100,7 @@ "sourceCodeHash": "0xdc7bd63134eeab163a635950f2afd16b59f40f9cf1306f2ed33ad661cc7b4962" }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { - "initCodeHash": "0x2ebab6af089a714df25888a4dea81dadcb1fb57146be84d2e079041a9396a810", + "initCodeHash": "0x2179f8438c980cbd14c354206c1253f5704b9100446cdf91fa537a15bfc094ff", "sourceCodeHash": "0xd6e94bc9df025855916aa4184d0bc739b0fbe786dfd037b99dbb51d0d3e46918" }, "src/L2/OptimismMintableERC721.sol:OptimismMintableERC721": { @@ -108,7 +112,7 @@ "sourceCodeHash": "0xb0be3deac32956251adb37d3ca61f619ca4348a1355a41c856a3a95adde0e4ff" }, "src/L2/SequencerFeeVault.sol:SequencerFeeVault": { - "initCodeHash": "0x2cf94abac28d35065c7d361055199d5bf2bd49ec3907f8b81eefee4fcf7df484", + "initCodeHash": "0xd80f5cee90a78db1f7a506d5b6eaa22ff672a78cb2fd7ce9b7130c1ce571ffa9", "sourceCodeHash": "0x5fa147acd34a5f1c451404234d22e114c79f1255decc51afd8930d5ce99d7e02" }, "src/L2/WETH.sol:WETH": { diff --git a/snapshots/storageLayout/AggregateVerifier.json b/snapshots/storageLayout/AggregateVerifier.json index 08fd716cb..1226af645 100644 --- a/snapshots/storageLayout/AggregateVerifier.json +++ b/snapshots/storageLayout/AggregateVerifier.json @@ -96,5 +96,12 @@ "offset": 8, "slot": "7", "type": "uint8" + }, + { + "bytes": "32", + "label": "scheduleId", + "offset": 0, + "slot": "8", + "type": "bytes32" } ] \ No newline at end of file diff --git a/snapshots/storageLayout/BaseTime.json b/snapshots/storageLayout/BaseTime.json new file mode 100644 index 000000000..7e863ba7b --- /dev/null +++ b/snapshots/storageLayout/BaseTime.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "2", + "label": "timestampMillisPart", + "offset": 0, + "slot": "0", + "type": "uint16" + } +] \ No newline at end of file diff --git a/snapshots/storageLayout/FeeDisburser.json b/snapshots/storageLayout/FeeDisburser.json index 53ffb02a4..093900bf6 100644 --- a/snapshots/storageLayout/FeeDisburser.json +++ b/snapshots/storageLayout/FeeDisburser.json @@ -12,5 +12,26 @@ "offset": 0, "slot": "1", "type": "uint256" + }, + { + "bytes": "32", + "label": "_disburseFeesEntered", + "offset": 0, + "slot": "2", + "type": "uint256" + }, + { + "bytes": "32", + "label": "systemAddresses", + "offset": 0, + "slot": "3", + "type": "address payable[]" + }, + { + "bytes": "32", + "label": "targetBalances", + "offset": 0, + "slot": "4", + "type": "uint256[]" } ] \ No newline at end of file diff --git a/snapshots/storageLayout/OptimismPortal2.json b/snapshots/storageLayout/OptimismPortal2.json index 649ad99cb..12b99bf49 100644 --- a/snapshots/storageLayout/OptimismPortal2.json +++ b/snapshots/storageLayout/OptimismPortal2.json @@ -134,10 +134,10 @@ }, { "bytes": "20", - "label": "ethLockbox", + "label": "spacer_63_0_20", "offset": 0, "slot": "63", - "type": "contract IETHLockbox" + "type": "address" }, { "bytes": "1", diff --git a/snapshots/storageLayout/ETHLockbox.json b/snapshots/storageLayout/ProtocolVersions.json similarity index 53% rename from snapshots/storageLayout/ETHLockbox.json rename to snapshots/storageLayout/ProtocolVersions.json index 2af24fd9c..27001da1d 100644 --- a/snapshots/storageLayout/ETHLockbox.json +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -13,25 +13,32 @@ "slot": "0", "type": "bool" }, - { - "bytes": "20", - "label": "systemConfig", - "offset": 2, - "slot": "0", - "type": "contract ISystemConfig" - }, { "bytes": "32", - "label": "authorizedPortals", + "label": "_timestamps", "offset": 0, "slot": "1", - "type": "mapping(contract IOptimismPortal2 => bool)" + "type": "uint64[]" }, { "bytes": "32", - "label": "authorizedLockboxes", + "label": "_upgradeScheduleId", "offset": 0, "slot": "2", - "type": "mapping(contract IETHLockbox => bool)" + "type": "bytes32[]" + }, + { + "bytes": "32", + "label": "minimumProtocolVersion", + "offset": 0, + "slot": "3", + "type": "uint256" + }, + { + "bytes": "20", + "label": "incidentResponder", + "offset": 0, + "slot": "4", + "type": "address" } ] \ No newline at end of file diff --git a/src/L1/ETHLockbox.sol b/src/L1/ETHLockbox.sol deleted file mode 100644 index a4d124cdf..000000000 --- a/src/L1/ETHLockbox.sol +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { Initializable } from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; -import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; -import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; - -// Libraries -import { Constants } from "src/libraries/Constants.sol"; - -// Interfaces -import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; - -/// @custom:proxied true -/// @title ETHLockbox -/// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity -/// management across chains in the superchain cluster. -contract ETHLockbox is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { - /// @notice Thrown when the lockbox is paused. - error ETHLockbox_Paused(); - - /// @notice Thrown when the caller is not authorized. - error ETHLockbox_Unauthorized(); - - /// @notice Thrown when the value to unlock is greater than the balance of the lockbox. - error ETHLockbox_InsufficientBalance(); - - /// @notice Thrown when attempting to unlock ETH from the lockbox through a withdrawal transaction. - error ETHLockbox_NoWithdrawalTransactions(); - - /// @notice Thrown when any authorized portal has a different SuperchainConfig. - error ETHLockbox_DifferentSuperchainConfig(); - - /// @notice Emitted when ETH is locked in the lockbox by an authorized portal. - /// @param portal The address of the portal that locked the ETH. - /// @param amount The amount of ETH locked. - event ETHLocked(IOptimismPortal indexed portal, uint256 amount); - - /// @notice Emitted when ETH is unlocked from the lockbox by an authorized portal. - /// @param portal The address of the portal that unlocked the ETH. - /// @param amount The amount of ETH unlocked. - event ETHUnlocked(IOptimismPortal indexed portal, uint256 amount); - - /// @notice Emitted when a portal is authorized to lock and unlock ETH. - /// @param portal The address of the portal that was authorized. - event PortalAuthorized(IOptimismPortal indexed portal); - - /// @notice Emitted when an ETH lockbox is authorized to migrate its liquidity to the current ETH lockbox. - /// @param lockbox The address of the ETH lockbox that was authorized. - event LockboxAuthorized(IETHLockbox indexed lockbox); - - /// @notice Emitted when ETH liquidity is migrated from the current ETH lockbox to another. - /// @param lockbox The address of the ETH lockbox that was migrated. - event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); - - /// @notice Emitted when ETH liquidity is received during an authorized lockbox migration. - /// @param lockbox The address of the ETH lockbox that received the liquidity. - /// @param amount The amount of ETH received. - event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); - - /// @notice The address of the SystemConfig contract. - ISystemConfig public systemConfig; - - /// @notice Mapping of authorized portals. - mapping(IOptimismPortal => bool) public authorizedPortals; - - /// @notice Mapping of authorized lockboxes. - mapping(IETHLockbox => bool) public authorizedLockboxes; - - /// @notice Semantic version. - /// @custom:semver 1.2.0 - function version() public view virtual returns (string memory) { - return "1.2.0"; - } - - /// @notice Constructs the ETHLockbox contract. - constructor() ReinitializableBase(1) { - _disableInitializers(); - } - - /// @notice Initializer. - /// @param _systemConfig The address of the SystemConfig contract. - /// @param _portals The addresses of the portals to authorize. - /// @dev Note: Multiple chains can share an ETHLockbox contract. In this case, all SystemConfig - /// contracts will point to the same pause identifier (the lockbox itself). Therefore, it - /// doesn't matter which SystemConfig is used here as long as it belongs to one of the - /// chains that share the lockbox. - function initialize( - ISystemConfig _systemConfig, - IOptimismPortal[] calldata _portals - ) - external - reinitializer(initVersion()) - { - // Initialization transactions must come from the ProxyAdmin or its owner. - _assertOnlyProxyAdminOrProxyAdminOwner(); - - // Now perform initialization logic. - systemConfig = _systemConfig; - for (uint256 i; i < _portals.length; i++) { - _authorizePortal(_portals[i]); - } - } - - /// @notice Getter for the current paused status. - function paused() public view returns (bool) { - return systemConfig.paused(); - } - - /// @notice Returns the SuperchainConfig contract. - /// @return ISuperchainConfig The SuperchainConfig contract. - function superchainConfig() public view returns (ISuperchainConfig) { - return systemConfig.superchainConfig(); - } - - /// @notice Authorizes a portal to lock and unlock ETH. - /// @param _portal The address of the portal to authorize. - function authorizePortal(IOptimismPortal _portal) external { - // Check that this transaction is coming from the ProxyAdmin owner. - _assertOnlyProxyAdminOwner(); - - // Authorize the portal. - _authorizePortal(_portal); - } - - /// @notice Receives the ETH liquidity migrated from an authorized lockbox. - function receiveLiquidity() external payable { - // Check that the sender is authorized to trigger this function. - IETHLockbox sender = IETHLockbox(payable(msg.sender)); - if (!authorizedLockboxes[sender]) revert ETHLockbox_Unauthorized(); - - // Emit the event. - emit LiquidityReceived(sender, msg.value); - } - - /// @notice Locks ETH in the lockbox. - /// Called by an authorized portal on a deposit to lock the ETH value. - function lockETH() external payable { - // Check that the sender is authorized to trigger this function. - IOptimismPortal sender = IOptimismPortal(payable(msg.sender)); - if (!authorizedPortals[sender]) revert ETHLockbox_Unauthorized(); - - // Emit the event. - emit ETHLocked(sender, msg.value); - } - - /// @notice Unlocks ETH from the lockbox. - /// Called by an authorized portal when finalizing a withdrawal that requires ETH. - /// Cannot be called if the lockbox is paused. - /// @param _value The amount of ETH to unlock. - function unlockETH(uint256 _value) external { - // Unlocks are blocked when paused, locks are not. - if (paused()) revert ETHLockbox_Paused(); - - // Check that the sender is authorized to trigger this function. - IOptimismPortal sender = IOptimismPortal(payable(msg.sender)); - if (!authorizedPortals[sender]) revert ETHLockbox_Unauthorized(); - - // Check that we have enough balance to process the unlock. - if (_value > address(this).balance) revert ETHLockbox_InsufficientBalance(); - - // Check that the sender is not executing a withdrawal transaction. - if (sender.l2Sender() != Constants.DEFAULT_L2_SENDER) { - revert ETHLockbox_NoWithdrawalTransactions(); - } - - // Using donateETH to avoid triggering a deposit. - sender.donateETH{ value: _value }(); - - // Emit the event. - emit ETHUnlocked(sender, _value); - } - - /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. We - /// allow this function to be called more than once for the same lockbox. A lockbox - /// cannot be removed from the authorized list once added. - /// @param _lockbox The address of the ETH lockbox to authorize. - function authorizeLockbox(IETHLockbox _lockbox) external { - // Check that this transaction is coming from the ProxyAdmin owner. - _assertOnlyProxyAdminOwner(); - - // Check that the lockbox has the same proxy admin owner. - _assertSharedProxyAdminOwner(address(_lockbox)); - - // Authorize the lockbox. - authorizedLockboxes[_lockbox] = true; - - // Emit the event. - emit LockboxAuthorized(_lockbox); - } - - /// @notice Migrates liquidity from the current ETH lockbox to another. - /// @dev Must be called atomically with `OptimismPortal.migrateToSuperRoots()` in the same - /// transaction batch, or otherwise the OptimismPortal may not be able to unlock ETH - /// from the ETHLockbox on finalized withdrawals. - /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. - function migrateLiquidity(IETHLockbox _lockbox) external { - // Check that this transaction is coming from the ProxyAdmin owner. - _assertOnlyProxyAdminOwner(); - - // Check that the lockbox has the same proxy admin owner. - _assertSharedProxyAdminOwner(address(_lockbox)); - - // Receive the liquidity. - uint256 balance = address(this).balance; - IETHLockbox(_lockbox).receiveLiquidity{ value: balance }(); - - // Emit the event. - emit LiquidityMigrated(_lockbox, balance); - } - - /// @notice Authorizes a portal to lock and unlock ETH. - /// @param _portal The address of the portal to authorize. - function _authorizePortal(IOptimismPortal _portal) internal { - // Check that the portal has the same proxy admin owner. - _assertSharedProxyAdminOwner(address(_portal)); - - // Check that the portal has the same superchain config. - if (_portal.superchainConfig() != superchainConfig()) revert ETHLockbox_DifferentSuperchainConfig(); - - // Authorize the portal. - authorizedPortals[_portal] = true; - - // Emit the event. - emit PortalAuthorized(_portal); - } -} diff --git a/src/L1/OptimismPortal2.sol b/src/L1/OptimismPortal2.sol index 9acf09bcd..9d9df5909 100644 --- a/src/L1/OptimismPortal2.sol +++ b/src/L1/OptimismPortal2.sol @@ -25,7 +25,6 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; /// @custom:proxied true @@ -120,11 +119,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Address of the AnchorStateRegistry contract. IAnchorStateRegistry public anchorStateRegistry; - /// @notice Address of the ETHLockbox contract. NOTE that as of v4.1.0 it is not possible to - /// set this value in storage and it is only possible for this value to be set if the - /// chain was first upgraded to v4.0.0. Chains that skip v4.0.0 will not have any - /// ETHLockbox set here. - IETHLockbox public ethLockbox; + /// @custom:legacy + /// @custom:spacer + /// @notice Spacer taking up a legacy address slot. + address private spacer_63_0_20; /// @custom:legacy /// @custom:spacer superRootsActive @@ -208,9 +206,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// not been configured for immediate finality (PROOF_MATURITY_DELAY_SECONDS != 0). error OptimismPortal_ImmediateFinalityNotEnabled(); - /// @notice Thrown when ETHLockbox is set/unset incorrectly depending on the feature flag. - error OptimismPortal_InvalidLockboxState(); - /// @notice Semantic version. /// @custom:semver 5.2.0 function version() public pure virtual returns (string memory) { @@ -240,9 +235,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase systemConfig = _systemConfig; anchorStateRegistry = _anchorStateRegistry; - // Assert that the lockbox state is valid. - _assertValidLockboxState(); - // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. if (l2Sender == address(0)) { @@ -445,11 +437,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase // Mark the withdrawal as finalized so it can't be replayed. finalizedWithdrawals[withdrawalHash] = true; - // If using ETHLockbox, unlock the ETH from the ETHLockbox. - if (_isUsingLockbox()) { - if (_tx.value > 0) ethLockbox.unlockETH(_tx.value); - } - // Set the l2Sender so contracts know who triggered this withdrawal on L2. l2Sender = _tx.sender; @@ -469,14 +456,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase // be achieved through contracts built on top of this contract emit WithdrawalFinalized(withdrawalHash, success); - // If using ETHLockbox, send ETH back to the Lockbox in the case of a failed transaction or - // it'll get stuck here and would need to be moved back via admin action. - if (_isUsingLockbox()) { - if (!success && _tx.value > 0) { - ethLockbox.lockETH{ value: _tx.value }(); - } - } - // Reverting here is useful for determining the exact gas cost to successfully execute the // sub call to the target contract if the minimum gas limit specified by the user would not // be sufficient to execute the sub call. @@ -609,11 +588,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase // Mark the withdrawal as finalized so it can't be replayed. finalizedWithdrawals[withdrawalHash] = true; - // If using ETHLockbox, unlock the ETH from the ETHLockbox. - if (_isUsingLockbox()) { - if (_tx.value > 0) ethLockbox.unlockETH(_tx.value); - } - // Set the l2Sender so contracts know who triggered this withdrawal on L2. l2Sender = _tx.sender; @@ -635,14 +609,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); emit WithdrawalFinalized(withdrawalHash, success); - // If using ETHLockbox, send ETH back to the Lockbox in the case of a failed transaction or - // it'll get stuck here and would need to be moved back via admin action. - if (_isUsingLockbox()) { - if (!success && _tx.value > 0) { - ethLockbox.lockETH{ value: _tx.value }(); - } - } - // Reverting here is useful for determining the exact gas cost to successfully execute the // sub call to the target contract if the minimum gas limit specified by the user would not // be sufficient to execute the sub call. @@ -677,8 +643,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider /// using the CrossDomainMessenger contracts for a simpler developer experience. - /// @dev The `msg.value` is locked on the ETHLockbox and minted as ETH when the deposit - /// arrives on L2, while `_value` specifies how much ETH to send to the target. + /// @dev The `msg.value` is minted as ETH when the deposit arrives on L2, while `_value` + /// specifies how much ETH to send to the target. /// @param _to Target address on L2. /// @param _value ETH value to send to the recipient. /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. @@ -699,11 +665,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase if (msg.value > 0) revert OptimismPortal_NotAllowedOnCGTMode(); } - // If using ETHLockbox, lock the ETH in the ETHLockbox. - if (_isUsingLockbox()) { - if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); - } - // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. if (_isCreation && _to != address(0)) { @@ -747,12 +708,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase return proofSubmitters[_withdrawalHash].length; } - /// @notice Checks if the ETHLockbox feature is enabled. - /// @return bool True if the ETHLockbox feature is enabled. - function _isUsingLockbox() internal view returns (bool) { - return systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(ethLockbox) != address(0); - } - /// @notice Checks if the Custom Gas Token feature is enabled. /// @return bool True if the Custom Gas Token feature is enabled. function _isUsingCustomGasToken() internal view returns (bool) { @@ -768,16 +723,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase } } - /// @notice Asserts that the ETHLockbox is set/unset correctly depending on the feature flag. - function _assertValidLockboxState() internal view { - if ( - systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(ethLockbox) == address(0) - || !systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(ethLockbox) != address(0) - ) { - revert OptimismPortal_InvalidLockboxState(); - } - } - /// @notice Verifies a withdrawal's output root proof and merkle inclusion proof against /// a dispute game's root claim. /// @param _tx Withdrawal transaction. @@ -832,7 +777,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Checks if a target address is unsafe. function _isUnsafeTarget(address _target) internal view virtual returns (bool) { // Prevent users from targeting an unsafe target address on a withdrawal transaction. - return _target == address(this) || _target == address(ethLockbox); + return _target == address(this); } /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol new file mode 100644 index 000000000..9d047d341 --- /dev/null +++ b/src/L1/ProtocolVersions.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; +import { Initializable } from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @custom:proxied true +/// @title ProtocolVersions +/// @notice Upgrade activation schedule contract, controlled by the ProxyAdmin owner (2-of-2 multisig). +/// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. +/// Each upgrade is identified by an ascending numeric `id` equal to its registration +/// index (0, 1, 2, ...). Human-readable names are intentionally kept offchain: a client +/// maps `id` => name via its own static configuration. Because the registry is strictly +/// append-only (upgrades are never removed or reordered), an `id` permanently refers to the +/// same upgrade. +/// +/// The canonical schedule commitment (`scheduleId`) is the tail of a hash chain that is +/// seeded at index 0 and extended by one link per registered upgrade: +/// +/// _upgradeScheduleId[0] = bytes32(0) (seed) +/// _upgradeScheduleId[i+1] = keccak256(abi.encode(_upgradeScheduleId[i], i, timestamp_i)) +/// scheduleId = _upgradeScheduleId[last] +/// +/// where timestamp_i is upgrade i's current activation timestamp (0 = not yet scheduled). +/// Changing any upgrade's timestamp recomputes its link and all subsequent links, making +/// scheduleId fully reproducible from (upgrade count, current timestamps). Keeping the seed as +/// the array's first element lets both `scheduleId` and the refresh loop avoid an +/// empty-registry special case. Proof journals bind to `scheduleId`, pinning every proof in a +/// dispute game to the schedule in effect at the game's L1 origin block; cross-chain domain +/// separation is provided by the journal itself, which commits the L2 chain id and registry +/// address alongside `scheduleId`. +/// +/// The contract is deployed behind an OP proxy: the implementation constructor disables +/// initializers, and `initialize` (run through the proxy) seeds the hash chain. +contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { + /// @notice Minimum notice period required when changing a preexisting activation timestamp. + uint64 public constant MIN_NOTICE = 1 hours; + + /// @notice Activation timestamp for each registered upgrade, indexed by upgrade id (0 = not scheduled). + /// An upgrade id is registered iff it is a valid index into this array. + /// @dev Every loop in this contract iterates this array. Its length is deliberately uncapped: it only + /// grows via owner-gated `registerUpgrade`, one entry per hardfork, so the unbounded iteration + /// poses no gas exhaustion risk. + uint64[] private _timestamps; + + /// @notice Hash chain links. Element 0 is the seed (`bytes32(0)`), pushed in `initialize`; + /// element `i + 1` is the cumulative hash through upgrade `i`: + /// _upgradeScheduleId[i + 1] = keccak256(abi.encode(_upgradeScheduleId[i], i, _timestamps[i])). + /// Stored per-link so that changing upgrade j's timestamp recomputes only j..n-1 links + /// rather than the entire chain. Non-empty iff the contract has been initialized. + bytes32[] private _upgradeScheduleId; + + /// @notice The minimum protocol version clients must run. Settable by the owner via + /// `setMinimumProtocolVersion`. Informational only — read offchain by clients; + /// not part of the scheduleId commitment. + uint256 public minimumProtocolVersion; + + /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. + /// @dev Appointed and revocable by the owner. This is a restricted secondary role: it can + /// only move an already-scheduled, not-yet-activated upgrade further into the future via + /// `delayTimestamp`. It cannot register upgrades, clear timestamps, pull an activation + /// earlier, or schedule a brand-new activation. Unset (zero) by default. + address public incidentResponder; + + /// @notice Emitted when a new upgrade is registered. + event UpgradeRegistered(uint256 indexed id); + /// @notice Emitted when the minimum protocol version clients must run is updated. + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); + /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. + event TimestampSet(uint256 indexed id, uint256 timestamp); + /// @notice Emitted when the schedule commitment changes. + event ScheduleIdUpdated(bytes32 indexed newScheduleId); + /// @notice Emitted when the incidentResponder role changes. + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); + + /// @notice Thrown when an upgrade id is not registered. + error ProtocolVersions_UnknownUpgrade(uint256 id); + /// @notice Thrown when a protocol version is zero. + error ProtocolVersions_InvalidProtocolVersion(); + /// @notice Thrown when modifying a timestamp whose activation has already passed. + error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); + /// @notice Thrown when the caller is not the incidentResponder. + error ProtocolVersions_NotIncidentResponder(); + /// @notice Thrown when delaying an upgrade that has no scheduled activation. + error ProtocolVersions_NotScheduled(uint256 id); + /// @notice Thrown when a new timestamp is not sufficiently later than the current one. + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + /// @notice Thrown when scheduling a zero-valued static hole below a scheduled successor. + error ProtocolVersions_StaticScheduleHole(uint256 id, uint256 nextScheduledId); + /// @notice Thrown when a non-zero timestamp is less than the previous scheduled upgrade. + error ProtocolVersions_TimestampNotAfterPrevious( + uint256 id, uint256 previousId, uint64 previousTimestamp, uint64 timestamp + ); + /// @notice Thrown when a non-zero timestamp is not less than the next scheduled upgrade. + error ProtocolVersions_TimestampNotBeforeNext(uint256 id, uint256 nextId, uint64 timestamp, uint64 nextTimestamp); + /// @notice Thrown when scheduleId is read before initialize has been called. + error ProtocolVersions_NotInitialized(); + /// @notice Thrown when a non-zero timestamp does not provide at least MIN_NOTICE seconds of notice. + error ProtocolVersions_InsufficientNotice(uint64 timestamp); + + /// @notice Disables initializers on the implementation so it can only be used behind a proxy. + constructor() ReinitializableBase(1) { + _disableInitializers(); + } + + /// @notice Initializes the registry by seeding the hash chain and appointing the initial + /// incidentResponder. Callable only by the ProxyAdmin or its owner. + /// @param _incidentResponder Initial incidentResponder allowed to delay activations, or address(0) to leave unset. + function initialize(address _incidentResponder) external reinitializer(initVersion()) { + // Initialization transactions must come from the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); + + // Seed the hash chain at index 0. Keeping the seed as the first array element lets + // `scheduleId` and `_refreshScheduleId` avoid an empty-registry special case, and makes a + // non-empty array double as the "initialized" flag. + _upgradeScheduleId.push(bytes32(0)); + emit ScheduleIdUpdated(bytes32(0)); + + incidentResponder = _incidentResponder; + emit IncidentResponderUpdated(address(0), _incidentResponder); + } + + /// @notice Registers a new upgrade, assigning it the next ascending id, optionally scheduling its + /// activation and bumping the minimum protocol version in the same call. ProxyAdmin owner only. + /// @dev Pass `timestamp` 0 to register without scheduling (schedule later via `setTimestamp`), or + /// a non-zero value to register and schedule at once. Either way registration extends the + /// scheduleId chain with the new upgrade's link. + /// @param timestamp Unix activation timestamp, or 0 to leave the upgrade unscheduled. + /// @param minProtocolVersion New minimum protocol version to set at registration, or 0 to leave + /// the current minimum unchanged. Must fit in 128 bits if non-zero. + /// @return The ascending id assigned to the newly registered upgrade. + function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256) { + _assertOnlyProxyAdminOwner(); + if (_upgradeScheduleId.length == 0) revert ProtocolVersions_NotInitialized(); + uint256 id = _timestamps.length; + _assertTimestampAfterPrevious(id, timestamp); + _timestamps.push(0); + // Reserve the link slot for this upgrade at index id + 1. + _upgradeScheduleId.push(); + emit UpgradeRegistered(id); + if (timestamp == 0) { + // Register-only: commit the new (id, 0) link. + _refreshScheduleId(id); + } else { + // Register and schedule at once via the shared pure-write helper. + _writeTimestamp(id, timestamp); + } + // Optionally bump the global minimum protocol version in the same call (0 = leave unchanged). + if (minProtocolVersion != 0) { + if (minProtocolVersion > type(uint128).max) revert ProtocolVersions_InvalidProtocolVersion(); + _writeMinimumProtocolVersion(minProtocolVersion); + } + return id; + } + + /// @notice Sets the minimum protocol version clients must run. ProxyAdmin owner only. + /// @dev Informational signal for offchain clients; independent of the upgrade schedule and NOT + /// part of the scheduleId commitment, so it can be updated at any time without shifting any + /// proof binding. + /// @param protocolVersion Packed semver uint256 (must be non-zero and fit in 128 bits). + function setMinimumProtocolVersion(uint256 protocolVersion) external { + _assertOnlyProxyAdminOwner(); + if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); + if (protocolVersion > type(uint128).max) revert ProtocolVersions_InvalidProtocolVersion(); + _writeMinimumProtocolVersion(protocolVersion); + } + + /// @notice Sets the activation timestamp for one upgrade by id. Pass 0 to clear. + /// @dev The activation timestamp must be at least MIN_NOTICE seconds in the future and the + /// upgrade must not have already activated. Pass 0 to remove a not-yet-activated scheduled + /// timestamp; reverts if the upgrade has already passed its activation time. + /// @param id The upgrade to schedule. + /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to + /// clear. + function setTimestamp(uint256 id, uint64 timestamp) external { + _assertOnlyProxyAdminOwner(); + _assertRegistered(id); + uint64 current = _timestamps[id]; + if (current == timestamp) return; + if (current != 0 && uint64(block.timestamp) >= current) { + revert ProtocolVersions_ActivationAlreadyPassed(id, current); + } + if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert ProtocolVersions_InsufficientNotice(timestamp); + } + if (timestamp != 0) { + if (current == 0) _assertNoScheduledSuccessor(id); + _assertTimestampAfterPrevious(id, timestamp); + _assertTimestampBeforeNext(id, timestamp); + } + _writeTimestamp(id, timestamp); + } + + /// @notice Appoints, replaces, or clears (set to zero) the incidentResponder role. ProxyAdmin owner only. + /// @param newIncidentResponder New incidentResponder address, or address(0) to revoke the role. + function setIncidentResponder(address newIncidentResponder) external { + _assertOnlyProxyAdminOwner(); + emit IncidentResponderUpdated(incidentResponder, newIncidentResponder); + incidentResponder = newIncidentResponder; + } + + /// @notice Pushes an already-scheduled upgrade's activation timestamp further into the future. + /// Can only be called by the incidentResponder. + /// @dev The upgrade must already have a non-zero activation timestamp that has not yet passed, + /// and `newTimestamp` must be strictly later than the current value. This role can only + /// delay an activation; it cannot pull one earlier, clear it, or schedule a new one — use + /// the owner's `setTimestamp` for those. Because `current` is in the future and `newTimestamp` + /// is later still, the new value is always in the future. + /// @param id The upgrade whose activation to delay. + /// @param newTimestamp New activation timestamp, must be strictly later than the current one. + function delayTimestamp(uint256 id, uint64 newTimestamp) external { + if (msg.sender != incidentResponder) revert ProtocolVersions_NotIncidentResponder(); + _assertRegistered(id); + uint64 current = _timestamps[id]; + + // The upgrade must already have a scheduled activation to delay. + if (current == 0) revert ProtocolVersions_NotScheduled(id); + // Cannot delay an activation that has already passed. + if (uint64(block.timestamp) >= current) revert ProtocolVersions_ActivationAlreadyPassed(id, current); + // The role can only push the activation later, never to the same time or earlier. + if (newTimestamp <= current) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); + // The new timestamp must also provide at least MIN_NOTICE seconds of notice from now. + uint64 minFloor = uint64(block.timestamp) + MIN_NOTICE; + if (newTimestamp < minFloor) revert ProtocolVersions_InsufficientNotice(newTimestamp); + _assertTimestampBeforeNext(id, newTimestamp); + _writeTimestamp(id, newTimestamp); + } + + /// @notice Returns the canonical schedule commitment. + /// @return The current scheduleId hash. + function scheduleId() external view returns (bytes32) { + uint256 n = _upgradeScheduleId.length; + if (n == 0) revert ProtocolVersions_NotInitialized(); + return _upgradeScheduleId[n - 1]; + } + + /// @notice Returns the schedule commitment as of a specific registered upgrade. + /// @param id The upgrade id to query. + /// @return The scheduleId hash committing to upgrades 0 through `id`. + function scheduleId(uint256 id) external view returns (bytes32) { + _assertRegistered(id); + // Upgrade `id`'s cumulative hash lives at index id + 1 (index 0 is the seed). + return _upgradeScheduleId[id + 1]; + } + + /// @notice Returns the schedule commitment through the highest upgrade active at `l2Timestamp`. + /// @dev Searches from the newest registration downward, returning the cached cumulative link + /// for the first active upgrade. Entries above that upgrade are excluded, while every + /// registered entry through it remains committed by the prefix. Non-zero timestamps are + /// ordered by id, and zero-valued holes below a scheduled successor cannot be scheduled + /// later, preventing inactive prefix holes from moving activated schedule commitments. + /// @param l2Timestamp Inclusive L2 activation cutoff. + /// @return Hash-chain commitment through the highest activated upgrade. + function activatedScheduleId(uint64 l2Timestamp) external view returns (bytes32) { + if (_upgradeScheduleId.length == 0) revert ProtocolVersions_NotInitialized(); + + for (uint256 i = _timestamps.length; i > 0; i--) { + uint64 activationTimestamp = _timestamps[i - 1]; + if (activationTimestamp != 0 && activationTimestamp <= l2Timestamp) return _upgradeScheduleId[i]; + } + + return _upgradeScheduleId[0]; + } + + /// @notice Returns the activation timestamp for every registered upgrade, ordered by upgrade id + /// (0 = not scheduled). The array index equals the upgrade `id`; names are resolved + /// offchain, and per-upgrade schedule hashes can be reproduced from these timestamps and + /// the seed or read via `scheduleId(id)`. + /// @dev Calling via eth_call is gas-free; no transaction is submitted. + /// @return Ordered activation timestamps, one per registered upgrade. + function getSchedule() external view returns (uint64[] memory) { + return _timestamps; + } + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + function version() public pure virtual returns (string memory) { + return "1.0.0"; + } + + /// @dev Writes newTs, emits TimestampSet, and refreshes the hash chain. All validation is + /// the caller's responsibility. + function _writeTimestamp(uint256 id, uint64 newTs) private { + _timestamps[id] = newTs; + emit TimestampSet(id, newTs); + _refreshScheduleId(id); + } + + /// @dev Writes the minimum protocol version and emits the update. Validation is the caller's + /// responsibility. + function _writeMinimumProtocolVersion(uint256 protocolVersion) private { + minimumProtocolVersion = protocolVersion; + emit MinimumProtocolVersionUpdated(protocolVersion); + } + + /// @dev Recomputes the per-upgrade cumulative hash chain starting from upgrade `startIndex` and + /// bubbles the result through all subsequent registered upgrades. Cost is O(n-startIndex). + /// Only ever called after a state change (registration, or a timestamp that actually moved), + /// so the resulting tail is always a new scheduleId. + function _refreshScheduleId(uint256 startIndex) private { + uint256 n = _timestamps.length; + + // _upgradeScheduleId[startIndex] is the link preceding upgrade `startIndex` (the seed when + // startIndex == 0). Recompute from startIndex onward, storing each link at index i + 1. + bytes32 prev = _upgradeScheduleId[startIndex]; + for (uint256 i = startIndex; i < n; i++) { + prev = keccak256(abi.encode(prev, i, _timestamps[i])); + _upgradeScheduleId[i + 1] = prev; + } + + emit ScheduleIdUpdated(prev); + } + + /// @dev Prevents scheduling a zero-valued hole once a later upgrade has a timestamp. + function _assertNoScheduledSuccessor(uint256 id) private view { + for (uint256 i = id + 1; i < _timestamps.length; i++) { + if (_timestamps[i] != 0) revert ProtocolVersions_StaticScheduleHole(id, i); + } + } + + /// @dev Requires `timestamp` to be greater than or equal to the closest lower-id scheduled upgrade. + function _assertTimestampAfterPrevious(uint256 id, uint64 timestamp) private view { + if (timestamp == 0) return; + + for (uint256 i = id; i > 0; i--) { + uint64 previous = _timestamps[i - 1]; + if (previous != 0) { + if (timestamp < previous) { + revert ProtocolVersions_TimestampNotAfterPrevious(id, i - 1, previous, timestamp); + } + return; + } + } + } + + /// @dev Requires `timestamp` to be less than the closest higher-id scheduled upgrade. + function _assertTimestampBeforeNext(uint256 id, uint64 timestamp) private view { + if (timestamp == 0) return; + + for (uint256 i = id + 1; i < _timestamps.length; i++) { + uint64 next = _timestamps[i]; + if (next != 0) { + if (timestamp >= next) revert ProtocolVersions_TimestampNotBeforeNext(id, i, timestamp, next); + return; + } + } + } + + /// @dev Reverts if `id` is not a registered upgrade. + function _assertRegistered(uint256 id) private view { + if (id >= _timestamps.length) revert ProtocolVersions_UnknownUpgrade(id); + } +} diff --git a/src/L1/ResourceMetering.sol b/src/L1/ResourceMetering.sol index 465c73813..3c963e4eb 100644 --- a/src/L1/ResourceMetering.sol +++ b/src/L1/ResourceMetering.sol @@ -135,11 +135,11 @@ abstract contract ResourceMetering is Initializable { uint256 resourceCost = uint256(_amount) * uint256(params.prevBaseFee); // We currently charge for this ETH amount as an L1 gas burn, so we convert the ETH amount - // into gas by dividing by the L1 base fee. We assume a minimum base fee of 1 gwei to avoid - // division by zero for L1s that don't support 1559 or to avoid excessive gas burns during - // periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei - // during any 1 day period in the last 5 years, so should be fine. - uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei); + // into gas by dividing by the L1 base fee. We clamp to a minimum of 0.01 gwei to avoid + // division by zero on L1s without EIP-1559 and to cap gas-burn requirements when L1 demand + // is extremely low. A higher floor reduces the gas burned per deposit but undercharges + // relative to the deposit fee market when L1 base fee is below that floor. + uint256 gasCost = resourceCost / Math.max(block.basefee, 0.01 gwei); // Give the user a refund based on the amount of gas they used to do all of the work up to // this point. Since we're at the end of the modifier, this should be pretty accurate. Acts diff --git a/src/L1/SystemConfig.sol b/src/L1/SystemConfig.sol index b5498da18..51c975dba 100644 --- a/src/L1/SystemConfig.sol +++ b/src/L1/SystemConfig.sol @@ -546,32 +546,6 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl revert SystemConfig_InvalidFeatureState(); } - // Handle feature-specific safety logic here. - if (_feature == Features.ETH_LOCKBOX) { - // It would probably better to check that the ETHLockbox contract is set inside the - // OptimismPortal2 contract before you're allowed to enable the feature here, but the - // portal checks that the feature is set before allowing you to set the lockbox, so - // these checks are good enough. - - // Lockbox shouldn't be unset if the ethLockbox address is still configured in the - // OptimismPortal2 contract. Doing so would cause the system to start keeping ETH in - // the portal. This check means there's no way to stop using ETHLockbox at the moment - // after it's been configured (which is expected). - if ( - isFeatureEnabled[_feature] && !_enabled - && address(IOptimismPortal2(payable(optimismPortal())).ethLockbox()) != address(0) - ) { - revert SystemConfig_InvalidFeatureState(); - } - - // Lockbox can't be set or unset if the system is currently paused because it would - // change the pause identifier which would potentially cause the system to become - // unpaused unexpectedly. - if (paused()) { - revert SystemConfig_InvalidFeatureState(); - } - } - // Set the feature. isFeatureEnabled[_feature] = _enabled; @@ -579,20 +553,10 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl emit FeatureSet(_feature, _enabled); } - /// @notice Returns the current pause state for this network. If the network is using - /// ETHLockbox, the system is paused if either the global pause is active or the pause - /// is active where the ETHLockbox address is used as the identifier. If the network is - /// not using ETHLockbox, the system is paused if either the global pause is active or - /// the pause is active where the OptimismPortal address is used as the identifier. + /// @notice Returns the current pause state for this network. /// @return bool True if the system is paused, false otherwise. function paused() public view returns (bool) { - // Determine the appropriate chain identifier based on the feature flags. - address identifier = isFeatureEnabled[Features.ETH_LOCKBOX] - ? address(IOptimismPortal2(payable(optimismPortal())).ethLockbox()) - : address(optimismPortal()); - - // Check if either global or local pause is active. - return superchainConfig.paused(address(0)) || superchainConfig.paused(identifier); + return superchainConfig.paused(address(0)) || superchainConfig.paused(optimismPortal()); } /// @notice Returns the guardian address of the SuperchainConfig. diff --git a/src/L1/proofs/AggregateVerifier.sol b/src/L1/proofs/AggregateVerifier.sol index 2682796c5..0508ccf01 100644 --- a/src/L1/proofs/AggregateVerifier.sol +++ b/src/L1/proofs/AggregateVerifier.sol @@ -26,6 +26,7 @@ import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { //////////////////////////////////////////////////////////////// @@ -50,6 +51,18 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { uint64 fast; } + /// @notice The ProtocolVersions registry and the highest upgrade id the prover image supports. + struct ScheduleConfig { + IProtocolVersions protocolVersions; + uint256 maxUpgradeId; + } + + /// @notice Finalization and upgrade schedule configuration for the dispute game. + struct GameConfig { + FinalizationDelays finalizationDelays; + ScheduleConfig schedule; + } + //////////////////////////////////////////////////////////////// // Constants // //////////////////////////////////////////////////////////////// @@ -119,6 +132,14 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The fast finalization delay (time after both proofs before game can resolve). uint64 public immutable FAST_FINALIZATION_DELAY; + /// @notice The ProtocolVersions upgrade schedule contract. + IProtocolVersions public immutable PROTOCOL_VERSIONS; + + /// @notice The highest ProtocolVersions upgrade id the prover image supports. Games pin the + /// schedule commitment at this id, so later registrations cannot affect this game type. + /// @dev Governance must retire this game type before an upgrade beyond the pin activates. + uint256 public immutable MAX_UPGRADE_ID; + //////////////////////////////////////////////////////////////// // State Vars // //////////////////////////////////////////////////////////////// @@ -168,6 +189,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The number of proofs provided. uint8 public proofCount; + /// @notice The ProtocolVersions schedule commitment through MAX_UPGRADE_ID, pinned at game + /// initialization. Every proof in this game commits to this value. + bytes32 public scheduleId; + //////////////////////////////////////////////////////////////// // Events // //////////////////////////////////////////////////////////////// @@ -265,7 +290,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @param l2ChainId The chain ID of the L2 network. /// @param blockInterval The block interval. /// @param intermediateBlockInterval The intermediate block interval. - /// @param delays Finalization delay configuration (slow = after single proof, fast = after both proofs). + /// @param gameConfig Finalization delays, ProtocolVersions registry, and the pinned max upgrade id. constructor( GameType gameType_, IAnchorStateRegistry anchorStateRegistry_, @@ -278,13 +303,12 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { uint256 l2ChainId, uint256 blockInterval, uint256 intermediateBlockInterval, - FinalizationDelays memory delays + GameConfig memory gameConfig ) { // Block interval and intermediate block interval must be positive and divisible. if (blockInterval == 0 || intermediateBlockInterval == 0 || blockInterval % intermediateBlockInterval != 0) { revert InvalidBlockInterval(blockInterval, intermediateBlockInterval); } - // Set up initial game state. GAME_TYPE = gameType_; ANCHOR_STATE_REGISTRY = anchorStateRegistry_; @@ -299,8 +323,13 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { L2_CHAIN_ID = l2ChainId; BLOCK_INTERVAL = blockInterval; INTERMEDIATE_BLOCK_INTERVAL = intermediateBlockInterval; - SLOW_FINALIZATION_DELAY = delays.slow; - FAST_FINALIZATION_DELAY = delays.fast; + SLOW_FINALIZATION_DELAY = gameConfig.finalizationDelays.slow; + FAST_FINALIZATION_DELAY = gameConfig.finalizationDelays.fast; + PROTOCOL_VERSIONS = gameConfig.schedule.protocolVersions; + MAX_UPGRADE_ID = gameConfig.schedule.maxUpgradeId; + + // Reverts if the registry has not yet registered upgrade `maxUpgradeId`. + gameConfig.schedule.protocolVersions.scheduleId(gameConfig.schedule.maxUpgradeId); INITIALIZE_CALLDATA_SIZE = 0x8E + 0x20 * intermediateOutputRootsCount(); } @@ -374,6 +403,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Set the game as initialized. initialized = true; + // Pin the schedule commitment through MAX_UPGRADE_ID; every proof in this game commits + // to it. + scheduleId = PROTOCOL_VERSIONS.scheduleId(MAX_UPGRADE_ID); + // Set the game's starting timestamp. createdAt = Timestamp.wrap(uint64(block.timestamp)); @@ -909,7 +942,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - TEE_IMAGE_HASH + TEE_IMAGE_HASH, + scheduleId ) ); @@ -943,7 +977,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - ZK_RANGE_HASH + ZK_RANGE_HASH, + scheduleId ) ); diff --git a/src/L1/proofs/tee/NitroEnclaveVerifier.sol b/src/L1/proofs/tee/NitroEnclaveVerifier.sol index 56b0d043e..40cfc03a7 100644 --- a/src/L1/proofs/tee/NitroEnclaveVerifier.sol +++ b/src/L1/proofs/tee/NitroEnclaveVerifier.sol @@ -14,34 +14,32 @@ import { IRiscZeroVerifier } from "lib/risc0-ethereum/contracts/src/IRiscZeroVer import { ISP1Verifier } from "interfaces/L1/proofs/zk/ISP1Verifier.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; -/** - * @title NitroEnclaveVerifier - * @dev Implementation contract for AWS Nitro Enclave attestation verification using zero-knowledge proofs - * @dev Custom version of Automata's NitroEnclaveVerifier contract at - * https://github.com/automata-network/aws-nitro-enclave-attestation/tree/26c90565cb009e6539643a0956f9502a12ade672 - * - * Differences from the upstream Automata contract: - * - Verification of ZK proofs is restricted to an authorized proof submitter address - * - All privileged actions emit events for monitoring - * - Removes verification-with-explicit-program-ID and Pico logic - * - * This contract provides on-chain verification of AWS Nitro Enclave attestation reports by validating - * zero-knowledge proofs generated off-chain. It supports both single and batch verification modes - * and can work with multiple ZK proof systems (RISC Zero and Succinct SP1). - * - * Key features: - * - Certificate chain management with automatic caching of newly discovered certificates - * - Timestamp validation with configurable time tolerance - * - Certificate revocation capabilities for compromised intermediate certificates - * - Gas-efficient batch verification for multiple attestations - * - Support for both RISC Zero and SP1 proving systems - * - * Security considerations: - * - Only the contract owner can manage certificates and configurations - * - Root certificate is immutable once set (requires owner to change) - * - Intermediate certificates are automatically cached but can be revoked - * - Timestamp validation prevents replay attacks within the configured time window - */ +/// @title NitroEnclaveVerifier +/// @dev Implementation contract for AWS Nitro Enclave attestation verification using zero-knowledge proofs +/// @dev Custom version of Automata's NitroEnclaveVerifier contract at +/// https://github.com/automata-network/aws-nitro-enclave-attestation/tree/26c90565cb009e6539643a0956f9502a12ade672 +/// +/// Differences from the upstream Automata contract: +/// - Verification of ZK proofs is restricted to an authorized proof submitter address +/// - All privileged actions emit events for monitoring +/// - Removes verification-with-explicit-program-ID and Pico logic +/// +/// This contract provides onchain verification of AWS Nitro Enclave attestation reports by validating +/// zero-knowledge proofs generated offchain. It supports both single and batch verification modes +/// and can work with multiple ZK proof systems (RISC Zero and Succinct SP1). +/// +/// Key features: +/// - Certificate chain management with automatic caching of newly discovered certificates +/// - Timestamp validation with configurable time tolerance +/// - Certificate revocation capabilities for compromised intermediate certificates +/// - Gas-efficient batch verification for multiple attestations +/// - Support for both RISC Zero and SP1 proving systems +/// +/// Security considerations: +/// - Only the contract owner can manage certificates and configurations +/// - Root certificate is immutable once set (requires owner to change) +/// - Intermediate certificates are automatically cached but can be revoked +/// - Timestamp validation prevents replay attacks within the configured time window contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { /// @dev Sentinel address to indicate a route has been permanently frozen address private constant FROZEN = address(0xdead); @@ -97,9 +95,6 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { /// @dev Thrown when a caller other than the authorized proof submitter calls verify or batchVerify error CallerNotProofSubmitter(); - /// @dev Thrown when a certificate hash is not found in the trusted intermediate certificates set - error CertificateNotFound(bytes32 certHash); - /// @dev Thrown when a program ID argument is bytes32(0) error ZeroProgramId(); @@ -180,19 +175,17 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { _; } - /** - * @dev Initializes the contract with owner, time tolerance and initial trusted certificates - * @param owner Address to be set as the contract owner - * @param initialMaxTimeDiff Maximum time difference in seconds for timestamp validation - * @param initializeTrustedCerts Array of initial trusted intermediate certificate hashes - * @param initializeTrustedCertExpiries Array of notAfter timestamps (seconds) for each initial cert - * @param initialRootCert Hash of the AWS Nitro Enclave root certificate - * @param initialProofSubmitter Address that is authorized to submit proofs - * @param initialRevoker Address authorized to revoke intermediate certificates (can be address(0) to disable) - * @param zkCoProcessor Type of ZK coprocessor to configure (RiscZero or Succinct) - * @param config Configuration parameters for the ZK coprocessor - * @param verifierProofId The verifierProofId corresponding to the verifierId in config - */ + /// @dev Initializes the contract with owner, time tolerance and initial trusted certificates + /// @param owner Address to be set as the contract owner + /// @param initialMaxTimeDiff Maximum time difference in seconds for timestamp validation + /// @param initializeTrustedCerts Array of initial trusted intermediate certificate hashes + /// @param initializeTrustedCertExpiries Array of notAfter timestamps (seconds) for each initial cert + /// @param initialRootCert Hash of the AWS Nitro Enclave root certificate + /// @param initialProofSubmitter Address that is authorized to submit proofs + /// @param initialRevoker Address authorized to revoke intermediate certificates (can be address(0) to disable) + /// @param zkCoProcessor Type of ZK coprocessor to configure (RiscZero or Succinct) + /// @param config Configuration parameters for the ZK coprocessor + /// @param verifierProofId The verifierProofId corresponding to the verifierId in config constructor( address owner, uint64 initialMaxTimeDiff, @@ -222,21 +215,17 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { // ============ Query Functions ============ - /** - * @dev Retrieves the configuration for a specific coprocessor - * @param zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) - * @return ZkCoProcessorConfig Configuration parameters including program IDs and verifier address - */ + /// @dev Retrieves the configuration for a specific coprocessor + /// @param zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) + /// @return ZkCoProcessorConfig Configuration parameters including program IDs and verifier address function getZkConfig(ZkCoProcessorType zkCoProcessor) external view returns (ZkCoProcessorConfig memory) { return zkConfig[zkCoProcessor]; } - /** - * @dev Gets the verifier address for a specific route - * @param zkCoProcessor Type of ZK coprocessor - * @param selector Proof selector - * @return Verifier address (route-specific or default fallback) - */ + /// @dev Gets the verifier address for a specific route + /// @param zkCoProcessor Type of ZK coprocessor + /// @param selector Proof selector + /// @return Verifier address (route-specific or default fallback) function getZkVerifier(ZkCoProcessorType zkCoProcessor, bytes4 selector) external view returns (address) { address verifier = _zkVerifierRoutes[zkCoProcessor][selector]; @@ -251,29 +240,25 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { return verifier; } - /** - * @dev Returns the verifierProofId for a given ZkCoProcessorType - * @param zkCoProcessor Type of ZK coprocessor - * @return The corresponding verifierProofId - */ + /// @dev Returns the verifierProofId for a given ZkCoProcessorType + /// @param zkCoProcessor Type of ZK coprocessor + /// @return The corresponding verifierProofId function getVerifierProofId(ZkCoProcessorType zkCoProcessor) external view returns (bytes32) { return _verifierProofIds[zkCoProcessor]; } - /** - * @dev Checks the prefix length of trusted certificates in each provided certificate chain for reports - * @param reportCerts Array of certificate chains, each containing certificate hashes - * @return Array indicating the prefix length of trusted certificates in each chain - * - * For each certificate chain: - * 1. Validates that the first certificate matches the stored root certificate - * 2. Counts consecutive trusted certificates starting from the root - * 3. Stops counting when an untrusted certificate is encountered - * - * This function is used to pre-validate certificate chains before generating proofs, - * helping to optimize the proving process by determining trusted certificate lengths. - * Usually called from off-chain - */ + /// @dev Checks the prefix length of trusted certificates in each provided certificate chain for reports + /// @param reportCerts Array of certificate chains, each containing certificate hashes + /// @return Array indicating the prefix length of trusted certificates in each chain + /// + /// For each certificate chain: + /// 1. Validates that the first certificate matches the stored root certificate + /// 2. Counts consecutive trusted certificates starting from the root + /// 3. Stops counting when an untrusted certificate is encountered + /// + /// This function is used to pre-validate certificate chains before generating proofs, + /// helping to optimize the proving process by determining trusted certificate lengths. + /// Usually called from offchain function checkTrustedIntermediateCerts(bytes32[][] calldata reportCerts) public view returns (uint8[] memory) { uint8[] memory results = new uint8[](reportCerts.length); bytes32 rootCertHash = rootCert; @@ -284,7 +269,7 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { revert RootCertMismatch(rootCertHash, certs[0]); } for (uint256 j = 1; j < certs.length; j++) { - // Stop counting at any revoked entry so off-chain callers cannot derive a + // Stop counting at any revoked entry so offchain callers cannot derive a // prefix-len that walks past a revoked cert and then claim it as the trusted boundary. if (revokedCerts[certs[j]]) { break; @@ -302,48 +287,42 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { // ============ Admin Functions ============ - /** - * @dev Sets the trusted root certificate hash - * @param newRootCert Hash of the AWS Nitro Enclave root certificate - * - * Requirements: - * - Only callable by contract owner - * - * The root certificate serves as the trust anchor for all certificate chain validations. - * This should be set to the hash of AWS's root certificate for Nitro Enclaves. - */ + /// @dev Sets the trusted root certificate hash + /// @param newRootCert Hash of the AWS Nitro Enclave root certificate + /// + /// Requirements: + /// - Only callable by contract owner + /// + /// The root certificate serves as the trust anchor for all certificate chain validations. + /// This should be set to the hash of AWS's root certificate for Nitro Enclaves. function setRootCert(bytes32 newRootCert) external onlyOwner { _setRootCert(newRootCert); } - /** - * @dev Updates the maximum allowed time difference for attestation timestamp validation - * @param newMaxTimeDiff New maximum time difference in seconds - * - * Requirements: - * - Only callable by contract owner - * - Must be greater than zero - */ + /// @dev Updates the maximum allowed time difference for attestation timestamp validation + /// @param newMaxTimeDiff New maximum time difference in seconds + /// + /// Requirements: + /// - Only callable by contract owner + /// - Must be greater than zero function setMaxTimeDiff(uint64 newMaxTimeDiff) external onlyOwner { if (newMaxTimeDiff == 0) revert ZeroMaxTimeDiff(); maxTimeDiff = newMaxTimeDiff; emit MaxTimeDiffUpdated(newMaxTimeDiff); } - /** - * @dev Configures zero-knowledge verification parameters for a specific coprocessor - * @param zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) - * @param config Configuration parameters including program IDs and verifier address - * @param verifierProofId The verifierProofId corresponding to the verifierId in config - * - * Requirements: - * - Only callable by contract owner - * - * This function sets up the necessary parameters for ZK proof verification: - * - verifierId: Program ID for single attestation verification - * - aggregatorId: Program ID for batch/aggregated verification - * - zkVerifier: Address of the deployed ZK verifier contract - */ + /// @dev Configures zero-knowledge verification parameters for a specific coprocessor + /// @param zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct) + /// @param config Configuration parameters including program IDs and verifier address + /// @param verifierProofId The verifierProofId corresponding to the verifierId in config + /// + /// Requirements: + /// - Only callable by contract owner + /// + /// This function sets up the necessary parameters for ZK proof verification: + /// - verifierId: Program ID for single attestation verification + /// - aggregatorId: Program ID for batch/aggregated verification + /// - zkVerifier: Address of the deployed ZK verifier contract function setZkConfiguration( ZkCoProcessorType zkCoProcessor, ZkCoProcessorConfig memory config, @@ -355,49 +334,44 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { _setZkConfiguration(zkCoProcessor, config, verifierProofId); } - /** - * @dev Revokes a trusted intermediate certificate. - * @param certHash Hash of the certificate to revoke - * - * Requirements: - * - Only callable by contract owner or revoker - * - Certificate must exist in the trusted intermediate certificates set - * - * This function allows the owner or revoker to revoke compromised intermediate certificates - * without affecting the root certificate or other trusted certificates. - * - * Durability: in addition to clearing `trustedIntermediateCerts[certHash]`, this - * function flips the persistent `revokedCerts[certHash]` sentinel. The sentinel - * survives subsequent `_cacheNewCert` overwrites and causes both `_verifyJournal` - * and `checkTrustedIntermediateCerts` to reject any chain whose suffix traverses - * the revoked hash, regardless of the journal's `trustedCertsPrefixLen`. Reproving - * the same chain therefore cannot silently restore trust; re-trust requires an - * explicit `unrevokeCert` call by the owner. - */ + /// @dev Revokes an intermediate certificate, whether or not it has been cached as trusted. + /// @param certHash Hash of the certificate to revoke + /// + /// Requirements: + /// - Only callable by contract owner or revoker + /// + /// Certificates that have never been seen onchain can be revoked preemptively; the + /// persistent `revokedCerts` sentinel blocks them from being trusted on first + /// verification. This function allows the owner or revoker to revoke compromised + /// intermediate certificates without affecting the root certificate or other trusted + /// certificates. + /// + /// Durability: in addition to clearing `trustedIntermediateCerts[certHash]`, this + /// function flips the persistent `revokedCerts[certHash]` sentinel. The sentinel + /// survives subsequent `_cacheNewCert` overwrites and causes both `_verifyJournal` + /// and `checkTrustedIntermediateCerts` to reject any chain whose suffix traverses + /// the revoked hash, regardless of the journal's `trustedCertsPrefixLen`. Reproving + /// the same chain therefore cannot silently restore trust; re-trust requires an + /// explicit `unrevokeCert` call by the owner. function revokeCert(bytes32 certHash) external onlyOwnerOrRevoker { - if (trustedIntermediateCerts[certHash] == 0) { - revert CertificateNotFound(certHash); - } delete trustedIntermediateCerts[certHash]; revokedCerts[certHash] = true; emit CertRevoked(certHash); } - /** - * @dev Explicitly re-trusts a previously revoked intermediate certificate. - * @param certHash Hash of the certificate to un-revoke - * - * Requirements: - * - Only callable by contract owner - * - Certificate must currently be marked as revoked - * - * Clearing the revocation sentinel does not by itself restore the cached - * expiry; the next successful verification whose chain traverses `certHash` - * will re-cache it via `_cacheNewCert`. This two-step design (admin clears - * the sentinel, verification re-caches the expiry) keeps re-trust an - * explicit, owner-only action while still letting the normal cache path - * supply the up-to-date `notAfter` timestamp. - */ + /// @dev Explicitly re-trusts a previously revoked intermediate certificate. + /// @param certHash Hash of the certificate to un-revoke + /// + /// Requirements: + /// - Only callable by contract owner + /// - Certificate must currently be marked as revoked + /// + /// Clearing the revocation sentinel does not by itself restore the cached + /// expiry; the next successful verification whose chain traverses `certHash` + /// will re-cache it via `_cacheNewCert`. This two-step design (admin clears + /// the sentinel, verification re-caches the expiry) keeps re-trust an + /// explicit, owner-only action while still letting the normal cache path + /// supply the up-to-date `notAfter` timestamp. function unrevokeCert(bytes32 certHash) external onlyOwner { if (!revokedCerts[certHash]) { revert CertificateNotRevoked(certHash); @@ -406,12 +380,10 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit CertUnrevoked(certHash); } - /** - * @dev Updates the verifier program ID, adding the new version to the supported set - * @param zkCoProcessor Type of ZK coprocessor - * @param newVerifierId New verifier program ID to set as latest - * @param newVerifierProofId New verifier proof ID (stored in mapping, used in batch verification) - */ + /// @dev Updates the verifier program ID, adding the new version to the supported set + /// @param zkCoProcessor Type of ZK coprocessor + /// @param newVerifierId New verifier program ID to set as latest + /// @param newVerifierProofId New verifier proof ID (stored in mapping, used in batch verification) function updateVerifierId( ZkCoProcessorType zkCoProcessor, bytes32 newVerifierId, @@ -431,11 +403,9 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit VerifierIdUpdated(zkCoProcessor, newVerifierId, newVerifierProofId); } - /** - * @dev Updates the aggregator program ID, adding the new version to the supported set - * @param zkCoProcessor Type of ZK coprocessor - * @param newAggregatorId New aggregator program ID to set as latest - */ + /// @dev Updates the aggregator program ID, adding the new version to the supported set + /// @param zkCoProcessor Type of ZK coprocessor + /// @param newAggregatorId New aggregator program ID to set as latest function updateAggregatorId(ZkCoProcessorType zkCoProcessor, bytes32 newAggregatorId) external onlyOwner { if (newAggregatorId == bytes32(0)) revert ZeroProgramId(); if (zkConfig[zkCoProcessor].aggregatorId == newAggregatorId) { @@ -447,12 +417,10 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit AggregatorIdUpdated(zkCoProcessor, newAggregatorId); } - /** - * @dev Adds a route-specific verifier override - * @param zkCoProcessor Type of ZK coprocessor - * @param selector Proof selector (first 4 bytes of proof data) - * @param verifier Address of the verifier contract for this route - */ + /// @dev Adds a route-specific verifier override + /// @param zkCoProcessor Type of ZK coprocessor + /// @param selector Proof selector (first 4 bytes of proof data) + /// @param verifier Address of the verifier contract for this route function addVerifyRoute(ZkCoProcessorType zkCoProcessor, bytes4 selector, address verifier) external onlyOwner { if (verifier == address(0)) revert ZeroVerifierAddress(); if (verifier == FROZEN) revert InvalidVerifierAddress(); @@ -465,13 +433,11 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit ZkRouteAdded(zkCoProcessor, selector, verifier); } - /** - * @dev Permanently freezes a verification route - * @param zkCoProcessor Type of ZK coprocessor - * @param selector Proof selector to freeze - * - * WARNING: This action is IRREVERSIBLE - */ + /// @dev Permanently freezes a verification route + /// @param zkCoProcessor Type of ZK coprocessor + /// @param selector Proof selector to freeze + /// + /// WARNING: This action is IRREVERSIBLE function freezeVerifyRoute(ZkCoProcessorType zkCoProcessor, bytes4 selector) external onlyOwner { address currentVerifier = _zkVerifierRoutes[zkCoProcessor][selector]; @@ -483,25 +449,21 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit ZkRouteWasFrozen(zkCoProcessor, selector); } - /** - * @dev Sets the proof submitter address - * @param submitter The address of the proof submitter - * - * Requirements: - * - Only callable by contract owner - * - Address must not be zero - */ + /// @dev Sets the proof submitter address + /// @param submitter The address of the proof submitter + /// + /// Requirements: + /// - Only callable by contract owner + /// - Address must not be zero function setProofSubmitter(address submitter) external onlyOwner { _setProofSubmitter(submitter); } - /** - * @dev Updates the revoker address - * @param newRevoker New revoker address (can be address(0) to disable the revoker role) - * - * Requirements: - * - Only callable by contract owner - */ + /// @dev Updates the revoker address + /// @param newRevoker New revoker address (can be address(0) to disable the revoker role) + /// + /// Requirements: + /// - Only callable by contract owner function setRevoker(address newRevoker) external onlyOwner { revoker = newRevoker; emit RevokerUpdated(newRevoker); @@ -509,27 +471,25 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { // ============ Verification Functions ============ - /** - * @dev Verifies a single attestation report using zero-knowledge proof - * @param output Encoded VerifierJournal containing the verification result - * @param zkCoprocessor Type of ZK coprocessor used to generate the proof - * @param proofBytes Zero-knowledge proof data for the attestation - * @return journal VerifierJournal containing the verification result and extracted data - * - * This function performs end-to-end verification of a single attestation: - * 1. Retrieves the single verification program ID from configuration - * 2. Verifies the zero-knowledge proof using the specified coprocessor - * 3. Decodes the verification journal from the output - * 4. Validates the journal through comprehensive checks - * 5. Returns the final verification result - * - * The returned journal contains all extracted attestation data including: - * - Verification status and any error conditions - * - Certificate chain information and trust levels - * - User data, nonce, and public key from the attestation - * - Platform Configuration Registers (PCRs) for integrity measurement - * - Module ID and timestamp information - */ + /// @dev Verifies a single attestation report using zero-knowledge proof + /// @param output Encoded VerifierJournal containing the verification result + /// @param zkCoprocessor Type of ZK coprocessor used to generate the proof + /// @param proofBytes Zero-knowledge proof data for the attestation + /// @return journal VerifierJournal containing the verification result and extracted data + /// + /// This function performs end-to-end verification of a single attestation: + /// 1. Retrieves the single verification program ID from configuration + /// 2. Verifies the zero-knowledge proof using the specified coprocessor + /// 3. Decodes the verification journal from the output + /// 4. Validates the journal through comprehensive checks + /// 5. Returns the final verification result + /// + /// The returned journal contains all extracted attestation data including: + /// - Verification status and any error conditions + /// - Certificate chain information and trust levels + /// - User data, nonce, and public key from the attestation + /// - Platform Configuration Registers (PCRs) for integrity measurement + /// - Module ID and timestamp information function verify( bytes calldata output, ZkCoProcessorType zkCoprocessor, @@ -546,22 +506,20 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit AttestationSubmitted(journal.result, zkCoprocessor, abi.encode(journal)); } - /** - * @dev Verifies multiple attestation reports in a single batch operation - * @param output Encoded BatchVerifierJournal containing aggregated verification results - * @param zkCoprocessor Type of ZK coprocessor used to generate the proof - * @param proofBytes Zero-knowledge proof data for batch verification - * @return results Array of VerifierJournal results, one for each attestation in the batch - * - * This function provides gas-efficient batch verification by: - * 1. Using the aggregator program ID for ZK proof verification - * 2. Validating the batch verifier key matches the expected value - * 3. Processing each individual attestation through standard validation - * 4. Returning comprehensive results for all attestations - * - * Batch verification is recommended when processing multiple attestations - * as it significantly reduces gas costs compared to individual verifications. - */ + /// @dev Verifies multiple attestation reports in a single batch operation + /// @param output Encoded BatchVerifierJournal containing aggregated verification results + /// @param zkCoprocessor Type of ZK coprocessor used to generate the proof + /// @param proofBytes Zero-knowledge proof data for batch verification + /// @return results Array of VerifierJournal results, one for each attestation in the batch + /// + /// This function provides gas-efficient batch verification by: + /// 1. Using the aggregator program ID for ZK proof verification + /// 2. Validating the batch verifier key matches the expected value + /// 3. Processing each individual attestation through standard validation + /// 4. Returning comprehensive results for all attestations + /// + /// Batch verification is recommended when processing multiple attestations + /// as it significantly reduces gas costs compared to individual verifications. function batchVerify( bytes calldata output, ZkCoProcessorType zkCoprocessor, @@ -617,26 +575,24 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { emit ZKConfigurationUpdated(zkCoProcessor, config, verifierProofId); } - /** - * @dev Internal function to cache newly discovered trusted certificates - * @param journal Verification journal containing certificate chain information - * - * This function automatically adds any certificates beyond the trusted length - * to the trusted intermediate certificates set. This optimizes future verifications - * by expanding the known trusted certificate set based on successful verifications. - * - * Revoked entries terminate caching: once `revokedCerts[certHash]` is set by - * `revokeCert`, no successful verification will silently restore the cache, - * regardless of the journal's `trustedCertsPrefixLen`. Because `certs[i+1]` is - * signed by `certs[i]`, every descendant of a revoked cert inherits its trust - * from a revoked parent and must not be cached either — so we `break` rather - * than `continue` on the first revoked entry, matching `checkTrustedIntermediateCerts`. - * - * Note: in current control flow this guard is unreachable because `_verifyJournal` - * Pass 2 already rejects any journal whose suffix contains a revoked digest before - * `_cacheNewCert` is invoked. The check is retained as defense-in-depth against - * future refactors. Re-trust requires an explicit `unrevokeCert`. - */ + /// @dev Internal function to cache newly discovered trusted certificates + /// @param journal Verification journal containing certificate chain information + /// + /// This function automatically adds any certificates beyond the trusted length + /// to the trusted intermediate certificates set. This optimizes future verifications + /// by expanding the known trusted certificate set based on successful verifications. + /// + /// Revoked entries terminate caching: once `revokedCerts[certHash]` is set by + /// `revokeCert`, no successful verification will silently restore the cache, + /// regardless of the journal's `trustedCertsPrefixLen`. Because `certs[i+1]` is + /// signed by `certs[i]`, every descendant of a revoked cert inherits its trust + /// from a revoked parent and must not be cached either — so we `break` rather + /// than `continue` on the first revoked entry, matching `checkTrustedIntermediateCerts`. + /// + /// Note: in current control flow this guard is unreachable because `_verifyJournal` + /// Pass 2 already rejects any journal whose suffix contains a revoked digest before + /// `_cacheNewCert` is invoked. The check is retained as defense-in-depth against + /// future refactors. Re-trust requires an explicit `unrevokeCert`. function _cacheNewCert(VerifierJournal memory journal) internal { for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length; i++) { bytes32 certHash = journal.certs[i]; @@ -647,35 +603,33 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { } } - /** - * @dev Internal function to verify and validate a journal entry - * @param journal Verification journal to validate - * @return Updated journal with final verification result - * - * This function performs comprehensive validation: - * 1. Checks if the initial ZK verification was successful - * 2. Validates the root certificate matches the trusted root - * 3. Ensures all trusted certificates in the prefix are still valid (not revoked, not expired) - * 4. Ensures no certificate in the suffix has been revoked, regardless of `trustedCertsPrefixLen` - * 5. Validates the attestation timestamp is within acceptable range - * 6. Caches newly discovered certificates for future use - * - * The suffix-side revocation check (step 4) is the load-bearing fix for the - * `revokeCert` durability gap exposed under the production - * `trustedCertsPrefixLen = 1` configuration. Without it, Pass 1 only walks - * the root and a journal whose chain traverses a revoked intermediate in - * the suffix would succeed and then re-cache the revoked entry via - * `_cacheNewCert`. Rejecting any suffix entry present in `revokedCerts` - * makes revocation durable independently of the journal-supplied prefix - * length. - * - * The timestamp validation converts milliseconds to seconds and checks: - * - Attestation is not too old (timestamp + maxTimeDiff > block.timestamp) - * - Attestation is not from the future (timestamp < block.timestamp) - * Note that due to truncating timestamp from milliseconds, to seconds, - * some valid attestations may be rejected. However, this ensures all invalid - * timestamps are rejected. - */ + /// @dev Internal function to verify and validate a journal entry + /// @param journal Verification journal to validate + /// @return Updated journal with final verification result + /// + /// This function performs comprehensive validation: + /// 1. Checks if the initial ZK verification was successful + /// 2. Validates the root certificate matches the trusted root + /// 3. Ensures all trusted certificates in the prefix are still valid (not revoked, not expired) + /// 4. Ensures no certificate in the suffix has been revoked, regardless of `trustedCertsPrefixLen` + /// 5. Validates the attestation timestamp is within acceptable range + /// 6. Caches newly discovered certificates for future use + /// + /// The suffix-side revocation check (step 4) is the load-bearing fix for the + /// `revokeCert` durability gap exposed under the production + /// `trustedCertsPrefixLen = 1` configuration. Without it, Pass 1 only walks + /// the root and a journal whose chain traverses a revoked intermediate in + /// the suffix would succeed and then re-cache the revoked entry via + /// `_cacheNewCert`. Rejecting any suffix entry present in `revokedCerts` + /// makes revocation durable independently of the journal-supplied prefix + /// length. + /// + /// The timestamp validation converts milliseconds to seconds and checks: + /// - Attestation is not too old (timestamp + maxTimeDiff > block.timestamp) + /// - Attestation is not from the future (timestamp < block.timestamp) + /// Note that due to truncating timestamp from milliseconds, to seconds, + /// some valid attestations may be rejected. However, this ensures all invalid + /// timestamps are rejected. function _verifyJournal(VerifierJournal memory journal) internal returns (VerifierJournal memory) { if (journal.result != VerificationResult.Success) { return journal; @@ -684,7 +638,7 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { journal.result = VerificationResult.RootCertNotTrusted; return journal; } - // Pass 1: trusted prefix — root must match the on-chain root, and every + // Pass 1: trusted prefix — root must match the onchain root, and every // intermediate must still hold a non-expired cached entry. for (uint256 i = 0; i < journal.trustedCertsPrefixLen; i++) { bytes32 certHash = journal.certs[i]; @@ -735,13 +689,11 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { return journal; } - /** - * @dev Internal function to verify zero-knowledge proofs using the appropriate coprocessor - * @param zkCoprocessor Type of ZK coprocessor (RiscZero or Succinct) - * @param programId Program identifier for the verification program - * @param output Encoded output data to verify - * @param proofBytes Zero-knowledge proof data - */ + /// @dev Internal function to verify zero-knowledge proofs using the appropriate coprocessor + /// @param zkCoprocessor Type of ZK coprocessor (RiscZero or Succinct) + /// @param programId Program identifier for the verification program + /// @param output Encoded output data to verify + /// @param proofBytes Zero-knowledge proof data function _verifyZk( ZkCoProcessorType zkCoprocessor, bytes32 programId, @@ -763,12 +715,10 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { } } - /** - * @dev Internal function to resolve the ZK verifier address based on route configuration - * @param zkCoprocessor Type of ZK coprocessor - * @param proofBytes Proof data (selector extracted from first 4 bytes) - * @return Resolved verifier address - */ + /// @dev Internal function to resolve the ZK verifier address based on route configuration + /// @param zkCoprocessor Type of ZK coprocessor + /// @param proofBytes Proof data (selector extracted from first 4 bytes) + /// @return Resolved verifier address function _resolveZkVerifier( ZkCoProcessorType zkCoprocessor, bytes calldata proofBytes diff --git a/src/L2/BaseTime.sol b/src/L2/BaseTime.sol new file mode 100644 index 000000000..74f2eefa3 --- /dev/null +++ b/src/L2/BaseTime.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000030 +/// @title BaseTime +/// @notice Exposes the millisecond component of the current L2 block timestamp. +contract BaseTime is IBaseTime { + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @inheritdoc IBaseTime + uint16 public timestampMillisPart; + + /// @inheritdoc IBaseTime + function timestampMs() external view returns (uint64 timestampMs_) { + timestampMs_ = uint64(block.timestamp * 1000 + timestampMillisPart); + } + + /// @inheritdoc IBaseTime + function setTimestampMillisPart(uint16 _timestampMillisPart) external { + if (msg.sender != Constants.DEPOSITOR_ACCOUNT) revert BaseTime_NotDepositor(); + if (_timestampMillisPart > 800 || _timestampMillisPart % 200 != 0) { + revert BaseTime_InvalidTimestampMillisPart(); + } + + timestampMillisPart = _timestampMillisPart; + } +} diff --git a/src/L2/FeeDisburser.sol b/src/L2/FeeDisburser.sol index aaa8b914d..2ce09a7ce 100644 --- a/src/L2/FeeDisburser.sol +++ b/src/L2/FeeDisburser.sol @@ -5,11 +5,14 @@ import { IL2StandardBridge } from "interfaces/L2/IL2StandardBridge.sol"; import { IFeeVault, Types } from "interfaces/L2/IFeeVault.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; +import { Initializable } from "src/vendor/Initializable.sol"; +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; /// @custom:proxied true /// @title FeeDisburser /// @notice Withdraws funds from system FeeVault contracts and bridges to L1. -contract FeeDisburser is ISemver { +contract FeeDisburser is Initializable, ProxyAdminOwnedBase, ISemver { //////////////////////////////////////////////////////////////// /// Constants //////////////////////////////////////////////////////////////// @@ -17,6 +20,9 @@ contract FeeDisburser is ISemver { /// @notice The minimum gas limit for the FeeDisburser withdrawal transaction to L1. uint32 public constant WITHDRAWAL_MIN_GAS = 35_000; + /// @notice The maximum number of system addresses that can be funded. + uint256 public constant MAX_SYSTEM_ADDRESS_COUNT = 20; + //////////////////////////////////////////////////////////////// /// Immutables //////////////////////////////////////////////////////////////// @@ -39,6 +45,15 @@ contract FeeDisburser is ISemver { /// This variable is deprecated and its value should not be relied upon. uint256 public netFeeRevenue; + /// @notice Reentrancy guard status for disburseFees. + uint256 private _disburseFeesEntered; + + /// @notice The L2 system addresses being funded. + address payable[] public systemAddresses; + + /// @notice The target balances for L2 system addresses. + uint256[] public targetBalances; + //////////////////////////////////////////////////////////////// /// Events //////////////////////////////////////////////////////////////// @@ -59,6 +74,17 @@ contract FeeDisburser is ISemver { /// @notice Emitted when no fees are collected from FeeVaults at time of disbursement. event NoFeesCollected(); + /// @notice Emitted when the FeeDisburser sends funds to a system address. + /// + /// @param systemAddress The system address being funded. + /// @param success A boolean denoting whether a fund send occurred and its success or failure. + /// @param balanceNeeded The amount of funds the given system address needs to reach its target balance. + /// @param balanceSent The amount of funds attempted to be sent. When success is false, the + /// recipient rejected the transfer and no funds were actually received. + event ProcessedFunds( + address indexed systemAddress, bool indexed success, uint256 balanceNeeded, uint256 balanceSent + ); + //////////////////////////////////////////////////////////////// /// Errors //////////////////////////////////////////////////////////////// @@ -78,6 +104,32 @@ contract FeeDisburser is ISemver { /// @notice Thrown when a FeeVault's recipient is not set to the FeeDisburser contract. error FeeVaultMustWithdrawToFeeDisburser(); + /// @notice Thrown when system address and target balance array lengths do not match. + error ArrayLengthMismatch(); + + /// @notice Thrown when system address configuration exceeds the maximum length. + error TooManySystemAddresses(); + + /// @notice Thrown when a system address target balance is zero. + error ZeroTargetBalance(); + + /// @notice Thrown when disburseFees is reentered. + error ReentrantCall(); + + //////////////////////////////////////////////////////////////// + /// Modifiers + //////////////////////////////////////////////////////////////// + + /// @notice Prevents reentrancy into disburseFees while preserving the existing storage layout. + /// Uses 1/2 sentinel values so the slot stays nonzero after first use, keeping + /// subsequent SSTOREs warm. Uninitialized (0) is treated as not-entered. + modifier nonReentrantDisbursement() { + if (_disburseFeesEntered == 2) revert ReentrantCall(); + _disburseFeesEntered = 2; + _; + _disburseFeesEntered = 1; + } + //////////////////////////////////////////////////////////////// /// Constructor //////////////////////////////////////////////////////////////// @@ -99,7 +151,7 @@ contract FeeDisburser is ISemver { //////////////////////////////////////////////////////////////// /// @notice Withdraws funds from FeeVaults and bridges to L1. - function disburseFees() external virtual { + function disburseFees() external virtual nonReentrantDisbursement { if (block.timestamp < lastDisbursementTime + FEE_DISBURSEMENT_INTERVAL) revert IntervalNotReached(); // Sequencer, base, and L1 FeeVaults will withdraw fees to the FeeDisburser contract. @@ -108,23 +160,63 @@ contract FeeDisburser is ISemver { _feeVaultWithdrawal(payable(Predeploys.L1_FEE_VAULT)); // Note: OPERATOR_FEE_VAULT is intentionally omitted because Base does not currently use it. - // Gross revenue is the sum of all fees - uint256 feeBalance = address(this).balance; - // Stop execution if no fees were collected - if (feeBalance == 0) { + if (address(this).balance == 0) { emit NoFeesCollected(); return; } lastDisbursementTime = block.timestamp; - // Send remaining funds to L1 wallet on L1 - IL2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{ value: address(this).balance }( - L1_WALLET, WITHDRAWAL_MIN_GAS, bytes("") - ); + uint256 systemAddressesLength = systemAddresses.length; + for (uint256 i; i < systemAddressesLength;) { + _refillBalanceIfNeeded({ systemAddress: systemAddresses[i], targetBalance: targetBalances[i] }); + unchecked { + i++; + } + } - emit FeesDisbursed(lastDisbursementTime, 0, feeBalance); + uint256 bridgeBalance = address(this).balance; + if (bridgeBalance != 0) { + // Send remaining funds to L1 wallet on L1 + IL2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{ value: bridgeBalance }( + L1_WALLET, WITHDRAWAL_MIN_GAS, bytes("") + ); + } + + emit FeesDisbursed(block.timestamp, 0, bridgeBalance); + } + + /// @notice Configures the L2 system addresses to refund and their target balances. + /// Called via upgradeAndCall when upgrading to this version. + /// + /// @dev Callable only by the ProxyAdmin or its owner. + /// + /// @param systemAddresses_ The system addresses being funded. + /// @param targetBalances_ The target balances for system addresses. + function initialize( + address payable[] memory systemAddresses_, + uint256[] memory targetBalances_ + ) + external + reinitializer(2) + { + _assertOnlyProxyAdminOrProxyAdminOwner(); + + uint256 systemAddressesLength = systemAddresses_.length; + if (systemAddressesLength > MAX_SYSTEM_ADDRESS_COUNT) revert TooManySystemAddresses(); + if (systemAddressesLength != targetBalances_.length) revert ArrayLengthMismatch(); + + for (uint256 i; i < systemAddressesLength;) { + if (systemAddresses_[i] == address(0)) revert ZeroAddress(); + if (targetBalances_[i] == 0) revert ZeroTargetBalance(); + unchecked { + i++; + } + } + + systemAddresses = systemAddresses_; + targetBalances = targetBalances_; } /// @notice Receives ETH fees withdrawn from L2 FeeVaults. @@ -132,9 +224,34 @@ contract FeeDisburser is ISemver { emit FeesReceived(msg.sender, msg.value); } - /// @custom:semver 1.0.0 + /// @custom:semver 1.1.0 function version() external pure virtual returns (string memory) { - return "1.0.0"; + return "1.1.0"; + } + + //////////////////////////////////////////////////////////////// + /// Internal Functions + //////////////////////////////////////////////////////////////// + + /// @notice Checks the balance of the target address and refills it back up to the target balance if needed. + /// + /// @param systemAddress The system address being funded. + /// @param targetBalance The target balance for the system address being funded. + function _refillBalanceIfNeeded(address systemAddress, uint256 targetBalance) internal { + uint256 systemAddressBalance = systemAddress.balance; + if (systemAddressBalance >= targetBalance) { + emit ProcessedFunds({ systemAddress: systemAddress, success: false, balanceNeeded: 0, balanceSent: 0 }); + return; + } + + uint256 valueNeeded = targetBalance - systemAddressBalance; + uint256 feeDisburserBalance = address(this).balance; + uint256 valueToSend = valueNeeded > feeDisburserBalance ? feeDisburserBalance : valueNeeded; + + bool success = SafeCall.send({ _target: systemAddress, _gas: gasleft(), _value: valueToSend }); + emit ProcessedFunds({ + systemAddress: systemAddress, success: success, balanceNeeded: valueNeeded, balanceSent: valueToSend + }); } //////////////////////////////////////////////////////////////// diff --git a/src/L2/FeeVault.sol b/src/L2/FeeVault.sol index dd0768b9c..b42dd76ae 100644 --- a/src/L2/FeeVault.sol +++ b/src/L2/FeeVault.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; +// Contracts +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; + // Libraries import { SafeCall } from "src/libraries/SafeCall.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -8,7 +11,6 @@ import { Types } from "src/libraries/Types.sol"; // Interfaces import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; // External import { Initializable } from "src/vendor/Initializable.sol"; @@ -16,10 +18,7 @@ import { Initializable } from "src/vendor/Initializable.sol"; /// @title FeeVault /// @notice The FeeVault contract contains the basic logic for the various different vault contracts /// used to hold fee revenue generated by the L2 system. -abstract contract FeeVault is Initializable { - /// @notice Error thrown when a function meant to be called by the ProxyAdmin owner is called by another account. - error FeeVault_OnlyProxyAdminOwner(); - +abstract contract FeeVault is ProxyAdminOwnedBase, Initializable { /// @notice The minimum gas limit for the FeeVault withdrawal transaction. uint32 internal constant _WITHDRAWAL_MIN_GAS = 400_000; @@ -78,6 +77,7 @@ abstract contract FeeVault is Initializable { } /// @notice Initializes the FeeVault contract. + /// @dev Callable only by the ProxyAdmin or its owner. /// @param _recipient Wallet that will receive the fees. /// @param _minWithdrawalAmount Minimum balance for withdrawals. /// @param _withdrawalNetwork Network which the recipient will receive fees on. @@ -89,6 +89,7 @@ abstract contract FeeVault is Initializable { external initializer { + _assertOnlyProxyAdminOrProxyAdminOwner(); recipient = _recipient; minWithdrawalAmount = _minWithdrawalAmount; withdrawalNetwork = _withdrawalNetwork; @@ -101,9 +102,7 @@ abstract contract FeeVault is Initializable { /// withdrawn. /// @param _newMinWithdrawalAmount The new minimum withdrawal amount. function setMinWithdrawalAmount(uint256 _newMinWithdrawalAmount) external { - if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { - revert FeeVault_OnlyProxyAdminOwner(); - } + _assertOnlyProxyAdminOwner(); uint256 oldWithdrawalAmount = minWithdrawalAmount; minWithdrawalAmount = _newMinWithdrawalAmount; @@ -114,9 +113,7 @@ abstract contract FeeVault is Initializable { /// @notice Updates the recipient of vault fees when they are withdrawn from the vault. /// @param _newRecipient The new recipient address. function setRecipient(address _newRecipient) external { - if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { - revert FeeVault_OnlyProxyAdminOwner(); - } + _assertOnlyProxyAdminOwner(); address oldRecipient = recipient; recipient = _newRecipient; @@ -129,9 +126,7 @@ abstract contract FeeVault is Initializable { /// withdraw them to an address on the same chain. /// @param _newWithdrawalNetwork The new withdrawal network. function setWithdrawalNetwork(Types.WithdrawalNetwork _newWithdrawalNetwork) external { - if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { - revert FeeVault_OnlyProxyAdminOwner(); - } + _assertOnlyProxyAdminOwner(); Types.WithdrawalNetwork oldWithdrawalNetwork = withdrawalNetwork; withdrawalNetwork = _newWithdrawalNetwork; diff --git a/src/libraries/Features.sol b/src/libraries/Features.sol index ffe8b8c3e..c630885fb 100644 --- a/src/libraries/Features.sol +++ b/src/libraries/Features.sol @@ -5,12 +5,6 @@ pragma solidity ^0.8.0; /// feature flagging functionality in the SystemConfig contract to selectively enable or /// disable customizable features of the OP Stack. library Features { - /// @notice The ETH_LOCKBOX feature determines if the system is configured to use the - /// ETHLockbox contract in the OptimismPortal. When the ETH_LOCKBOX feature is active - /// and the ETHLockbox contract has been configured, the OptimismPortal will use the - /// ETHLockbox to store ETH instead of storing ETH directly in the portal itself. - bytes32 internal constant ETH_LOCKBOX = "ETH_LOCKBOX"; - /// @notice The CUSTOM_GAS_TOKEN feature determines if the system is configured to use a custom /// gas token in the OptimismPortal. When the CUSTOM_GAS_TOKEN feature is active, the /// deposits and withdrawals of native ETH are disabled. diff --git a/src/libraries/Predeploys.sol b/src/libraries/Predeploys.sol index 79a55612a..86425f3b2 100644 --- a/src/libraries/Predeploys.sol +++ b/src/libraries/Predeploys.sol @@ -63,6 +63,9 @@ library Predeploys { /// @notice Address of the EAS predeploy. address internal constant EAS = 0x4200000000000000000000000000000000000021; + /// @notice Address of the BaseTime predeploy. + address internal constant BASE_TIME = 0x4200000000000000000000000000000000000030; + /// @custom:legacy /// @notice Address of the LegacyERC20ETH predeploy. Deprecated. Balances are migrated to the /// state trie as of the Bedrock upgrade. Contract has been locked and write functions @@ -90,6 +93,7 @@ library Predeploys { if (_addr == SCHEMA_REGISTRY) return "SchemaRegistry"; if (_addr == EAS) return "EAS"; if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; + if (_addr == BASE_TIME) return "BaseTime"; revert("Predeploys: unnamed predeploy"); } @@ -104,7 +108,8 @@ library Predeploys { || _addr == L2_STANDARD_BRIDGE || _addr == SEQUENCER_FEE_WALLET || _addr == OPTIMISM_MINTABLE_ERC20_FACTORY || _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT - || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS; + || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS + || _addr == BASE_TIME; } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/test/L1/ETHLockbox.t.sol b/test/L1/ETHLockbox.t.sol deleted file mode 100644 index 1f96d859d..000000000 --- a/test/L1/ETHLockbox.t.sol +++ /dev/null @@ -1,623 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Testing -import { CommonTest } from "test/setup/CommonTest.sol"; - -// Contracts -import { Proxy } from "src/universal/Proxy.sol"; - -// Libraries -import { Constants } from "src/libraries/Constants.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; -import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; -import { Features } from "src/libraries/Features.sol"; - -// Interfaces -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; - -/// @title ETHLockbox_TestInit -/// @notice Base contract that sets up the testing environment for ETHLockbox tests. -abstract contract ETHLockbox_TestInit is CommonTest { - event ETHLocked(IOptimismPortal2 indexed portal, uint256 amount); - event ETHUnlocked(IOptimismPortal2 indexed portal, uint256 amount); - event PortalAuthorized(IOptimismPortal2 indexed portal); - event LockboxAuthorized(IETHLockbox indexed lockbox); - event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); - event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); - - function setUp() public virtual override { - super.setUp(); - - // If not on the last upgrade network, we skip the test since the `ETHLockbox` won't be yet - // deployed - // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. - if (isForkTest() && !deploy.cfg().useUpgradedFork()) vm.skip(true); - - // If the ETHLockbox system feature is not enabled, skip these tests. - skipIfSysFeatureDisabled(Features.ETH_LOCKBOX); - } - - function _mockPortalSharedOwnerAndSuperchainConfig(IOptimismPortal2 _portal) internal { - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - vm.mockCall( - address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(superchainConfig) - ); - } - - function _authorizePortalIfNeeded(IOptimismPortal2 _portal) internal { - if (!ethLockbox.authorizedPortals(_portal)) { - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - } - } - - function _mockLockboxSharedOwner(address _lockbox) internal { - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - } -} - -/// @title ETHLockbox_Version_Test -/// @notice Test contract for the `version` function. -contract ETHLockbox_Version_Test is ETHLockbox_TestInit { - /// @notice Tests that the `version` function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() public view { - assert(bytes(ethLockbox.version()).length > 0); - } -} - -/// @title ETHLockbox_Initialize_Test -/// @notice Test contract for the initialize function. -contract ETHLockbox_Initialize_Test is ETHLockbox_TestInit { - StorageSlot internal initializedSlot; - - function setUp() public override { - super.setUp(); - - initializedSlot = ForgeArtifacts.getSlot("ETHLockbox", "_initialized"); - } - - /// @notice Tests the superchain config was correctly set during initialization. - function test_initialize_succeeds() public view { - assertEq(address(ethLockbox.systemConfig().superchainConfig()), address(superchainConfig)); - assertEq(ethLockbox.authorizedPortals(optimismPortal2), true); - assertEq(address(ethLockbox.superchainConfig()), address(superchainConfig)); - } - - /// @notice Tests that the initializer value is correct. Trivial test for normal initialization - /// but confirms that the initValue is not incremented incorrectly if an upgrade - /// function is not present. - function test_initialize_correctInitializerValue_succeeds() public view { - bytes32 slotVal = vm.load(address(ethLockbox), bytes32(initializedSlot.slot)); - uint8 val = uint8(uint256(slotVal) & 0xFF); - - assertEq(val, ethLockbox.initVersion()); - } - - /// @notice Tests that the `initialize` function reverts if called by a non-proxy admin or - /// owner. - /// @param _sender The address of the sender to test. - function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { - // Prank as the not ProxyAdmin or ProxyAdmin owner. - vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - - // Set the initialized slot to 0. - vm.store(address(ethLockbox), bytes32(initializedSlot.slot), bytes32(0)); - - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); - - // Call the `initialize` function with the sender - vm.prank(_sender); - IOptimismPortal2[] memory _portals = new IOptimismPortal2[](1); - ethLockbox.initialize(systemConfig, _portals); - } - - /// @notice Tests it reverts when the contract is already initialized. - function test_initialize_alreadyInitialized_reverts() public { - vm.expectRevert("Initializable: contract is already initialized"); - IOptimismPortal2[] memory _portals = new IOptimismPortal2[](1); - ethLockbox.initialize(systemConfig, _portals); - } -} - -/// @title ETHLockbox_Paused_Test -/// @notice Test contract for the `paused` function. -contract ETHLockbox_Paused_Test is ETHLockbox_TestInit { - /// @notice Tests the `paused` status is correctly returned. - function test_paused_succeeds() public { - // Assert the paused status is false - assertEq(ethLockbox.paused(), false); - - // Mock the superchain config to return true for the paused status - // We use abi.encodeWithSignature because paused is overloaded. - // nosemgrep: sol-style-use-abi-encodecall - vm.mockCall(address(superchainConfig), abi.encodeWithSignature("paused(address)", address(0)), abi.encode(true)); - - // Assert the paused status is true - assertEq(ethLockbox.paused(), true); - } -} - -/// @title ETHLockbox_AuthorizePortal_Test -/// @notice Test contract for the authorizePortal function. -contract ETHLockbox_AuthorizePortal_Test is ETHLockbox_TestInit { - /// @notice Tests the `authorizePortal` function reverts when the caller is not the proxy - /// admin. - function testFuzz_authorizePortal_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdminOwner); - - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); - - // Call the `authorizePortal` function with an unauthorized caller - vm.prank(_caller); - ethLockbox.authorizePortal(optimismPortal2); - } - - /// @notice Tests the `authorizePortal` function reverts when the proxy admin owner of the - /// portal is not the same as the one of the lockbox. - function testFuzz_authorizePortal_differentProxyAdminOwner_reverts(IOptimismPortal2 _portal) public { - assumeNotForgeAddress(address(_portal)); - vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); - - // Expect the revert with `DifferentOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotSharedProxyAdminOwner.selector); - - // Call the `authorizePortal` function - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - } - - /// @notice Tests the `authorizePortal` function reverts when the portal has a different - /// SuperchainConfig than the one configured in the lockbox. - /// @param _portal The portal to authorize. - function testFuzz_authorizePortal_differentSuperchainConfig_reverts(IOptimismPortal2 _portal) public { - assumeNotForgeAddress(address(_portal)); - vm.assume(address(_portal) != address(systemConfig)); - vm.assume(address(_portal) != EIP1967Helper.getImplementation(address(systemConfig))); - - // Mock the portal to have the right proxyAdminOwner. - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - - // Mock the portal to have the wrong SuperchainConfig. - vm.mockCall(address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(address(0))); - - // Expect the revert with `DifferentSuperchainConfig` selector - vm.expectRevert(IETHLockbox.ETHLockbox_DifferentSuperchainConfig.selector); - - // Call the `authorizePortal` function - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - } - - /// @notice Tests the `authorizePortal` function succeeds using the `optimismPortal2` address - /// as the portal. - function test_authorizePortal_succeeds() public { - StorageSlot memory authorizedPortalsSlot = ForgeArtifacts.getSlot("ETHLockbox", "authorizedPortals"); - address key = address(optimismPortal2); - bytes32 slot = keccak256(abi.encode(key, bytes32(authorizedPortalsSlot.slot))); - - // Reset the authorization status to false - vm.store(address(ethLockbox), slot, bytes32(0)); - - // Expect the `PortalAuthorized` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit PortalAuthorized(optimismPortal2); - - // Call the `authorizePortal` function with the portal - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(optimismPortal2); - - // Assert the portal is authorized - assertTrue(ethLockbox.authorizedPortals(optimismPortal2)); - } - - /// @notice Tests the `authorizePortal` function succeeds - function testFuzz_authorizePortal_succeeds(IOptimismPortal2 _portal) public { - assumeNotForgeAddress(address(_portal)); - - _mockPortalSharedOwnerAndSuperchainConfig(_portal); - - // Expect the `PortalAuthorized` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit PortalAuthorized(_portal); - - // Call the `authorizePortal` function with the portal - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - - // Assert the portal is authorized - assertTrue(ethLockbox.authorizedPortals(_portal)); - } -} - -/// @title ETHLockbox_ReceiveLiquidity_Test -/// @notice Test contract for the receiveLiquidity function. -contract ETHLockbox_ReceiveLiquidity_Test is ETHLockbox_TestInit { - /// @notice Tests the liquidity is correctly received. - function testFuzz_receiveLiquidity_succeeds(address _lockbox, uint256 _value) public { - // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test - if (isForkTest()) vm.skip(true); - assumeNotForgeAddress(_lockbox); - vm.assume(address(_lockbox) != address(ethLockbox)); - - // Deal the value to the lockbox - deal(address(_lockbox), _value); - - _mockLockboxSharedOwner(_lockbox); - - // Authorize the lockbox if needed - if (!ethLockbox.authorizedLockboxes(IETHLockbox(_lockbox))) { - vm.prank(proxyAdminOwner); - ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); - } - - // Get the balance of the lockbox before the receive - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; - - // Expect the `LiquidityReceived` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit LiquidityReceived(IETHLockbox(_lockbox), _value); - - // Call the `receiveLiquidity` function - vm.prank(address(_lockbox)); - ethLockbox.receiveLiquidity{ value: _value }(); - - // Assert the lockbox's balance increased by the amount received - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + _value); - } -} - -/// @title ETHLockbox_LockETH_Test -/// @notice Test contract for the lockETH function. -contract ETHLockbox_LockETH_Test is ETHLockbox_TestInit { - /// @notice Tests it reverts when the caller is not an authorized portal. - function testFuzz_lockETH_unauthorizedPortal_reverts(address _caller) public { - vm.assume(!ethLockbox.authorizedPortals(IOptimismPortal2(payable(_caller)))); - - // Expect the revert with `Unauthorized` selector - vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); - - // Call the `lockETH` function with an unauthorized caller - vm.prank(_caller); - ethLockbox.lockETH(); - } - - /// @notice Tests the ETH is correctly locked when the caller is an authorized portal. - function testFuzz_lockETH_succeeds(uint256 _amount) public { - // Prevent overflow on an upgrade context - _amount = bound(_amount, 0, type(uint256).max - address(ethLockbox).balance); - - // Deal the ETH amount to the portal - vm.deal(address(optimismPortal2), _amount); - - // Get the balance of the portal and lockbox before the lock to compare later on the - // assertions - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - - // Look for the emit of the `ETHLocked` event - vm.expectEmit(address(ethLockbox)); - emit ETHLocked(optimismPortal2, _amount); - - // Call the `lockETH` function with the portal - vm.prank(address(optimismPortal2)); - ethLockbox.lockETH{ value: _amount }(); - - // Assert the portal's balance decreased and the lockbox's balance increased by the - // amount locked - assertEq(address(optimismPortal2).balance, portalBalanceBefore - _amount); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); - } - - /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with - /// different portals. - function testFuzz_lockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _amount) public { - // Since on the fork the `_portal` fuzzed address doesn't exist, we skip the test - if (isForkTest()) vm.skip(true); - assumeNotForgeAddress(address(_portal)); - vm.assume(address(_portal) != address(ethLockbox)); - - _mockPortalSharedOwnerAndSuperchainConfig(_portal); - - // Set the portal as an authorized portal if needed - _authorizePortalIfNeeded(_portal); - - // Deal the ETH amount to the portal - vm.deal(address(_portal), _amount); - - // Get the balance of the lockbox before the lock to compare later on the assertions - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - - // Look for the emit of the `ETHLocked` event - vm.expectEmit(address(ethLockbox)); - emit ETHLocked(_portal, _amount); - - // Call the `lockETH` function with the portal - vm.prank(address(_portal)); - ethLockbox.lockETH{ value: _amount }(); - - // Assert the portal's balance decreased and the lockbox's balance increased by the - // amount locked - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); - } -} - -/// @title ETHLockbox_UnlockETH_Test -/// @notice Test contract for the unlockETH function. -contract ETHLockbox_UnlockETH_Test is ETHLockbox_TestInit { - /// @notice Tests `unlockETH` reverts when the contract is paused. - function testFuzz_unlockETH_paused_reverts(address _caller, uint256 _value) public { - // Mock the superchain config to return true for the paused status - // We use abi.encodeWithSignature because paused is overloaded. - // nosemgrep: sol-style-use-abi-encodecall - vm.mockCall(address(superchainConfig), abi.encodeWithSignature("paused(address)", address(0)), abi.encode(true)); - - // Expect the revert with `Paused` selector - vm.expectRevert(IETHLockbox.ETHLockbox_Paused.selector); - - // Call the `unlockETH` function with the caller - vm.prank(_caller); - ethLockbox.unlockETH(_value); - } - - /// @notice Tests it reverts when the caller is not an authorized portal. - function testFuzz_unlockETH_unauthorizedPortal_reverts(address _caller, uint256 _value) public { - vm.assume(!ethLockbox.authorizedPortals(IOptimismPortal2(payable(_caller)))); - - // Expect the revert with `Unauthorized` selector - vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); - - // Call the `unlockETH` function with an unauthorized caller - vm.prank(_caller); - ethLockbox.unlockETH(_value); - } - - /// @notice Tests `unlockETH` reverts when the `_value` input is greater than the balance of - /// the lockbox. - function testFuzz_unlockETH_insufficientBalance_reverts(uint256 _value) public { - _value = bound(_value, address(ethLockbox).balance + 1, type(uint256).max); - - // Expect the revert with `InsufficientBalance` selector - vm.expectRevert(IETHLockbox.ETHLockbox_InsufficientBalance.selector); - - // Call the `unlockETH` function with the portal - vm.prank(address(optimismPortal2)); - ethLockbox.unlockETH(_value); - } - - /// @notice Tests `unlockETH` reverts when the portal is not the L2 sender to prevent - /// unlocking ETH from the lockbox through a withdrawal transaction. - function testFuzz_unlockETH_withdrawalTransaction_reverts(uint256 _value, address _l2Sender) public { - _value = bound(_value, 0, address(ethLockbox).balance); - vm.assume(_l2Sender != Constants.DEFAULT_L2_SENDER); - - // Mock the L2 sender - vm.mockCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal2.l2Sender, ()), abi.encode(_l2Sender)); - - // Expect the revert with `NoWithdrawalTransactions` selector - vm.expectRevert(IETHLockbox.ETHLockbox_NoWithdrawalTransactions.selector); - - // Call the `unlockETH` function with the portal - vm.prank(address(optimismPortal2)); - ethLockbox.unlockETH(_value); - } - - /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. - function testFuzz_unlockETH_succeeds(uint256 _value) public { - // Deal the ETH amount to the lockbox - vm.deal(address(ethLockbox), _value); - - // Get the balance of the portal and lockbox before the unlock to compare later on the - // assertions - uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - - // Expect `donateETH` function to be called on Portal - vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal2.donateETH, ())); - - // Look for the emit of the `ETHUnlocked` event - vm.expectEmit(address(ethLockbox)); - emit ETHUnlocked(optimismPortal2, _value); - - // Call the `unlockETH` function with the portal - vm.prank(address(optimismPortal2)); - ethLockbox.unlockETH(_value); - - // Assert the portal's balance increased and the lockbox's balance decreased by the amount - // unlocked - assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); - } - - /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. - function testFuzz_unlockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _value) public { - // Since on the fork the `_portal` fuzzed address doesn't exist, we skip the test - if (isForkTest()) vm.skip(true); - assumeNotForgeAddress(address(_portal)); - vm.assume(address(_portal) != address(ethLockbox)); - - _mockPortalSharedOwnerAndSuperchainConfig(_portal); - vm.mockCall( - address(_portal), abi.encodeCall(IOptimismPortal2.l2Sender, ()), abi.encode(Constants.DEFAULT_L2_SENDER) - ); - - // Set the portal as an authorized portal if needed - _authorizePortalIfNeeded(_portal); - - // Deal the ETH amount to the lockbox - vm.deal(address(ethLockbox), _value); - - // Get the balance of the portal and lockbox before the unlock to compare later on the - // assertions - uint256 portalBalanceBefore = address(_portal).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - - // Expect `donateETH` function to be called on Portal - vm.expectCall(address(_portal), abi.encodeCall(IOptimismPortal2.donateETH, ())); - - // Look for the emit of the `ETHUnlocked` event - vm.expectEmit(address(ethLockbox)); - emit ETHUnlocked(_portal, _value); - - // Call the `unlockETH` function with the portal - vm.prank(address(_portal)); - ethLockbox.unlockETH(_value); - - // Assert the portal's balance increased and the lockbox's balance decreased by the amount - // unlocked - assertEq(address(_portal).balance, portalBalanceBefore + _value); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); - } -} - -/// @title ETHLockbox_AuthorizeLockbox_Test -/// @notice Test contract for the authorizeLockbox function. -contract ETHLockbox_AuthorizeLockbox_Test is ETHLockbox_TestInit { - /// @notice Tests the `authorizeLockbox` function reverts when the caller is not the proxy - /// admin. - function testFuzz_authorizeLockbox_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdminOwner); - - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); - - // Call the `authorizeLockbox` function with an unauthorized caller - vm.prank(_caller); - ethLockbox.authorizeLockbox(ethLockbox); - } - - /// @notice Tests the `authorizeLockbox` function reverts when the proxy admin owner of the - /// lockbox is not the same as the proxy admin owner of the proxy admin. - function testFuzz_authorizeLockbox_differentProxyAdminOwner_reverts(address _lockbox) public { - assumeNotForgeAddress(_lockbox); - - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); - - // Expect the revert with `NotSharedProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotSharedProxyAdminOwner.selector); - - // Call the `authorizeLockbox` function with the lockbox - vm.prank(proxyAdminOwner); - ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); - } - - /// @notice Tests the `authorizeLockbox` function succeeds - function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { - assumeNotForgeAddress(_lockbox); - - _mockLockboxSharedOwner(_lockbox); - - // Expect the `LockboxAuthorized` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit LockboxAuthorized(IETHLockbox(_lockbox)); - - // Authorize the lockbox - vm.prank(proxyAdminOwner); - ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); - - // Assert the lockbox is authorized - assertTrue(ethLockbox.authorizedLockboxes(IETHLockbox(_lockbox))); - } -} - -/// @title ETHLockbox_MigrateLiquidity_Test -/// @notice Test contract for the migrateLiquidity function. -contract ETHLockbox_MigrateLiquidity_Test is ETHLockbox_TestInit { - /// @notice Tests the `migrateLiquidity` function reverts when the caller is not the proxy - /// admin. - function testFuzz_migrateLiquidity_unauthorized_reverts(address _caller) public { - vm.assume(_caller != proxyAdminOwner); - - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); - - // Call the `migrateLiquidity` function with an unauthorized caller - vm.prank(_caller); - ethLockbox.migrateLiquidity(ethLockbox); - } - - /// @notice Tests the `migrateLiquidity` function reverts when the proxy admin owner of the - /// lockbox is not the same as the proxy admin owner of the proxy admin. - function testFuzz_migrateLiquidity_differentProxyAdminOwner_reverts(address _lockbox) public { - assumeNotForgeAddress(_lockbox); - - vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); - - // Expect the revert with `NotSharedProxyAdminOwner` selector - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotSharedProxyAdminOwner.selector); - - // Call the `migrateLiquidity` function with the lockbox - vm.prank(proxyAdminOwner); - ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); - } - - /// @notice Tests the `migrateLiquidity` function succeeds - function testFuzz_migrateLiquidity_succeeds( - uint256 _originLockboxBalance, - uint256 _destinationLockboxBalance - ) - public - { - // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test - if (isForkTest()) vm.skip(true); - - // Bound balances to avoid overflow - _originLockboxBalance = bound(_originLockboxBalance, 0, type(uint256).max - address(ethLockbox).balance); - _destinationLockboxBalance = bound(_destinationLockboxBalance, 0, type(uint256).max - _originLockboxBalance); - - // Deploy a new Proxy for the destination lockbox - address destinationLockbox = address(new Proxy(address(proxyAdmin))); - - // Get the ETHLockbox implementation of the origin `ethLockbox` proxy - vm.prank(address(proxyAdmin)); - address implementation = Proxy(payable(address(ethLockbox))).implementation(); - - // Upgrade the destination lockbox proxy to the `ETHLockbox` implementation - vm.prank(address(proxyAdmin)); - Proxy(payable(destinationLockbox)).upgradeTo(implementation); - - // Authorize the origin lockbox on the destination lockbox - vm.prank(proxyAdminOwner); - IETHLockbox(destinationLockbox).authorizeLockbox(ethLockbox); - - // Deal the balance to both lockboxes - deal(address(ethLockbox), _originLockboxBalance); - deal(address(destinationLockbox), _destinationLockboxBalance); - - // Get balances before the migration - uint256 originLockboxBalanceBefore = address(ethLockbox).balance; - uint256 destLockboxBalanceBefore = address(destinationLockbox).balance; - - // Expect the `LiquidityMigrated` event to be emitted - vm.expectEmit(address(ethLockbox)); - emit LiquidityMigrated(IETHLockbox(destinationLockbox), originLockboxBalanceBefore); - - // Call the `migrateLiquidity` function with the lockbox - vm.prank(proxyAdminOwner); - ethLockbox.migrateLiquidity(IETHLockbox(destinationLockbox)); - - // Assert the liquidity was migrated - assertEq(address(ethLockbox).balance, 0); - assertEq(address(destinationLockbox).balance, destLockboxBalanceBefore + originLockboxBalanceBefore); - } -} - -/// @title ETHLockbox_Uncategorized_Test -/// @notice Contains uncategorized tests related to ETHLockbox. -contract ETHLockbox_Uncategorized_Test is ETHLockbox_TestInit { - /// @notice Tests the proxy admin owner is correctly returned. - function test_proxyProxyAdminOwner_succeeds() public view { - assertEq(ethLockbox.proxyAdminOwner(), proxyAdminOwner); - } -} diff --git a/test/L1/L1StandardBridge.t.sol b/test/L1/L1StandardBridge.t.sol index 206087776..c944b2d99 100644 --- a/test/L1/L1StandardBridge.t.sol +++ b/test/L1/L1StandardBridge.t.sol @@ -25,20 +25,8 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// @title L1StandardBridge_TestInit /// @notice Reusable test initialization for `L1StandardBridge` tests. abstract contract L1StandardBridge_TestInit is CommonTest { - function _assertETHBridgeCustody( - uint256 _portalBalanceBefore, - uint256 _ethLockboxBalanceBefore, - uint256 _amount - ) - internal - view - { - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, _portalBalanceBefore); - assertEq(address(ethLockbox).balance, _ethLockboxBalanceBefore + _amount); - } else { - assertEq(address(optimismPortal2).balance, _portalBalanceBefore + _amount); - } + function _assertETHBridgeCustody(uint256 _portalBalanceBefore, uint256 _amount) internal view { + assertEq(address(optimismPortal2).balance, _portalBalanceBefore + _amount); } function _mockXDomainMessageSender(address _sender) internal { @@ -356,7 +344,6 @@ contract L1StandardBridge_Receive_Test is L1StandardBridge_TestInit { function test_receive_succeeds() external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; // The legacy event must be emitted for backwards compatibility vm.expectEmit(address(l1StandardBridge)); @@ -381,7 +368,7 @@ contract L1StandardBridge_Receive_Test is L1StandardBridge_TestInit { (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); assertTrue(success); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 100); + _assertETHBridgeCustody(portalBalanceBefore, 100); } /// @notice Verifies receive function reverts when called by contracts @@ -407,10 +394,9 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: true, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); + _assertETHBridgeCustody(portalBalanceBefore, 500); } /// @notice Tests that depositing ETH succeeds for an EOA using 7702 delegation. @@ -421,10 +407,9 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { _preBridgeETH({ isLegacy: true, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); + _assertETHBridgeCustody(portalBalanceBefore, 500); } /// @notice Tests that depositing ETH reverts if the call is not from an EOA. @@ -448,10 +433,9 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: true, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETHTo{ value: 600 }(bob, 60000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 600); + _assertETHBridgeCustody(portalBalanceBefore, 600); } /// @notice Verifies depositETHTo succeeds with various recipients and amounts @@ -465,12 +449,11 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { vm.deal(alice, _amount); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; vm.prank(alice); l1StandardBridge.depositETHTo{ value: _amount }(_to, 60000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, _amount); + _assertETHBridgeCustody(portalBalanceBefore, _amount); } } @@ -739,10 +722,9 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: false, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETH{ value: 500 }(50000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); + _assertETHBridgeCustody(portalBalanceBefore, 500); } /// @notice Tests that bridging ETH to a different address succeeds. @@ -754,10 +736,9 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: false, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETHTo{ value: 600 }(bob, 60000, hex"dead"); - _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 600); + _assertETHBridgeCustody(portalBalanceBefore, 600); } /// @notice Tests that finalizing bridged ETH succeeds. diff --git a/test/L1/OptimismPortal2.t.sol b/test/L1/OptimismPortal2.t.sol index f302f5163..b653d2746 100644 --- a/test/L1/OptimismPortal2.t.sol +++ b/test/L1/OptimismPortal2.t.sol @@ -80,6 +80,9 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { respectedGameType = optimismPortal2.respectedGameType(); MockVerifier teeVerifier = new MockVerifier(anchorStateRegistry); MockVerifier zkVerifier = new MockVerifier(anchorStateRegistry); + // The AggregateVerifier constructor requires the pinned upgrade id to be registered. + vm.prank(proxyAdminOwner); + protocolVersions.registerUpgrade(0, 0); AggregateVerifier gameImpl = new AggregateVerifier( respectedGameType, anchorStateRegistry, @@ -92,7 +95,12 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { deploy.cfg().l2ChainId(), 100, 10, - AggregateVerifier.FinalizationDelays({ slow: 0, fast: 0 }) + AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays({ slow: 0, fast: 0 }), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: protocolVersions, maxUpgradeId: protocolVersions.getSchedule().length - 1 + }) + }) ); disputeGameFactory.setImplementation(respectedGameType, IDisputeGame(address(gameImpl))); disputeGameFactory.setInitBond(respectedGameType, 0); @@ -115,9 +123,6 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { // Fund the portal so that we can withdraw ETH. vm.deal(address(optimismPortal2), 0xFFFFFFFF); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), 0xFFFFFFFF); - } } function _createDisputeGame(Claim _rootClaim, uint256 _salt) internal returns (IAggregateVerifier game_) { @@ -190,9 +195,6 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { { uint256 value = bound(_value, 0, 200_000_000 ether); vm.deal(address(optimismPortal2), value); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), value); - } uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); withdrawalTx_ = Types.WithdrawalTransaction({ @@ -240,31 +242,6 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { // Assert that the withdrawal was not finalized. assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } - - /// @notice Checks if the ETHLockbox feature is enabled. - /// @return bool True if the ETHLockbox feature is enabled. - function isUsingLockbox() internal view returns (bool) { - return - systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(optimismPortal2.ethLockbox()) != address(0); - } - - /// @notice Enables the ETHLockbox feature if not enabled. - /// @param _lockbox Address of the lockbox to enable. - function forceEnableLockbox(address _lockbox) internal { - if (!isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(address(proxyAdmin)); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - } - - // Overwrite the lockbox either way. - StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "ethLockbox"); - vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(uint256(uint160(address(_lockbox))))); - - // If the recipient address has no code, store STOP so we don't get reverts. - if (address(_lockbox).code.length == 0) { - vm.etch(address(_lockbox), hex"00"); - } - } } /// @title OptimismPortal2_Version_Test @@ -286,7 +263,6 @@ contract OptimismPortal2_Constructor_Test is OptimismPortal2_TestInit { assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(opImpl.l2Sender(), address(0)); - assertEq(address(opImpl.ethLockbox()), address(0)); } } @@ -302,16 +278,6 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { assertEq(optimismPortal2.paused(), false); assertEq(address(optimismPortal2.systemConfig()), address(systemConfig)); - if (isUsingLockbox()) { - assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); - } else { - assertEq(address(optimismPortal2.ethLockbox()), address(0)); - } - - if (!isUsingLockbox()) { - assertFalse(optimismPortal2.systemConfig().isFeatureEnabled(Features.CUSTOM_GAS_TOKEN)); - } - returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" ); @@ -339,32 +305,6 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { assertEq(val, optimismPortal2.initVersion()); } - /// @notice Tests that the initialize function reverts when lockbox state is invalid. - function test_initialize_invalidLockboxState_reverts() external { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); - - // Enable ETH_LOCKBOX feature but clear the lockbox address to create invalid state. - if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(address(proxyAdmin)); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - } - - // Clear the lockbox address. - StorageSlot memory lockboxSlot = ForgeArtifacts.getSlot("OptimismPortal2", "ethLockbox"); - vm.store(address(optimismPortal2), bytes32(lockboxSlot.slot), bytes32(0)); - - // Expect the revert with `OptimismPortal_InvalidLockboxState` selector. - vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidLockboxState.selector); - - // Call the `initialize` function - vm.prank(address(proxyAdmin)); - optimismPortal2.initialize(systemConfig, anchorStateRegistry); - } - /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. /// @param _sender The address of the sender to test. function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { @@ -547,9 +487,7 @@ contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { function testFuzz_receive_succeeds(uint256 _value) external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); // Prevent overflow on an upgrade context - _value = bound(_value, 0, type(uint256).max - address(ethLockbox).balance); uint256 balanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; _value = bound(_value, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); @@ -563,11 +501,6 @@ contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { _data: hex"" }); - if (isUsingLockbox()) { - // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. - vm.expectCall(address(ethLockbox), _value, abi.encodeCall(ethLockbox.lockETH, ()), _value > 0 ? 1 : 0); - } - // give alice money and send as an eoa vm.deal(alice, _value); vm.prank(alice, alice); @@ -575,50 +508,7 @@ contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { assertTrue(s); - if (isUsingLockbox()) { - assertEq(address(optimismPortal2).balance, balanceBefore); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _value); - } else { - assertEq(address(optimismPortal2).balance, balanceBefore + _value); - } - } - - function testFuzz_receive_withLockbox_succeeds(uint256 _value) external { - skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); - // Prevent overflow on an upgrade context. - // We use a dummy lockbox here because the real one won't work for upgrade tests. - address dummyLockbox = address(0xdeadbeef); - _value = bound(_value, 0, type(uint256).max - address(dummyLockbox).balance); - uint256 balanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(dummyLockbox).balance; - _value = bound(_value, 0, type(uint256).max - balanceBefore); - - // Enable the lockbox. - forceEnableLockbox(dummyLockbox); - - // Expect the transaction deposited event. - vm.expectEmit(address(optimismPortal2)); - emitTransactionDeposited({ - _from: alice, - _to: alice, - _value: _value, - _mint: _value, - _gasLimit: 100_000, - _isCreation: false, - _data: hex"" - }); - - // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. - vm.expectCall(address(dummyLockbox), _value, abi.encodeCall(ethLockbox.lockETH, ()), _value > 0 ? 1 : 0); - - // give alice money and send as an eoa - vm.deal(alice, _value); - vm.prank(alice, alice); - (bool s,) = address(optimismPortal2).call{ value: _value }(hex""); - - assertTrue(s); - assertEq(address(optimismPortal2).balance, balanceBefore); - assertEq(address(dummyLockbox).balance, lockboxBalanceBefore + _value); + assertEq(address(optimismPortal2).balance, balanceBefore + _value); } } @@ -631,7 +521,6 @@ contract OptimismPortal2_DonateETH_Test is OptimismPortal2_TestInit { vm.deal(alice, _amount); uint256 preBalance = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; _amount = bound(_amount, 0, type(uint256).max - preBalance); vm.startStateDiffRecording(); @@ -640,8 +529,6 @@ contract OptimismPortal2_DonateETH_Test is OptimismPortal2_TestInit { assertEq(address(optimismPortal2).balance, preBalance + _amount); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore); - // 0 for extcodesize of proxy before being called by this test, // 1 for the call to the proxy by the pranked address // 2 for the delegate call to the impl by the proxy @@ -692,17 +579,6 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test _outputRootProof: _outputRootProof, _withdrawalProof: _withdrawalProof }); - - if (isUsingLockbox()) { - _defaultTx.target = address(ethLockbox); - vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - } } /// @notice Tests that `proveWithdrawalTransaction` reverts when the current timestamp is less @@ -888,17 +764,11 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @notice Test contract for OptimismPortal2 `finalizeWithdrawalTransaction` function. contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_TestInit { /// @notice Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal - /// contract or the lockbox. + /// contract. function test_finalizeWithdrawalTransaction_badTarget_reverts() external { _defaultTx.target = address(optimismPortal2); vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - - if (isUsingLockbox()) { - _defaultTx.target = address(ethLockbox); - vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); - optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - } } /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the target reverts and caller @@ -947,9 +817,6 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T vm.warp(gameNoData.expectedResolution().raw() + 1 seconds); vm.deal(address(optimismPortal2), 0xFFFFFFFF); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), 0xFFFFFFFF); - } uint256 bobBalanceBefore = bob.balance; @@ -1105,53 +972,20 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the target reverts. function test_finalizeWithdrawalTransaction_targetFails_fails() external { - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.deal(address(optimismPortal2), 0); // no balance - } - - uint256 bobBalanceBefore = address(bob).balance; - vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. - - _proveDefaultWithdrawal(); - - _resolveGameAndWarpPastProofMaturity(game); - vm.expectEmit(true, true, true, true); - emit WithdrawalFinalized(_withdrawalHash, false); - optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - - // Bob's balance should not have changed. - assertEq(address(bob).balance, bobBalanceBefore); - - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - // OptimismPortal2 should not have any stuck ETH. - assertEq(address(optimismPortal2).balance, 0); - } - } - - /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the target reverts when - /// using the ETHLockbox. - function test_finalizeWithdrawalTransaction_lockboxAndTargetFails_fails() external { - // Enable the ETHLockbox. - address dummyLockbox = address(0xdeadbeef); - forceEnableLockbox(dummyLockbox); - vm.deal(address(dummyLockbox), 0xFFFFFFFF); - vm.deal(address(optimismPortal2), _defaultTx.value); - uint256 bobBalanceBefore = address(bob).balance; vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. _proveDefaultWithdrawal(); _resolveGameAndWarpPastProofMaturity(game); + uint256 portalBalanceBefore = address(optimismPortal2).balance; vm.expectEmit(true, true, true, true); emit WithdrawalFinalized(_withdrawalHash, false); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Bob's balance should not have changed. assertEq(address(bob).balance, bobBalanceBefore); - - // OptimismPortal2 should not have any stuck ETH. - assertEq(address(optimismPortal2).balance, 0); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); } /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already @@ -1640,13 +1474,8 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { external { // Prevent overflow on an upgrade context - // Since the value always goes through the portal _mint = bound(_mint, 0, type(uint256).max - address(optimismPortal2).balance); - if (isUsingLockbox() && address(optimismPortal2).balance > address(ethLockbox).balance) { - _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); - } - _gasLimit = uint64( bound( _gasLimit, @@ -1657,7 +1486,6 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -1671,23 +1499,13 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { _data: _data }); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. - vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); - } - vm.deal(depositor, _mint); vm.prank(depositor, depositor); optimismPortal2.depositTransaction{ value: _mint }({ _to: _to, _value: _value, _gasLimit: _gasLimit, _isCreation: _isCreation, _data: _data }); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, balanceBefore); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); - } else { - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); - } + assertEq(address(optimismPortal2).balance, balanceBefore + _mint); } /// @notice Tests that `depositTransaction` succeeds for an EOA using 7702 delegation. @@ -1706,7 +1524,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { vm.assume(_7702Target != address(0)); // Prevent overflow on an upgrade context - _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + _mint = bound(_mint, 0, type(uint256).max - address(optimismPortal2).balance); _gasLimit = uint64( bound( @@ -1718,8 +1536,6 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { if (_isCreation) _to = address(0); uint256 portalBalanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - _mint = bound(_mint, 0, type(uint256).max - portalBalanceBefore); // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -1742,12 +1558,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { _to: _to, _value: _value, _gasLimit: _gasLimit, _isCreation: _isCreation, _data: _data }); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + _mint); - } + assertEq(address(optimismPortal2).balance, portalBalanceBefore + _mint); } /// @notice Tests that `depositTransaction` succeeds for a contract. @@ -1762,7 +1573,7 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { external { // Prevent overflow on an upgrade context - _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + _mint = bound(_mint, 0, type(uint256).max - address(optimismPortal2).balance); _gasLimit = uint64( bound( _gasLimit, @@ -1773,8 +1584,6 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; - uint256 lockboxBalanceBefore = address(ethLockbox).balance; - _mint = bound(_mint, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); emitTransactionDeposited({ @@ -1787,23 +1596,13 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { _data: _data }); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. - vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); - } - vm.deal(address(this), _mint); vm.prank(address(this)); optimismPortal2.depositTransaction{ value: _mint }({ _to: _to, _value: _value, _gasLimit: _gasLimit, _isCreation: _isCreation, _data: _data }); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, balanceBefore); - assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); - } else { - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); - } + assertEq(address(optimismPortal2).balance, balanceBefore + _mint); } } @@ -1929,15 +1728,8 @@ contract OptimismPortal2_ProveAndFinalizeWithdrawalTransaction_Test is OptimismP && _target != CONSOLE // The console has no code but behaves like a contract && uint160(_target) > 9 // No precompiles (or zero address) ); - if (isUsingLockbox()) { - vm.assume(_target != address(ethLockbox)); - } - uint256 value = bound(_value, 0, 200_000_000 ether); vm.deal(address(optimismPortal2), value); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), value); - } uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); Types.WithdrawalTransaction memory withdrawalTx = Types.WithdrawalTransaction({ @@ -2075,7 +1867,7 @@ contract OptimismPortal2_ProveAndFinalizeWithdrawalTransaction_Test is OptimismP } /// @notice Tests that `proveAndFinalizeWithdrawalTransaction` reverts when the target is - /// the portal itself or the ETH lockbox. + /// the portal itself. function test_proveAndFinalizeWithdrawalTransaction_unsafeTarget_reverts() external { game.resolve(); @@ -2089,17 +1881,6 @@ contract OptimismPortal2_ProveAndFinalizeWithdrawalTransaction_Test is OptimismP _outputRootProof: _outputRootProof, _withdrawalProof: _withdrawalProof }); - - if (isUsingLockbox()) { - badTx.target = address(ethLockbox); - vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); - optimismPortal2.proveAndFinalizeWithdrawalTransaction({ - _tx: badTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - } } /// @notice Tests that `proveAndFinalizeWithdrawalTransaction` correctly handles reentrancy. @@ -2141,34 +1922,8 @@ contract OptimismPortal2_ProveAndFinalizeWithdrawalTransaction_Test is OptimismP assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); } - /// @notice Tests that `proveAndFinalizeWithdrawalTransaction` calls unlockETH on the - /// ETHLockbox when the lockbox feature is enabled. - function test_proveAndFinalizeWithdrawalTransaction_withETHLockbox_succeeds() external { - address dummyLockbox = address(0xdeadbeef); - forceEnableLockbox(dummyLockbox); - vm.deal(address(dummyLockbox), 0xFFFFFFFF); - vm.deal(address(optimismPortal2), _defaultTx.value); - - uint256 bobBalanceBefore = address(bob).balance; - game.resolve(); - - vm.expectCall(address(dummyLockbox), abi.encodeCall(ethLockbox.unlockETH, (_defaultTx.value))); - optimismPortal2.proveAndFinalizeWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value); - assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); - } - - /// @notice Tests that when the target call fails, ETH is re-locked in the lockbox. - function test_proveAndFinalizeWithdrawalTransaction_targetFailsAndRelocks_fails() external { - address dummyLockbox = address(0xdeadbeef); - forceEnableLockbox(dummyLockbox); - vm.deal(address(dummyLockbox), 0xFFFFFFFF); + /// @notice Tests that a failed target call is recorded without reverting the withdrawal. + function test_proveAndFinalizeWithdrawalTransaction_targetFails_succeeds() external { vm.deal(address(optimismPortal2), _defaultTx.value); uint256 bobBalanceBefore = address(bob).balance; diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol new file mode 100644 index 000000000..1c0f77c65 --- /dev/null +++ b/test/L1/ProtocolVersions.t.sol @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Contracts +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + +// Interfaces +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; + +/// @title ProtocolVersions_TestInit +/// @notice Reusable test initialization for ProtocolVersions tests. Runs against the +/// `protocolVersions` instance deployed by the standard SystemDeploy script. +abstract contract ProtocolVersions_TestInit is CommonTest { + event UpgradeRegistered(uint256 indexed id); + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); + event TimestampSet(uint256 indexed id, uint256 timestamp); + + /// @dev Ascending ids assigned by registration order in these tests. + uint256 internal constant CANYON = 0; + uint256 internal constant ECOTONE = 1; + + address internal _owner; + address internal _nonOwner = makeAddr("non-owner"); + address internal _incidentResponder = makeAddr("incident-responder"); + + function setUp() public virtual override { + super.setUp(); + skipIfForkTest("ProtocolVersions_TestInit: cannot test on forked network"); + _owner = proxyAdminOwner; + } + + /// @dev Registers the first upgrade (id CANYON) and schedules it for block.timestamp + MIN_NOTICE + delay. + function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts_); + } +} + +/// @title ProtocolVersions_Initialize_Test +/// @notice Test contract for the ProtocolVersions initializer. +contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { + /// @notice Tests that initialization sets the correct initial state. The seed is bytes32(0), so + /// the initial scheduleId is bytes32(0) until the first upgrade is registered. + function test_initialize_setsInitialState_succeeds() external view { + // The owner is inherited from the shared ProxyAdmin; initialize records the incident + // responder from config and seeds the hash chain (scheduleId == the bytes32(0) seed). + assertEq(protocolVersions.proxyAdminOwner(), proxyAdminOwner); + assertEq(protocolVersions.incidentResponder(), deploy.cfg().superchainConfigIncidentResponder()); + assertEq(protocolVersions.scheduleId(), bytes32(0)); + } + + /// @notice Tests that initialization appoints the provided incidentResponder and emits the event. + /// @dev Requires a fresh uninitialized proxy rather than the already-initialized shared instance. + function test_initialize_setsIncidentResponder_succeeds() external { + IProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectEmit(true, true, false, false, address(uninitialized)); + emit IncidentResponderUpdated(address(0), _incidentResponder); + vm.prank(EIP1967Helper.getAdmin(address(uninitialized))); + uninitialized.initialize(_incidentResponder); + assertEq(uninitialized.incidentResponder(), _incidentResponder); + } + + /// @notice Tests that only the ProxyAdmin or its owner can initialize. + /// @dev Requires a fresh uninitialized proxy rather than the already-initialized shared instance. + function test_initialize_notProxyAdminOrOwner_reverts() external { + IProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(_nonOwner); + uninitialized.initialize(_incidentResponder); + } + + /// @notice Tests that the contract cannot be initialized twice. + function test_initialize_alreadyInitialized_reverts() external { + vm.expectRevert("Initializable: contract is already initialized"); + vm.prank(EIP1967Helper.getAdmin(address(protocolVersions))); + protocolVersions.initialize(address(0)); + } + + /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). + function test_initialize_implementationDisabled_reverts() external { + IProtocolVersions impl = IProtocolVersions(EIP1967Helper.getImplementation(address(protocolVersions))); + vm.expectRevert("Initializable: contract is already initialized"); + impl.initialize(address(0)); + } + + /// @dev Deploys a fresh uninitialized proxy over the impl produced by SystemDeploy for the two + /// initializer tests that genuinely need one. proxyAdminOwner() resolves by calling owner() + /// on the ProxyAdmin stored in the proxy slot, so the mock provides one. + function _deployUninitializedProxy() internal returns (IProtocolVersions) { + address proxyAdmin = makeAddr("proxy-admin"); + vm.mockCall(proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); + Proxy proxy = new Proxy(proxyAdmin); + address impl = EIP1967Helper.getImplementation(address(protocolVersions)); + vm.prank(proxyAdmin); + proxy.upgradeTo(impl); + return IProtocolVersions(address(proxy)); + } +} + +/// @title ProtocolVersions_Version_Test +/// @notice Test contract for the `version` function. +contract ProtocolVersions_Version_Test is ProtocolVersions_TestInit { + /// @notice Tests that the `version` function returns the expected value. + function test_version_succeeds() external view { + assertEq(protocolVersions.version(), "1.0.0"); + } +} + +/// @title ProtocolVersions_RegisterUpgrade_Test +/// @notice Test contract for the `registerUpgrade` function. +contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { + /// @notice Tests that registering an upgrade extends the scheduleId chain. + function test_registerUpgrade_changesScheduleId_succeeds() external { + bytes32 idBefore = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + assertNotEq(protocolVersions.scheduleId(), idBefore); + } + + /// @notice Tests that `registerUpgrade` assigns ascending ids and returns them. + function test_registerUpgrade_returnsAscendingIds_succeeds() external { + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), 0); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), 1); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), 2); + } + + /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with the assigned id. + function test_registerUpgrade_emitsEvent_succeeds() external { + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(1); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + } + + /// @notice Tests that only the owner can call `registerUpgrade`. + function test_registerUpgrade_callerNotOwner_reverts() external { + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + protocolVersions.registerUpgrade(0, 0); + } + + /// @notice Tests that registering with a future timestamp schedules the upgrade in one call. + function test_registerUpgrade_withTimestamp_succeeds() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(CANYON); + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit TimestampSet(CANYON, ts); + vm.prank(_owner); + uint256 id = protocolVersions.registerUpgrade(ts, 0); + + assertEq(id, CANYON); + assertEq(protocolVersions.getSchedule()[CANYON], ts); + assertNotEq(protocolVersions.scheduleId(), bytes32(0)); + } + + /// @notice Tests that registering with a timestamp inside the notice window succeeds — the + /// notice period is not enforced at registration, only via `setTimestamp`/`delayTimestamp`. + function test_registerUpgrade_shortNotice_succeeds() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; + vm.prank(_owner); + protocolVersions.registerUpgrade(ts, 0); + assertEq(protocolVersions.getSchedule()[CANYON], ts); + } + + /// @notice Tests that a scheduled registration may share the previous scheduled upgrade's timestamp. + function test_registerUpgrade_timestampEqualToPrevious_succeeds() external { + uint64 first = uint64(block.timestamp) + 100; + uint64 second = first; + + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(first, 0), CANYON); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(second, 0), ECOTONE); + + uint64[] memory schedule = protocolVersions.getSchedule(); + assertEq(schedule[CANYON], first); + assertEq(schedule[ECOTONE], second); + } + + /// @notice Tests that zero remains the unscheduled/disabled value and is exempt from ordering. + function test_registerUpgrade_zeroTimestampAfterScheduledPrevious_succeeds() external { + uint64 first = uint64(block.timestamp) + 100; + + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(first, 0), CANYON); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), ECOTONE); + + uint64[] memory schedule = protocolVersions.getSchedule(); + assertEq(schedule[CANYON], first); + assertEq(schedule[ECOTONE], 0); + } + + /// @notice Tests that scheduled registration scans past zero holes to the previous timestamp. + function test_registerUpgrade_timestampAfterPreviousSkipsZeroHoles_succeeds() external { + uint64 first = uint64(block.timestamp) + 100; + uint64 second = first + 1; + + vm.startPrank(_owner); + assertEq(protocolVersions.registerUpgrade(first, 0), CANYON); + assertEq(protocolVersions.registerUpgrade(0, 0), ECOTONE); + assertEq(protocolVersions.registerUpgrade(second, 0), 2); + vm.stopPrank(); + + uint64[] memory schedule = protocolVersions.getSchedule(); + assertEq(schedule[CANYON], first); + assertEq(schedule[ECOTONE], 0); + assertEq(schedule[2], second); + } + + /// @notice Tests that registering a timestamp before the previous scheduled upgrade reverts. + function test_registerUpgrade_timestampNotAfterPrevious_reverts() external { + uint64 first = uint64(block.timestamp) + 100; + + vm.prank(_owner); + protocolVersions.registerUpgrade(first, 0); + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious.selector, ECOTONE, CANYON, first, first - 1 + ) + ); + vm.prank(_owner); + protocolVersions.registerUpgrade(first - 1, 0); + } + + /// @notice Tests that a non-zero minProtocolVersion bumps the minimum during registration. + function test_registerUpgrade_setsMinProtocolVersion_succeeds() external { + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit MinimumProtocolVersionUpdated(42); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 42); + + assertEq(protocolVersions.minimumProtocolVersion(), 42); + } + + /// @notice Tests that a zero minProtocolVersion leaves the current minimum unchanged. + function test_registerUpgrade_zeroMinProtocolVersion_leavesUnchanged_succeeds() external { + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(7); + + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + assertEq(protocolVersions.minimumProtocolVersion(), 7); + } + + /// @notice Tests that a minProtocolVersion exceeding 128 bits reverts. + function test_registerUpgrade_minProtocolVersionTooLarge_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, uint256(type(uint128).max) + 1); + } + + /// @notice Tests that the maximum representable minProtocolVersion (128 bits set) succeeds. + function test_registerUpgrade_minProtocolVersionMaxValue_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, type(uint128).max); + assertEq(protocolVersions.minimumProtocolVersion(), type(uint128).max); + } +} + +/// @title ProtocolVersions_SetMinimumProtocolVersion_Test +/// @notice Test contract for the `setMinimumProtocolVersion` function. +contract ProtocolVersions_SetMinimumProtocolVersion_Test is ProtocolVersions_TestInit { + /// @notice Tests that the owner can set the minimum protocol version. + function test_setMinimumProtocolVersion_updates_succeeds() external { + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(42); + assertEq(protocolVersions.minimumProtocolVersion(), 42); + } + + /// @notice Tests that setting the minimum protocol version does not change the scheduleId. + function test_setMinimumProtocolVersion_doesNotChangeScheduleId_succeeds() external { + bytes32 scheduleIdBefore = protocolVersions.scheduleId(); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(42); + assertEq(protocolVersions.scheduleId(), scheduleIdBefore); + } + + /// @notice Tests that `setMinimumProtocolVersion` emits the `MinimumProtocolVersionUpdated` event. + function test_setMinimumProtocolVersion_emitsEvent_succeeds() external { + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit MinimumProtocolVersionUpdated(42); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(42); + } + + /// @notice Tests that setting a zero protocol version reverts. + function test_setMinimumProtocolVersion_zero_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(0); + } + + /// @notice Tests that a protocol version exceeding 128 bits reverts. + function test_setMinimumProtocolVersion_tooLarge_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(uint256(type(uint128).max) + 1); + } + + /// @notice Tests that the maximum representable protocol version (128 bits set) succeeds. + function test_setMinimumProtocolVersion_maxValue_succeeds() external { + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(type(uint128).max); + assertEq(protocolVersions.minimumProtocolVersion(), type(uint128).max); + } + + /// @notice Tests that only the owner can call `setMinimumProtocolVersion`. + function test_setMinimumProtocolVersion_callerNotOwner_reverts() external { + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + protocolVersions.setMinimumProtocolVersion(42); + } +} + +/// @title ProtocolVersions_SetTimestamp_Test +/// @notice Test contract for the `setTimestamp` function. +contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { + /// @notice Tests that setting a timestamp updates the stored value and extends the scheduleId. + function test_setTimestamp_updatesTimestampAndScheduleId_succeeds() external { + bytes32 initialScheduleId = protocolVersions.scheduleId(); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + assertEq(protocolVersions.getSchedule()[CANYON], ts); + assertNotEq(protocolVersions.scheduleId(), initialScheduleId); + } + + /// @notice Tests that calling `setTimestamp` with the same value is a no-op for scheduleId. + function test_setTimestamp_sameTimestamp_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + assertEq(protocolVersions.getSchedule()[CANYON], ts); + assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); + } + + /// @notice Tests that passing 0 clears a scheduled timestamp, changes the scheduleId, and + /// restores it to the value it held immediately after registration (ts=0 link). + function test_setTimestamp_clearTimestamp_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + assertNotEq(scheduleIdAfterSet, scheduleIdAfterRegister); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, 0); + + assertEq(protocolVersions.getSchedule()[CANYON], 0); + assertEq(protocolVersions.scheduleId(), scheduleIdAfterRegister); + } + + /// @notice Tests that `setTimestamp` emits a `TimestampSet` event. + function test_setTimestamp_emitsEvent_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit TimestampSet(CANYON, ts); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + } + + /// @notice Tests that only the owner can call `setTimestamp`. + function test_setTimestamp_callerNotOwner_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + protocolVersions.setTimestamp(CANYON, ts); + } + + /// @notice Tests that `setTimestamp` reverts when the timestamp is in the past. + function test_setTimestamp_timestampInPast_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.warp(1000); + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, uint64(500)) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, 500); + } + + /// @notice Tests that `setTimestamp` reverts when the timestamp is within MIN_NOTICE of now. + function test_setTimestamp_insufficientNotice_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts)); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + } + + /// @notice Tests that a zero-valued hole below a scheduled successor cannot be scheduled later. + function test_setTimestamp_staticScheduleHole_reverts() external { + uint64 successor = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(successor, 0); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_StaticScheduleHole.selector, CANYON, ECOTONE) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + } + + /// @notice Tests that static-hole detection scans past zero holes to the next scheduled upgrade. + function test_setTimestamp_staticScheduleHoleSkipsZeroHoles_reverts() external { + uint64 successor = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(successor, 0); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_StaticScheduleHole.selector, CANYON, uint256(2)) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + } + + /// @notice Tests that `setTimestamp` may share the previous scheduled upgrade's timestamp. + function test_setTimestamp_timestampEqualToPrevious_succeeds() external { + uint64 previous = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(previous, 0); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.setTimestamp(ECOTONE, previous); + vm.stopPrank(); + + uint64[] memory schedule = protocolVersions.getSchedule(); + assertEq(schedule[CANYON], previous); + assertEq(schedule[ECOTONE], previous); + } + + /// @notice Tests that `setTimestamp` reverts when the timestamp is before the previous one. + function test_setTimestamp_timestampNotAfterPrevious_reverts() external { + uint64 previous = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(previous, 0); + protocolVersions.registerUpgrade(0, 0); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious.selector, + ECOTONE, + CANYON, + previous, + previous - 1 + ) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(ECOTONE, previous - 1); + } + + /// @notice Tests that `setTimestamp` reverts when the timestamp is not before the next one. + function test_setTimestamp_timestampNotBeforeNext_reverts() external { + uint64 current = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + uint64 next = current + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(current, 0); + protocolVersions.registerUpgrade(next, 0); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_TimestampNotBeforeNext.selector, CANYON, ECOTONE, next, next + ) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, next); + } + + /// @notice Tests that `setTimestamp` reverts when the upgrade has already activated. + function test_setTimestamp_afterActivation_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.warp(100); + uint64 activationTs = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, activationTs); + + vm.warp(activationTs + 1); + uint64 laterTs = activationTs + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, activationTs + ) + ); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, laterTs); + } + + /// @notice Tests that `setTimestamp` reverts for an unregistered upgrade. + function test_setTimestamp_unregisteredUpgrade_reverts() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); + vm.prank(_owner); + protocolVersions.setTimestamp(0, ts); + } + + /// @notice Tests that scheduleId is reproducible from (ascending ids, timestamps). + function test_setTimestamp_scheduleIdReproducible_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts1 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + uint64 ts2 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts1); + vm.prank(_owner); + protocolVersions.setTimestamp(ECOTONE, ts2); + + // Reproduce the chain from scratch, starting from the bytes32(0) seed. + bytes32 seed = bytes32(0); + bytes32 link0 = keccak256(abi.encode(seed, uint256(0), ts1)); + bytes32 link1 = keccak256(abi.encode(link0, uint256(1), ts2)); + + assertEq(protocolVersions.scheduleId(), link1); + } +} + +/// @title ProtocolVersions_DelayTimestamp_Test +/// @notice Test contract for the `delayTimestamp` function. +contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { + /// @notice Tests that `delayTimestamp` pushes the activation timestamp later and updates scheduleId. + function test_delayTimestamp_pushesTimestampLater_succeeds() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + bytes32 scheduleIdBefore = protocolVersions.scheduleId(); + vm.roll(block.number + 1); + + uint64 later = ts + 50; + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, later); + + assertEq(protocolVersions.getSchedule()[CANYON], later); + assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); + } + + /// @notice Tests that only the incidentResponder can call `delayTimestamp`. + function test_delayTimestamp_callerNotIncidentResponder_reverts() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); + vm.prank(_owner); + protocolVersions.delayTimestamp(CANYON, ts + 50); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); + vm.prank(_nonOwner); + protocolVersions.delayTimestamp(CANYON, ts + 50); + } + + /// @notice Tests that `delayTimestamp` reverts when the new timestamp is earlier than current. + function test_delayTimestamp_earlierTimestamp_reverts() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10) + ); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts - 10); + } + + /// @notice Tests that `delayTimestamp` reverts when the new timestamp equals the current one. + function test_delayTimestamp_equalTimestamp_reverts() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts)); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts); + } + + /// @notice Tests that `delayTimestamp` cannot move an upgrade to or beyond its next scheduled successor. + function test_delayTimestamp_timestampNotBeforeNext_reverts() external { + uint64 current = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + uint64 next = current + 100; + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(current, 0); + protocolVersions.registerUpgrade(next, 0); + protocolVersions.setIncidentResponder(_incidentResponder); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_TimestampNotBeforeNext.selector, CANYON, ECOTONE, next, next + ) + ); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, next); + } + + /// @notice Tests that `delayTimestamp` reverts when the upgrade has no scheduled timestamp. + function test_delayTimestamp_notScheduled_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, CANYON)); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts); + } + + /// @notice Tests that `delayTimestamp` reverts when the upgrade has already activated. + function test_delayTimestamp_afterActivation_reverts() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.warp(ts + 1); + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, ts) + ); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts + 100); + } + + /// @notice Tests that `delayTimestamp` reverts for an unregistered upgrade. + function test_delayTimestamp_unregisteredUpgrade_reverts() external { + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(0, ts); + } +} + +/// @title ProtocolVersions_IncidentResponder_Test +/// @notice Test contract for the `setIncidentResponder` function and incidentResponder role. +contract ProtocolVersions_IncidentResponder_Test is ProtocolVersions_TestInit { + /// @notice Tests that `incidentResponder` starts as address(0). + function test_incidentResponder_startsUnset_succeeds() external view { + assertEq(protocolVersions.incidentResponder(), address(0)); + } + + /// @notice Tests that the owner can appoint a incidentResponder address. + function test_setIncidentResponder_setsAddress_succeeds() external { + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + assertEq(protocolVersions.incidentResponder(), _incidentResponder); + } + + /// @notice Tests that only the owner can call `setIncidentResponder`. + function test_setIncidentResponder_callerNotOwner_reverts() external { + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + protocolVersions.setIncidentResponder(_incidentResponder); + } + + /// @notice Tests that `setIncidentResponder` emits a `IncidentResponderUpdated` event. + function test_setIncidentResponder_emitsEvent_succeeds() external { + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit IncidentResponderUpdated(address(0), _incidentResponder); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + } + + /// @notice Tests that the owner can clear the incidentResponder role by setting it to address(0). + function test_setIncidentResponder_clear_succeeds() external { + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit IncidentResponderUpdated(_incidentResponder, address(0)); + vm.prank(_owner); + protocolVersions.setIncidentResponder(address(0)); + + assertEq(protocolVersions.incidentResponder(), address(0)); + } +} + +/// @title ProtocolVersions_Uncategorized_Test +/// @notice Test contract for view functions and the upgrade registry. +contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { + /// @notice Tests that `getSchedule` returns an empty array when no upgrades are registered. + function test_getSchedule_empty_succeeds() external view { + assertEq(protocolVersions.getSchedule().length, 0); + } + + /// @notice Tests that `getSchedule` returns all upgrades in registration order with correct fields. + function test_getSchedule_returnsFullSchedule_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + uint64[] memory s = protocolVersions.getSchedule(); + + assertEq(s.length, 2); + assertEq(s[CANYON], ts); + assertEq(s[ECOTONE], 0); + } + + /// @notice Tests that `scheduleId(id)` returns each upgrade's cumulative commitment, and that + /// the last upgrade's commitment equals the current scheduleId. + function test_scheduleId_byId_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + assertNotEq(protocolVersions.scheduleId(CANYON), protocolVersions.scheduleId(ECOTONE)); + assertEq(protocolVersions.scheduleId(ECOTONE), protocolVersions.scheduleId()); + } + + /// @notice Tests that `scheduleId(id)` reverts for an unregistered upgrade. + function test_scheduleId_byId_unregistered_reverts() external { + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); + protocolVersions.scheduleId(0); + } +} + +/// @title ProtocolVersions_ActivatedScheduleId_Test +/// @notice Tests for commitments to the upgrades active at a supplied L2 timestamp. +contract ProtocolVersions_ActivatedScheduleId_Test is ProtocolVersions_TestInit { + /// @notice An empty registry and a registry containing only inactive entries both commit to the + /// zero hash-chain seed. + function test_activatedScheduleId_noneActivated_returnsSeed() external { + assertEq(protocolVersions.activatedScheduleId(uint64(block.timestamp)), bytes32(0)); + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(uint64(block.timestamp + 100), 0); + vm.stopPrank(); + + assertEq(protocolVersions.activatedScheduleId(uint64(block.timestamp)), bytes32(0)); + } + + /// @notice Activation is inclusive at the supplied L2 timestamp. + function test_activatedScheduleId_boundaryInclusive_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(100, 0); + + assertEq(protocolVersions.activatedScheduleId(99), bytes32(0)); + assertEq( + protocolVersions.activatedScheduleId(100), keccak256(abi.encode(bytes32(0), uint256(CANYON), uint64(100))) + ); + } + + /// @notice The commitment includes every registered entry through the highest active upgrade, + /// including a static zero-valued hole below it. + function test_activatedScheduleId_commitsPrefixThroughHighestActive_succeeds() external { + vm.startPrank(_owner); + protocolVersions.registerUpgrade(10, 0); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(30, 0); + vm.stopPrank(); + + bytes32 link0 = keccak256(abi.encode(bytes32(0), uint256(0), uint64(10))); + bytes32 link1 = keccak256(abi.encode(link0, uint256(1), uint64(0))); + bytes32 link2 = keccak256(abi.encode(link1, uint256(2), uint64(30))); + + assertEq(protocolVersions.activatedScheduleId(30), link2); + } + + /// @notice Appending unscheduled or future upgrades above the active prefix cannot move the + /// activated commitment selected by that prefix. + function test_activatedScheduleId_stableAcrossAppendsAboveActivePrefix_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(10, 0); + bytes32 pinned = protocolVersions.activatedScheduleId(10); + + vm.startPrank(_owner); + protocolVersions.registerUpgrade(0, 0); + protocolVersions.registerUpgrade(100, 0); + vm.stopPrank(); + + assertEq(protocolVersions.activatedScheduleId(10), pinned); + assertNotEq(protocolVersions.scheduleId(), pinned); + } + + /// @notice Cross-implementation golden shared with Base's `ScheduleId` tests for the real Base + /// mainnet static schedule. The activation cutoff is Beryl, so the commitment contains + /// the prefix through id 11, including the static PectraBlobSchedule hole at id 7, and + /// excludes the unscheduled Cobalt placeholder at id 12. + function test_activatedScheduleId_matchesBaseMainnetGoldenValue_succeeds() external { + _registerBaseMainnetStaticSchedule(); + + assertEq( + protocolVersions.activatedScheduleId(BASE_MAINNET_BERYL_TIMESTAMP), + 0xadd4aa9bd3532969035a9543c16b8c7d71298e15836f0ac731fdd3eea552c6e2 + ); + } + + /// @notice Cross-implementation golden for the full real Base mainnet contract-backed schedule. + /// The live tail commits to all 13 entries, including PectraBlobSchedule and Cobalt as + /// unscheduled zero-timestamp entries. + function test_scheduleId_matchesBaseMainnetFullScheduleGoldenValue_succeeds() external { + _registerBaseMainnetStaticSchedule(); + + assertEq(protocolVersions.scheduleId(), 0x5ee41f186b0a439783060587cfbb942f6f1d94ecc76376c9782580c943ff2b6d); + assertEq(protocolVersions.scheduleId(12), protocolVersions.scheduleId()); + } + + uint64 private constant BASE_MAINNET_GENESIS_TIMESTAMP = 1_686_789_347; + uint64 private constant BASE_MAINNET_CANYON_TIMESTAMP = 1_704_992_401; + uint64 private constant BASE_MAINNET_DELTA_TIMESTAMP = 1_708_560_000; + uint64 private constant BASE_MAINNET_ECOTONE_TIMESTAMP = 1_710_374_401; + uint64 private constant BASE_MAINNET_FJORD_TIMESTAMP = 1_720_627_201; + uint64 private constant BASE_MAINNET_GRANITE_TIMESTAMP = 1_726_070_401; + uint64 private constant BASE_MAINNET_HOLOCENE_TIMESTAMP = 1_736_445_601; + uint64 private constant BASE_MAINNET_ISTHMUS_TIMESTAMP = 1_746_806_401; + uint64 private constant BASE_MAINNET_JOVIAN_TIMESTAMP = 1_764_691_201; + uint64 private constant BASE_MAINNET_AZUL_TIMESTAMP = 1_779_991_200; + uint64 private constant BASE_MAINNET_BERYL_TIMESTAMP = 1_782_410_400; + + function _registerBaseMainnetStaticSchedule() private { + vm.startPrank(_owner); + protocolVersions.registerUpgrade(BASE_MAINNET_GENESIS_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_CANYON_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_DELTA_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_ECOTONE_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_FJORD_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_GRANITE_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_HOLOCENE_TIMESTAMP, 0); + protocolVersions.registerUpgrade(0, 0); // PectraBlobSchedule is unscheduled on Base mainnet. + protocolVersions.registerUpgrade(BASE_MAINNET_ISTHMUS_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_JOVIAN_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_AZUL_TIMESTAMP, 0); + protocolVersions.registerUpgrade(BASE_MAINNET_BERYL_TIMESTAMP, 0); + protocolVersions.registerUpgrade(0, 0); // Cobalt is unscheduled on Base mainnet. + vm.stopPrank(); + } +} diff --git a/test/L1/ResourceMetering.t.sol b/test/L1/ResourceMetering.t.sol index 20e8cd922..6c35cb66d 100644 --- a/test/L1/ResourceMetering.t.sol +++ b/test/L1/ResourceMetering.t.sol @@ -72,6 +72,9 @@ abstract contract ResourceMetering_TestInit is Test { /// @dev Tests are based on the default config values. It is expected that these config values are /// used in production. contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { + /// @dev Gas overhead from the metering logic and `measuredUse` wrapper, excluding the burn loop. + uint256 internal constant METERING_GAS_OVERHEAD = 1000; + /// @notice Tests that updating the resource params to the same values works correctly. function test_metered_updateParamsNoChange_succeeds() external { meter.use(0); // equivalent to just updating the base fee and block number @@ -190,6 +193,42 @@ contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { assertGt(prevBaseFee, 0); } + /// @notice Tests that deposits are charged the full resource cost when L1 base fee is below + /// 1 gwei but above the 0.01 gwei floor. + function test_metered_subGweiBaseFee_collectsFullResourceCost_succeeds() external { + uint64 amount = 100_000; + uint128 prevBaseFee = 1 gwei; + uint256 l1BaseFee = 0.1 gwei; + uint256 resourceCost = uint256(amount) * uint256(prevBaseFee); + uint256 expectedGasCost = resourceCost / l1BaseFee; + + meter.set(prevBaseFee, 0, uint64(block.number)); + vm.fee(l1BaseFee); + + uint256 gasConsumed = meter.measuredUse(amount); + + assertApproxEqAbs(gasConsumed, expectedGasCost, METERING_GAS_OVERHEAD); + assertApproxEqAbs(gasConsumed * l1BaseFee, resourceCost, METERING_GAS_OVERHEAD * l1BaseFee); + } + + /// @notice Tests that the 0.01 gwei floor is applied when L1 base fee falls below it. + function test_metered_belowFloorBaseFee_usesFloor_succeeds() external { + uint64 amount = 100_000; + uint128 prevBaseFee = 1 gwei; + uint256 l1BaseFee = 0.005 gwei; + uint256 floor = 0.01 gwei; + uint256 resourceCost = uint256(amount) * uint256(prevBaseFee); + uint256 expectedGasCost = resourceCost / floor; + + meter.set(prevBaseFee, 0, uint64(block.number)); + vm.fee(l1BaseFee); + + uint256 gasConsumed = meter.measuredUse(amount); + + assertApproxEqAbs(gasConsumed, expectedGasCost, METERING_GAS_OVERHEAD); + assertApproxEqAbs(gasConsumed * l1BaseFee, resourceCost / 2, METERING_GAS_OVERHEAD * l1BaseFee); + } + /// @notice Tests that base fee decreases when gas usage is below target. function test_metered_belowTargetUsage_succeeds() external { ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig(); diff --git a/test/L1/SystemConfig.t.sol b/test/L1/SystemConfig.t.sol index 796edaedf..30d8cbab1 100644 --- a/test/L1/SystemConfig.t.sol +++ b/test/L1/SystemConfig.t.sol @@ -5,12 +5,10 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Scripts -import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; -import { Features } from "src/libraries/Features.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -563,11 +561,8 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { assertTrue(systemConfig.paused()); } - /// @notice Tests that `paused()` returns true when OptimismPortal identifier is paused and - /// the ETH_LOCKBOX feature is disabled. + /// @notice Tests that `paused()` returns true when the OptimismPortal identifier is paused. function test_paused_optimismPortalIdentifier_succeeds() external { - skipIfSysFeatureEnabled(Features.ETH_LOCKBOX); - // Initially not paused assertFalse(systemConfig.paused()); @@ -579,22 +574,6 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { assertTrue(systemConfig.paused()); } - /// @notice Tests that `paused()` returns true when ETHLockbox identifier is paused and - /// ETH_LOCKBOX feature is enabled. - function test_paused_ethLockboxIdentifier_succeeds() external { - skipIfSysFeatureDisabled(Features.ETH_LOCKBOX); - - // Initially not paused - assertFalse(systemConfig.paused()); - - // Pause the system with ETHLockbox identifier - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(ethLockbox)); - - // Verify paused state - assertTrue(systemConfig.paused()); - } - /// @notice Tests that `paused()` returns true when both pauses are active. function test_paused_bothPausesActive_succeeds() external { assertFalse(systemConfig.paused()); @@ -614,7 +593,6 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { function testFuzz_paused_otherAddress_succeeds(address _address) external { vm.assume(_address != address(0)); vm.assume(_address != address(optimismPortal2)); - vm.assume(_address != address(ethLockbox)); // Initially not paused assertFalse(systemConfig.paused()); @@ -633,15 +611,6 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { event FeatureSet(bytes32 indexed feature, bool indexed enabled); - function _enableEthLockboxIfDisabled(address proxyAdmin) internal { - if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - } - - assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); - } - /// @notice Tests that `setFeature` reverts if the caller is not ProxyAdmin or ProxyAdmin owner. /// @param _sender The address to test. function testFuzz_setFeature_notProxyAdminOrProxyAdminOwner_reverts(address _sender) external { @@ -733,55 +702,6 @@ contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); systemConfig.setFeature(EXAMPLE_FEATURE, false); } - - /// @notice Tests that disabling ETH_LOCKBOX reverts if the OptimismPortal has a non-zero - /// ETHLockbox configured. - function test_setFeature_ethLockboxDisableWhileConfigured_reverts() external { - address proxyAdmin = address(systemConfig.proxyAdmin()); - - _enableEthLockboxIfDisabled(proxyAdmin); - - // Force the portal to have a configured ETHLockbox address. - StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "ethLockbox"); - vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(uint256(uint160(address(1))))); - - // Disabling should revert due to safety check while lockbox is configured. - vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, false); - } - - /// @notice Tests that enabling ETH_LOCKBOX while the system is paused (global) reverts. - function test_setFeature_ethLockboxEnableWhilePaused_reverts() external { - address proxyAdmin = address(systemConfig.proxyAdmin()); - - _enableEthLockboxIfDisabled(proxyAdmin); - - // Pause globally. - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); - - // Enabling while paused should revert. - vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - } - - /// @notice Tests that disabling ETH_LOCKBOX while the system is paused (global) reverts. - function test_setFeature_ethLockboxDisableWhilePaused_reverts() external { - address proxyAdmin = address(systemConfig.proxyAdmin()); - - _enableEthLockboxIfDisabled(proxyAdmin); - - // Pause globally. - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); - - // Disabling while paused should revert. - vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, false); - } } /// @title SystemConfig_IsFeatureEnabled_Test diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index 8914ec123..7ae59b5ca 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -11,6 +11,7 @@ import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { LibClone } from "lib/solady/src/utils/LibClone.sol"; @@ -36,6 +37,69 @@ contract AggregateVerifierTest is BaseTest { _createAndAssertInitializedGame("zk-proof", AggregateVerifier.ProofType.ZK, ZK_PROVER, address(0), ZK_PROVER); } + /// @notice init pins scheduleId(MAX_UPGRADE_ID): upgrades registered beyond the pin affect + /// neither existing snapshots nor what new games snapshot. + function test_initialize_pinsScheduleIdAtMaxUpgradeId_succeeds() public { + bytes32 pinned = protocolVersions.scheduleId(MAX_UPGRADE_ID); + assertTrue(pinned != bytes32(0)); + // Nothing is registered beyond the pin yet, so the pinned link is the live tail. + assertEq(protocolVersions.scheduleId(), pinned); + + Claim rootClaim = _advanceL2BlockAndClaim(); + bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); + + AggregateVerifier game = _createAggregateVerifierGame( + TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof + ); + + assertEq(game.scheduleId(), pinned); + + // Registering beyond the pin moves the live tail but not the pinned commitment. + protocolVersions.registerUpgrade(uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100, 1); + assertNotEq(protocolVersions.scheduleId(), pinned); + assertEq(protocolVersions.scheduleId(MAX_UPGRADE_ID), pinned); + assertEq(game.scheduleId(), pinned); + + // A game created after the live tail moved still snapshots the pinned commitment. + rootClaim = _advanceL2BlockAndClaim(); + proof = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + AggregateVerifier game2 = + _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, address(game), proof); + assertEq(game2.scheduleId(), pinned); + } + + /// @notice Scheduling an upgrade inside the pinned prefix moves the pinned commitment, so a + /// game created afterwards snapshots the new value. + function test_initialize_scheduleChangeWithinPin_movesSnapshot_succeeds() public { + bytes32 unscheduled = protocolVersions.scheduleId(MAX_UPGRADE_ID); + + // Schedule the last pinned upgrade; the pinned commitment must move. + protocolVersions.setTimestamp(MAX_UPGRADE_ID, uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100); + bytes32 scheduled = protocolVersions.scheduleId(MAX_UPGRADE_ID); + assertNotEq(scheduled, unscheduled); + + Claim rootClaim = _advanceL2BlockAndClaim(); + bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); + + AggregateVerifier game = _createAggregateVerifierGame( + TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof + ); + + assertEq(game.scheduleId(), scheduled); + } + + /// @notice The constructor stores MAX_UPGRADE_ID and reverts if the registry has not + /// registered up to it. + function test_constructor_maxUpgradeId_works() public { + AggregateVerifier impl = _deployAggregateVerifierWithMaxUpgradeId(MAX_UPGRADE_ID); + assertEq(impl.MAX_UPGRADE_ID(), MAX_UPGRADE_ID); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, MAX_UPGRADE_ID + 1) + ); + _deployAggregateVerifierWithMaxUpgradeId(MAX_UPGRADE_ID + 1); + } + function testInitializeFailsIfInvalidCallDataSize() public { Claim rootClaim = _advanceL2BlockAndClaim(); @@ -406,6 +470,28 @@ contract AggregateVerifierTest is BaseTest { private returns (AggregateVerifier) { + return _deployAggregateVerifier(blockInterval, intermediateBlockInterval, MAX_UPGRADE_ID); + } + + function _deployAggregateVerifierWithMaxUpgradeId(uint256 maxUpgradeId) private returns (AggregateVerifier) { + return _deployAggregateVerifier(BLOCK_INTERVAL, INTERMEDIATE_BLOCK_INTERVAL, maxUpgradeId); + } + + function _deployAggregateVerifier( + uint256 blockInterval, + uint256 intermediateBlockInterval, + uint256 maxUpgradeId + ) + private + returns (AggregateVerifier) + { + AggregateVerifier.GameConfig memory gameConfig = AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: IProtocolVersions(address(protocolVersions)), maxUpgradeId: maxUpgradeId + }) + }); + return new AggregateVerifier( GameTypes.AGGREGATE_VERIFIER, IAnchorStateRegistry(address(anchorStateRegistry)), @@ -418,7 +504,7 @@ contract AggregateVerifierTest is BaseTest { L2_CHAIN_ID, blockInterval, intermediateBlockInterval, - AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }) + gameConfig ); } } diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index fc5fca382..bd5ee6473 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -19,6 +19,8 @@ import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; @@ -35,6 +37,9 @@ contract BaseTest is Test { // Finality delay handled by the AggregateVerifier uint256 internal constant FINALITY_DELAY = 0 days; + // The ProtocolVersions upgrade id the AggregateVerifier under test is pinned to. + uint256 internal constant MAX_UPGRADE_ID = 2; + uint256 internal currentL2BlockNumber; address internal immutable TEE_PROVER = makeAddr("tee-prover"); @@ -54,6 +59,7 @@ contract BaseTest is Test { MockVerifier internal teeVerifier; MockVerifier internal zkVerifier; + ProtocolVersions internal protocolVersions; function setUp() public virtual { _deployContractsAndProxies(); @@ -78,9 +84,12 @@ contract BaseTest is Test { proxyAdmin = new ProxyAdmin(address(this)); + ProtocolVersions _protocolVersions = new ProtocolVersions(); + anchorStateRegistry = AnchorStateRegistry(_deployProxy(address(_anchorStateRegistry))); factory = DisputeGameFactory(_deployProxy(address(_factory))); delayedWETH = DelayedWETH(payable(_deployProxy(address(_delayedWETH)))); + protocolVersions = ProtocolVersions(_deployProxy(address(_protocolVersions))); teeVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); zkVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); @@ -103,6 +112,13 @@ contract BaseTest is Test { ); factory.initialize(address(this)); delayedWETH.initialize(systemConfig); + protocolVersions.initialize(address(0)); + + // Seed unscheduled upgrades through MAX_UPGRADE_ID so the AggregateVerifier constructor + // check passes. + for (uint256 i = 0; i <= MAX_UPGRADE_ID; i++) { + protocolVersions.registerUpgrade(0, 0); + } } function _deployAndSetAggregateVerifier() internal { @@ -118,7 +134,12 @@ contract BaseTest is Test { L2_CHAIN_ID, BLOCK_INTERVAL, INTERMEDIATE_BLOCK_INTERVAL, - AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }) + AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: IProtocolVersions(address(protocolVersions)), maxUpgradeId: MAX_UPGRADE_ID + }) + }) ); factory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(aggregateVerifierImpl))); @@ -180,7 +201,7 @@ contract BaseTest is Test { return abi.encodePacked(l2BlockNumber, parentAddress, _generateIntermediateRoots(l2BlockNumber, rootClaim)); } - function _generateIntermediateRoots(uint256 l2BlockNumber, Claim rootClaim) private pure returns (bytes memory) { + function _generateIntermediateRoots(uint256 l2BlockNumber, Claim rootClaim) internal pure returns (bytes memory) { bytes32[] memory intermediateRoots = new bytes32[](INTERMEDIATE_ROOTS_COUNT); uint256 startingL2BlockNumber = l2BlockNumber - BLOCK_INTERVAL; for (uint256 i = 1; i < INTERMEDIATE_ROOTS_COUNT; i++) { diff --git a/test/L1/proofs/DisputeGameFactory.t.sol b/test/L1/proofs/DisputeGameFactory.t.sol index eb237ae20..fc357a912 100644 --- a/test/L1/proofs/DisputeGameFactory.t.sol +++ b/test/L1/proofs/DisputeGameFactory.t.sol @@ -219,6 +219,9 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { function test_create_implArgs_succeeds() public { MockVerifier teeVerifier = new MockVerifier(anchorStateRegistry); MockVerifier zkVerifier = new MockVerifier(anchorStateRegistry); + // The AggregateVerifier constructor requires the pinned upgrade id to be registered. + vm.prank(proxyAdminOwner); + protocolVersions.registerUpgrade(0, 0); AggregateVerifier gameImpl = new AggregateVerifier( GameTypes.AGGREGATE_VERIFIER, anchorStateRegistry, @@ -231,7 +234,12 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { L2_CHAIN_ID, AGGREGATE_BLOCK_INTERVAL, AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL, - AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }) + AggregateVerifier.GameConfig({ + finalizationDelays: AggregateVerifier.FinalizationDelays({ slow: 5 days, fast: 1 days }), + schedule: AggregateVerifier.ScheduleConfig({ + protocolVersions: protocolVersions, maxUpgradeId: protocolVersions.getSchedule().length - 1 + }) + }) ); _setGame(address(gameImpl), GameTypes.AGGREGATE_VERIFIER); diff --git a/test/L1/proofs/NitroEnclaveVerifier.t.sol b/test/L1/proofs/NitroEnclaveVerifier.t.sol index f62afdd13..f1bfe1d85 100644 --- a/test/L1/proofs/NitroEnclaveVerifier.t.sol +++ b/test/L1/proofs/NitroEnclaveVerifier.t.sol @@ -205,12 +205,6 @@ contract NitroEnclaveVerifierTest is Test { assertEq(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), 0); } - function testRevokeCertRevertsIfNotTrusted() public { - bytes32 unknown = keccak256("unknown-cert"); - vm.expectRevert(abi.encodeWithSelector(NitroEnclaveVerifier.CertificateNotFound.selector, unknown)); - verifier.revokeCert(unknown); - } - function testRevokeCertRevertsIfNotOwnerOrRevoker() public { vm.prank(submitter); vm.expectRevert(NitroEnclaveVerifier.CallerNotOwnerOrRevoker.selector); @@ -223,6 +217,64 @@ contract NitroEnclaveVerifierTest is Test { assertTrue(verifier.revokedCerts(INTERMEDIATE_CERT_1)); } + function testRevokeCertPreemptiveUnknownCert() public { + bytes32 unknown = keccak256("unknown-cert"); + assertEq(verifier.trustedIntermediateCerts(unknown), 0); + assertFalse(verifier.revokedCerts(unknown)); + + vm.expectEmit(false, false, false, true); + emit NitroEnclaveVerifier.CertRevoked(unknown); + verifier.revokeCert(unknown); + + assertTrue(verifier.revokedCerts(unknown)); + assertEq(verifier.trustedIntermediateCerts(unknown), 0); + } + + function testVerifyRejectsPreemptivelyRevokedCertInSuffix() public { + _setUpRiscZeroConfig(); + bytes32 unknown = keccak256("unknown-compromised-intermediate"); + verifier.revokeCert(unknown); + + VerifierJournal memory journal = _createSuccessJournal(); + bytes32[] memory certs = new bytes32[](3); + certs[0] = ROOT_CERT; + certs[1] = unknown; // preemptively revoked, lives in the suffix + certs[2] = keccak256("attacker-leaf"); + journal.certs = certs; + + uint64[] memory expiries = new uint64[](3); + expiries[0] = ROOT_CERT_EXPIRY; + expiries[1] = NEW_LEAF_CERT_EXPIRY; + expiries[2] = NEW_LEAF_CERT_EXPIRY; + journal.certExpiries = expiries; + journal.trustedCertsPrefixLen = 1; + + bytes memory output = abi.encode(journal); + bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + + vm.prank(submitter); + VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + + assertEq(uint8(result.result), uint8(VerificationResult.IntermediateCertsNotTrusted)); + assertEq(verifier.trustedIntermediateCerts(unknown), 0); + assertTrue(verifier.revokedCerts(unknown)); + } + + function testCheckTrustedIntermediateCertsBreaksAtPreemptivelyRevokedEntry() public { + bytes32 unknown = keccak256("unknown-compromised-intermediate"); + verifier.revokeCert(unknown); + + bytes32[][] memory reportCerts = new bytes32[][](1); + reportCerts[0] = new bytes32[](3); + reportCerts[0][0] = ROOT_CERT; + reportCerts[0][1] = unknown; + reportCerts[0][2] = keccak256("leaf"); + + uint8[] memory prefixLens = verifier.checkTrustedIntermediateCerts(reportCerts); + assertEq(prefixLens[0], 1); + } + // ============ unrevokeCert Tests ============ function testUnrevokeCertClearsSentinel() public { diff --git a/test/L2/BaseTime.t.sol b/test/L2/BaseTime.t.sol new file mode 100644 index 000000000..b5106232b --- /dev/null +++ b/test/L2/BaseTime.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Target contract +import { BaseTime } from "src/L2/BaseTime.sol"; +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; + +/// @title BaseTime_TestInit +/// @notice Reusable test initialization for BaseTime tests. +abstract contract BaseTime_TestInit is Test { + BaseTime internal baseTime; + + /// @notice Sets up the test suite. + function setUp() public { + baseTime = new BaseTime(); + } + + /// @notice Sets the millisecond component as the protocol depositor. + function setTimestampMillisPart(uint16 _timestampMillisPart) internal { + vm.prank(Constants.DEPOSITOR_ACCOUNT); + baseTime.setTimestampMillisPart(_timestampMillisPart); + } +} + +/// @title BaseTime_TimestampMillisPart_Test +/// @notice Tests BaseTime's initial value. +contract BaseTime_TimestampMillisPart_Test is BaseTime_TestInit { + /// @notice Tests that the millisecond component initially equals zero. + function test_initialValue_succeeds() external view { + assertEq(baseTime.timestampMillisPart(), 0); + } +} + +/// @title BaseTime_SetTimestampMillisPart_Test +/// @notice Tests updating BaseTime's millisecond component. +contract BaseTime_SetTimestampMillisPart_Test is BaseTime_TestInit { + /// @notice Tests every valid millisecond component. + function test_setTimestampMillisPart_succeeds() external { + uint16[5] memory validValues = [uint16(0), 200, 400, 600, 800]; + + for (uint256 i; i < validValues.length; i++) { + setTimestampMillisPart(validValues[i]); + assertEq(baseTime.timestampMillisPart(), validValues[i]); + } + } + + /// @notice Tests that an invalid millisecond component is rejected. + function testFuzz_setTimestampMillisPart_invalidValue_reverts(uint16 _timestampMillisPart) external { + vm.assume(_timestampMillisPart > 800 || _timestampMillisPart % 200 != 0); + vm.expectRevert(IBaseTime.BaseTime_InvalidTimestampMillisPart.selector); + vm.prank(Constants.DEPOSITOR_ACCOUNT); + baseTime.setTimestampMillisPart(_timestampMillisPart); + } + + /// @notice Tests that callers other than the protocol depositor are rejected. + function testFuzz_setTimestampMillisPart_notDepositor_reverts(address _caller) external { + vm.assume(_caller != Constants.DEPOSITOR_ACCOUNT); + vm.expectRevert(IBaseTime.BaseTime_NotDepositor.selector); + vm.prank(_caller); + baseTime.setTimestampMillisPart(200); + } + + /// @notice Tests that the millisecond component occupies slot zero as a uint16. + function test_setTimestampMillisPart_usesSlotZero_succeeds() external { + setTimestampMillisPart(600); + + assertEq(vm.load(address(baseTime), bytes32(0)), bytes32(uint256(600))); + } +} + +/// @title BaseTime_TimestampMs_Test +/// @notice Tests BaseTime's full millisecond timestamp getter. +contract BaseTime_TimestampMs_Test is BaseTime_TestInit { + /// @notice Tests that timestampMs combines block.timestamp with the millisecond component. + function test_timestampMs_succeeds() external { + vm.warp(1725); + setTimestampMillisPart(600); + + assertEq(baseTime.timestampMs(), 1_725_600); + } +} diff --git a/test/L2/FeeDisburser.t.sol b/test/L2/FeeDisburser.t.sol index 75287a33d..bf27ec2d2 100644 --- a/test/L2/FeeDisburser.t.sol +++ b/test/L2/FeeDisburser.t.sol @@ -6,7 +6,16 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { FeeDisburser } from "src/L2/FeeDisburser.sol"; import { IFeeVault, Types } from "interfaces/L2/IFeeVault.sol"; import { IStandardBridge } from "interfaces/universal/IStandardBridge.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Constants } from "src/libraries/Constants.sol"; +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; + +contract ReentrantReceiver { + receive() external payable { + FeeDisburser(payable(msg.sender)).disburseFees(); + } +} /// @title FeeDisburserTest /// @notice Comprehensive unit and fuzz tests for the FeeDisburser contract @@ -15,6 +24,9 @@ contract FeeDisburserTest is Test { event FeesDisbursed(uint256 disbursementTime, uint256 deprecated, uint256 totalFeesDisbursed); event FeesReceived(address indexed sender, uint256 amount); event NoFeesCollected(); + event ProcessedFunds( + address indexed systemAddress, bool indexed success, uint256 balanceNeeded, uint256 balanceSent + ); // Constants uint32 constant WITHDRAWAL_MIN_GAS = 35_000; @@ -26,6 +38,9 @@ contract FeeDisburserTest is Test { address payable constant L1_WALLET = payable(address(0x1001)); address constant ALICE = address(0xA11CE); address constant BOB = address(0xB0B); + address constant PROXY_ADMIN = address(0xAD1); + address constant PROXY_ADMIN_OWNER = address(0xAD2); + address payable constant SYSTEM_ADDR = payable(address(0x5001)); // Contract instances FeeDisburser feeDisburser; @@ -97,6 +112,34 @@ contract FeeDisburserTest is Test { emit NoFeesCollected(); } + function _setProxyAdmin() internal { + vm.store(address(feeDisburser), Constants.PROXY_OWNER_ADDRESS, bytes32(uint256(uint160(PROXY_ADMIN)))); + } + + function _mockProxyAdminOwner() internal { + vm.mockCall(PROXY_ADMIN, abi.encodeCall(IProxyAdmin.owner, ()), abi.encode(PROXY_ADMIN_OWNER)); + } + + function _initializeAsProxyAdmin(address payable[] memory addrs, uint256[] memory balances) internal { + _setProxyAdmin(); + vm.prank(PROXY_ADMIN); + feeDisburser.initialize(addrs, balances); + } + + function _makeSingleConfig( + address payable addr, + uint256 balance + ) + internal + pure + returns (address payable[] memory addrs, uint256[] memory balances) + { + addrs = new address payable[](1); + addrs[0] = addr; + balances = new uint256[](1); + balances[0] = balance; + } + // ============================================================ // Constructor Tests // ============================================================ @@ -768,4 +811,235 @@ contract FeeDisburserTest is Test { assertTrue(gasUsed < 200_000, "Gas usage too high for all-vault case"); } + + // ============================================================ + // initialize Tests + // ============================================================ + + function test_initialize_success_proxyAdmin() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, 1 ether); + + _initializeAsProxyAdmin(addrs, balances); + + assertEq(feeDisburser.systemAddresses(0), SYSTEM_ADDR); + assertEq(feeDisburser.targetBalances(0), 1 ether); + } + + function test_initialize_success_proxyAdminOwner() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, 1 ether); + + _setProxyAdmin(); + _mockProxyAdminOwner(); + vm.prank(PROXY_ADMIN_OWNER); + feeDisburser.initialize(addrs, balances); + + assertEq(feeDisburser.systemAddresses(0), SYSTEM_ADDR); + assertEq(feeDisburser.targetBalances(0), 1 ether); + } + + function test_initialize_revert_notProxyAdminOrOwner() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, 1 ether); + + _setProxyAdmin(); + _mockProxyAdminOwner(); + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(ALICE); + feeDisburser.initialize(addrs, balances); + } + + function test_initialize_revert_alreadyInitialized() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, 1 ether); + _initializeAsProxyAdmin(addrs, balances); + + vm.expectRevert(); + _initializeAsProxyAdmin(addrs, balances); + } + + function test_initialize_revert_tooManySystemAddresses() public { + uint256 n = feeDisburser.MAX_SYSTEM_ADDRESS_COUNT() + 1; + address payable[] memory addrs = new address payable[](n); + uint256[] memory balances = new uint256[](n); + + vm.expectRevert(FeeDisburser.TooManySystemAddresses.selector); + _initializeAsProxyAdmin(addrs, balances); + } + + function test_initialize_revert_arrayLengthMismatch() public { + address payable[] memory addrs = new address payable[](2); + uint256[] memory balances = new uint256[](1); + + vm.expectRevert(FeeDisburser.ArrayLengthMismatch.selector); + _initializeAsProxyAdmin(addrs, balances); + } + + function test_initialize_revert_zeroAddress() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(payable(address(0)), 1 ether); + + vm.expectRevert(FeeDisburser.ZeroAddress.selector); + _initializeAsProxyAdmin(addrs, balances); + } + + function test_initialize_revert_zeroTargetBalance() public { + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, 0); + + vm.expectRevert(FeeDisburser.ZeroTargetBalance.selector); + _initializeAsProxyAdmin(addrs, balances); + } + + // ============================================================ + // disburseFees with System Address Refunds + // ============================================================ + + function test_disburseFees_success_systemAddressRefunded() public { + uint256 feeAmount = 10 ether; + uint256 targetBal = 2 ether; + + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, targetBal); + _initializeAsProxyAdmin(addrs, balances); + + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + uint256 bridgeAmount = feeAmount - targetBal; + _expectBridgeETH(bridgeAmount); + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(SYSTEM_ADDR, true, targetBal, targetBal); + + _expectFeesDisbursed(bridgeAmount); + + feeDisburser.disburseFees(); + + assertEq(SYSTEM_ADDR.balance, targetBal); + } + + function test_disburseFees_success_systemAddressAlreadyFunded() public { + uint256 feeAmount = 10 ether; + uint256 targetBal = 2 ether; + + vm.deal(SYSTEM_ADDR, targetBal); + + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, targetBal); + _initializeAsProxyAdmin(addrs, balances); + + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + _expectBridgeETH(feeAmount); + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(SYSTEM_ADDR, false, 0, 0); + + _expectFeesDisbursed(feeAmount); + + feeDisburser.disburseFees(); + + assertEq(SYSTEM_ADDR.balance, targetBal); + } + + function test_disburseFees_success_partialRefundDueToLowBalance() public { + uint256 targetBal = 5 ether; + uint256 systemAddrStartBal = 1 ether; + uint256 feeAmount = 2 ether; // not enough to fully top up (needs 4 ether, has 2) + + vm.deal(SYSTEM_ADDR, systemAddrStartBal); + + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(SYSTEM_ADDR, targetBal); + _initializeAsProxyAdmin(addrs, balances); + + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + uint256 valueNeeded = targetBal - systemAddrStartBal; // 4 ether + uint256 valueSent = feeAmount; // only 2 ether available + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(SYSTEM_ADDR, true, valueNeeded, valueSent); + + _expectFeesDisbursed(0); + + feeDisburser.disburseFees(); + + assertEq(SYSTEM_ADDR.balance, systemAddrStartBal + valueSent); + assertEq(address(feeDisburser).balance, 0); + } + + function test_disburseFees_success_multipleSystemAddresses() public { + address payable addr1 = payable(address(0x6001)); + address payable addr2 = payable(address(0x6002)); + uint256 target1 = 3 ether; + uint256 target2 = 2 ether; + + address payable[] memory addrs = new address payable[](2); + addrs[0] = addr1; + addrs[1] = addr2; + uint256[] memory bals = new uint256[](2); + bals[0] = target1; + bals[1] = target2; + _initializeAsProxyAdmin(addrs, bals); + + uint256 feeAmount = 10 ether; + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + uint256 bridgeAmount = feeAmount - target1 - target2; + _expectBridgeETH(bridgeAmount); + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(addr1, true, target1, target1); + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(addr2, true, target2, target2); + + _expectFeesDisbursed(bridgeAmount); + + feeDisburser.disburseFees(); + + assertEq(addr1.balance, target1); + assertEq(addr2.balance, target2); + // expectCall verifies the bridged amount; mocked payable balance semantics vary by Foundry version. + } + + function test_disburseFees_success_revertingRecipient() public { + address payable bad = payable(address(0x9999)); + + (address payable[] memory addrs, uint256[] memory balances) = _makeSingleConfig(bad, 1 ether); + _initializeAsProxyAdmin(addrs, balances); + + uint256 feeAmount = 2 ether; + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + // SafeCall.send catches the revert; full fee amount still bridges + vm.mockCallRevert(bad, 1 ether, bytes(""), bytes("")); + _expectBridgeETH(feeAmount); + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(bad, false, 1 ether, 1 ether); + + _expectFeesDisbursed(feeAmount); + + feeDisburser.disburseFees(); + + assertEq(bad.balance, 0); + } + + // ============================================================ + // Reentrancy Test + // ============================================================ + + function test_disburseFees_reentrancy_innerCallBlocked() public { + ReentrantReceiver attacker = new ReentrantReceiver(); + + (address payable[] memory addrs, uint256[] memory balances) = + _makeSingleConfig(payable(address(attacker)), 1 ether); + _initializeAsProxyAdmin(addrs, balances); + + uint256 feeAmount = 2 ether; + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); + + // Inner disburseFees reverts (reentrancy), so SafeCall returns success=false and ETH is not sent + _expectBridgeETH(feeAmount); + + vm.expectEmit(address(feeDisburser)); + emit ProcessedFunds(address(attacker), false, 1 ether, 1 ether); + + feeDisburser.disburseFees(); + + assertEq(address(attacker).balance, 0); + } } diff --git a/test/L2/FeeVault.t.sol b/test/L2/FeeVault.t.sol index d424c4c2c..6fae688cd 100644 --- a/test/L2/FeeVault.t.sol +++ b/test/L2/FeeVault.t.sol @@ -4,10 +4,18 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { Reverter } from "test/mocks/Callers.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Contracts +import { Proxy } from "src/universal/Proxy.sol"; // Interfaces import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; // Libraries import { Hashing } from "src/libraries/Hashing.sol"; @@ -41,8 +49,19 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { emit Withdrawal(_amount, _recipient, address(this), _network); } + /// @notice Deploys an uninitialized fee vault proxy for initialize access-control tests. + function _deployFeeVaultProxy() internal returns (IFeeVault vault_) { + string memory cname = Predeploys.getName(address(feeVault)); + bytes memory constructorArgs = DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())); + address impl = DeployUtils.create1({ _name: cname, _args: constructorArgs }); + address vault = address(new Proxy(Predeploys.PROXY_ADMIN)); + EIP1967Helper.setImplementation(vault, impl); + vault_ = IFeeVault(payable(vault)); + } + /// @notice Tests that the initialize function succeeds. function test_initialize_succeeds() external view { + assertEq(IProxyAdminOwnedBase(address(feeVault)).proxyAdminOwner(), proxyAdminOwner); assertEq(feeVault.recipient(), recipient); assertEq(feeVault.minWithdrawalAmount(), minWithdrawalAmount); assertEq(uint8(feeVault.withdrawalNetwork()), uint8(withdrawalNetwork)); @@ -52,10 +71,36 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { function test_initialize_reinitialization_reverts() external { _setupL2Withdrawal(); + vm.prank(proxyAdminOwner); vm.expectRevert(IFeeVault.InvalidInitialization.selector); feeVault.initialize(recipient, minWithdrawalAmount, Types.WithdrawalNetwork.L1); } + /// @notice Tests that the ProxyAdmin can initialize an uninitialized fee vault proxy. + function test_initialize_proxyAdmin_succeeds() external { + IFeeVault vault = _deployFeeVaultProxy(); + + vm.prank(Predeploys.PROXY_ADMIN); + vault.initialize(recipient, minWithdrawalAmount, withdrawalNetwork); + + assertEq(IProxyAdminOwnedBase(address(vault)).proxyAdminOwner(), proxyAdminOwner); + assertEq(vault.recipient(), recipient); + assertEq(vault.minWithdrawalAmount(), minWithdrawalAmount); + assertEq(uint8(vault.withdrawalNetwork()), uint8(withdrawalNetwork)); + } + + /// @notice Tests that initialization reverts if called by a non-proxy admin or proxy admin owner. + /// @param _sender The address of the sender to test. + function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) external { + IFeeVault vault = _deployFeeVaultProxy(); + + vm.assume(_sender != proxyAdminOwner && _sender != Predeploys.PROXY_ADMIN); + + vm.prank(_sender); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vault.initialize(recipient, minWithdrawalAmount, withdrawalNetwork); + } + /// @notice Tests that the immutable values match the storage getters. function test_immutableMatchesStorageVariables_succeeds() external view { assertEq(feeVault.RECIPIENT(), feeVault.recipient()); @@ -182,7 +227,7 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { uint256 initialAmount = feeVault.minWithdrawalAmount(); vm.prank(_caller); - vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); feeVault.setMinWithdrawalAmount(_newAmount); assertEq(feeVault.minWithdrawalAmount(), initialAmount); @@ -203,7 +248,7 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { address initialRecipient = feeVault.recipient(); vm.prank(_caller); - vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); feeVault.setRecipient(_newRecipient); assertEq(feeVault.recipient(), initialRecipient); @@ -230,7 +275,7 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { Types.WithdrawalNetwork initialNetwork = feeVault.withdrawalNetwork(); vm.prank(_caller); - vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); feeVault.setWithdrawalNetwork(newNetwork); assertEq(uint8(feeVault.withdrawalNetwork()), uint8(initialNetwork)); diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 1d7582428..e5a0ebc31 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -14,6 +14,8 @@ import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; import { ISP1Verifier } from "interfaces/L1/proofs/zk/ISP1Verifier.sol"; import { DevTEEProverRegistry } from "test/mocks/MockDevTEEProverRegistry.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; import { ZKVerifier } from "src/L1/proofs/zk/ZKVerifier.sol"; @@ -138,8 +140,13 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertNotEq(address(output.opChain.opChainProxyAdmin), address(0), "proxy admin"); assertNotEq(address(output.opChain.systemConfigProxy), address(0), "system config"); assertNotEq(address(output.opChain.optimismPortalProxy), address(0), "portal"); - assertNotEq(address(output.opChain.ethLockboxProxy), address(0), "lockbox"); assertNotEq(address(output.opChain.delayedWETHProxy), address(0), "delayed weth"); + assertNotEq(address(output.opChain.protocolVersionsProxy), address(0), "protocol versions"); + assertEq( + output.opChain.protocolVersionsProxy.incidentResponder(), + incidentResponder, + "protocol versions incident responder" + ); assertEq(output.opChain.opChainProxyAdmin.owner(), owner, "op chain proxy admin owner"); assertEq(output.opChain.systemConfigProxy.batchInbox(), Types.chainIdToBatchInboxAddress(l2ChainId), "inbox"); @@ -152,16 +159,50 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertValidStandardSystem(_expected(output, input)); } + /// @notice Deployment seeds the same 13-slot unscheduled schedule hashed by the Base Rust prover. + function test_deploy_seedsRustProverSchedule_succeeds() public { + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + + assertEq(output.opChain.protocolVersionsProxy.getSchedule().length, 13, "registered upgrade slots"); + assertEq( + output.opChain.protocolVersionsProxy.scheduleId(input.implementationsInput.multiproofMaxUpgradeId), + 0xc61ddfdfe1ff9422919909549df660a43d53127a318e01274a0448443e54146d, + "Rust prover schedule golden" + ); + assertEq( + IAggregateVerifier(address(output.opChain.aggregateVerifier)).MAX_UPGRADE_ID(), + input.implementationsInput.multiproofMaxUpgradeId, + "aggregate verifier max upgrade id" + ); + } + + function test_deploy_multiproofDisabled_succeeds() public { + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.implementationsInput.multiproofConfigHash = bytes32(0); + + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + + assertNotEq(address(output.opChain.protocolVersionsProxy), address(0), "protocol versions"); + assertEq(address(output.opChain.aggregateVerifier), address(0), "aggregate verifier"); + assertEq(address(output.opChain.teeProverRegistryProxy), address(0), "tee registry"); + assertEq(output.impls.aggregateVerifierImpl, address(0), "aggregate verifier impl"); + } + function test_upgrade_withoutManagerDelegatecall_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + Types.Implementations memory implementations = output.impls; + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); + implementations.protocolVersionsImpl = address(protocolVersionsImpl); SystemDeploy.UpgradeOutput memory upgradeOutput = systemDeploy.upgrade( SystemDeploy.UpgradeInput({ saveArtifacts: false, superchainConfigProxy: output.superchain.superchainConfigProxy, - implementations: output.impls, - systemConfigProxy: output.opChain.systemConfigProxy + implementations: implementations, + systemConfigProxy: output.opChain.systemConfigProxy, + protocolVersionsProxy: output.opChain.protocolVersionsProxy }) ); @@ -173,9 +214,42 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { output.impls.superchainConfigImpl, "superchain config impl" ); + assertEq( + output.opChain.opChainProxyAdmin.getProxyImplementation(address(output.opChain.protocolVersionsProxy)), + address(protocolVersionsImpl), + "protocol versions impl" + ); assertValidStandardSystem(_expected(output, input)); } + function test_upgrade_discoversProtocolVersionsProxyFromArtifacts_succeeds() public { + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + Types.Implementations memory implementations = output.impls; + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); + implementations.protocolVersionsImpl = address(protocolVersionsImpl); + + _saveArtifact("ProtocolVersionsProxy", address(output.opChain.protocolVersionsProxy)); + + SystemDeploy.UpgradeOutput memory upgradeOutput = systemDeploy.upgrade( + SystemDeploy.UpgradeInput({ + saveArtifacts: false, + superchainConfigProxy: output.superchain.superchainConfigProxy, + implementations: implementations, + systemConfigProxy: output.opChain.systemConfigProxy, + protocolVersionsProxy: IProtocolVersions(address(0)) + }) + ); + + assertFalse(upgradeOutput.superchainConfigUpgraded, "superchain already current"); + assertTrue(upgradeOutput.chainUpgraded, "chain upgraded"); + assertEq( + output.opChain.opChainProxyAdmin.getProxyImplementation(address(output.opChain.protocolVersionsProxy)), + address(protocolVersionsImpl), + "protocol versions impl" + ); + } + function test_deploy_reusingImplementations_doesNotSaveZeroImplementationOnlyArtifacts() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); @@ -330,6 +404,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { nitroEnclaveVerifier: address(nitroEnclaveVerifier), multiproofBlockInterval: 100, multiproofIntermediateBlockInterval: 10, + multiproofMaxUpgradeId: 12, sp1Verifier: ISP1Verifier(address(sp1Verifier)), teeProposer: proposer, teeChallenger: challenger, @@ -344,7 +419,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { opChainProxyAdminOwner: owner, systemConfigOwner: owner, batcher: batcher, - unsafeBlockSigner: unsafeBlockSigner + unsafeBlockSigner: unsafeBlockSigner, + incidentResponder: incidentResponder }), basefeeScalar: 100, blobBasefeeScalar: 200, @@ -418,6 +494,13 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { } } + function _saveArtifact(string memory _name, address _addr) internal { + vm.etch(address(artifacts), vm.getDeployedCode("Artifacts.s.sol:Artifacts")); + bytes32 slot = keccak256(abi.encodePacked(_name, uint256(0))); + vm.store(address(artifacts), slot, bytes32(uint256(uint160(_addr)))); + assertEq(artifacts.getAddress(_name), _addr, "artifact saved"); + } + function _expected( SystemDeploy.DeployOutput memory _output, SystemDeploy.DeployInput memory _input @@ -432,7 +515,6 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { superchainConfig: _output.superchain.superchainConfigProxy, implementations: _output.impls, delayedWETH: _output.opChain.delayedWETHProxy, - ethLockbox: _output.opChain.ethLockboxProxy, proxyAdminOwner: _input.opChainInput.roles.opChainProxyAdminOwner, multiproofGameType: GameType.wrap(uint32(_input.implementationsInput.multiproofGameType)), teeImageHash: _input.implementationsInput.teeImageHash, @@ -442,6 +524,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { l2ChainId: _input.opChainInput.l2ChainId, multiproofBlockInterval: _input.implementationsInput.multiproofBlockInterval, multiproofIntermediateBlockInterval: _input.implementationsInput.multiproofIntermediateBlockInterval, + multiproofMaxUpgradeId: _input.implementationsInput.multiproofMaxUpgradeId, withdrawalDelaySeconds: _input.implementationsInput.withdrawalDelaySeconds }); } @@ -574,7 +657,8 @@ contract ZKBricking_Test is Test { endingL2SeqNum, intermediateRoots, input.implementationsInput.multiproofConfigHash, - input.implementationsInput.teeImageHash + input.implementationsInput.teeImageHash, + _scheduleId() ) ); @@ -584,6 +668,10 @@ contract ZKBricking_Test is Test { return abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, signature); } + function _scheduleId() internal view returns (bytes32) { + return output.opChain.protocolVersionsProxy.scheduleId(input.implementationsInput.multiproofMaxUpgradeId); + } + function _extractIntermediateRoots(bytes memory extraData) internal pure returns (bytes memory) { uint256 headerLen = 32 + 20; uint256 rootsLen = extraData.length - headerLen; @@ -654,6 +742,7 @@ contract ZKBricking_Test is Test { nitroEnclaveVerifier: address(0), multiproofBlockInterval: BLOCK_INTERVAL, multiproofIntermediateBlockInterval: INTERMEDIATE_BLOCK_INTERVAL, + multiproofMaxUpgradeId: 12, sp1Verifier: ISP1Verifier(address(0)), teeProposer: proposer, teeChallenger: challenger, @@ -668,7 +757,8 @@ contract ZKBricking_Test is Test { opChainProxyAdminOwner: owner, systemConfigOwner: owner, batcher: makeAddr("batcher"), - unsafeBlockSigner: makeAddr("unsafeBlockSigner") + unsafeBlockSigner: makeAddr("unsafeBlockSigner"), + incidentResponder: incidentResponder }), basefeeScalar: 100, blobBasefeeScalar: 200, diff --git a/test/deploy/SystemDeployAssertions.sol b/test/deploy/SystemDeployAssertions.sol index fb3aa1c1e..650f541bc 100644 --- a/test/deploy/SystemDeployAssertions.sol +++ b/test/deploy/SystemDeployAssertions.sol @@ -6,7 +6,6 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { Types } from "scripts/libraries/Types.sol"; import { Constants } from "src/libraries/Constants.sol"; -import { Features } from "src/libraries/Features.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { GameType, Hash } from "src/libraries/bridge/Types.sol"; import { Claim } from "src/libraries/bridge/LibUDT.sol"; @@ -24,7 +23,6 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -36,7 +34,6 @@ abstract contract SystemDeployAssertions is Test { ISuperchainConfig superchainConfig; Types.Implementations implementations; IDelayedWETH delayedWETH; - IETHLockbox ethLockbox; address proxyAdminOwner; GameType multiproofGameType; bytes32 teeImageHash; @@ -46,6 +43,7 @@ abstract contract SystemDeployAssertions is Test { uint256 l2ChainId; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + uint256 multiproofMaxUpgradeId; uint256 withdrawalDelaySeconds; } @@ -58,7 +56,6 @@ abstract contract SystemDeployAssertions is Test { _assertBridgeAndPortalWiring(_expected, proxyAdmin); _assertDisputeGameFactory(_expected, proxyAdmin); _assertGame(_expected, proxyAdmin, _expected.multiproofGameType); - _assertETHLockbox(_expected, proxyAdmin); } function _assertSuperchainConfig(ExpectedSystemDeployState memory _expected) private view { @@ -259,6 +256,7 @@ abstract contract SystemDeployAssertions is Test { assertEq( _aggregateVerifier.INTERMEDIATE_BLOCK_INTERVAL(), _expected.multiproofIntermediateBlockInterval, "AV-150" ); + assertEq(_aggregateVerifier.MAX_UPGRADE_ID(), _expected.multiproofMaxUpgradeId, "AV-160"); } function _assertDelayedWETH( @@ -300,24 +298,6 @@ abstract contract SystemDeployAssertions is Test { assertGt(_asr.retirementTimestamp(), 0, "AV-ANCHORP-60"); } - function _assertETHLockbox(ExpectedSystemDeployState memory _expected, IProxyAdmin _proxyAdmin) private view { - IOptimismPortal2 portal = IOptimismPortal2(payable(_expected.systemConfig.optimismPortal())); - IETHLockbox lockbox = _expected.ethLockbox; - - assertNotEq(address(lockbox), address(0), "LOCKBOX-05"); - assertEq(_version(address(lockbox)), _version(_expected.implementations.ethLockboxImpl), "LOCKBOX-10"); - assertEq( - _proxyAdmin.getProxyImplementation(address(lockbox)), _expected.implementations.ethLockboxImpl, "LOCKBOX-20" - ); - assertEq(address(_proxyAdminFor(address(lockbox))), address(_proxyAdmin), "LOCKBOX-30"); - assertEq(address(lockbox.systemConfig()), address(_expected.systemConfig), "LOCKBOX-40"); - assertTrue(lockbox.authorizedPortals(portal), "LOCKBOX-50"); - - if (_expected.systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(portal.ethLockbox()), address(lockbox), "LOCKBOX-60"); - } - } - function _proxyAdminFor(address _contract) private view returns (IProxyAdmin) { return IProxyAdminOwnedBase(_contract).proxyAdmin(); } diff --git a/test/scripts/L2Genesis.t.sol b/test/scripts/L2Genesis.t.sol index c0bd80933..13f232731 100644 --- a/test/scripts/L2Genesis.t.sol +++ b/test/scripts/L2Genesis.t.sol @@ -6,6 +6,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { L2Genesis } from "scripts/L2Genesis.s.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; +import { IBaseTime } from "interfaces/L2/IBaseTime.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; @@ -59,6 +60,12 @@ abstract contract L2Genesis_TestInit is Test { assertGt(Predeploys.WETH.code.length, 0); } + function _assertBaseTime() internal view { + assertEq(Predeploys.BASE_TIME, 0x4200000000000000000000000000000000000030); + assertTrue(Predeploys.isSupportedPredeploy(Predeploys.BASE_TIME)); + assertEq(IBaseTime(Predeploys.BASE_TIME).timestampMillisPart(), 0); + } + function _assertFeeVaultsWithoutRevenueShare() internal view { _assertFeeVault( Predeploys.BASE_FEE_VAULT, @@ -160,6 +167,7 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { _assertProxyAdmin(); _assertPredeploys(); + _assertBaseTime(); _assertFeeVaultsWithoutRevenueShare(); _assertFactories(); _assertForks(); diff --git a/test/setup/ForkLive.s.sol b/test/setup/ForkLive.s.sol index 2b1b0cea2..6b58d140f 100644 --- a/test/setup/ForkLive.s.sol +++ b/test/setup/ForkLive.s.sol @@ -21,9 +21,8 @@ import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.so import { IAggregateVerifier } from "interfaces/L1/proofs/IAggregateVerifier.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title ForkLive /// @notice This script is called by Setup.sol as a preparation step for the foundry test suite, and is run as an @@ -142,15 +141,6 @@ contract ForkLive is Script { artifacts.save("OptimismPortalProxy", optimismPortal); artifacts.save("OptimismPortal2Impl", EIP1967Helper.getImplementation(optimismPortal)); - // Get the lockbox address from the portal, and save it - /// NOTE: Using try catch because this function could be called before or after the upgrade. - try IOptimismPortal2(payable(optimismPortal)).ethLockbox() returns (IETHLockbox ethLockbox_) { - console.log("ForkLive: ETHLockboxProxy found: %s", address(ethLockbox_)); - artifacts.save("ETHLockboxProxy", address(ethLockbox_)); - } catch { - console.log("ForkLive: ETHLockboxProxy not found"); - } - address l1CrossDomainMessenger = systemConfigAddresses.l1CrossDomainMessenger; address addressManager = _legacyAddressManager(l1CrossDomainMessenger); artifacts.save("AddressManager", addressManager); @@ -190,6 +180,7 @@ contract ForkLive is Script { ISuperchainConfig superchainConfig = ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")); IProxyAdmin superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); address superchainPAO = superchainProxyAdmin.owner(); + IProtocolVersions protocolVersionsProxy = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); // Run the shared SuperchainConfig upgrade as the Superchain ProxyAdmin owner. The script // skips this step when the proxy is already at or above the target implementation version. @@ -199,7 +190,8 @@ contract ForkLive is Script { saveArtifacts: false, superchainConfigProxy: superchainConfig, implementations: implementations, - systemConfigProxy: ISystemConfig(address(0)) + systemConfigProxy: ISystemConfig(address(0)), + protocolVersionsProxy: IProtocolVersions(address(0)) }) ); @@ -210,7 +202,8 @@ contract ForkLive is Script { saveArtifacts: false, superchainConfigProxy: ISuperchainConfig(address(0)), implementations: implementations, - systemConfigProxy: _systemConfigProxy + systemConfigProxy: _systemConfigProxy, + protocolVersionsProxy: protocolVersionsProxy }) ); } @@ -238,10 +231,6 @@ contract ForkLive is Script { IAggregateVerifier(address(disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER))); artifacts.save("AggregateVerifier", address(aggregateVerifier)); - IOptimismPortal2 portal = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); - address lockboxAddress = address(portal.ethLockbox()); - artifacts.save("ETHLockboxProxy", lockboxAddress); - GameAddresses memory gameAddresses = _aggregateVerifierAddresses(aggregateVerifier); artifacts.save("AnchorStateRegistryProxy", gameAddresses.anchorStateRegistry); artifacts.save("DelayedWETHProxy", gameAddresses.weth); diff --git a/test/setup/Setup.sol b/test/setup/Setup.sol index a415ccfd1..7ee901cb4 100644 --- a/test/setup/Setup.sol +++ b/test/setup/Setup.sol @@ -25,10 +25,10 @@ import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; // Interfaces import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; @@ -97,7 +97,6 @@ abstract contract Setup is FeatureFlags { address superchainProxyAdminOwner; IProxyAdmin superchainProxyAdmin; IOptimismPortal optimismPortal2; - IETHLockbox ethLockbox; ISystemConfig systemConfig; IL1StandardBridge l1StandardBridge; IL1CrossDomainMessenger l1CrossDomainMessenger; @@ -105,6 +104,7 @@ abstract contract Setup is FeatureFlags { IL1ERC721Bridge l1ERC721Bridge; IOptimismMintableERC20Factory l1OptimismMintableERC20Factory; ISuperchainConfig superchainConfig; + IProtocolVersions protocolVersions; // L2 contracts IL2CrossDomainMessenger l2CrossDomainMessenger = @@ -218,15 +218,6 @@ abstract contract Setup is FeatureFlags { optimismPortal2 = IOptimismPortal(artifacts.mustGetAddress("OptimismPortalProxy")); - // Only skip ETHLockbox assignment if we're in a fork test with non-upgraded fork - // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. - if (!forkTest || deploy.cfg().useUpgradedFork()) { - // Here we use getAddress instead of mustGetAddress because some chains might not have - // the ETHLockbox proxy. Chains that don't have the ETHLockbox proxy will just return - // address(0) and cause a revert if we use mustGetAddress. - ethLockbox = IETHLockbox(artifacts.getAddress("ETHLockboxProxy")); - } - systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); l1CrossDomainMessenger = IL1CrossDomainMessenger(artifacts.mustGetAddress("L1CrossDomainMessengerProxy")); @@ -241,6 +232,9 @@ abstract contract Setup is FeatureFlags { anchorStateRegistry = IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")); disputeGameFactory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); delayedWeth = IDelayedWETH(artifacts.mustGetAddress("DelayedWETHProxy")); + // Use getAddress instead of mustGetAddress because forked production chains predating + // ProtocolVersions won't have the proxy; those return address(0) rather than reverting. + protocolVersions = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); proxyAdmin = IProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); proxyAdminOwner = proxyAdmin.owner(); superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index 9bdd9df94..9adb1e132 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -15,9 +15,9 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; /// @title Initializer_Test @@ -210,20 +210,20 @@ contract Initializer_Test is CommonTest { }) ); - // ETHLockbox is only deployed when interop is enabled - if (address(ethLockbox) != address(0)) { - initCalldata = abi.encodeCall(ethLockbox.initialize, (ISystemConfig(address(0)), new IOptimismPortal2[](0))); + // ProtocolVersions is deployed by the standard deployment script but is absent on older + // forked chains, so only track it when the proxy is present. + if (address(protocolVersions) != address(0)) { + initCalldata = abi.encodeCall(protocolVersions.initialize, (address(0))); contracts.push( InitializeableContract({ - name: "ETHLockboxImpl", - target: EIP1967Helper.getImplementation(address(ethLockbox)), + name: "ProtocolVersionsImpl", + target: EIP1967Helper.getImplementation(address(protocolVersions)), initCalldata: initCalldata }) ); - contracts.push( InitializeableContract({ - name: "ETHLockboxProxy", target: address(ethLockbox), initCalldata: initCalldata + name: "ProtocolVersionsProxy", target: address(protocolVersions), initCalldata: initCalldata }) ); } @@ -260,9 +260,9 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/L1/BalanceTracker.sol"; // AggregateVerifier uses a custom `bool initialized` instead of OpenZeppelin's `_initialized` uint8. excludes[j++] = "src/L1/proofs/AggregateVerifier.sol"; - // ETHLockbox is only deployed when interop is enabled. - if (address(ethLockbox) == address(0)) { - excludes[j++] = "src/L1/ETHLockbox.sol"; + // ProtocolVersions is not deployed on older forked chains. + if (address(protocolVersions) == address(0)) { + excludes[j++] = "src/L1/ProtocolVersions.sol"; } // TEEProverRegistry is only deployed when multiproof is enabled. if (address(teeProverRegistry) == address(0)) {