diff --git a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go index add97bfe4adbe..e5f60ab0eb3f7 100644 --- a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go +++ b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go @@ -6,21 +6,23 @@ import ( ) type ReadSuperchainDeploymentInput struct { - OPCMAddress common.Address `abi:"opcmAddress"` + OPCMAddress common.Address `abi:"opcmAddress"` // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + SuperchainConfigProxy common.Address `abi:"superchainConfigProxy"` } type ReadSuperchainDeploymentOutput struct { - ProtocolVersionsImpl common.Address - ProtocolVersionsProxy common.Address - SuperchainConfigImpl common.Address - SuperchainConfigProxy common.Address - SuperchainProxyAdmin common.Address - - Guardian common.Address + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsImpl common.Address + ProtocolVersionsProxy common.Address ProtocolVersionsOwner common.Address - SuperchainProxyAdminOwner common.Address RecommendedProtocolVersion [32]byte RequiredProtocolVersion [32]byte + + SuperchainConfigImpl common.Address + SuperchainConfigProxy common.Address + SuperchainProxyAdmin common.Address + Guardian common.Address + SuperchainProxyAdminOwner common.Address } type ReadSuperchainDeploymentScript script.DeployScriptWithOutput[ReadSuperchainDeploymentInput, ReadSuperchainDeploymentOutput] diff --git a/op-deployer/pkg/deployer/pipeline/init.go b/op-deployer/pkg/deployer/pipeline/init.go index f17fe61c00c58..00e26709df3c2 100644 --- a/op-deployer/pkg/deployer/pipeline/init.go +++ b/op-deployer/pkg/deployer/pipeline/init.go @@ -27,25 +27,37 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s } hasPredeployedOPCM := intent.OPCMAddress != nil + hasSuperchainConfigProxy := intent.SuperchainConfigProxy != nil - if hasPredeployedOPCM { - if intent.SuperchainConfigProxy != nil { - return fmt.Errorf("cannot set superchain config proxy for predeployed OPCM") + if hasPredeployedOPCM || hasSuperchainConfigProxy { + if intent.SuperchainRoles != nil { + return fmt.Errorf("cannot set superchain roles when using predeployed OPCM or SuperchainConfig") } - if intent.SuperchainRoles != nil { - return fmt.Errorf("cannot set superchain roles for predeployed OPCM") + opcmAddr := common.Address{} + if hasPredeployedOPCM { + opcmAddr = *intent.OPCMAddress } - superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, *intent.OPCMAddress) + superchainConfigAddr := common.Address{} + if hasSuperchainConfigProxy { + superchainConfigAddr = *intent.SuperchainConfigProxy + } + + // The ReadSuperchainDeployment script (packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol) + // uses the OPCM's semver version (>= 7.0.0 indicates v2) to determine how to populate the superchain state: + // - OPCMv1 (< 7.0.0): Queries the OPCM contract to get SuperchainConfig and ProtocolVersions + // - OPCMv2 (>= 7.0.0): Uses the provided SuperchainConfigProxy address; ProtocolVersions is deprecated + superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, opcmAddr, superchainConfigAddr) if err != nil { return fmt.Errorf("error populating superchain state: %w", err) } st.SuperchainDeployment = superDeployment st.SuperchainRoles = superRoles - if st.ImplementationsDeployment == nil { + + if hasPredeployedOPCM && st.ImplementationsDeployment == nil { st.ImplementationsDeployment = &addresses.ImplementationsContracts{ - OpcmImpl: *intent.OPCMAddress, + OpcmImpl: opcmAddr, } } } @@ -125,14 +137,17 @@ func immutableErr(field string, was, is any) error { return fmt.Errorf("%s is immutable: was %v, is %v", field, was, is) } -func PopulateSuperchainState(host *script.Host, opcmAddr common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { +// TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated +// TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated +func PopulateSuperchainState(host *script.Host, opcmAddr common.Address, superchainConfigProxy common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { readScript, err := opcm.NewReadSuperchainDeploymentScript(host) if err != nil { return nil, nil, fmt.Errorf("error generating read superchain deployment script: %w", err) } out, err := readScript.Run(opcm.ReadSuperchainDeploymentInput{ - OPCMAddress: opcmAddr, + OPCMAddress: opcmAddr, + SuperchainConfigProxy: superchainConfigProxy, }) if err != nil { return nil, nil, fmt.Errorf("error reading superchain deployment: %w", err) diff --git a/op-deployer/pkg/deployer/pipeline/init_test.go b/op-deployer/pkg/deployer/pipeline/init_test.go index 0a3df1b37b96d..c08b4c57a7e9a 100644 --- a/op-deployer/pkg/deployer/pipeline/init_test.go +++ b/op-deployer/pkg/deployer/pipeline/init_test.go @@ -236,18 +236,600 @@ func TestPopulateSuperchainState(t *testing.T) { superchain, err := standard.SuperchainFor(11155111) require.NoError(t, err) opcmAddr := l1Versions["op-contracts/v2.0.0-rc.1"].OPContractsManager.Address - dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr)) - require.NoError(t, err) - require.Equal(t, addresses.SuperchainContracts{ - SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), - SuperchainConfigProxy: superchain.SuperchainConfigAddr, - SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), - ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, - ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), - }, *dep) - require.Equal(t, addresses.SuperchainRoles{ - SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), - ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), - SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), - }, *roles) + + t.Run("valid OPCM address only", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, + ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("OPCM address with SuperchainConfigProxy", func(t *testing.T) { + // When both are provided and OPCM version < 7.0.0, the script uses v1 flow + // The SuperchainConfigProxy parameter is ignored in v1 flow + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // For OPCMv1, ProtocolVersions should be populated (read from OPCM) + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify that values match what OPCM returns (not the SuperchainConfigProxy parameter) + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) + + t.Run("invalid OPCM address", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidOpcmAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, invalidOpcmAddr, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify all SuperchainContracts fields are populated correctly + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + + // Verify implementations are different from proxies + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + require.NotEqual(t, dep.ProtocolVersionsImpl, dep.ProtocolVersionsProxy, "ProtocolVersionsImpl should differ from proxy") + + // Verify all SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) +} + +// TestPopulateSuperchainState_OPCMV2 validates that PopulateSuperchainState handles the OPCM v2 flow, where only a SuperchainConfigProxy +// is provided. This test uses a forked script host configured to a pinned Sepolia block to guarantee deterministic results. +// It asserts that returned roles and addresses are correct for the superchain config under OPCM v2, and that ProtocolVersions +// contract fields—which are not present in OPCM v2—are zeroed out as expected. +func TestPopulateSuperchainState_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.ForkedScriptHost( + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + // corresponds to the latest block on sepolia as of 04/30/2025. used to prevent config drift on sepolia + // from failing this test + big.NewInt(8227159), + ) + require.NoError(t, err) + + superchain, err := standard.SuperchainFor(11155111) + require.NoError(t, err) + + t.Run("SuperchainConfigProxy only", func(t *testing.T) { + // opcmAddr is set to 0, all config is provided in the superchainConfigProxy + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsProxy: common.Address{}, + ProtocolVersionsImpl: common.Address{}, + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsOwner: common.Address{}, + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("both addresses zero", func(t *testing.T) { + // When both are zero, the script detects OPCMv2 flow (because opcmAddr == 0) + // but then requires SuperchainConfigProxy to be set, so it should error + dep, roles, err := PopulateSuperchainState(host, common.Address{}, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "superchainConfigProxy required for OPCM v2") + }) + + t.Run("invalid SuperchainConfigProxy", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidSuperchainConfigProxy := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, common.Address{}, invalidSuperchainConfigProxy) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify SuperchainConfig fields are populated + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + + // Verify SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify ProtocolVersionsOwner is zeroed for v2 + require.Equal(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + }) +} + +// Validates the OPCM v2 flow in InitLiveStrategy +// when SuperchainConfigProxy is provided and opcmV2Enabled is true. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify state was populated + require.NotNil(t, st.SuperchainDeployment, "SuperchainDeployment should be populated") + require.NotNil(t, st.SuperchainRoles, "SuperchainRoles should be populated") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig fields are populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainProxyAdminImpl) +} + +// Validates that providing both +// SuperchainConfigProxy and SuperchainRoles with opcmV2Enabled returns an error. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxyAndRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that providing both OPCMAddress and SuperchainConfigProxy works correctly +// The script will use the OPCM's semver to determine the version +func TestInitLiveStrategy_OPCMV1WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Provide both OPCM address and SuperchainConfigProxy + // The script will check the OPCM version and handle accordingly + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + // Should succeed - the script handles version detection + require.NoError(t, err) + + // For OPCMv1, ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl) +} + +// Validates that providing both +// OPCMAddress and SuperchainRoles with opcmV2Enabled=false returns an error. +func TestInitLiveStrategy_OPCMV1WithSuperchainRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that the correct flow is chosen when +// hasPredeployedOPCM && !opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV1(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v1 flow was used - ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify ImplementationsDeployment was set + require.NotNil(t, st.ImplementationsDeployment) + require.Equal(t, opcmAddr, st.ImplementationsDeployment.OpcmImpl) +} + +// Validates that the correct flow is chosen when +// hasSuperchainConfigProxy && opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v2 flow was used - ProtocolVersions should be zeroed + require.NotNil(t, st.SuperchainDeployment) + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig is populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) } diff --git a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol index 6c7b60a714f0d..dc2422d819c29 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol @@ -9,49 +9,81 @@ import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { SemverComp } from "src/libraries/SemverComp.sol"; contract ReadSuperchainDeployment is Script { struct Input { - IOPContractsManager opcmAddress; + IOPContractsManager opcmAddress; // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + ISuperchainConfig superchainConfigProxy; } struct Output { + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated IProtocolVersions protocolVersionsImpl; IProtocolVersions protocolVersionsProxy; + address protocolVersionsOwner; + bytes32 recommendedProtocolVersion; + bytes32 requiredProtocolVersion; + // Superchain config ISuperchainConfig superchainConfigImpl; ISuperchainConfig superchainConfigProxy; IProxyAdmin superchainProxyAdmin; address guardian; - address protocolVersionsOwner; address superchainProxyAdminOwner; - bytes32 recommendedProtocolVersion; - bytes32 requiredProtocolVersion; } function run(Input memory _input) public returns (Output memory output_) { - require(address(_input.opcmAddress) != address(0), "ReadSuperchainDeployment: opcmAddress not set"); - + // Determine OPCM version by checking the semver or if the OPCM address is set. OPCM v2 starts at version 7.0.0. IOPContractsManager opcm = IOPContractsManager(_input.opcmAddress); + bool isOPCMV2; + if (address(opcm) == address(0)) { + isOPCMV2 = true; + } else { + require(address(opcm).code.length > 0, "ReadSuperchainDeployment: OPCM address has no code"); + isOPCMV2 = SemverComp.gte(opcm.version(), "7.0.0"); + } + + if (isOPCMV2) { + require( + address(_input.superchainConfigProxy) != address(0), + "ReadSuperchainDeployment: superchainConfigProxy required for OPCM v2" + ); + + // For OPCM v2, ProtocolVersions is being removed. Therefore, the ProtocolVersions-related fields + // (protocolVersionsImpl, protocolVersionsProxy, protocolVersionsOwner, recommendedProtocolVersion, + // requiredProtocolVersion) are intentionally left uninitialized. + output_.superchainConfigProxy = _input.superchainConfigProxy; + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); + + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + } else { + // When running on OPCM v1, the OPCM address is used to read the ProtocolVersions contract and + // SuperchainConfig. + output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); + output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); - output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); - output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); - output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); - - IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); - IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); - - vm.startPrank(address(0)); - output_.protocolVersionsImpl = IProtocolVersions(address(protocolVersionsProxy.implementation())); - output_.superchainConfigImpl = ISuperchainConfig(address(superchainConfigProxy.implementation())); - output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); - output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); - vm.stopPrank(); - - output_.guardian = output_.superchainConfigProxy.guardian(); - output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); - output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); - output_.recommendedProtocolVersion = - bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); - output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + output_.recommendedProtocolVersion = + bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); + output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + } } }