diff --git a/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol b/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol new file mode 100644 index 0000000000000..34d09ae94941b --- /dev/null +++ b/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol @@ -0,0 +1,44 @@ +// 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/src/L2/ConditionalDeployer.sol b/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol new file mode 100644 index 0000000000000..549e014f798c5 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/ConditionalDeployer.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { ICreate2Deployer } from "interfaces/preinstalls/ICreate2Deployer.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +/// @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 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 caller is not authorized. + error UnauthorizedCaller(); + + /// @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 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); + + // Check if implementation already exists + if (implementation.code.length != 0) { + emit ImplementationExists(implementation); + return implementation; + } + + // Deploy the implementation + ICreate2Deployer(CREATE2_DEPLOYER).deploy(value, salt, code); + + emit ImplementationDeployed(implementation, salt); + return implementation; + } +} 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..a7c18682d3b7c --- /dev/null +++ b/packages/contracts-bedrock/test/L2/ConditionalDeployer.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { ConditionalDeployer } from "src/L2/ConditionalDeployer.sol"; +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 { + uint256 public immutable value; + + constructor(uint256 _value) { + value = _value; + } +} + +/// @title ConditionalDeployer_TestInit +/// @notice Reusable test initialization for `ConditionalDeployer` tests. +contract ConditionalDeployer_TestInit is Test { + ConditionalDeployer public conditionalDeployer; + bytes public simpleContractCreationCode; + + function setUp() public { + vm.createSelectFork(Config.forkRpcUrl(), Config.forkBlockNumber()); + conditionalDeployer = new ConditionalDeployer(); + simpleContractCreationCode = type(SimpleContract).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(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); + + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationDeployed(expectedImplementation, _salt); + + vm.prank(Constants.DEPOSITOR_ACCOUNT); + address implementation = conditionalDeployer.deploy(0, _salt, _initCode); + + assertEq(implementation, expectedImplementation); + assertEq(SimpleContract(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 { + 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); + } + + /// @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.prank(Constants.DEPOSITOR_ACCOUNT); + 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 + vm.expectEmit(address(conditionalDeployer)); + emit ImplementationExists(implementation1); + + vm.prank(Constants.DEPOSITOR_ACCOUNT); + 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)); + + bytes memory _initCode = abi.encodePacked(simpleContractCreationCode, abi.encode(0)); + + vm.prank(_sender); + vm.expectRevert(ConditionalDeployer.UnauthorizedCaller.selector); + conditionalDeployer.deploy(0, bytes32(0), _initCode); + } +}