diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index fefd390e06b..85fb8e7ddd2 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -17,6 +17,9 @@ var ( // DeployV2DisputeGamesDevFlag enables deployment of V2 dispute game contracts. DeployV2DisputeGamesDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000100") + + // OpcmV2DevFlag enables deployment of OPCM V2. + OpcmV2DevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") ) // IsDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index b028b09f82e..253c3902647 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -172,6 +172,7 @@ func TestEndToEndBootstrapApplyWithUpgrade(t *testing.T) { {"default", common.Hash{}}, {"deploy-v2-disputegames", deployer.DeployV2DisputeGamesDevFlag}, {"cannon-kona", deployer.EnableDevFeature(deployer.DeployV2DisputeGamesDevFlag, deployer.CannonKonaDevFlag)}, + {"opcm-v2", deployer.OpcmV2DevFlag}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -221,6 +222,9 @@ func TestEndToEndBootstrapApplyWithUpgrade(t *testing.T) { cfg.FaultGameClockExtension = standard.DisputeClockExtension cfg.FaultGameMaxClockDuration = standard.DisputeMaxClockDuration } + if deployer.IsDevFeatureEnabled(tt.devFeature, deployer.OpcmV2DevFlag) { + cfg.DevFeatureBitmap = deployer.OpcmV2DevFlag + } runEndToEndBootstrapAndApplyUpgradeTest(t, afactsFS, cfg) }) } @@ -768,7 +772,7 @@ func TestIntentConfiguration(t *testing.T) { func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.StatDirFs, implementationsConfig bootstrap.ImplementationsConfig) { lgr := implementationsConfig.Logger - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) defer cancel() superchainProxyAdminOwner := implementationsConfig.L1ProxyAdminOwner @@ -827,6 +831,10 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat cannonKonaPrestate = common.Hash{'K', 'O', 'N', 'A'} } t.Run("upgrade opcm", func(t *testing.T) { + if deployer.IsDevFeatureEnabled(implementationsConfig.DevFeatureBitmap, deployer.OpcmV2DevFlag) { + t.Skip("Skipping OPCM upgrade for OPCM V2") + return + } upgradeConfig := embedded.UpgradeOPChainInput{ Prank: superchainProxyAdminOwner, Opcm: impls.Opcm, @@ -844,9 +852,188 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat err = embedded.DefaultUpgrader.Upgrade(host, upgradeConfigBytes) require.NoError(t, err, "OPCM upgrade should succeed") }) + t.Run("upgrade opcm v2", func(t *testing.T) { + if !deployer.IsDevFeatureEnabled(implementationsConfig.DevFeatureBitmap, deployer.OpcmV2DevFlag) { + t.Skip("Skipping OPCM V2 upgrade for non-OPCM V2 dev feature") + return + } + require.NotEqual(t, common.Address{}, impls.OpcmV2, "OpcmV2 address should not be zero") + t.Logf("Using OpcmV2 at address: %s", impls.OpcmV2.Hex()) + t.Logf("Using OpcmUtils at address: %s", impls.OpcmUtils.Hex()) + t.Logf("Using OpcmContainer at address: %s", impls.OpcmContainer.Hex()) + + // Verify OPCM V2 has code deployed + opcmCode, err := versionClient.CodeAt(ctx, impls.OpcmV2, nil) + require.NoError(t, err) + require.NotEmpty(t, opcmCode, "OPCM V2 should have code deployed") + t.Logf("OPCM V2 code size: %d bytes", len(opcmCode)) + + // Verify OpcmUtils has code deployed + utilsCode, err := versionClient.CodeAt(ctx, impls.OpcmUtils, nil) + require.NoError(t, err) + require.NotEmpty(t, utilsCode, "OpcmUtils should have code deployed") + t.Logf("OpcmUtils code size: %d bytes", len(utilsCode)) + + // Verify OpcmContainer has code deployed + containerCode, err := versionClient.CodeAt(ctx, impls.OpcmContainer, nil) + require.NoError(t, err) + require.NotEmpty(t, containerCode, "OpcmContainer should have code deployed") + t.Logf("OpcmContainer code size: %d bytes", len(containerCode)) + + // First, upgrade the superchain with V2 + t.Run("upgrade superchain v2", func(t *testing.T) { + superchainUpgradeConfig := embedded.UpgradeSuperchainV2Input{ + Prank: superchainProxyAdminOwner, + Opcm: impls.OpcmV2, + SuperchainConfig: implementationsConfig.SuperchainConfigProxy, + SuperchainInstructions: []embedded.ExtraInstruction{}, + } + err := embedded.UpgradeSuperchainV2(host, superchainUpgradeConfig) + if err != nil { + t.Logf("Superchain upgrade may have failed (could already be upgraded): %v", err) + } else { + t.Log("Superchain V2 upgrade succeeded") + } + }) + + // Deploy a new chain using OPCM V2 + var deployedSystemConfig common.Address + t.Run("deploy chain with opcm v2", func(t *testing.T) { + // Construct FullConfig for deploy + cannonArgs := mustEncodeGameArgs(common.Hash{'C', 'A', 'N', 'N', 'O', 'N'}, common.Address{}, common.Address{}) + permissionedArgs := mustEncodeGameArgs(common.Hash{'C', 'A', 'N', 'N', 'O', 'N'}, superchainProxyAdminOwner, superchainProxyAdminOwner) + konaArgs := mustEncodeGameArgs(common.Hash{'K', 'O', 'N', 'A'}, common.Address{}, common.Address{}) + + t.Logf("CANNON game args (len=%d): %x", len(cannonArgs), cannonArgs) + t.Logf("PERMISSIONED_CANNON game args (len=%d): %x", len(permissionedArgs), permissionedArgs) + t.Logf("KONA game args (len=%d): %x", len(konaArgs), konaArgs) + + deployInput := embedded.DeployOPChainV2Input{ + Opcm: impls.OpcmV2, + FullConfigV2: embedded.FullConfigV2{ + SaltMixer: "test-salt-mixer-v2", + SuperchainConfig: implementationsConfig.SuperchainConfigProxy, + ProxyAdminOwner: superchainProxyAdminOwner, + SystemConfigOwner: superchainProxyAdminOwner, + UnsafeBlockSigner: superchainProxyAdminOwner, + Batcher: superchainProxyAdminOwner, + StartingAnchorRoot: embedded.Proposal{ + Root: [32]byte{'D', 'E', 'A', 'D'}, + L2SequenceNumber: 0, + }, + StartingRespectedGameType: 1, // PERMISSIONED_CANNON + BasefeeScalar: 1368, + BlobBasefeeScalar: 810949, + GasLimit: 30000000, + L2ChainId: big.NewInt(999999999), + ResourceConfig: embedded.ResourceConfig{ + MaxResourceLimit: 20000000, + ElasticityMultiplier: 10, + BaseFeeMaxChangeDenominator: 8, + MinimumBaseFee: 1000000000, + SystemTxMaxGas: 1000000, + MaximumBaseFee: big.NewInt(20000000), + }, + DisputeGameConfigs: []embedded.DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: embedded.GameTypeCannon, + GameArgs: cannonArgs, + }, + { + Enabled: true, + InitBond: big.NewInt(0), + GameType: embedded.GameTypePermissionedCannon, + GameArgs: permissionedArgs, + }, + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: embedded.GameTypeCannonKona, + GameArgs: konaArgs, + }, + }, + }, + } + + output, err := embedded.DeployOPChainV2(host, deployInput) + require.NoError(t, err, "OPCM V2 deploy should succeed") + require.NotEqual(t, common.Address{}, output.ChainContractsV2.SystemConfig, "SystemConfig should be deployed") + + deployedSystemConfig = output.ChainContractsV2.SystemConfig + t.Logf("Deployed SystemConfig at: %s", deployedSystemConfig.Hex()) + }) + + // Then test upgrade on the V2-deployed chain + t.Run("upgrade chain v2", func(t *testing.T) { + if deployedSystemConfig == (common.Address{}) { + t.Skip("Skipping upgrade test - no chain was deployed") + return + } + + upgradeConfig := embedded.UpgradeOPChainV2Input{ + Prank: superchainProxyAdminOwner, + Opcm: impls.OpcmV2, + UpgradeInputV2: embedded.UpgradeInputV2{ + SystemConfig: deployedSystemConfig, + DisputeGameConfigs: []embedded.DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(0), + GameType: embedded.GameTypeCannon, + GameArgs: []byte{}, + }, + { + Enabled: true, + InitBond: big.NewInt(0), + GameType: embedded.GameTypePermissionedCannon, + GameArgs: []byte{}, + }, + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: embedded.GameTypeCannonKona, + GameArgs: []byte{}, + }, + }, + ExtraInstructions: []embedded.ExtraInstruction{ + { + Key: "PermittedProxyDeployment", + Data: []byte("DelayedWETH"), + }, + }, + }, + } + upgradeConfigBytes, err := json.Marshal(upgradeConfig) + require.NoError(t, err, "UpgradeOPChainV2Input should marshal to JSON") + err = embedded.DefaultUpgraderV2.Upgrade(host, upgradeConfigBytes) + require.NoError(t, err, "OPCM V2 chain upgrade should succeed") + }) + }) }) } +func mustEncodeGameArgs(absolutePrestate common.Hash, proposer, challenger common.Address) []byte { + // Use Ethereum ABI encoding for the game args + // In Solidity, abi.encode(MyStruct{...}) encodes the struct fields as a tuple + // For FaultDisputeGameConfig: abi.encode((bytes32)) = 32 bytes + // For PermissionedDisputeGameConfig: abi.encode((bytes32,address,address)) = 96 bytes (3 * 32) + + if proposer == (common.Address{}) { + // FaultDisputeGameConfig: abi.encode((bytes32 absolutePrestate)) + // This is just the raw bytes32 value (32 bytes) + return absolutePrestate[:] + } + // PermissionedDisputeGameConfig: abi.encode((bytes32,address,address)) + // This is 96 bytes: bytes32 + address (left-padded to 32) + address (left-padded to 32) + result := make([]byte, 96) + copy(result[0:32], absolutePrestate[:]) + copy(result[44:64], proposer[:]) // address at offset 32, left-padded (12 zero bytes + 20 address bytes) + copy(result[76:96], challenger[:]) // address at offset 64, left-padded (12 zero bytes + 20 address bytes) + return result +} + func needsSuperchainConfigUpgrade( ctx context.Context, client *ethclient.Client, diff --git a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go index 5e5f5a36124..ea20a606f1c 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go @@ -54,6 +54,7 @@ func TestCLIUpgrade(t *testing.T) { version: "v4.1.0", forkBlock: 9165154, // one block past the opcm deployment block }, + // TODO: Add v5.0.0 test case } for _, tc := range testCases { diff --git a/op-deployer/pkg/deployer/upgrade/embedded/deploy_v2.go b/op-deployer/pkg/deployer/upgrade/embedded/deploy_v2.go new file mode 100644 index 00000000000..17e686af52f --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/embedded/deploy_v2.go @@ -0,0 +1,107 @@ +package embedded + +import ( + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum/go-ethereum/common" + "github.com/lmittmann/w3" +) + +type DeployOPChainV2Input struct { + Opcm common.Address `json:"opcm"` + FullConfigV2 FullConfigV2 `evm:"-" json:"fullConfig"` +} + +type FullConfigV2 struct { + SaltMixer string `json:"saltMixer"` + SuperchainConfig common.Address `json:"superchainConfig"` + ProxyAdminOwner common.Address `json:"proxyAdminOwner"` + SystemConfigOwner common.Address `json:"systemConfigOwner"` + UnsafeBlockSigner common.Address `json:"unsafeBlockSigner"` + Batcher common.Address `json:"batcher"` + StartingAnchorRoot Proposal `json:"startingAnchorRoot"` + StartingRespectedGameType uint32 `json:"startingRespectedGameType"` + BasefeeScalar uint32 `json:"basefeeScalar"` + BlobBasefeeScalar uint32 `json:"blobBasefeeScalar"` + GasLimit uint64 `json:"gasLimit"` + L2ChainId *big.Int `json:"l2ChainId"` + ResourceConfig ResourceConfig `json:"resourceConfig"` + DisputeGameConfigs []DisputeGameConfig `json:"disputeGameConfigs"` +} + +type Proposal struct { + Root [32]byte `json:"root"` + L2SequenceNumber uint64 `json:"l2SequenceNumber"` +} + +type ResourceConfig struct { + MaxResourceLimit uint32 `json:"maxResourceLimit"` + ElasticityMultiplier uint8 `json:"elasticityMultiplier"` + BaseFeeMaxChangeDenominator uint8 `json:"baseFeeMaxChangeDenominator"` + MinimumBaseFee uint32 `json:"minimumBaseFee"` + SystemTxMaxGas uint32 `json:"systemTxMaxGas"` + MaximumBaseFee *big.Int `json:"maximumBaseFee"` +} + +type DeployOPChainV2Output struct { + ChainContractsV2 ChainContracts `evm:"-" json:"chainContracts"` +} + +func (d *DeployOPChainV2Output) ChainContracts() ([]byte, error) { + return chainContractsEncoder.EncodeArgs(&d.ChainContractsV2) +} + +func (d *DeployOPChainV2Output) SetChainContracts(data []byte) error { + return chainContractsEncoder.DecodeReturns(data, &d.ChainContractsV2) +} + +type ChainContracts struct { + SystemConfig common.Address `json:"systemConfig"` + ProxyAdmin common.Address `json:"proxyAdmin"` + AddressManager common.Address `json:"addressManager"` + L1CrossDomainMessenger common.Address `json:"l1CrossDomainMessenger"` + L1ERC721Bridge common.Address `json:"l1ERC721Bridge"` + L1StandardBridge common.Address `json:"l1StandardBridge"` + OptimismPortal common.Address `json:"optimismPortal"` + EthLockbox common.Address `json:"ethLockbox"` + OptimismMintableERC20Factory common.Address `json:"optimismMintableERC20Factory"` + DisputeGameFactory common.Address `json:"disputeGameFactory"` + AnchorStateRegistry common.Address `json:"anchorStateRegistry"` + DelayedWETH common.Address `json:"delayedWETH"` +} + +var fullConfigEncoder = w3.MustNewFunc( + "dummy((string saltMixer,address superchainConfig,address proxyAdminOwner,address systemConfigOwner,address unsafeBlockSigner,address batcher,(bytes32 root,uint256 l2SequenceNumber) startingAnchorRoot,uint32 startingRespectedGameType,uint32 basefeeScalar,uint32 blobBasefeeScalar,uint64 gasLimit,uint256 l2ChainId,(uint32 maxResourceLimit,uint8 elasticityMultiplier,uint8 baseFeeMaxChangeDenominator,uint32 minimumBaseFee,uint32 systemTxMaxGas,uint128 maximumBaseFee) resourceConfig,(bool enabled,uint256 initBond,uint32 gameType,bytes gameArgs)[] disputeGameConfigs))", + "", +) + +var chainContractsEncoder = w3.MustNewFunc( + "dummy()", + "(address systemConfig,address proxyAdmin,address addressManager,address l1CrossDomainMessenger,address l1ERC721Bridge,address l1StandardBridge,address optimismPortal,address ethLockbox,address optimismMintableERC20Factory,address disputeGameFactory,address anchorStateRegistry,address delayedWETH)", +) + +func (d *DeployOPChainV2Input) FullConfig() ([]byte, error) { + data, err := fullConfigEncoder.EncodeArgs(&d.FullConfigV2) + if err != nil { + return nil, fmt.Errorf("failed to encode full config: %w", err) + } + + // Strip the 4-byte function selector + return data[4:], nil +} + +func DeployOPChainV2(host *script.Host, input DeployOPChainV2Input) (*DeployOPChainV2Output, error) { + output, err := opcm.RunScriptSingle[DeployOPChainV2Input, DeployOPChainV2Output]( + host, + input, + "DeployOPChainV2.s.sol", + "DeployOPChainV2", + ) + if err != nil { + return nil, fmt.Errorf("failed to deploy OP chain V2: %w", err) + } + return &output, nil +} diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_superchain_v2.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_superchain_v2.go new file mode 100644 index 00000000000..abf3dcf4cf1 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_superchain_v2.go @@ -0,0 +1,46 @@ +package embedded + +import ( + "fmt" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum/go-ethereum/common" + "github.com/lmittmann/w3" +) + +type UpgradeSuperchainV2Input struct { + Prank common.Address `json:"prank"` + Opcm common.Address `json:"opcm"` + SuperchainConfig common.Address `evm:"-" json:"superchainConfig"` + SuperchainInstructions []ExtraInstruction `evm:"-" json:"superchainInstructions"` +} + +type SuperchainUpgradeInputV2 struct { + SuperchainConfig common.Address + ExtraInstructions []ExtraInstruction +} + +var superchainUpgradeInputEncoder = w3.MustNewFunc( + "dummy((address superchainConfig,(string key,bytes data)[] extraInstructions))", + "", +) + +func (u *UpgradeSuperchainV2Input) SuperchainUpgradeInput() ([]byte, error) { + input := SuperchainUpgradeInputV2{ + SuperchainConfig: u.SuperchainConfig, + ExtraInstructions: u.SuperchainInstructions, + } + + data, err := superchainUpgradeInputEncoder.EncodeArgs(&input) + if err != nil { + return nil, fmt.Errorf("failed to encode superchain upgrade input: %w", err) + } + + // Strip the 4-byte function selector + return data[4:], nil +} + +func UpgradeSuperchainV2(host *script.Host, input UpgradeSuperchainV2Input) error { + return opcm.RunScriptVoid(host, input, "UpgradeSuperchainV2.s.sol", "UpgradeSuperchain") +} diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2.go new file mode 100644 index 00000000000..90b332dc053 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2.go @@ -0,0 +1,84 @@ +package embedded + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum/go-ethereum/common" + "github.com/lmittmann/w3" +) + +type GameType uint32 + +const ( + GameTypeCannon GameType = 0 + GameTypePermissionedCannon GameType = 1 + GameTypeCannonKona GameType = 2 +) + +type UpgradeOPChainV2Input struct { + Prank common.Address `json:"prank"` + Opcm common.Address `json:"opcm"` + UpgradeInputV2 UpgradeInputV2 `evm:"-" json:"upgradeInputV2"` +} + +type UpgradeInputV2 struct { + SystemConfig common.Address `json:"systemConfig"` + DisputeGameConfigs []DisputeGameConfig `json:"disputeGameConfigs"` + ExtraInstructions []ExtraInstruction `json:"extraInstructions"` +} + +type DisputeGameConfig struct { + Enabled bool `json:"enabled"` + InitBond *big.Int `json:"initBond"` + GameType GameType `json:"gameType"` + GameArgs []byte `json:"gameArgs"` +} + +type ExtraInstruction struct { + Key string `json:"key"` + Data []byte `json:"data"` +} + +var upgradeInputEncoder = w3.MustNewFunc( + "dummy((address systemConfig,(bool enabled,uint256 initBond,uint32 gameType,bytes gameArgs)[] disputeGameConfigs,(string key,bytes data)[] extraInstructions))", + "", +) + +func (u *UpgradeOPChainV2Input) UpgradeInput() ([]byte, error) { + data, err := upgradeInputEncoder.EncodeArgs(&u.UpgradeInputV2) + if err != nil { + return nil, fmt.Errorf("failed to encode upgrade input: %w", err) + } + + // Strip the 4-byte function selector + return data[4:], nil +} + +type UpgradeOPChainV2 struct { + Run func(input common.Address) +} + +func UpgradeV2(host *script.Host, input UpgradeOPChainV2Input) error { + return opcm.RunScriptVoid(host, input, "UpgradeOPChainV2.s.sol", "UpgradeOPChain") +} + +type UpgraderV2 struct{} + +func (u *UpgraderV2) Upgrade(host *script.Host, input json.RawMessage) error { + var upgradeInput UpgradeOPChainV2Input + if err := json.Unmarshal(input, &upgradeInput); err != nil { + return fmt.Errorf("failed to unmarshal input: %w", err) + } + return UpgradeV2(host, upgradeInput) +} + +func (u *UpgraderV2) ArtifactsURL() string { + return artifacts.EmbeddedLocatorString +} + +var DefaultUpgraderV2 = new(UpgraderV2) diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2_test.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2_test.go new file mode 100644 index 00000000000..a17cefea629 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_v2_test.go @@ -0,0 +1,104 @@ +package embedded + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestUpgradeOPChainV2Input_UpgradeInput tests that the UpgradeInput encoding works correctly. +func TestUpgradeOPChainV2Input_UpgradeInput(t *testing.T) { + input := &UpgradeOPChainV2Input{ + Prank: common.Address{0xaa}, + Opcm: common.Address{0xbb}, + UpgradeInputV2: UpgradeInputV2{ + SystemConfig: common.Address{0x01}, + DisputeGameConfigs: []DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannon, + GameArgs: []byte{0x01, 0x02, 0x03}, + }, + }, + ExtraInstructions: []ExtraInstruction{ + { + Key: "test-key", + Data: []byte{0x04, 0x05, 0x06}, + }, + }, + }, + } + + // Test encoding + data, err := input.UpgradeInput() + require.NoError(t, err) + require.NotEmpty(t, data) + + // Verify expected encoding structure matches v5_0_0 + expected := "0000000000000000000000000000000000000000000000000000000000000020" + // offset to tuple + "0000000000000000000000000100000000000000000000000000000000000000" + // systemConfig + "0000000000000000000000000000000000000000000000000000000000000060" + // offset to disputeGameConfigs + "0000000000000000000000000000000000000000000000000000000000000160" + // offset to extraInstructions + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to disputeGameConfigs[0] + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].enabled + "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) + "0000000000000000000000000000000000000000000000000000000000000000" + // disputeGameConfigs[0].gameType + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs + "0000000000000000000000000000000000000000000000000000000000000003" + // gameArgs.length + "0102030000000000000000000000000000000000000000000000000000000000" + // gameArgs data + "0000000000000000000000000000000000000000000000000000000000000001" + // extraInstructions.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to extraInstructions[0] + "0000000000000000000000000000000000000000000000000000000000000040" + // offset to key + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to data + "0000000000000000000000000000000000000000000000000000000000000008" + // key.length + "746573742d6b65790000000000000000000000000000000000000000000000" + // "test-key" + "00" + // padding + "0000000000000000000000000000000000000000000000000000000000000003" + // data.length + "0405060000000000000000000000000000000000000000000000000000000000" // data + + require.Equal(t, expected, hex.EncodeToString(data)) +} + +// TestUpgradeOPChainV2Input_JSONMarshaling tests that the input can be marshaled and unmarshaled correctly. +func TestUpgradeOPChainV2Input_JSONMarshaling(t *testing.T) { + input := &UpgradeOPChainV2Input{ + Prank: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + Opcm: common.HexToAddress("0xEA055C82D6B0543CE0931b52B206242Cb9D262F9"), + UpgradeInputV2: UpgradeInputV2{ + SystemConfig: common.HexToAddress("0x034edD2A225f7f429A63E0f1D2084B9E0A93b538"), + DisputeGameConfigs: []DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(0), + GameType: GameTypeCannon, + GameArgs: []byte{}, + }, + }, + ExtraInstructions: []ExtraInstruction{}, + }, + } + + // Marshal to JSON + jsonData, err := json.Marshal(input) + require.NoError(t, err) + require.NotEmpty(t, jsonData) + + // Unmarshal back + var decoded UpgradeOPChainV2Input + err = json.Unmarshal(jsonData, &decoded) + require.NoError(t, err) + + // Verify fields + require.Equal(t, input.Prank, decoded.Prank) + require.Equal(t, input.Opcm, decoded.Opcm) + require.Equal(t, input.UpgradeInputV2.SystemConfig, decoded.UpgradeInputV2.SystemConfig) + require.Len(t, decoded.UpgradeInputV2.DisputeGameConfigs, 1) + require.Equal(t, input.UpgradeInputV2.DisputeGameConfigs[0].Enabled, decoded.UpgradeInputV2.DisputeGameConfigs[0].Enabled) + require.Equal(t, input.UpgradeInputV2.DisputeGameConfigs[0].GameType, decoded.UpgradeInputV2.DisputeGameConfigs[0].GameType) +} diff --git a/op-deployer/pkg/deployer/upgrade/flags.go b/op-deployer/pkg/deployer/upgrade/flags.go index 71bfdd916a2..90f39ea8b00 100644 --- a/op-deployer/pkg/deployer/upgrade/flags.go +++ b/op-deployer/pkg/deployer/upgrade/flags.go @@ -72,6 +72,7 @@ var Commands = cli.Commands{ }, oplog.CLIFlags(deployer.EnvVarPrefix)...), Action: UpgradeCLI(v410.DefaultUpgrader), }, + // TODO: Add v5.0.0 test case &cli.Command{ Name: "embedded", Usage: "upgrades a chain to version of contracts embedded in op-deployer", diff --git a/op-deployer/pkg/deployer/upgrade/v5_0_0/testdata/config.json b/op-deployer/pkg/deployer/upgrade/v5_0_0/testdata/config.json new file mode 100644 index 00000000000..07e0c2046f1 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/v5_0_0/testdata/config.json @@ -0,0 +1,16 @@ +{ + "prank": "0x1Eb2fFc903729a0F03966B917003800b145F56E2", + "opcm": "0xaf334f4537e87f5155d135392ff6d52f1866465e", + "upgradeInput": { + "systemConfig": "0x034edD2A225f7f429A63E0f1D2084B9E0A93b538", + "disputeGameConfigs": [ + { + "enabled": true, + "initBond": "0x0", + "gameType": 0, + "gameArgs": "0x" + } + ], + "extraInstructions": [] + } +} diff --git a/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go new file mode 100644 index 00000000000..e8df85c2337 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go @@ -0,0 +1,118 @@ +package v5_0_0 + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum/go-ethereum/common" + "github.com/lmittmann/w3" +) + +// GameType represents the type of dispute game. +type GameType uint32 + +const ( + GameTypeCannon GameType = 0 + GameTypePermissionedCannon GameType = 1 + GameTypeCannonKona GameType = 2 +) + +// UpgradeOPChainInput is the top-level input for upgrading an OP Chain. +type UpgradeOPChainInput struct { + Prank common.Address `json:"prank"` + Opcm common.Address `json:"opcm"` + UpgradeInputV2 UpgradeInputV2 `evm:"-" json:"upgradeInput"` +} + +// UpgradeInput contains the configuration for upgrading an OP Chain. +type UpgradeInputV2 struct { + SystemConfig common.Address + DisputeGameConfigs []DisputeGameConfig + ExtraInstructions []ExtraInstruction +} + +// DisputeGameConfig contains configuration for a dispute game. +type DisputeGameConfig struct { + Enabled bool + InitBond *big.Int + GameType GameType + GameArgs []byte +} + +// ExtraInstruction represents additional upgrade instructions. +type ExtraInstruction struct { + Key string `json:"key"` + Data []byte `json:"data"` +} + +var upgradeInputEncoder = w3.MustNewFunc( + "dummy((address systemConfig,(bool enabled,uint256 initBond,uint32 gameType,bytes gameArgs)[] disputeGameConfigs,(string key,bytes data)[] extraInstructions))", + "", +) + +// UpgradeInput returns the ABI-encoded upgrade input. +func (u *UpgradeOPChainInput) UpgradeInput() ([]byte, error) { + input := UpgradeInputV2{ + SystemConfig: u.UpgradeInputV2.SystemConfig, + DisputeGameConfigs: make([]DisputeGameConfig, len(u.UpgradeInputV2.DisputeGameConfigs)), + ExtraInstructions: make([]ExtraInstruction, len(u.UpgradeInputV2.ExtraInstructions)), + } + + for i, dgc := range u.UpgradeInputV2.DisputeGameConfigs { + input.DisputeGameConfigs[i] = DisputeGameConfig{ + Enabled: dgc.Enabled, + InitBond: (*big.Int)(dgc.InitBond), + GameType: dgc.GameType, + GameArgs: dgc.GameArgs, + } + } + + for i, ei := range u.UpgradeInputV2.ExtraInstructions { + input.ExtraInstructions[i] = ExtraInstruction{ + Key: ei.Key, + Data: ei.Data, + } + } + + data, err := upgradeInputEncoder.EncodeArgs(&input) + if err != nil { + return nil, fmt.Errorf("failed to encode upgrade input: %w", err) + } + + // Strip the 4-byte function selector + return data[4:], nil +} + +// UpgradeOPChain is the script interface for upgrading an OP Chain. +type UpgradeOPChain struct { + Run func(input common.Address) +} + +// Upgrade executes the OP Chain upgrade script. +func Upgrade(host *script.Host, input UpgradeOPChainInput) error { + return opcm.RunScriptVoid(host, input, "UpgradeOPChain.s.sol", "UpgradeOPChain") +} + +// Upgrader implements the upgrade interface for v5.0.0. +type Upgrader struct{} + +// Upgrade executes the upgrade with the given input. +func (u *Upgrader) Upgrade(host *script.Host, input json.RawMessage) error { + var upgradeInput UpgradeOPChainInput + if err := json.Unmarshal(input, &upgradeInput); err != nil { + return fmt.Errorf("failed to unmarshal input: %w", err) + } + return Upgrade(host, upgradeInput) +} + +// ArtifactsURL returns the URL for the artifacts for this version. +func (u *Upgrader) ArtifactsURL() string { + return artifacts.CreateHttpLocator("579f43b5bbb43e74216b7ed33125280567df86eaf00f7621f354e4a68c07323e") +} + +// DefaultUpgrader is the default upgrader instance for v5.0.0. +var DefaultUpgrader = new(Upgrader) diff --git a/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade_test.go new file mode 100644 index 00000000000..54e40c3b3f2 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade_test.go @@ -0,0 +1,62 @@ +package v5_0_0 + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestUpgradeOPChainInput_UpgradeInput(t *testing.T) { + input := &UpgradeOPChainInput{ + Prank: common.Address{0xaa}, + Opcm: common.Address{0xbb}, + UpgradeInputV2: UpgradeInputV2{ + SystemConfig: common.Address{0x01}, + DisputeGameConfigs: []DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannon, + GameArgs: []byte{0x01, 0x02, 0x03}, + }, + }, + ExtraInstructions: []ExtraInstruction{ + { + Key: "test-key", + Data: []byte{0x04, 0x05, 0x06}, + }, + }, + }, + } + data, err := input.UpgradeInput() + + require.NoError(t, err) + require.NotEmpty(t, data) + + expected := "0000000000000000000000000000000000000000000000000000000000000020" + // offset to tuple + "0000000000000000000000000100000000000000000000000000000000000000" + // systemConfig + "0000000000000000000000000000000000000000000000000000000000000060" + // offset to disputeGameConfigs + "0000000000000000000000000000000000000000000000000000000000000160" + // offset to extraInstructions + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to disputeGameConfigs[0] + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].enabled + "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) + "0000000000000000000000000000000000000000000000000000000000000000" + // disputeGameConfigs[0].gameType + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs + "0000000000000000000000000000000000000000000000000000000000000003" + // gameArgs.length + "0102030000000000000000000000000000000000000000000000000000000000" + // gameArgs data + "0000000000000000000000000000000000000000000000000000000000000001" + // extraInstructions.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to extraInstructions[0] + "0000000000000000000000000000000000000000000000000000000000000040" + // offset to key + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to data + "0000000000000000000000000000000000000000000000000000000000000008" + // key.length + "746573742d6b65790000000000000000000000000000000000000000000000" + // "test-key" + "00" + // padding + "0000000000000000000000000000000000000000000000000000000000000003" + // data.length + "0405060000000000000000000000000000000000000000000000000000000000" // data + + require.Equal(t, expected, hex.EncodeToString(data)) +} diff --git a/packages/contracts-bedrock/scripts/deploy/UpgradeOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/UpgradeOPChain.s.sol index d238b55edc2..d26fa116559 100644 --- a/packages/contracts-bedrock/scripts/deploy/UpgradeOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/UpgradeOPChain.s.sol @@ -3,26 +3,47 @@ pragma solidity ^0.8.0; import { Script } from "forge-std/Script.sol"; import { OPContractsManager } from "src/L1/OPContractsManager.sol"; +import { OPContractsManagerV2 } from "src/L1/opcm/OPContractsManagerV2.sol"; import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract UpgradeOPChainInput is BaseDeployIO { address internal _prank; - OPContractsManager internal _opcm; - bytes _opChainConfigs; + address internal _opcm; + /// @notice The upgrade input is stored as opaque bytes to allow storing both OPCM v1 and v2 upgrade inputs. + bytes _upgradeInput; // Setter for OPContractsManager type function set(bytes4 _sel, address _value) public { require(address(_value) != address(0), "UpgradeOPCMInput: cannot set zero address"); if (_sel == this.prank.selector) _prank = _value; - else if (_sel == this.opcm.selector) _opcm = OPContractsManager(_value); + else if (_sel == this.opcm.selector) _opcm = _value; else revert("UpgradeOPCMInput: unknown selector"); } + /// @notice Sets the upgrade input using the OPContractsManager.OpChainConfig[] type, + /// this is used when upgrading chains using OPCM v1. + /// @param _sel The selector of the field to set. + /// @param _value The value to set. function set(bytes4 _sel, OPContractsManager.OpChainConfig[] memory _value) public { require(_value.length > 0, "UpgradeOPCMInput: cannot set empty array"); - if (_sel == this.opChainConfigs.selector) _opChainConfigs = abi.encode(_value); + if (_sel == this.upgradeInput.selector) _upgradeInput = abi.encode(_value); + else revert("UpgradeOPCMInput: unknown selector"); + } + + /// @notice Sets the upgrade input using the OPContractsManagerV2.UpgradeInput type, + /// this is used when upgrading chains using OPCM v2. + /// Minimal validation is performed, relying on the OPCM v2 contract to perform the proper validation. + /// This is done to avoid duplicating the validation logic in the script. + /// @param _sel The selector of the field to set. + /// @param _value The value to set. + function set(bytes4 _sel, OPContractsManagerV2.UpgradeInput memory _value) public { + require(address(_value.systemConfig) != address(0), "UpgradeOPCMInput: cannot set zero address"); + require(_value.disputeGameConfigs.length > 0, "UpgradeOPCMInput: cannot set empty dispute game configs array"); + + if (_sel == this.upgradeInput.selector) _upgradeInput = abi.encode(_value); else revert("UpgradeOPCMInput: unknown selector"); } @@ -31,45 +52,91 @@ contract UpgradeOPChainInput is BaseDeployIO { return _prank; } - function opcm() public view returns (OPContractsManager) { - require(address(_opcm) != address(0), "UpgradeOPCMInput: not set"); + function opcm() public view returns (address) { + require(_opcm != address(0), "UpgradeOPCMInput: not set"); return _opcm; } - function opChainConfigs() public view returns (bytes memory) { - require(_opChainConfigs.length > 0, "UpgradeOPCMInput: not set"); - return _opChainConfigs; + function upgradeInput() public view returns (bytes memory) { + require(_upgradeInput.length > 0, "UpgradeOPCMInput: not set"); + return _upgradeInput; } } contract UpgradeOPChain is Script { function run(UpgradeOPChainInput _uoci) external { - OPContractsManager opcm = _uoci.opcm(); - OPContractsManager.OpChainConfig[] memory opChainConfigs = - abi.decode(_uoci.opChainConfigs(), (OPContractsManager.OpChainConfig[])); + address opcm = _uoci.opcm(); + + // First, we need to check what version of OPCM is being used. + bool useOPCMv2 = OPContractsManager(opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2); // Etch DummyCaller contract. This contract is used to mimic the contract that is used // as the source of the delegatecall to the OPCM. In practice this will be the governance // 2/2 or similar. address prank = _uoci.prank(); - bytes memory code = vm.getDeployedCode("UpgradeOPChain.s.sol:DummyCaller"); + bytes memory code = _getDummyCallerCode(useOPCMv2); vm.etch(prank, code); vm.store(prank, bytes32(0), bytes32(uint256(uint160(address(opcm))))); vm.label(prank, "DummyCaller"); // Call into the DummyCaller. This will perform the delegatecall under the hood and // return the result. - vm.broadcast(msg.sender); - (bool success,) = DummyCaller(prank).upgrade(opChainConfigs); + (bool success,) = _upgrade(prank, useOPCMv2, _uoci.upgradeInput()); require(success, "UpgradeChain: upgrade failed"); } + + /// @notice Helper function to get the proper dummy caller code based on the OPCM version. + /// @param _useOPCMv2 Whether to use OPCM v2. + /// @return code The code of the dummy caller. + function _getDummyCallerCode(bool _useOPCMv2) internal view returns (bytes memory) { + if (_useOPCMv2) return vm.getDeployedCode("UpgradeOPChain.s.sol:DummyCallerV2"); + else return vm.getDeployedCode("UpgradeOPChain.s.sol:DummyCallerV1"); + } + + /// @notice Helper function to upgrade the OPCM based on the OPCM version. Performs the decoding of the upgrade + /// input and the delegatecall to the OPCM. + /// @param _prank The address of the dummy caller contract. + /// @param _useOPCMv2 Whether to use OPCM v2. + /// @param _upgradeInput The upgrade input. + /// @return success Whether the upgrade succeeded. + /// @return result The result of the upgrade (bool, bytes memory). + function _upgrade( + address _prank, + bool _useOPCMv2, + bytes memory _upgradeInput + ) + internal + returns (bool, bytes memory) + { + vm.broadcast(msg.sender); + if (_useOPCMv2) { + return DummyCallerV2(_prank).upgrade(abi.decode(_upgradeInput, (OPContractsManagerV2.UpgradeInput))); + } else { + return DummyCallerV1(_prank).upgrade(abi.decode(_upgradeInput, (OPContractsManager.OpChainConfig[]))); + } + } +} +/// @title DummyCallerV2 +/// @notice This contract is used to mimic the contract that is used as the source of the delegatecall to the OPCM v2. +/// Uses OPContractsManagerV2.UpgradeInput type for the upgrade input. + +contract DummyCallerV2 { + address internal _opcmAddr; + + function upgrade(OPContractsManagerV2.UpgradeInput memory _upgradeInput) external returns (bool, bytes memory) { + bytes memory data = abi.encodeCall(OPContractsManagerV2.upgrade, _upgradeInput); + (bool success, bytes memory result) = _opcmAddr.delegatecall(data); + return (success, result); + } } +/// @notice This contract is used to mimic the contract that is used as the source of the delegatecall to the OPCM v1. +/// Uses OPContractsManager.OpChainConfig[] type for the upgrade input. -contract DummyCaller { +contract DummyCallerV1 { address internal _opcmAddr; function upgrade(OPContractsManager.OpChainConfig[] memory _opChainConfigs) external returns (bool, bytes memory) { - bytes memory data = abi.encodeCall(DummyCaller.upgrade, _opChainConfigs); + bytes memory data = abi.encodeCall(OPContractsManager.upgrade, _opChainConfigs); (bool success, bytes memory result) = _opcmAddr.delegatecall(data); return (success, result); } diff --git a/packages/contracts-bedrock/scripts/deploy/UpgradeSuperchainConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/UpgradeSuperchainConfig.s.sol index 0f4d110f811..ed30491e839 100644 --- a/packages/contracts-bedrock/scripts/deploy/UpgradeSuperchainConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/UpgradeSuperchainConfig.s.sol @@ -3,13 +3,17 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract UpgradeSuperchainConfig is Script { struct Input { address prank; - IOPContractsManager opcm; + address opcm; ISuperchainConfig superchainConfig; + IOPContractsManagerUtils.ExtraInstruction[] extraInstructions; } /// @notice Delegate calls upgradeSuperchainConfig on the OPCM from the input.prank address. @@ -17,34 +21,64 @@ contract UpgradeSuperchainConfig is Script { // Make sure the input is valid assertValidInput(_input); - IOPContractsManager opcm = _input.opcm; + // Both OPCM v1 and v2 implement the isDevFeatureEnabled function. + bool useOPCMv2 = IOPContractsManager(_input.opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2); + + address opcm = _input.opcm; // Etch DummyCaller contract. This contract is used to mimic the contract that is used // as the source of the delegatecall to the OPCM. In practice this will be the governance // 2/2 or similar. address prank = _input.prank; - bytes memory code = vm.getDeployedCode("UpgradeSuperchainConfig.s.sol:DummyCaller"); + bytes memory code = _getDummyCallerCode(useOPCMv2); vm.etch(prank, code); - vm.store(prank, bytes32(0), bytes32(uint256(uint160(address(opcm))))); + vm.store(prank, bytes32(0), bytes32(uint256(uint160(opcm)))); vm.label(prank, "DummyCaller"); - ISuperchainConfig superchainConfig = _input.superchainConfig; - - // Call into the DummyCaller to perform the delegatecall - vm.broadcast(msg.sender); - - (bool success,) = DummyCaller(prank).upgradeSuperchainConfig(superchainConfig); + (bool success,) = _upgrade(prank, useOPCMv2, _input); require(success, "UpgradeSuperchainConfig: upgradeSuperchainConfig failed"); } /// @notice Asserts that the input is valid. function assertValidInput(Input memory _input) internal pure { + // Note: Intentionally not checking extra instructions for OPCM v2 as they are not required in some upgrades. + // This responsibility is delegated to the OPCM v2 contract. require(_input.prank != address(0), "UpgradeSuperchainConfig: prank not set"); require(address(_input.opcm) != address(0), "UpgradeSuperchainConfig: opcm not set"); require(address(_input.superchainConfig) != address(0), "UpgradeSuperchainConfig: superchainConfig not set"); } + + /// @notice Helper function to get the proper dummy caller code based on the OPCM version. + /// @param _useOPCMv2 Whether to use OPCM v2. + /// @return code The code of the dummy caller. + function _getDummyCallerCode(bool _useOPCMv2) internal view returns (bytes memory) { + if (_useOPCMv2) return vm.getDeployedCode("UpgradeSuperchainConfig.s.sol:DummyCallerV2"); + else return vm.getDeployedCode("UpgradeSuperchainConfig.s.sol:DummyCaller"); + } + + /// @notice Helper function to upgrade the OPCM based on the OPCM version. Performs the decoding of the upgrade + /// input and the delegatecall to the OPCM. + /// @param _prank The address of the dummy caller contract. + /// @param _useOPCMv2 Whether to use OPCM v2. + /// @param _input The input. + /// @return success Whether the upgrade succeeded. + /// @return result The result of the upgrade (bool, bytes memory). + function _upgrade(address _prank, bool _useOPCMv2, Input memory _input) internal returns (bool, bytes memory) { + // Call into the DummyCaller to perform the delegatecall + vm.broadcast(msg.sender); + if (_useOPCMv2) { + return DummyCallerV2(_prank).upgradeSuperchain( + IOPContractsManagerV2.SuperchainUpgradeInput({ + superchainConfig: _input.superchainConfig, + extraInstructions: _input.extraInstructions + }) + ); + } else { + return DummyCaller(_prank).upgradeSuperchainConfig(_input.superchainConfig); + } + } } /// @title DummyCaller @@ -58,3 +92,19 @@ contract DummyCaller { return (success, result); } } +/// @title DummyCallerV2 +/// @notice This contract is used to mimic the contract that is used as the source of the delegatecall to the OPCM v2. +/// Uses IOPContractsManagerV2.SuperchainUpgradeInput type for the upgrade input. + +contract DummyCallerV2 { + address internal _opcmAddr; + + function upgradeSuperchain(IOPContractsManagerV2.SuperchainUpgradeInput memory _superchainUpgradeInput) + external + returns (bool, bytes memory) + { + bytes memory data = abi.encodeCall(IOPContractsManagerV2.upgradeSuperchain, (_superchainUpgradeInput)); + (bool success, bytes memory result) = _opcmAddr.delegatecall(data); + return (success, result); + } +} diff --git a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol index 6a662631580..60f2226372b 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol @@ -2,11 +2,19 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; + +// Libraries import { Claim } from "src/dispute/lib/Types.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +// Interfaces +import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +// Contracts import { OPContractsManager } from "src/L1/OPContractsManager.sol"; +import { OPContractsManagerV2 } from "src/L1/opcm/OPContractsManagerV2.sol"; import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; contract UpgradeOPChainInput_Test is Test { @@ -24,7 +32,7 @@ contract UpgradeOPChainInput_Test is Test { input.opcm(); vm.expectRevert("UpgradeOPCMInput: not set"); - input.opChainConfigs(); + input.upgradeInput(); } function test_setAddress_succeeds() public { @@ -38,7 +46,7 @@ contract UpgradeOPChainInput_Test is Test { input.set(input.opcm.selector, mockOPCM); assertEq(input.prank(), mockPrank); - assertEq(address(input.opcm()), mockOPCM); + assertEq(input.opcm(), mockOPCM); } function test_setOpChainConfigs_succeeds() public { @@ -69,9 +77,9 @@ contract UpgradeOPChainInput_Test is Test { cannonKonaPrestate: Claim.wrap(bytes32(uint256(3))) }); - input.set(input.opChainConfigs.selector, configs); + input.set(input.upgradeInput.selector, configs); - bytes memory storedConfigs = input.opChainConfigs(); + bytes memory storedConfigs = input.upgradeInput(); assertEq(storedConfigs, abi.encode(configs)); // Additional verification of stored claims if needed @@ -81,6 +89,55 @@ contract UpgradeOPChainInput_Test is Test { assertEq(Claim.unwrap(decodedConfigs[1].cannonPrestate), bytes32(uint256(2))); } + /// @notice Tests that the upgrade input can be set using the OPContractsManagerV2.UpgradeInput type. + function test_setUpgradeInputV2_succeeds() public { + // Create sample UpgradeInputV2 + OPContractsManagerV2.DisputeGameConfig[] memory disputeGameConfigs = + new OPContractsManagerV2.DisputeGameConfig[](1); + disputeGameConfigs[0] = OPContractsManagerV2.DisputeGameConfig({ + enabled: true, + initBond: 1000, + gameType: GameType.wrap(1), + gameArgs: abi.encode("test") + }); + + IOPContractsManagerUtils.ExtraInstruction[] memory extraInstructions = + new IOPContractsManagerUtils.ExtraInstruction[](1); + extraInstructions[0] = IOPContractsManagerUtils.ExtraInstruction({ key: "test", data: abi.encode("test") }); + + OPContractsManagerV2.UpgradeInput memory upgradeInput = OPContractsManagerV2.UpgradeInput({ + systemConfig: ISystemConfig(makeAddr("systemConfig")), + disputeGameConfigs: disputeGameConfigs, + extraInstructions: extraInstructions + }); + + input.set(input.upgradeInput.selector, upgradeInput); + + bytes memory storedUpgradeInput = input.upgradeInput(); + assertEq(storedUpgradeInput, abi.encode(upgradeInput)); + + // Additional verification of stored values if needed + OPContractsManagerV2.UpgradeInput memory decodedUpgradeInput = + abi.decode(storedUpgradeInput, (OPContractsManagerV2.UpgradeInput)); + // Check system config matches + assertEq(address(decodedUpgradeInput.systemConfig), address(upgradeInput.systemConfig)); + // Check dispute game configs match + assertEq(decodedUpgradeInput.disputeGameConfigs.length, disputeGameConfigs.length); + assertEq(decodedUpgradeInput.disputeGameConfigs[0].enabled, disputeGameConfigs[0].enabled); + assertEq(decodedUpgradeInput.disputeGameConfigs[0].initBond, disputeGameConfigs[0].initBond); + assertEq( + GameType.unwrap(decodedUpgradeInput.disputeGameConfigs[0].gameType), + GameType.unwrap(disputeGameConfigs[0].gameType) + ); + assertEq( + keccak256(decodedUpgradeInput.disputeGameConfigs[0].gameArgs), keccak256(disputeGameConfigs[0].gameArgs) + ); + // Check extra instructions match + assertEq(decodedUpgradeInput.extraInstructions.length, extraInstructions.length); + assertEq(decodedUpgradeInput.extraInstructions[0].key, extraInstructions[0].key); + assertEq(keccak256(decodedUpgradeInput.extraInstructions[0].data), keccak256(extraInstructions[0].data)); + } + function test_setAddress_withZeroAddress_reverts() public { vm.expectRevert("UpgradeOPCMInput: cannot set zero address"); input.set(input.prank.selector, address(0)); @@ -93,7 +150,7 @@ contract UpgradeOPChainInput_Test is Test { OPContractsManager.OpChainConfig[] memory emptyConfigs = new OPContractsManager.OpChainConfig[](0); vm.expectRevert("UpgradeOPCMInput: cannot set empty array"); - input.set(input.opChainConfigs.selector, emptyConfigs); + input.set(input.upgradeInput.selector, emptyConfigs); } function test_set_withInvalidSelector_reverts() public { @@ -118,11 +175,15 @@ contract UpgradeOPChainInput_Test is Test { } } -contract MockOPCM { +contract MockOPCMV1 { event UpgradeCalled( address indexed sysCfgProxy, bytes32 indexed absolutePrestate, bytes32 indexed cannonKonaPrestate ); + function isDevFeatureEnabled(bytes32 /* _feature */ ) public pure returns (bool) { + return false; + } + function upgrade(OPContractsManager.OpChainConfig[] memory _opChainConfigs) public { emit UpgradeCalled( address(_opChainConfigs[0].systemConfigProxy), @@ -132,8 +193,26 @@ contract MockOPCM { } } +contract MockOPCMV2 { + event UpgradeCalled( + address indexed systemConfig, + OPContractsManagerV2.DisputeGameConfig[] indexed disputeGameConfigs, + IOPContractsManagerUtils.ExtraInstruction[] indexed extraInstructions + ); + + function isDevFeatureEnabled(bytes32 _feature) public pure returns (bool) { + return _feature == DevFeatures.OPCM_V2; + } + + function upgrade(OPContractsManagerV2.UpgradeInput memory _upgradeInput) public { + emit UpgradeCalled( + address(_upgradeInput.systemConfig), _upgradeInput.disputeGameConfigs, _upgradeInput.extraInstructions + ); + } +} + contract UpgradeOPChain_Test is Test { - MockOPCM mockOPCM; + MockOPCMV1 mockOPCM; UpgradeOPChainInput uoci; OPContractsManager.OpChainConfig config; UpgradeOPChain upgradeOPChain; @@ -144,7 +223,7 @@ contract UpgradeOPChain_Test is Test { ); function setUp() public virtual { - mockOPCM = new MockOPCM(); + mockOPCM = new MockOPCMV1(); uoci = new UpgradeOPChainInput(); uoci.set(uoci.opcm.selector, address(mockOPCM)); config = OPContractsManager.OpChainConfig({ @@ -154,7 +233,7 @@ contract UpgradeOPChain_Test is Test { }); OPContractsManager.OpChainConfig[] memory configs = new OPContractsManager.OpChainConfig[](1); configs[0] = config; - uoci.set(uoci.opChainConfigs.selector, configs); + uoci.set(uoci.upgradeInput.selector, configs); prank = makeAddr("prank"); uoci.set(uoci.prank.selector, prank); upgradeOPChain = new UpgradeOPChain(); @@ -171,3 +250,44 @@ contract UpgradeOPChain_Test is Test { upgradeOPChain.run(uoci); } } + +contract UpgradeOPChain_TestV2 is Test { + MockOPCMV2 mockOPCM; + UpgradeOPChainInput uoci; + UpgradeOPChain upgradeOPChain; + address prank; + + event UpgradeCalled( + address indexed systemConfig, + OPContractsManagerV2.DisputeGameConfig[] indexed disputeGameConfigs, + IOPContractsManagerUtils.ExtraInstruction[] indexed extraInstructions + ); + + function setUp() public { + mockOPCM = new MockOPCMV2(); + uoci = new UpgradeOPChainInput(); + uoci.set(uoci.opcm.selector, address(mockOPCM)); + + prank = makeAddr("prank"); + uoci.set(uoci.prank.selector, prank); + upgradeOPChain = new UpgradeOPChain(); + } + + function test_upgrade_succeeds() public { + // NOTE: Setting the upgrade input here to avoid `Copying of type struct OPContractsManagerV2.DisputeGameConfig + // memory[] memory to storage not yet supported.` error. + OPContractsManagerV2.UpgradeInput memory upgradeInput = OPContractsManagerV2.UpgradeInput({ + systemConfig: ISystemConfig(makeAddr("systemConfig")), + disputeGameConfigs: new OPContractsManagerV2.DisputeGameConfig[](1), + extraInstructions: new IOPContractsManagerUtils.ExtraInstruction[](0) + }); + uoci.set(uoci.upgradeInput.selector, upgradeInput); + + // UpgradeCalled should be emitted by the prank since it's a delegate call. + vm.expectEmit(address(prank)); + emit UpgradeCalled( + address(upgradeInput.systemConfig), upgradeInput.disputeGameConfigs, upgradeInput.extraInstructions + ); + upgradeOPChain.run(uoci); + } +} diff --git a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol index da09590493a..62665dab16b 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol @@ -7,21 +7,43 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { UpgradeSuperchainConfig } from "scripts/deploy/UpgradeSuperchainConfig.s.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; -/// @title MockOPCM +import { DevFeatures } from "src/libraries/DevFeatures.sol"; + +/// @title MockOPCMV1 /// @notice This contract is used to mock the OPCM contract and emit an event which we check for in the test. -contract MockOPCM { +contract MockOPCMV1 { event UpgradeCalled(address indexed superchainConfig); + function isDevFeatureEnabled(bytes32 /* _feature */ ) public pure returns (bool) { + return false; + } + function upgradeSuperchainConfig(ISuperchainConfig _superchainConfig) public { emit UpgradeCalled(address(_superchainConfig)); } } +/// @title MockOPCMV2 +/// @notice This contract is used to mock the OPCM v2 contract and emit an event which we check for in the test. +contract MockOPCMV2 { + event UpgradeCalled(IOPContractsManagerV2.SuperchainUpgradeInput indexed superchainUpgradeInput); + + function isDevFeatureEnabled(bytes32 _feature) public pure returns (bool) { + return _feature == DevFeatures.OPCM_V2; + } + + function upgradeSuperchain(IOPContractsManagerV2.SuperchainUpgradeInput memory _superchainUpgradeInput) public { + emit UpgradeCalled(_superchainUpgradeInput); + } +} + /// @title UpgradeSuperchainConfig_Test /// @notice This test is used to test the UpgradeSuperchainConfig script. -contract UpgradeSuperchainConfig_Run_Test is Test { - MockOPCM mockOPCM; +contract UpgradeSuperchainConfigV1_Run_Test is Test { + MockOPCMV1 mockOPCM; UpgradeSuperchainConfig.Input input; UpgradeSuperchainConfig upgradeSuperchainConfig; address prank; @@ -31,9 +53,9 @@ contract UpgradeSuperchainConfig_Run_Test is Test { /// @notice Sets up the test suite. function setUp() public virtual { - mockOPCM = new MockOPCM(); + mockOPCM = new MockOPCMV1(); - input.opcm = IOPContractsManager(address(mockOPCM)); + input.opcm = address(mockOPCM); superchainConfig = ISuperchainConfig(makeAddr("superchainConfig")); prank = makeAddr("prank"); @@ -59,10 +81,10 @@ contract UpgradeSuperchainConfig_Run_Test is Test { upgradeSuperchainConfig.run(input); input.prank = prank; - input.opcm = IOPContractsManager(address(0)); + input.opcm = address(0); vm.expectRevert("UpgradeSuperchainConfig: opcm not set"); upgradeSuperchainConfig.run(input); - input.opcm = IOPContractsManager(address(mockOPCM)); + input.opcm = address(mockOPCM); input.superchainConfig = ISuperchainConfig(address(0)); vm.expectRevert("UpgradeSuperchainConfig: superchainConfig not set"); @@ -70,3 +92,52 @@ contract UpgradeSuperchainConfig_Run_Test is Test { input.superchainConfig = ISuperchainConfig(address(superchainConfig)); } } + +/// @title UpgradeSuperchainConfigV2_Run_Test +/// @notice This test is used to test the UpgradeSuperchainConfig script with OPCM v2. +contract UpgradeSuperchainConfigV2_Run_Test is Test { + MockOPCMV2 mockOPCM; + UpgradeSuperchainConfig upgradeSuperchainConfig; + address prank; + ISuperchainConfig superchainConfig; + + event UpgradeCalled(IOPContractsManagerV2.SuperchainUpgradeInput indexed superchainUpgradeInput); + + /// @notice Sets up the test suite. + function setUp() public { + mockOPCM = new MockOPCMV2(); + + superchainConfig = ISuperchainConfig(makeAddr("superchainConfig")); + prank = makeAddr("prank"); + + upgradeSuperchainConfig = new UpgradeSuperchainConfig(); + } + + /// @notice Tests that the UpgradeSuperchainConfig script succeeds when called with non-zero input values. + function testFuzz_upgrade_succeeds(IOPContractsManagerUtils.ExtraInstruction[] memory extraInstructions) public { + UpgradeSuperchainConfig.Input memory input = _getInput(extraInstructions); + + // UpgradeCalled should be emitted by the prank since it's a delegate call. + vm.expectEmit(address(prank)); + emit UpgradeCalled( + IOPContractsManagerV2.SuperchainUpgradeInput({ + superchainConfig: superchainConfig, + extraInstructions: extraInstructions + }) + ); + upgradeSuperchainConfig.run(input); + } + + function _getInput(IOPContractsManagerUtils.ExtraInstruction[] memory extraInstructions) + internal + view + returns (UpgradeSuperchainConfig.Input memory) + { + return UpgradeSuperchainConfig.Input({ + prank: prank, + opcm: address(mockOPCM), + superchainConfig: superchainConfig, + extraInstructions: extraInstructions + }); + } +}