Skip to content

feat: Add L2CM implementation#19111

Merged
maurelian merged 16 commits intoethereum-optimism:developfrom
defi-wonderland:sc-feat/l2cm-impl-l2contractsmanager
Feb 25, 2026
Merged

feat: Add L2CM implementation#19111
maurelian merged 16 commits intoethereum-optimism:developfrom
defi-wonderland:sc-feat/l2cm-impl-l2contractsmanager

Conversation

@0xiamflux
Copy link
Contributor

@0xiamflux 0xiamflux commented Feb 6, 2026

Overview

Adds the L2ContractsManager contract that handles upgrades of L2 predeploys via DELEGATECALL from ProxyAdmin. It's based on the L2ContractsManager specs here.

  • Implements clear-and-reinitialize pattern using StorageSetter for initializable contracts
  • Preserves network-specific configuration by reading from existing predeploys before upgrade
  • Handles Custom Gas Token conditional upgrades (L1BlockCGT, L2ToL1MessagePasserCGT, LiquidityController, NativeAssetLiquidity).

Note: Feature flag handling beyond Custom Gas Token is deferred to future work.
Reference Issue: #18830

* feat: add initial iteration of L2ContractsManager

* feat: add network configuration structs

* feat: load full config for L2ContractsManager

* feat: implement L2CM::_apply

* feat: add gas price oracle

* refactor: move L2CM types to library

* fix: upgrade ProxyAdmin predeploy

* chore: enforce delegatecall for L2CM::upgrade

* feat: add conditional upgrade for CGT

* refactor: remove non-proxied predeploys

* chore: renamed l2cm
* refactor: rename _fullConfig to _loadFullConfig to match OPCM v2

* chore: remove non-proxied weth from implementations struct

* test: add config preservation test

* test: add CGT specific tests

* refactor: avoid casting network config values to address

* test: add test cases
* chore: remove unnecesary casting on L2CM

* feat: add interface for XForkL2ContractsManager

* chore: add natspec to XForkL2ContractsManager

* chore: pr ready
* chore: add comment clarifying use `useCustomGasToken`

* chore: upgrade both native native asset liquidity and liquidity controller predeploys together

* feat: prohibit downgrading predeploy implementations

* refactor: make isCustomGasToken part of the network full config

* fix: add missing import

* fix: use FeeVault legacy getters for backward compat

* chore: update name XForkL2ContractsManager to L2ContractsManager
@0xniha 0xniha mentioned this pull request Feb 11, 2026
* chore: add todo tracking removal of L2ProxyAdmin skips

* chore: add natspec comment for isPredeployNamespace

* chore: use vm.prank(address,bool) to prank a delegatecall

* chore: add todo for dev flags for CrossL2Inbox and L2ToL2CrossDomainMessenger

* feat: allow immutables for L2CM in semgrep rules

* chore: pr ready
* test: add coverage test for predeploy upgrades

* chore: update test natspec
@0xiamflux 0xiamflux marked this pull request as ready for review February 17, 2026 21:57
@0xiamflux 0xiamflux requested a review from a team as a code owner February 17, 2026 21:57
* refactor: move helper function into Predeploys.s.sol

* fix: add conditional deployer to L2CM

* chore: update to l1block and l1blockCGT

* test: fixes issue where OptimismSuperchainERC20 tests fail due to profile ambiguity

* chore: just pr ready
* fix: move code length check out of isUpgradeable

* chore: inline fullCofig_.isCustomGasToken initialization

* chore: add public getters for the implementations on the L2CM

* chore: remove XForkL2ContractsManager sol rule exclusion

* test: add downgrade prevention test suite

* chore: just pr ready

* refactor: check for address 0 instead code length

* Revert "refactor: check for address 0 instead code length"

This reverts commit 1fa8694.

* chore: remove non-needed check
@0xiamflux 0xiamflux requested a review from a team as a code owner February 20, 2026 17:55
* refactor: remove individual getters in favor of a unified one

* test: add test for getImplementations
@0xOneTony
Copy link
Contributor

/ci authorize ed617ef

@maurelian maurelian added this pull request to the merge queue Feb 25, 2026
Merged via the queue into ethereum-optimism:develop with commit a7369cf Feb 25, 2026
225 of 226 checks passed
Inphi added a commit that referenced this pull request Feb 25, 2026
The L2CM PR (#19111) added new functions to Predeploys.sol which changed
the init code hashes of all L2 contracts that import it, but only added
the new L2ContractsManager entry without regenerating the existing hashes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inphi added a commit that referenced this pull request Feb 25, 2026
The L2CM PR (#19111) added new functions to Predeploys.sol which changed
the init code hashes of all L2 contracts that import it, but only added
the new L2ContractsManager entry without regenerating the existing hashes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mds1 added a commit that referenced this pull request Feb 26, 2026
…piler profile ambiguity

When `additional_compiler_profiles` is configured in foundry.toml, contracts
pulled into the dispute profile's compilation graph get compiled with both
default (999999 optimizer runs) and dispute (5000 runs) profiles. PR #19111
added L2ProxyAdmin extending ProxyAdmin, which pulled ProxyAdmin (and
transitively OptimismMintableERC20Factory) into the dispute profile graph.

On CI (Linux), `vm.getCode("ProxyAdmin")` non-deterministically resolves to
the dispute profile artifact (6149 bytes creation code), while VerifyOPCM reads
the default profile artifact from disk (6751 bytes). This mismatch causes
VerifyOPCM_Failed() across all chains and feature flags on CI, while passing
locally on macOS where the resolution order differs.

The fix adds `DeployUtils.getCode()` which constructs explicit artifact file
paths (`forge-artifacts/<Name>.sol/<Name>.json`) to always resolve the default
profile. All `vm.getCode()` callsites in scripts and tests are migrated to use
this helper. A semgrep rule enforces this going forward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mds1 added a commit that referenced this pull request Feb 26, 2026
…piler profile ambiguity

When `additional_compiler_profiles` is configured in foundry.toml, contracts
pulled into the dispute profile's compilation graph get compiled with both
default (999999 optimizer runs) and dispute (5000 runs) profiles. PR #19111
added L2ProxyAdmin extending ProxyAdmin, which pulled ProxyAdmin (and
transitively OptimismMintableERC20Factory) into the dispute profile graph.

On CI (Linux), `vm.getCode("ProxyAdmin")` non-deterministically resolves to
the dispute profile artifact (6149 bytes creation code), while VerifyOPCM reads
the default profile artifact from disk (6751 bytes). This mismatch causes
VerifyOPCM_Failed() across all chains and feature flags on CI, while passing
locally on macOS where the resolution order differs.

The fix adds `DeployUtils.getCode()` which constructs explicit artifact file
paths (`forge-artifacts/<Name>.sol/<Name>.json`) to always resolve the default
profile. All `vm.getCode()` callsites in scripts and tests are migrated to use
this helper. A semgrep rule enforces this going forward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
github-merge-queue bot pushed a commit that referenced this pull request Feb 27, 2026
* fix(contracts-bedrock): resolve VerifyOPCM bytecode mismatch from compiler profile ambiguity

When `additional_compiler_profiles` is configured in foundry.toml, contracts
pulled into the dispute profile's compilation graph get compiled with both
default (999999 optimizer runs) and dispute (5000 runs) profiles. PR #19111
added L2ProxyAdmin extending ProxyAdmin, which pulled ProxyAdmin (and
transitively OptimismMintableERC20Factory) into the dispute profile graph.

On CI (Linux), `vm.getCode("ProxyAdmin")` non-deterministically resolves to
the dispute profile artifact (6149 bytes creation code), while VerifyOPCM reads
the default profile artifact from disk (6751 bytes). This mismatch causes
VerifyOPCM_Failed() across all chains and feature flags on CI, while passing
locally on macOS where the resolution order differs.

The fix adds `DeployUtils.getCode()` which constructs explicit artifact file
paths (`forge-artifacts/<Name>.sol/<Name>.json`) to always resolve the default
profile. All `vm.getCode()` callsites in scripts and tests are migrated to use
this helper. A semgrep rule enforces this going forward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(contracts-bedrock): add try/catch fallback and cicoverage gas test fix

Add try/catch fallback to DeployUtils.getCode() so the Go script host
(which doesn't support explicit artifact paths) gracefully falls back
to vm.getCode(_name). Also add "/" passthrough for callers passing
explicit paths.

Fix L1ChugSplashProxy OOG gas test: under cicoverage, the now-correct
default-profile proxy bytecode is larger, leaving insufficient retained
gas (1/64 rule) for the require message. Use generic vm.expectRevert()
for unoptimized profiles — the test still verifies the revert occurs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(contracts-bedrock): fix semgrep findings in DeployUtils and L1ChugSplashProxy

Rename try/catch return variable to `code_` (trailing underscore convention)
and add L1ChugSplashProxy.t.sol to expectrevert-no-args exclusion list since
the bare vm.expectRevert() is intentional (OOG produces no revert data).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(contracts-bedrock): skip explicit artifact path under coverage

Under coverage profiles, forge-artifacts/ contains the default profile's
(optimized) artifacts, not the coverage profile's. Since coverage profiles
have no additional_compiler_profiles, there is no profile ambiguity, so
plain vm.getCode() resolves correctly. Skip the explicit artifact path
under vm.isContext(Coverage) to avoid bytecode mismatches between artifact-
loaded code and fresh compilation in tests (DeployFeesDepositor, DeployMIPS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(contracts-bedrock): wrap isContext in try/catch for Go host compat

The Go script host doesn't implement vm.isContext(), causing a revert
that propagates up as an unrecognized selector error. Wrap the coverage
detection in try/catch so the Go host silently falls through to the
artifact-path resolution (which itself falls back to vm.getCode).

Also adds a comment explaining why the catch block is intentionally empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants