Skip to content

Conversation

@smartcontracts
Copy link
Contributor

@smartcontracts smartcontracts commented Oct 29, 2025

Description
This PR introduces OPCMv2. Refer to the design document and the product requirements document for more information.

@codecov
Copy link

codecov bot commented Oct 30, 2025

Codecov Report

❌ Patch coverage is 90.14778% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.47%. Comparing base (5b96584) to head (a8fe8d1).
⚠️ Report is 13 commits behind head on develop.

Files with missing lines Patch % Lines
...racts-bedrock/src/L1/opcm/OPContractsManagerV2.sol 89.94% 19 Missing ⚠️
...edrock/src/L1/opcm/OPContractsManagerContainer.sol 92.85% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #18079      +/-   ##
===========================================
- Coverage    76.58%   76.47%   -0.12%     
===========================================
  Files          179      181       +2     
  Lines        10732    10935     +203     
===========================================
+ Hits          8219     8362     +143     
- Misses        2367     2429      +62     
+ Partials       146      144       -2     
Flag Coverage Δ
cannon-go-tests-64 66.58% <ø> (-0.82%) ⬇️
contracts-bedrock-tests 82.24% <90.14%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ages/contracts-bedrock/src/libraries/Constants.sol 100.00% <ø> (ø)
...es/contracts-bedrock/src/libraries/DevFeatures.sol 100.00% <ø> (ø)
...edrock/src/L1/opcm/OPContractsManagerContainer.sol 92.85% <92.85%> (ø)
...racts-bedrock/src/L1/opcm/OPContractsManagerV2.sol 89.94% <89.94%> (ø)

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@smartcontracts smartcontracts force-pushed the sc/opcm-v2-v2 branch 4 times, most recently from 6fb6a47 to 73eb1b6 Compare November 3, 2025 16:54
Comment on lines 2818 to 3100
ISystemConfig.initialize,
(
_cfg.systemConfigOwner,
_cfg.basefeeScalar,
_cfg.blobBasefeeScalar,
bytes32(uint256(uint160(_cfg.batcher))),
_cfg.gasLimit,
_cfg.unsafeBlockSigner,
_cfg.resourceConfig,
chainIdToBatchInboxAddress(_cfg.l2ChainId),
addrs,
_cfg.l2ChainId,
_cfg.superchainConfig
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, let's refactor this initializer to use some structs, then we won't get the stack too deep problem here and it will be easier to extend this in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might be a good future improvement, I didn't want to modify too much in the contracts for now

Comment on lines 2781 to 3059
// Upgrade to StorageSetter.
_proxyAdmin.upgrade(payable(_target), address(implementations().storageSetterImpl));

// Reset the initialized slot by zeroing the single byte at `_offset` (from the right).
bytes32 current = StorageSetter(_target).getBytes32(_slot);
uint256 mask = ~(uint256(0xff) << (uint256(_offset) * 8));
StorageSetter(_target).setBytes32(_slot, bytes32(uint256(current) & mask));

// Upgrade to the implementation and call the initializer.
_proxyAdmin.upgradeAndCall(payable(address(_target)), _implementation, _data);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wish there were initializer getter methods that Initializable has so we can assert onchain that we properly reset the initializer and did not change anything else, but seems those don't exist

Copy link
Contributor

Choose a reason for hiding this comment

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

Wonder if it's overkill to fork initializable and add these getters? Would be a consistent philosophy to #18079 (comment) around doing things out of an abundance of caution

@smartcontracts smartcontracts force-pushed the sc/opcm-v2-v2 branch 7 times, most recently from 5c89c68 to 049f5bb Compare November 18, 2025 17:30
@smartcontracts smartcontracts marked this pull request as ready for review November 18, 2025 19:14
@smartcontracts smartcontracts requested review from a team as code owners November 18, 2025 19:14
Copy link
Contributor

@maurelian maurelian left a comment

Choose a reason for hiding this comment

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

first round of comments and questions

cfg.blobBasefeeScalar = _input.blobBasefeeScalar;
cfg.gasLimit = _input.gasLimit;
cfg.l2ChainId = _input.l2ChainId;
cfg.resourceConfig = Constants.DEFAULT_RESOURCE_CONFIG();
Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm, why is resourceConfig becoming a configurable input? Is that wanted or needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one is mostly just because it's hard to figure out how to pass this around if you don't have it as a field on FullConfig. It needs to make its way to the _execute function somehow. resourceConfig is already configurable by chain operators if they want it to be, easy enough to just set it to the recommended default value manually.

)
});
cfg.disputeGameConfigs[1] = IOPContractsManagerV2.DisputeGameConfig({
enabled: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the only one enabled. Is there any benefit to including disable game types in the array or could we cut it down to a single game config?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes - including disabled game types in the array forces users to explicitly consider each game type. If you don't force the user to include an input for each game type then they can easily fail to provide an input and potentially misconfigure a game. Better to be very explicit.

/// have any code in production environments but can be made to have code in tests.
/// @return True if the contract is running in a testing environment, false otherwise.
function _isTestingEnvironment() internal view returns (bool) {
return address(0xbeefcafe).code.length > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this should be in Constants.sol

Comment on lines +1072 to +1103
address ret;
if (LibString.eq(_contractName, "L1StandardBridge")) {
// L1StandardBridge is a special case ChugSplashProxy (legacy).
ret = Blueprint.deployFrom(
blueprints().l1ChugSplashProxy,
_computeSalt(_args.l2ChainId, _args.saltMixer, "L1StandardBridge"),
abi.encode(_args.proxyAdmin)
);

// ChugSplashProxy requires setting the proxy type on the ProxyAdmin.
_args.proxyAdmin.setProxyType(ret, IProxyAdmin.ProxyType.CHUGSPLASH);
} else if (LibString.eq(_contractName, "L1CrossDomainMessenger")) {
// L1CrossDomainMessenger is a special case ResolvedDelegateProxy (legacy).
string memory l1XdmName = "OVM_L1CrossDomainMessenger";
ret = Blueprint.deployFrom(
blueprints().resolvedDelegateProxy,
_computeSalt(_args.l2ChainId, _args.saltMixer, "L1CrossDomainMessenger"),
abi.encode(_args.addressManager, l1XdmName)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it better to have this function be really big and detect which contracts get the nonstandard proxies, or should we break them out?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This felt marginally cleaner to me than having three different functions. Do you have a specific alternative in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

What about just using an early return pattern instead of if/else? I think that would improve readability somewhat.

I acknowledge this is a bikeshed, if you feel strongly I'm OK with it.

}

// All V1 upgrade tests can safely be skipped for V2.
skipIfDevFeatureEnabled(DevFeatures.OPCM_V2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this still make sense to use? Maybe it saves some test running time, but the OPCMv1 works the same either way now right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm purposefully making it so the OPCMv1 doesn't work either way by not labeling the OPCMv1 address when OPCMv2 is enabled. This breaks all the tests that rely on OPCMv1 which makes it a lot easier to figure out what coverage we need iteratively (can delete these skips, see what fails, those are the tests we need to copy over to V2). I'll remove all these skips once we're completely happy with the V2 coverage.

Copy link
Contributor

Choose a reason for hiding this comment

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

comment only: fork tests and feature flags look to be quite a hassle in this file.

if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) {
addGameType(GameTypes.CANNON_KONA, cannonKonaPrestate);
}
} else {
Copy link
Contributor

@maurelian maurelian Nov 20, 2025

Choose a reason for hiding this comment

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

This block would have made more sense to me on first reading if the comment in FeatureFlags.sol was reflected here too:

OPCMv2 also automatically implies DEPLOY_V2_DISPUTE_GAMES and CANNON_KONA.

_assertValidUpgradeInstructions(_inp.extraInstructions);

// Load the chain contracts.
ChainContracts memory cts =
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: revert with error in case _inp.systemConfig is address(0).

assertEq("PDDG-DWETH-20,PLDG-DWETH-20,CKDG-DWETH-20", _validate(true));
} else {
assertEq("PLDG-DWETH-20", _validate(true));
assertEq("PDDG-DWETH-20,PLDG-DWETH-20", _validate(true));
Copy link
Contributor

Choose a reason for hiding this comment

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

OPCMv2 also automatically implies DEPLOY_V2_DISPUTE_GAMES and CANNON_KONA.

I think this block is unreachable?

Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise for a bunch of other occurences in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point

if (isForkTest()) {
assertEq("PDDG-DWETH-30", _validate(true));
} else {
if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

For most occurences in this file I don't see why we need to branch on this flag, as there are no changes to the StandardValidator. Both OPCMs should be able to get the system to the same valid state.

The only place that I think we really need isDevFeatureEnabled(DevFeatures.OPCM_V2) is in setUp() where you decide whether to add the enabled games using either opcm.addGameType() or opcmv2.upgrade().

--

OK, having written this, I realize that the difference is that V2 sets the same WETH for all games, whereas V1 keeps them separate. Would it be easier to just update V1 to act like V2 to save us from all this branching? I suppose that since it's already written we could maybe just live with it for now and look forward to deleting the flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd rather not update V1 to act like V2. Updating V1 means either committing to immediately shipping this change in V1, or it means adding yet another feature flag in the V1 which we'd probably not actually use because the V1 would be replaced by the V2 by the time it's used.

)
);
} else {
// Special case handling, don't bother with the standard flow.
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this a "special case" isn't it just what happens whenever this is called by upgrade()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Special case" here means that we're not using the _loadOrDeployProxy pattern. I should clarify the comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. The placement of the comment seems to suggest that it applies to the full block. I think it can be handled just by expanding the text a bit more.

Comment on lines +228 to +229
ExtraInstruction[] memory instructions = new ExtraInstruction[](1);
instructions[0] = ExtraInstruction({ key: "PermittedProxyDeployment", data: bytes("ALL") });
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this has become slightly confusing now that fd308b4 has added an ExtraInstruction field to FullConfig, ie. there is :

  1. _cfg.extraInstructions provided as an arg to deploy() which is passed into _execute(), and
  2. instructions created here and passed into _loadChainContracts()

WDYT if _loadChainContracts() accepted a boolean instead, and then use that boolean to create the PermittedProxyDeployment: ALL instruction inline?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok will handle it this way. Separately, I think we probably need to add an _assertValidDeployInstructions function.

return payable(abi.decode(result, (address)));
} else if (!loadCanFail) {
// Load not permitted to fail but did, revert.
revert OPContractsManagerV2_ProxyMustLoad();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: might be nicer for debugging:

Suggested change
revert OPContractsManagerV2_ProxyMustLoad();
revert OPContractsManagerV2_ProxyMustLoad(_contractName);

_loadChainContracts(ISystemConfig(address(0)), _cfg.l2ChainId, _cfg.saltMixer, instructions);

// Execute the deployment.
return _execute(_cfg, cts, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe rename to apply

// Dispute game configuration.
DisputeGameConfig[] disputeGameConfigs;
// Extra deployment instructions.
ExtraInstruction[] extraInstructions;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's get rid of this

IProxyAdmin proxyAdmin;
IAddressManager addressManager;
ISystemConfig systemConfig;
if (isInitialDeployment) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This config could have it's own separate function:

if(isInitialDeployment){
  (proxyAdmin, addressManager, systemConfig, ethLockbox) = _setUpInitialDeployment(...);
} else {
  (proxyAdmin, addressManager, systemConfig, ethLockbox) = _setUpUpgrade(...);
}


/// @notice Validates the deployment/upgrade config.
/// @param _cfg The full config.
function _assertValidFullConfig(FullConfig memory _cfg) internal pure {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of validating the input, we should consider updating _execute() to call standardValidator.validateWithOverrides() at the end. The overrides should be taken from the FullConfig.

We would need to add a new string expectedErrors arg, which will allow us to define which errors are allowed. We had discussed including a boolean like useStandardValidator, but that is not necessary, we should always call it and ensure that its response matches expectedErrors.

{
bool loadCanFail = _hasInstruction(
_instructions, ExtraInstruction({ key: "PermittedProxyDeployment", data: bytes(_contractName) })
) || _hasInstruction(_instructions, ExtraInstruction({ key: "PermittedProxyDeployment", data: bytes("ALL") }));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be a good idea to remove this magic value ALL and use a constant instead, wdyt?

// We've failed to load, but we allowed that failure.
// Deploy the right proxy depending on the contract name.
address ret;
if (LibString.eq(_contractName, "L1StandardBridge")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We might prefer to use a constant instead of hardcoded contract names.


// ChugSplashProxy requires setting the proxy type on the ProxyAdmin.
_args.proxyAdmin.setProxyType(ret, IProxyAdmin.ProxyType.CHUGSPLASH);
} else if (LibString.eq(_contractName, "L1CrossDomainMessenger")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

_proxyAdmin.getProxyImplementation(payable(_target)) != address(0)
&& SemverComp.gt(ISemver(_target).version(), ISemver(_implementation).version())
) {
revert OPContractsManagerV2_DowngradeNotAllowed();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be useful to add the contract address that is violating the downgrade check to the error.

revert OPContractsManagerV2_DowngradeNotAllowed(_target);

/// have any code in production environments but can be made to have code in tests.
/// @return True if the contract is running in a testing environment, false otherwise.
function _isTestingEnvironment() internal view returns (bool) {
return address(0xbeefcafe).code.length > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

We could use Constants.TESTING_ENVIRONMENT_ADDRESS here.

/// @notice Helper function to load data from a source contract as bytes.
/// @param _source The source contract to load the data from.
/// @param _selector The selector of the function to call on the source contract.
/// @param _instructions The extra upgrade instructions for the data load.
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing _name param in natspec.

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.

6 participants