Sitelet https://github.com/ethereum-optimism/optimism/pull/22366
Skip to content

contracts-bedrock: Bind the SystemConfig chain ID in the standard validator - #22366

Merged
ajsutton merged 4 commits into
developfrom
aj/fix/u20-validator-syscfg-chainid
Aug 12, 2026
Merged

ajsutton merged 4 commits into
developfrom
aj/fix/u20-validator-syscfg-chainid

Conversation

@ajsutton

@ajsutton ajsutton commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

The standard validator took the expected L2 chain ID as caller-supplied input but never compared it with SystemConfig.l2ChainId(). Check it as SYSCON-140.

Without it, validation of chain X returns success for a SystemConfig configured for chain Y. The Portal then settles X's withdrawals against Y.

The second commit fixes the fork test fixtures, which took the expected chain ID from the local deploy config rather than the forked chain. They now read the L2ChainId artifact from the superchain registry, as the non-super fixture already does.

Test plan

  • New test mocks SystemConfig.l2ChainId() to a different value. It fails on the unmodified validator with an empty success string.
  • just test-dev and just pr pass.
  • Upgrade fork tests over test/{L1,dispute,cannon}/** pass for op-mainnet, ink-mainnet, unichain-mainnet, and the ZK_DISPUTE_GAME and OPTIMISM_PORTAL_INTEROP variants.

…idator

The standard validator accepted the expected L2 chain ID as caller-supplied
input but never compared it with SystemConfig.l2ChainId(). The only predicate
that used the input compared it with the dispute game args, and super game
implementations deliberately store zero there, so the super-permissioned branch
returned an empty success string for a SystemConfig pointing at another chain.

OptimismPortal2.proveWithdrawalTransaction selects the member output root with
rootClaimByChainId(systemConfig.l2ChainId()). Without the comparison, validating
chain X can succeed while the Portal settles withdrawals against chain Y.

Check the chain ID as SYSCON-140 in the SystemConfig assertions, where it binds
both game modes, and keep the game args predicate as an additional check.
…tor tests

The super mode and ZK mode fixtures took the expected L2 chain ID from the
local deploy config and from the super game args. On fork tests neither matches
the forked chain, so SYSCON-140 fired on every super mode fork test.

Use the L2ChainId artifact that ForkL1Live saves from the superchain registry,
matching the convention the non-super fixture already follows.
@ajsutton
ajsutton marked this pull request as ready for review August 11, 2026 00:18
@ajsutton
ajsutton requested review from a team and maurelian August 11, 2026 00:18
@ajsutton

Copy link
Copy Markdown
Contributor Author

@OptimismBot Please review this PR

@OptimismBot OptimismBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. The fix is correct, minimal, and closes a real gap. I verified the premise rather than taking it from the description:

  • StandardValidatorUtils.sol:436 sets expectedL2ChainId = isSuperGame(gameType) ? 0 : _args.l2ChainID, so in super mode the -60 game-args predicate was the only consumer of _input.l2ChainID and it deliberately ignores it. Nothing else bound the caller's expected chain ID to SystemConfig.
  • OptimismPortal2.sol:427 resolves the member output root with rootClaimByChainId(systemConfig.l2ChainId()), so the unbound input is exactly the value that decides which chain's withdrawals settle. Adding SYSCON-140 in assertValidSystemConfig is the right placement — it's on the unconditional path in validateWithOverrides, so it binds both super and non-super modes.
  • In non-super mode the PDDG-60/CKDG-60 game-args check already caught a mismatched SystemConfig, so this is a strict tightening, not a behavior change for standard chains.

On the two things that looked suspicious and turned out fine:

  • Dropping the SYSCON-140 test from OPContractsManagerStandardValidator_SystemConfig_Test is legitimate. Config.devFeatureSuperRootGamesMigration() is hardcoded return true (scripts/libraries/Config.sol:341), so OPContractsManagerStandardValidator_TestInit.setUp() always hits vm.skip(true, ...) and that whole suite is dead. The surviving test in OPContractsManagerStandardValidator_SuperModeCoreValidation_Test is the one that actually runs, and its suite never skips.
  • The fixture change to uint256(uint160(address(artifacts.mustGetAddress("L2ChainId")))) is safe: that artifact is only saved by test/setup/ForkL1Live.s.sol:133 from the registry's .chain_id, and both new reads sit inside isL1ForkTest() branches. The ZK fixture previously took the chain ID from super game args, which are pinned to 0, so it was guaranteed wrong once SYSCON-140 exists.

Non-blocking observation, no action needed here: validateMigratedChain gets no equivalent binding — MigrationValidationInput carries no expected chain IDs, so a caller checking a shared super-root DGF still can't assert "this set includes chain X". OPContractsManagerMigrator already rejects zero and duplicate l2ChainIds at migration time, so the loss-of-funds shape is covered; it's just a weaker guarantee for that entrypoint.

OPContractsManagerV2.sol is untouched, no storage layout snapshots change, ABI snapshot correctly unchanged (the modified function is internal), and all 77 checks are green including the op-mainnet / ink-mainnet / unichain-mainnet upgrade fork jobs and the ZK and interop variants.

Requested by @ajsutton in GitHub

@ajsutton
ajsutton added this pull request to the merge queue Aug 12, 2026
Merged via the queue into develop with commit cba37e8 Aug 12, 2026
81 of 83 checks passed
@ajsutton
ajsutton deleted the aj/fix/u20-validator-syscfg-chainid branch August 12, 2026 23:36
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.

3 participants