Skip to content

contracts-bedrock: Resolve shared contract pause state via ETHLockbox - #22095

Draft
ajsutton wants to merge 1 commit into
developfrom
aj/shared-contract-pause-source
Draft

contracts-bedrock: Resolve shared contract pause state via ETHLockbox#22095
ajsutton wants to merge 1 commit into
developfrom
aj/shared-contract-pause-source

Conversation

@ajsutton

@ajsutton ajsutton commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #22093.

The shared ETHLockbox, AnchorStateRegistry and DelayedWETH of an interop set each stored a pointer to chain 0's SystemConfig — a per-chain contract — and used it for paused(), guardian() and superchainConfig(). That makes one member the governance root of the whole set, and it makes per-chain upgrade() non-idempotent: _upgradeChain re-initialises all three with _cts.systemConfig, so upgrading chain N silently repoints the set at chain N, and the validator's _asr.systemConfig() == _sysCfg predicate can only hold for one member at a time. That is a direct obstacle to the "all chains always upgrade the shared components" resolution in #21731.

Each contract now points at an IPauseSourcepaused() / guardian() / superchainConfig() — whose scope matches its own:

  • ETHLockbox holds the SuperchainConfig directly, paused() keys on address(this), and it gains a guardian() passthrough. This also collapses the old 4-hop pause lookup (lockbox → systemConfig → portal → its own address → superchainConfig) to one call on the unlockETH path.
  • AnchorStateRegistry and DelayedWETH hold an IPauseSource: the shared ETHLockbox when the chain uses one, otherwise the chain's own SystemConfig. Both satisfy the interface, so chains without a lockbox keep exactly today's behaviour.
  • migrate() now also repoints the reused chain-0 DelayedWETH at the shared lockbox, and setInteropDisputeGames() re-derives the pause source from the portal's lockbox, so a set migrated by an older release is fixed up there.
  • The validator checks become _asr.pauseSource() == expectedPauseSource(sysCfg), which is true for every member of a migrated set simultaneously.

ETHLockbox 1.3.1 → 2.0.0, AnchorStateRegistry 3.9.0 → 4.0.0 and DelayedWETH 1.5.1 → 2.0.0 are major bumps: the systemConfig() getter is gone, and initialize keeps its selector (ISystemConfig, ISuperchainConfig and IPauseSource all encode as address) while the meaning of its first argument changes. Anything calling these initializers must be updated deliberately.

Storage-layout neutral: the field is a plain 20-byte address slot in all three contracts, so only the label and type change in snapshots/storageLayout.

Two migration-validator tests gained extra expected error codes: with the lockbox missing or ETH_LOCKBOX disabled, the pause-source checks now also fire. That is intended — a shared registry pointing at a lockbox the chain does not acknowledge is a real misconfiguration.

Not addressed here: the shared proxies are still administered by chain 0's ProxyAdmin, tracked in #21731.

Test plan

  • just test-dev green in three configurations: default, DEV_FEATURE__OPTIMISM_PORTAL_INTEROP + DEV_FEATURE__ZK_DISPUTE_GAME, and SYS_FEATURE__CUSTOM_GAS_TOKEN.
  • New test_migrate_sharedContractsUseLockboxAsPauseSource_succeeds asserts that after migrate() the shared ASR and DelayedWETH point at the shared lockbox, that both chains resolve that same lockbox, and that pausing it pauses both chains at once.

@ajsutton
ajsutton force-pushed the aj/shared-contract-pause-source branch from 64ebb50 to 98a5a02 Compare July 29, 2026 01:48
/// shared by every chain authorized on that lockbox.
interface IPauseSource {
function paused() external view returns (bool);
function guardian() external view returns (address);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AFAICT the ETHLockbox doesn't have guardian(), and I don't think it's needed on this interface.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, I see that is being added in this PR.

Comment on lines +877 to +878
IPauseSource pauseSource =
isEthLockboxEnabled ? IPauseSource(address(_cts.ethLockbox)) : IPauseSource(address(_cts.systemConfig));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is set on the AnchorStateRegistry and DelayedWETH below, but not other pausable contracts like OptimismPortal2, L1CrossDomainMessenger, and L1StandardBridge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, non-shared contracts continue to use the SystemConfig because they're always chain specific. I think the ideal case for simplifying things would be to enable the lockbox on all chains, then all chains would use the lockbox as their pause source. I'm already worried this is more work than I can actually commit to and should be planned - that expanded scope definitely should be planned.

Comment on lines -67 to +68
ISystemConfig public systemConfig;
/// @notice The address of the SuperchainConfig contract. The lockbox is its own pause
/// identifier, so every contract that resolves its pause state through this lockbox
/// shares one pause state, regardless of how many chains are authorized on it.
ISuperchainConfig public superchainConfig;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I can't think of what could go wrong, but I think we should follow our convention of having a bytes spacer_slot_offset_length here and adding the new variable to the end of the layout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like the implementation of SystemConfig.paused() has a lot of complexity in it that could also be removed here.

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.

Shared interop contracts depend on a single chain's SystemConfig

2 participants