diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 9400474af9674..634177b022e6c 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -355,6 +355,7 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul GasPayingTokenSymbol: cfg.GasPayingTokenSymbol, NativeAssetLiquidityAmount: cfg.NativeAssetLiquidityAmount.ToInt(), LiquidityControllerOwner: cfg.LiquidityControllerOwner, + UseL2CM: false, // TODO(#19102): add support for L2CM }); err != nil { return fmt.Errorf("failed L2 genesis: %w", err) } diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index 88993d916ef69..68d867366fac4 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -20,6 +20,9 @@ var ( // OPCMV2DevFlag enables the OPContractsManagerV2 contract. OPCMV2DevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + + // L2CMDevFlag enables L2CM. + L2CMDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000100000") ) // 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 54880ece9b6d5..cae2024ae685f 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -359,6 +359,32 @@ func TestEndToEndApply(t *testing.T) { require.Equal(t, amount, account.Balance, "Native asset liquidity predeploy should have the configured balance") }) + t.Run("with L2CM", func(t *testing.T) { + intent, st := shared.NewIntent(t, l1ChainID, dk, l2ChainID1, loc, loc, testCustomGasLimit) + + intent.GlobalDeployOverrides = map[string]any{ + "devFeatureBitmap": deployer.L2CMDevFlag, + } + + require.NoError(t, deployer.ApplyPipeline(ctx, deployer.ApplyPipelineOpts{ + DeploymentTarget: deployer.DeploymentTargetLive, + L1RPCUrl: l1RPC, + DeployerPrivateKey: pk, + Intent: intent, + State: st, + Logger: lgr, + StateWriter: pipeline.NoopStateWriter(), + CacheDir: testCacheDir, + })) + + // Check that the conditional deployer predeploy is deployed in L2 genesis + conditionalDeployerAddr := common.HexToAddress("0x420000000000000000000000000000000000002C") + l2Genesis := st.Chains[0].Allocs.Data.Accounts + account, exists := l2Genesis[conditionalDeployerAddr] + require.True(t, exists, "Conditional deployer should exist in L2 genesis") + require.NotEmpty(t, account.Code, "Conditional deployer should have code deployed") + }) + t.Run("OPCMV2 deployment", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 6a0c71cba1e4c..29deabfd74024 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -39,6 +39,7 @@ type L2GenesisInput struct { GasPayingTokenSymbol string NativeAssetLiquidityAmount *big.Int LiquidityControllerOwner common.Address + UseL2CM bool } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index b173f0e6f310d..08e3f10b26cf5 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -7,15 +7,13 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum-optimism/optimism/op-service/jsonutil" - - "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" - "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" + "github.com/ethereum-optimism/optimism/op-service/jsonutil" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum/go-ethereum/common" @@ -85,6 +83,16 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, cgt := buildCGTConfig(thisIntent) + // Check if L2CM feature is enabled + var useL2CM bool + if devFeatureBitmap, ok := intent.GlobalDeployOverrides["devFeatureBitmap"].(common.Hash); ok { + // TODO(#19151): Replace this with the L2CMDevFlag constant when we fix import cycles. + l2CMFlag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000100000") + if isDevFeatureEnabled(devFeatureBitmap, l2CMFlag) { + useL2CM = true + } + } + if err := script.Run(opcm.L2GenesisInput{ L1ChainID: new(big.Int).SetUint64(intent.L1ChainID), L2ChainID: chainID.Big(), @@ -118,6 +126,7 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, GasPayingTokenSymbol: cgt.GasPayingTokenSymbol, NativeAssetLiquidityAmount: cgt.NativeAssetLiquidityAmount, LiquidityControllerOwner: cgt.LiquidityControllerOwner, + UseL2CM: useL2CM, }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 302a2c3354707..c923bc54aeb42 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -134,6 +134,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common // Select which OPCM to use based on dev feature flag opcmAddr := st.ImplementationsDeployment.OpcmImpl if devFeatureBitmap, ok := intent.GlobalDeployOverrides["devFeatureBitmap"].(common.Hash); ok { + // TODO(#19151): Replace this with the OPCMV2DevFlag constant when we fix import cycles. opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") if isDevFeatureEnabled(devFeatureBitmap, opcmV2Flag) { opcmAddr = st.ImplementationsDeployment.OpcmV2Impl @@ -211,6 +212,7 @@ func shouldDeployOPChain(st *state.State, chainID common.Hash) bool { return true } +// TODO(#19151): Remove this function when we fix import cycles. // isDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. // This mirrors the function in devfeatures.go to avoid import cycles. func isDevFeatureEnabled(bitmap, flag common.Hash) bool { diff --git a/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol b/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol new file mode 100644 index 0000000000000..9f4afa505763e --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @title IConditionalDeployer +/// @notice Interface for the ConditionalDeployer contract. +interface IConditionalDeployer is ISemver { + /// @notice Emitted when an implementation is deployed. + /// @param implementation The address of the deployed implementation. + /// @param salt The salt used for deployment. + event ImplementationDeployed(address indexed implementation, bytes32 salt); + + /// @notice Emitted when deployment is skipped because implementation already exists. + /// @param implementation The address of the existing implementation. + event ImplementationExists(address indexed implementation); + + /// @notice Error thrown when deployment fails. + /// @param data The data returned from the deployment call. + error ConditionalDeployer_DeploymentFailed(bytes data); + + /// @notice Deploys an implementation using CREATE2 if it doesn't already exist. + /// @param _salt The salt to use for CREATE2 deployment. + /// @param _code The initialization code for the contract. + /// @return implementation_ The address of the deployed or existing implementation. + function deploy(bytes32 _salt, bytes memory _code) external returns (address implementation_); + + /// @notice Address of the Arachnid's DeterministicDeploymentProxy. + /// @return deterministicDeploymentProxy_ The address of the Arachnid's DeterministicDeploymentProxy. + function deterministicDeploymentProxy() external pure returns (address deterministicDeploymentProxy_); +} diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index c189309fddd70..635e2dfacd7eb 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -81,6 +81,7 @@ contract L2Genesis is Script { string gasPayingTokenSymbol; uint256 nativeAssetLiquidityAmount; address liquidityControllerOwner; + bool useL2CM; } using ForkUtils for Fork; @@ -221,8 +222,11 @@ contract L2Genesis is Script { vm.etch(addr, code); EIP1967Helper.setAdmin(addr, Predeploys.PROXY_ADMIN); - if (Predeploys.isSupportedPredeploy(addr, _input.fork, _input.deployCrossL2Inbox, _input.useCustomGasToken)) - { + if ( + Predeploys.isSupportedPredeploy( + addr, _input.fork, _input.deployCrossL2Inbox, _input.useCustomGasToken, _input.useL2CM + ) + ) { address implementation = Predeploys.predeployToCodeNamespace(addr); EIP1967Helper.setImplementation(addr, implementation); } @@ -268,6 +272,9 @@ contract L2Genesis is Script { setLiquidityController(_input); // 29 setNativeAssetLiquidity(_input); // 2A } + if (_input.useL2CM) { + setConditionalDeployer(); // 2C + } } function setInteropPredeployProxies() internal { } @@ -578,6 +585,11 @@ contract L2Genesis is Script { vm.deal(Predeploys.NATIVE_ASSET_LIQUIDITY, _input.nativeAssetLiquidityAmount); } + /// @notice This predeploy is following the safety invariant #1. + function setConditionalDeployer() internal { + _setImplementationCode(Predeploys.CONDITIONAL_DEPLOYER); + } + /// @notice Sets all the preinstalls. function setPreinstalls() internal { address tmpSetPreinstalls = address(uint160(uint256(keccak256("SetPreinstalls")))); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 56fee1ab6c27d..8ef6f83876b78 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -92,6 +92,8 @@ contract DeployConfig is Script { uint256 public faultGameV2ClockExtension; uint256 public faultGameV2MaxClockDuration; + bool public useL2CM; + bool public useInterop; bool public useUpgradedFork; bytes32 public devFeatureBitmap; @@ -181,6 +183,8 @@ contract DeployConfig is Script { daBondSize = _readOr(_json, "$.daBondSize", 1000000000); daResolverRefundPercentage = _readOr(_json, "$.daResolverRefundPercentage", 0); + useL2CM = _readOr(_json, "$.useL2CM", false); + useInterop = _readOr(_json, "$.useInterop", false); devFeatureBitmap = bytes32(_readOr(_json, "$.devFeatureBitmap", 0)); useUpgradedFork; @@ -317,6 +321,11 @@ contract DeployConfig is Script { operatorFeeVaultWithdrawalNetwork = _operatorFeeVaultWithdrawalNetwork; } + /// @notice Allow the `useL2CM` config to be overridden in testing environments + function setUseL2CM(bool _useL2CM) public { + useL2CM = _useL2CM; + } + function latestGenesisFork() internal view returns (Fork) { if (l2GenesisJovianTimeOffset == 0) { return Fork.JOVIAN; diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index 70988a582cff8..6d087b1fc0ddb 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -281,6 +281,11 @@ library Config { return vm.envOr("DEV_FEATURE__OPCM_V2", false); } + /// @notice Returns true if the development feature l2cm is enabled. + function devFeatureL2CM() internal view returns (bool) { + return vm.envOr("DEV_FEATURE__L2CM", false); + } + /// @notice Returns true if the system feature custom_gas_token is enabled. function sysFeatureCustomGasToken() internal view returns (bool) { return vm.envOr("SYS_FEATURE__CUSTOM_GAS_TOKEN", false); diff --git a/packages/contracts-bedrock/snapshots/abi/ConditionalDeployer.json b/packages/contracts-bedrock/snapshots/abi/ConditionalDeployer.json new file mode 100644 index 0000000000000..12e31561c842b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ConditionalDeployer.json @@ -0,0 +1,95 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_code", + "type": "bytes" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "deterministicDeploymentProxy", + "outputs": [ + { + "internalType": "address", + "name": "deterministicDeploymentProxy_", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "ImplementationDeployed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ImplementationExists", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "ConditionalDeployer_DeploymentFailed", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index c1b2cd9b2f531..cb2bb3a0419e0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -59,6 +59,10 @@ "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", "sourceCodeHash": "0xcb329746df0baddd3dc03c6c88da5d6bdc0f0a96d30e6dc78d0891bb1e935032" }, + "src/L2/ConditionalDeployer.sol:ConditionalDeployer": { + "initCodeHash": "0x580996fd149ac478b9363399eab8ba479ed55a324a215ea8e4841f9e9f033ac5", + "sourceCodeHash": "0x7c83a95f65cfd963af0caf90579e8f8544e3f883a48bc803720ec251c72aeffe" + }, "src/L2/CrossL2Inbox.sol:CrossL2Inbox": { "initCodeHash": "0x56f868e561c4abe539043f98b16aad9305479e68fd03ece2233249b0c73a24ea", "sourceCodeHash": "0x7c6d362a69a480a06a079542a7fd2ce48cb1dd80d6b9043fba60218569371349" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ConditionalDeployer.json b/packages/contracts-bedrock/snapshots/storageLayout/ConditionalDeployer.json new file mode 100644 index 0000000000000..0637a088a01e8 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ConditionalDeployer.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol b/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol new file mode 100644 index 0000000000000..fce8e86677ed8 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x420000000000000000000000000000000000002C +/// @title ConditionalDeployer +/// @notice ConditionalDeployer is used to deploy implementations for predeploys during network upgrades. +/// It uses Arachnid's DeterministicDeploymentProxy to deploy the implementations. +contract ConditionalDeployer is ISemver { + /// @notice Emitted when an implementation is deployed. + /// @param implementation The address of the deployed implementation. + /// @param salt The salt used for deployment. + event ImplementationDeployed(address indexed implementation, bytes32 salt); + + /// @notice Emitted when deployment is skipped because implementation already exists. + /// @param implementation The address of the existing implementation. + event ImplementationExists(address indexed implementation); + + /// @notice Error thrown when deployment fails. + /// @param data The data returned from the deployment call. + error ConditionalDeployer_DeploymentFailed(bytes data); + + /// @notice Address of the Arachnid DeterministicDeploymentProxy. + address payable internal constant DETERMINISTIC_DEPLOYMENT_PROXY = + payable(0x4e59b44847b379578588920cA78FbF26c0B4956C); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Deploys an implementation using CREATE2 if it doesn't already exist. + /// @dev Does not support deployments requiring ETH. + /// @dev Reverts when the deployment call to the DeterministicDeploymentProxy fails. + /// @param _salt The salt to use for CREATE2 deployment. + /// @param _code The initialization code for the contract. + /// @return implementation_ The address of the deployed or existing implementation. + function deploy(bytes32 _salt, bytes memory _code) external returns (address implementation_) { + // Compute the address where the contract will be deployed using CREATE2 formula + bytes32 codeHash = keccak256(_code); + address expectedImplementation = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), DETERMINISTIC_DEPLOYMENT_PROXY, _salt, codeHash)))) + ); + + // Check if implementation already exists + if (expectedImplementation.code.length != 0) { + emit ImplementationExists(expectedImplementation); + return expectedImplementation; + } + + // Deploy using Arachnid's DeterministicDeploymentProxy + // Calldata format: salt + initcode + // Returns: raw 20 bytes (deployed address, not ABI-encoded) + (bool success, bytes memory data) = DETERMINISTIC_DEPLOYMENT_PROXY.call(abi.encodePacked(_salt, _code)); + + // Decode the returned address (raw 20 bytes) + implementation_ = address(bytes20(data)); + if (!success || implementation_ != expectedImplementation) { + revert ConditionalDeployer_DeploymentFailed(data); + } + + emit ImplementationDeployed(implementation_, _salt); + } + + /// @notice Returns the address of the Arachnid's DeterministicDeploymentProxy. + /// @return deterministicDeploymentProxy_ The address of the Arachnid's DeterministicDeploymentProxy. + function deterministicDeploymentProxy() external pure returns (address deterministicDeploymentProxy_) { + deterministicDeploymentProxy_ = DETERMINISTIC_DEPLOYMENT_PROXY; + } +} diff --git a/packages/contracts-bedrock/src/libraries/DevFeatures.sol b/packages/contracts-bedrock/src/libraries/DevFeatures.sol index 2a1dc1854c18d..4fa5287c1b578 100644 --- a/packages/contracts-bedrock/src/libraries/DevFeatures.sol +++ b/packages/contracts-bedrock/src/libraries/DevFeatures.sol @@ -28,6 +28,9 @@ library DevFeatures { /// @notice The feature that enables the OPContractsManagerV2 contract. bytes32 public constant OPCM_V2 = bytes32(0x0000000000000000000000000000000000000000000000000000000000010000); + /// @notice The feature that enables L2CM. + bytes32 public constant L2CM = bytes32(0x0000000000000000000000000000000000000000000000000000000000100000); + /// @notice Checks if a feature is enabled in a bitmap. Note that this function does not check /// that the input feature represents a single feature and the bitwise AND operation /// allows for multiple features to be enabled at once. Users should generally check diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 890e46e2b71a3..9cd4ff08d89eb 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -122,6 +122,9 @@ library Predeploys { /// @notice Address of the FeeSplitter predeploy. address internal constant FEE_SPLITTER = 0x420000000000000000000000000000000000002B; + /// @notice Address of the ConditionalDeployer predeploy. + address internal constant CONDITIONAL_DEPLOYER = 0x420000000000000000000000000000000000002C; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -157,6 +160,7 @@ library Predeploys { if (_addr == LIQUIDITY_CONTROLLER) return "LiquidityController"; if (_addr == NATIVE_ASSET_LIQUIDITY) return "NativeAssetLiquidity"; if (_addr == FEE_SPLITTER) return "FeeSplitter"; + if (_addr == CONDITIONAL_DEPLOYER) return "ConditionalDeployer"; revert("Predeploys: unnamed predeploy"); } @@ -170,7 +174,8 @@ library Predeploys { address _addr, uint256 _fork, bool _enableCrossL2Inbox, - bool _isCustomGasToken + bool _isCustomGasToken, + bool _useL2CM ) internal pure @@ -186,7 +191,7 @@ library Predeploys { || (_fork >= uint256(Fork.INTEROP) && _enableCrossL2Inbox && _addr == CROSS_L2_INBOX) || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) || (_isCustomGasToken && _addr == LIQUIDITY_CONTROLLER) - || (_isCustomGasToken && _addr == NATIVE_ASSET_LIQUIDITY); + || (_isCustomGasToken && _addr == NATIVE_ASSET_LIQUIDITY) || (_useL2CM && _addr == CONDITIONAL_DEPLOYER); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol b/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol new file mode 100644 index 0000000000000..af24eee2c964e --- /dev/null +++ b/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Preinstalls } from "src/libraries/Preinstalls.sol"; + +// Contracts +import { ConditionalDeployer } from "src/L2/ConditionalDeployer.sol"; + +/// @title ConditionalDeployer_Harness +/// @notice This contract is deployed by the ConditionalDeployer to test the deployment of an +/// implementation. +contract ConditionalDeployer_Harness { + uint256 public immutable number; + + constructor(uint256 _number) { + number = _number; + } +} + +/// @title ConditionalDeployer_TestInit +/// @notice Reusable test initialization for `ConditionalDeployer` tests. +contract ConditionalDeployer_TestInit is CommonTest { + // Test contracts + bytes public simpleContractCreationCode; + + function setUp() public override { + super.setUp(); + skipIfDevFeatureDisabled(DevFeatures.L2CM); + // Deploy contracts + simpleContractCreationCode = type(ConditionalDeployer_Harness).creationCode; + } +} + +/// @title ConditionalDeployer_Deploy_Test +/// @notice Tests the `deploy` function of the `ConditionalDeployer` contract. +contract ConditionalDeployer_Deploy_Test is ConditionalDeployer_TestInit { + /// @notice Event emitted when an implementation is deployed. + event ImplementationDeployed(address indexed implementation, bytes32 salt); + + /// @notice Event emitted when deployment is skipped because implementation already exists. + event ImplementationExists(address indexed implementation); + + /// @notice Tests that `deploy` succeeds and emits the correct event. + function testFuzz_deploy_succeeds(address _caller, bytes32 _salt, uint256 _number) public { + bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_number)); + address expectedImplementation = getExpectedImplementation(_initCode, _salt); + + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationDeployed(expectedImplementation, _salt); + + vm.prank(_caller); + address implementation = conditionalDeployer.deploy(_salt, _initCode); + + assertEq(implementation, expectedImplementation); + assertEq(ConditionalDeployer_Harness(implementation).number(), _number); + assert(implementation.code.length != 0); + } + + /// @notice Tests that `deploy` is idempotent and produces the same address when called multiple times. + function testFuzz_deploy_idempotent_succeeds(address _caller, bytes32 _salt, uint256 _number) public { + bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_number)); + address expectedImplementation = getExpectedImplementation(_initCode, _salt); + + // First Deployment + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationDeployed(expectedImplementation, _salt); + + assertEq(expectedImplementation.code.length, 0); + + vm.prank(_caller); + address implementation1 = conditionalDeployer.deploy(_salt, _initCode); + + // Assert that the implementation was deployed + assertEq(implementation1, expectedImplementation); + assert(implementation1.code.length != 0); + assertEq(ConditionalDeployer_Harness(implementation1).number(), _number); + + // Second Deployment + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationExists(implementation1); + + vm.prank(_caller); + address implementation2 = conditionalDeployer.deploy(_salt, _initCode); + + assertEq(implementation1, implementation2); + } + + /// @notice Tests that `deploy` reverts when the deployment call to the DeterministicDeploymentProxy fails. + /// @dev The deployment call to the DeterministicDeploymentProxy is mocked to revert. + function testFuzz_deploy_deploymentFailed_reverts(address _caller, bytes32 _salt, uint256 _number) public { + bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_number)); + + // Mock the deployment call to the DeterministicDeploymentProxy to revert + vm.mockCallRevert( + conditionalDeployer.deterministicDeploymentProxy(), + 0, + abi.encodePacked(_salt, _initCode), + bytes("deployment failed") + ); + + vm.prank(_caller); + vm.expectRevert( + abi.encodeWithSelector( + ConditionalDeployer.ConditionalDeployer_DeploymentFailed.selector, bytes("deployment failed") + ) + ); + conditionalDeployer.deploy(_salt, _initCode); + } + + /// @notice Tests that `deploy` reverts when the deployment call to the DeterministicDeploymentProxy returns the + /// wrong address. + /// @dev The deployment call to the DeterministicDeploymentProxy is mocked to return the wrong address. + function testFuzz_deploy_notExpectedAddress_reverts( + address _caller, + bytes32 _salt, + address _notExpectedAddress, + uint256 _number + ) + public + { + bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_number)); + address expectedImplementation = getExpectedImplementation(_initCode, _salt); + vm.assume(_notExpectedAddress != expectedImplementation); + + vm.mockCall( + conditionalDeployer.deterministicDeploymentProxy(), + 0, + abi.encodePacked(_salt, _initCode), + abi.encodePacked(_notExpectedAddress) + ); + vm.prank(_caller); + vm.expectRevert( + abi.encodeWithSelector( + ConditionalDeployer.ConditionalDeployer_DeploymentFailed.selector, abi.encodePacked(_notExpectedAddress) + ) + ); + conditionalDeployer.deploy(_salt, _initCode); + } + + /// @notice Returns the expected implementation address for the given initialization code and salt. + /// @dev Uses the CREATE2 formula to compute the expected implementation address. + /// @param _initCode The initialization code for the contract. + /// @param _salt The salt to use for deployment. + /// @return expectedImplementation_ The expected implementation address. + function getExpectedImplementation( + bytes memory _initCode, + bytes32 _salt + ) + internal + view + returns (address expectedImplementation_) + { + bytes32 codeHash = keccak256(_initCode); + expectedImplementation_ = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), conditionalDeployer.deterministicDeploymentProxy(), _salt, codeHash + ) + ) + ) + ) + ); + } +} + +/// @title ConditionalDeployer_Uncategorized_Test +/// @notice General tests that are not testing any function directly of the `ConditionalDeployer` +/// contract or are testing multiple functions. +contract ConditionalDeployer_Uncategorized_Test is ConditionalDeployer_TestInit { + /// @notice Tests that the getters return valid values. + function test_getters_succeeds() external view { + assert(bytes(conditionalDeployer.version()).length > 0); + assertEq(conditionalDeployer.deterministicDeploymentProxy(), payable(Preinstalls.DeterministicDeploymentProxy)); + } +} diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index 2a025cd5c09fe..e16c00057720c 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -10,6 +10,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Fork } from "scripts/libraries/Config.sol"; import { Features } from "src/libraries/Features.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title Predeploys_TestInit /// @notice Reusable test initialization for `Predeploys` tests. @@ -57,7 +58,7 @@ abstract contract Predeploys_TestInit is CommonTest { } /// @notice Internal test function for predeploys validation across different forks. - function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox, bool _isCustomGasToken) internal { + function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox, bool _isCustomGasToken, bool _useL2CM) internal { uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; @@ -73,7 +74,7 @@ abstract contract Predeploys_TestInit is CommonTest { } bool isPredeploy = - Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox, _isCustomGasToken); + Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox, _isCustomGasToken, _useL2CM); bytes memory code = addr.code; if (isPredeploy) assertTrue(code.length > 0); @@ -159,14 +160,21 @@ contract Predeploys_Uncategorized_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code /// and the proxied accounts have the correct admin. function test_predeploys_succeeds() external { - _test_predeploys(Fork.ISTHMUS, false, false); + _test_predeploys(Fork.ISTHMUS, false, false, false); } /// @notice Tests that the predeploy addresses are set correctly. They have code /// and the proxied accounts have the correct admin. Using custom gas token. function test_predeploys_customGasToken_succeeds() external { skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); - _test_predeploys(Fork.ISTHMUS, false, true); + _test_predeploys(Fork.ISTHMUS, false, true, false); + } + + /// @notice Tests that the predeploy addresses are set correctly. They have code + /// and the proxied accounts have the correct admin. Using l2cm. + function test_predeploys_l2cm_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.L2CM); + _test_predeploys(Fork.ISTHMUS, false, false, true); } } @@ -183,12 +191,12 @@ contract Predeploys_UncategorizedInterop_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop with inbox. function test_predeploysWithInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, true, false); + _test_predeploys(Fork.INTEROP, true, false, false); } /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop without inbox. function test_predeploysWithoutInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, false, false); + _test_predeploys(Fork.INTEROP, false, false, false); } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index e157c1cff9970..1054f4a5de82c 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -71,7 +71,11 @@ abstract contract L2Genesis_TestInit is Test { assertEq(Predeploys.PROXY_ADMIN, EIP1967Helper.getAdmin(addr)); // If it's not a supported predeploy, skip next checks. - if (!Predeploys.isSupportedPredeploy(addr, uint256(LATEST_FORK), true, input.useCustomGasToken)) { + if ( + !Predeploys.isSupportedPredeploy( + addr, uint256(LATEST_FORK), true, input.useCustomGasToken, input.useL2CM + ) + ) { continue; } @@ -260,7 +264,8 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { gasPayingTokenName: "", gasPayingTokenSymbol: "", nativeAssetLiquidityAmount: type(uint248).max, - liquidityControllerOwner: address(0x000000000000000000000000000000000000000d) + liquidityControllerOwner: address(0x000000000000000000000000000000000000000d), + useL2CM: false }); } @@ -440,4 +445,18 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { vm.expectRevert("FeeVault: custom gas token and revenue share cannot be enabled together"); genesis.run(input); } + + /// @notice Tests that enabling l2cm succeeds. + function test_run_l2cm_succeeds() external { + input.useL2CM = true; + genesis.run(input); + + testProxyAdmin(); + testPredeploys(); + testVaultsWithRevenueShare(); + testGovernance(); + testFactories(); + testForks(); + testFeeSplitter(); + } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index daea37745ae3e..5bfbcd7f0721c 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -105,6 +105,11 @@ abstract contract CommonTest is Test, Setup, Events { deploy.cfg().setOperatorFeeVaultWithdrawalNetwork(1); } + if (Config.devFeatureL2CM()) { + console.log("CommonTest: enabling l2cm"); + deploy.cfg().setUseL2CM(true); + } + if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. if (useAltDAOverride || useInteropOverride) { diff --git a/packages/contracts-bedrock/test/setup/FeatureFlags.sol b/packages/contracts-bedrock/test/setup/FeatureFlags.sol index 300bfea09d982..b9c22cae02229 100644 --- a/packages/contracts-bedrock/test/setup/FeatureFlags.sol +++ b/packages/contracts-bedrock/test/setup/FeatureFlags.sol @@ -45,6 +45,10 @@ abstract contract FeatureFlags { console.log("Setup: DEV_FEATURE__OPCM_V2 is enabled"); devFeatureBitmap |= DevFeatures.OPCM_V2; } + if (Config.devFeatureL2CM()) { + console.log("Setup: DEV_FEATURE__L2CM is enabled"); + devFeatureBitmap |= DevFeatures.L2CM; + } } /// @notice Returns the string name of a feature. @@ -55,6 +59,8 @@ abstract contract FeatureFlags { return "DEV_FEATURE__OPTIMISM_PORTAL_INTEROP"; } else if (_feature == DevFeatures.OPCM_V2) { return "DEV_FEATURE__OPCM_V2"; + } else if (_feature == DevFeatures.L2CM) { + return "DEV_FEATURE__L2CM"; } else if (_feature == Features.CUSTOM_GAS_TOKEN) { return "SYS_FEATURE__CUSTOM_GAS_TOKEN"; } else if (_feature == Features.ETH_LOCKBOX) { diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index b83c68cf5ff60..2bb4b1b6eb5f7 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -70,6 +70,7 @@ import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IConditionalDeployer } from "interfaces/L2/IConditionalDeployer.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -160,6 +161,7 @@ abstract contract Setup is FeatureFlags { IFeeSplitter feeSplitter = IFeeSplitter(payable(Predeploys.FEE_SPLITTER)); IL1Withdrawer l1Withdrawer; ISuperchainRevSharesCalculator superchainRevSharesCalculator; + IConditionalDeployer conditionalDeployer = IConditionalDeployer(Predeploys.CONDITIONAL_DEPLOYER); /// @notice Indicates whether a test is running against a forked production network. function isForkTest() public view returns (bool) { @@ -363,7 +365,8 @@ abstract contract Setup is FeatureFlags { gasPayingTokenName: deploy.cfg().gasPayingTokenName(), gasPayingTokenSymbol: deploy.cfg().gasPayingTokenSymbol(), nativeAssetLiquidityAmount: deploy.cfg().nativeAssetLiquidityAmount(), - liquidityControllerOwner: deploy.cfg().liquidityControllerOwner() + liquidityControllerOwner: deploy.cfg().liquidityControllerOwner(), + useL2CM: deploy.cfg().useL2CM() }) ); @@ -405,6 +408,7 @@ abstract contract Setup is FeatureFlags { labelPredeploy(Predeploys.NATIVE_ASSET_LIQUIDITY); labelPredeploy(Predeploys.LIQUIDITY_CONTROLLER); labelPredeploy(Predeploys.FEE_SPLITTER); + labelPredeploy(Predeploys.CONDITIONAL_DEPLOYER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3);