diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index bf12da1962d..0011088b224 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -348,6 +348,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 88993d916ef..68d867366fa 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 d477104a129..0e9187d5a64 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -364,6 +364,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 native asset liquidity predeploy has the configured amount 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 6a0c71cba1e..29deabfd740 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 b173f0e6f31..739926ce01f 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,15 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, cgt := buildCGTConfig(thisIntent) + // Check if L2CM feature is enabled + var useL2CM bool = false + if devFeatureBitmap, ok := intent.GlobalDeployOverrides["devFeatureBitmap"].(common.Hash); ok { + 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 +125,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/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol b/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol new file mode 100644 index 00000000000..bf14e8e435c --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IConditionalDeployer.sol @@ -0,0 +1,30 @@ +// 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. + error ConditionalDeployer_DeploymentFailed(bytes data); + + /// @notice Address of the DeterministicDeploymentProxy (Nick's method). + function DETERMINISTIC_DEPLOYMENT_PROXY() external view returns (address payable); + + /// @notice Deploys an implementation using CREATE2 if it doesn't already exist. + /// @param _value The amount of ETH to send with the deployment. + /// @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(uint256 _value, bytes32 _salt, bytes memory _code) external returns (address implementation_); +} diff --git a/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol b/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol deleted file mode 100644 index 34d09ae9494..00000000000 --- a/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -interface ICreate2Deployer { - /** - * @dev Deploys a contract using `CREATE2`. The address where the - * contract will be deployed can be known in advance via {computeAddress}. - * - * The bytecode for a contract can be obtained from Solidity with - * `type(contractName).creationCode`. - * - * Requirements: - * - `bytecode` must not be empty. - * - `salt` must have not been used for `bytecode` already. - * - the factory must have a balance of at least `value`. - * - if `value` is non-zero, `bytecode` must have a `payable` constructor. - */ - function deploy(uint256 value, bytes32 salt, bytes memory code) external; - /** - * @dev Deployment of the {ERC1820Implementer}. - * Further information: https://eips.ethereum.org/EIPS/eip-1820 - */ - function deployERC1820Implementer(uint256 value, bytes32 salt) external; - /** - * @dev Returns the address where a contract will be stored if deployed via {deploy}. - * Any change in the `bytecodeHash` or `salt` will result in a new destination address. - */ - function computeAddress(bytes32 salt, bytes32 codeHash) external view returns (address); - /** - * @dev Returns the address where a contract will be stored if deployed via {deploy} from a - * contract located at `deployer`. If `deployer` is this contract's address, returns the - * same value as {computeAddress}. - */ - function computeAddressWithDeployer( - bytes32 salt, - bytes32 codeHash, - address deployer - ) - external - pure - returns (address); - - receive() external payable; -} diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index c189309fddd..635e2dfacd7 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 56fee1ab6c2..8ef6f83876b 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 70988a582cf..6d087b1fc0d 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 00000000000..922177b8e24 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ConditionalDeployer.json @@ -0,0 +1,100 @@ +[ + { + "inputs": [], + "name": "DETERMINISTIC_DEPLOYMENT_PROXY", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "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": "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 895e2ec1469..873c9fa81d7 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": "0x6b88fe95359f7166b90bcf9414b91cef3662be2e02b4a0540e486a5040c9e84c", + "sourceCodeHash": "0xaf67802e6fc99cb9a267bef3a736cf97e032ec215fc8fb8ca15c3f17eb978543" + }, "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 00000000000..0637a088a01 --- /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 index 549e014f798..893922f15b7 100644 --- a/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol +++ b/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { ICreate2Deployer } from "interfaces/preinstalls/ICreate2Deployer.sol"; -import { Constants } from "src/libraries/Constants.sol"; +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; +/// @custom:proxied true +/// @custom:predeploy 0x420000000000000000000000000000000000002C /// @title ConditionalDeployer -/// @notice Intermediary contract for deploying predeploy implementations during network upgrades. -contract ConditionalDeployer { - /// @notice Address of the Create2Deployer preinstall. - address payable private constant CREATE2_DEPLOYER = payable(0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2); +/// @notice ConditionalDeployer is used to deploy implementations for predeploys during network upgrades. +/// It uses the DeterministicDeploymentProxy (Nick's method) to deploy the implementations. +contract ConditionalDeployer is ISemver { + /// @notice Address of the DeterministicDeploymentProxy (Nick's method). + address payable public constant DETERMINISTIC_DEPLOYMENT_PROXY = payable(0x4e59b44847b379578588920cA78FbF26c0B4956C); /// @notice Emitted when an implementation is deployed. /// @param implementation The address of the deployed implementation. @@ -19,37 +22,40 @@ contract ConditionalDeployer { /// @param implementation The address of the existing implementation. event ImplementationExists(address indexed implementation); - /// @notice Error thrown when caller is not authorized. - error UnauthorizedCaller(); + /// @notice Error thrown when deployment fails. + error ConditionalDeployer_DeploymentFailed(bytes data); - /// @notice Modifier to restrict access to depositor account or address(0). - modifier onlyAuthorized() { - if (msg.sender != Constants.DEPOSITOR_ACCOUNT && msg.sender != address(0)) { - revert UnauthorizedCaller(); - } - _; - } + /// @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. - /// @param value The amount of ETH to send with the deployment. - /// @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(uint256 value, bytes32 salt, bytes memory code) external onlyAuthorized returns (address) { - // Compute the address where the contract will be deployed - bytes32 codeHash = keccak256(code); - address implementation = ICreate2Deployer(CREATE2_DEPLOYER).computeAddress(salt, codeHash); + /// @param _value The amount of ETH to send with the deployment. + /// @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(uint256 _value, 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); + implementation_ = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), DETERMINISTIC_DEPLOYMENT_PROXY, _salt, codeHash)))) + ); // Check if implementation already exists - if (implementation.code.length != 0) { - emit ImplementationExists(implementation); - return implementation; + if (implementation_.code.length != 0) { + emit ImplementationExists(implementation_); + return implementation_; } - // Deploy the implementation - ICreate2Deployer(CREATE2_DEPLOYER).deploy(value, salt, code); + // Deploy using DeterministicDeploymentProxy (Nick's method) + // Calldata format: salt + initcode + (bool success, bytes memory data) = + DETERMINISTIC_DEPLOYMENT_PROXY.call{ value: _value }(abi.encodePacked(_salt, _code)); + if (!success) { + revert ConditionalDeployer_DeploymentFailed(data); + } - emit ImplementationDeployed(implementation, salt); - return implementation; + emit ImplementationDeployed(implementation_, _salt); + return implementation_; } } diff --git a/packages/contracts-bedrock/src/libraries/DevFeatures.sol b/packages/contracts-bedrock/src/libraries/DevFeatures.sol index 2a1dc1854c1..4fa5287c1b5 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 890e46e2b71..9cd4ff08d89 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 index a7c18682d3b..d33d4e7134e 100644 --- a/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol +++ b/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol @@ -1,16 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing import { Test } from "forge-std/Test.sol"; -import { ConditionalDeployer } from "src/L2/ConditionalDeployer.sol"; + +// Libraries import { Config } from "scripts/libraries/Config.sol"; -import { Constants } from "src/libraries/Constants.sol"; -import { ICreate2Deployer } from "interfaces/preinstalls/ICreate2Deployer.sol"; -import { Preinstalls } from "src/libraries/Preinstalls.sol"; -/// @title SimpleContract -/// @notice A simple contract to deploy using the ConditionalDeployer. -contract SimpleContract { +// Contracts +import { ConditionalDeployer } from "src/L2/ConditionalDeployer.sol"; + +/// @title ConditionalDeployer_Harness +/// @notice A simple contract harness used for deployment testing of the ConditionalDeployer. +contract ConditionalDeployer_Harness { uint256 public immutable value; constructor(uint256 _value) { @@ -21,13 +23,17 @@ contract SimpleContract { /// @title ConditionalDeployer_TestInit /// @notice Reusable test initialization for `ConditionalDeployer` tests. contract ConditionalDeployer_TestInit is Test { + // Test contracts ConditionalDeployer public conditionalDeployer; bytes public simpleContractCreationCode; function setUp() public { - vm.createSelectFork(Config.forkRpcUrl(), Config.forkBlockNumber()); + // Create fork + vm.createSelectFork(Config.forkRpcUrl()); + + // Deploy contracts conditionalDeployer = new ConditionalDeployer(); - simpleContractCreationCode = type(SimpleContract).creationCode; + simpleContractCreationCode = type(ConditionalDeployer_Harness).creationCode; } } @@ -41,66 +47,82 @@ contract ConditionalDeployer_Deploy_Test is ConditionalDeployer_TestInit { event ImplementationExists(address indexed implementation); /// @notice Tests that `deploy` succeeds and emits the correct event. - function testFuzz_deploy_succeeds(bytes32 _salt, uint256 _value) public { + function testFuzz_deploy_succeeds(address _caller, bytes32 _salt, uint256 _value) public { bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_value)); bytes32 codeHash = keccak256(_initCode); - address expectedImplementation = - ICreate2Deployer(payable(Preinstalls.Create2Deployer)).computeAddress(_salt, codeHash); + address expectedImplementation = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), conditionalDeployer.DETERMINISTIC_DEPLOYMENT_PROXY(), _salt, codeHash + ) + ) + ) + ) + ); vm.expectEmit(address(conditionalDeployer)); emit ImplementationDeployed(expectedImplementation, _salt); - vm.prank(Constants.DEPOSITOR_ACCOUNT); + vm.prank(_caller); address implementation = conditionalDeployer.deploy(0, _salt, _initCode); assertEq(implementation, expectedImplementation); - assertEq(SimpleContract(implementation).value(), _value); + assertEq(ConditionalDeployer_Harness(implementation).value(), _value); assert(implementation.code.length != 0); } - /// @notice Tests that `deploy` succeeds when called by `address(0)`. - function testFuzz_deploy_fromAddressZero_succeeds(bytes32 _salt, uint256 _value) public { + /// @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 _value) public { bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_value)); - vm.prank(address(0)); - address implementation = conditionalDeployer.deploy(0, _salt, _initCode); - - assertEq(SimpleContract(implementation).value(), _value); - assert(implementation.code.length != 0); - } + // First Deployment + bytes32 codeHash = keccak256(_initCode); + address expectedImplementation = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), conditionalDeployer.DETERMINISTIC_DEPLOYMENT_PROXY(), _salt, codeHash + ) + ) + ) + ) + ); - /// @notice Tests that `deploy` produces the same address when called multiple times. - function testFuzz_deploy_produces_same_address_succeeds(bytes32 _salt, uint256 _value) public { - bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(_value)); + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationDeployed(expectedImplementation, _salt); - vm.prank(Constants.DEPOSITOR_ACCOUNT); + vm.prank(_caller); address implementation1 = conditionalDeployer.deploy(0, _salt, _initCode); // Assert that the implementation was deployed assert(implementation1.code.length != 0); - // Attempt to deploy the same implementation again + // Second Deployment vm.expectEmit(address(conditionalDeployer)); emit ImplementationExists(implementation1); - vm.prank(Constants.DEPOSITOR_ACCOUNT); + vm.prank(_caller); address implementation2 = conditionalDeployer.deploy(0, _salt, _initCode); assertEq(implementation1, implementation2); } -} - -/// @title ConditionalDeployer_Deploy_TestFail -/// @notice Tests failure cases for the `deploy` function of the `ConditionalDeployer` contract. -contract ConditionalDeployer_Deploy_TestFail is ConditionalDeployer_TestInit { - /// @notice Tests that `deploy` reverts when called by an address other than the depositor account or address(0). - function testFuzz_deploy_when_not_authorized_reverts(address _sender) public { - vm.assume(_sender != Constants.DEPOSITOR_ACCOUNT && _sender != address(0)); + /// @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 _value) public { bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(0)); - vm.prank(_sender); - vm.expectRevert(ConditionalDeployer.UnauthorizedCaller.selector); - conditionalDeployer.deploy(0, bytes32(0), _initCode); + vm.mockCallRevert( + conditionalDeployer.DETERMINISTIC_DEPLOYMENT_PROXY(), _value, abi.encodePacked(_salt, _initCode), bytes("") + ); + + vm.prank(_caller); + vm.expectRevert( + abi.encodeWithSelector(ConditionalDeployer.ConditionalDeployer_DeploymentFailed.selector, bytes("")) + ); + conditionalDeployer.deploy(_value, _salt, _initCode); } } diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index 2a025cd5c09..e16c0005772 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 e157c1cff99..1054f4a5de8 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 daea37745ae..5bfbcd7f072 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 300bfea09d9..772b6a9bb91 100644 --- a/packages/contracts-bedrock/test/setup/FeatureFlags.sol +++ b/packages/contracts-bedrock/test/setup/FeatureFlags.sol @@ -55,6 +55,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 b83c68cf5ff..2bb4b1b6eb5 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);