diff --git a/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol b/packages/contracts-bedrock/interfaces/preinstalls/ICreate2Deployer.sol new file mode 100644 index 00000000000..34d09ae9494 --- /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/scripts/deploy/PredeployHelper.sol b/packages/contracts-bedrock/scripts/deploy/PredeployHelper.sol new file mode 100644 index 00000000000..1d0783fd4f8 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/PredeployHelper.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Script } from "forge-std/Script.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { console } from "forge-std/console.sol"; +import { Preinstalls } from "src/libraries/Preinstalls.sol"; +import { ICreate2Deployer } from "interfaces/preinstalls/ICreate2Deployer.sol"; +import { TransactionGeneration } from "scripts/deploy/TransactionGeneration.s.sol"; + +/// @title PredeployHelper +/// @notice Helper script for managing predeploy configurations during network upgrades. +/// This contract collects all predeploys that need to be deployed, computes their +/// CREATE2 addresses, and handles special cases requiring constructor arguments. +contract PredeployHelper is Script { + /// @notice Address of the Create2Deployer predeploy. + address payable immutable CREATE2_DEPLOYER = payable(Preinstalls.Create2Deployer); + + /// @notice Represents a predeploy contract to be deployed during a network upgrade. + /// @param proxy The address of the proxy contract that will be upgraded. + /// @param name The name of the predeploy contract. + /// @param initCode The initialization code (bytecode + constructor args) for deployment. + /// @param implementation The computed CREATE2 address where the implementation will be deployed. + struct Predeploy { + address proxy; + string name; + bytes initCode; + address implementation; + } + + /// @notice Array storing all predeploys to be deployed. + Predeploy[] private predeploys; + + /// @notice The fork version being deployed. + uint256 private fork; + + /// @notice Whether the CrossL2Inbox predeploy should be enabled. + bool private enableCrossL2Inbox; + + /// @notice Constructs a new PredeployHelper with the specified fork configuration. + /// @param _fork The fork version to deploy predeploys for. + /// @param _enableCrossL2Inbox Whether to enable the CrossL2Inbox predeploy. + constructor(uint256 _fork, bool _enableCrossL2Inbox) { + fork = _fork; + enableCrossL2Inbox = _enableCrossL2Inbox; + } + + /// @notice Collects all predeploys that need to be deployed for the configured fork. + /// @param _input The input struct containing chain configuration and deployment parameters. + /// @return Array of Predeploy structs containing deployment information for each predeploy. + function getPredeploys(TransactionGeneration.Input memory _input) external returns (Predeploy[] memory) { + uint160 prefix = uint160(0x420) << 148; + + for (uint256 i = 0; i < Predeploys.PREDEPLOY_COUNT; i++) { + address addr = address(prefix | uint160(i)); + // Skip if not supported or not proxied or needs constructor args + if (_needsConstructorArgs(addr)) { + continue; + } + _addPredeploy(addr, bytes("")); + } + + // Add predeploys with constructor arguments + _addPredeploysWithArgs(_input); + + // Copy storage array to memory for return + Predeploy[] memory result = new Predeploy[](predeploys.length); + for (uint256 i = 0; i < predeploys.length; i++) { + result[i] = predeploys[i]; + } + return result; + } + + /// @notice Adds a predeploy to the deployment list with optional constructor arguments. + /// @param _addr The proxy address of the predeploy contract. + /// @param _args ABI-encoded constructor arguments (empty bytes for no-arg constructors). + function _addPredeploy(address _addr, bytes memory _args) internal { + if (!Predeploys.isSupportedPredeploy(_addr, fork, enableCrossL2Inbox) || Predeploys.notProxied(_addr)) { + return; + } + string memory _name = Predeploys.getName(_addr); + bytes memory initCode = abi.encodePacked(vm.getCode(_name), _args); + bytes32 salt = keccak256(abi.encode(_name)); + address implementation = ICreate2Deployer(CREATE2_DEPLOYER).computeAddress(salt, keccak256(initCode)); + + predeploys.push(Predeploy({ proxy: _addr, name: _name, initCode: initCode, implementation: implementation })); + } + + /// @notice Checks if a predeploy requires constructor arguments or special handling. + /// @param _proxy The address of the proxy contract to check. + /// @return True if the predeploy requires constructor arguments, false otherwise. + function _needsConstructorArgs(address _proxy) private pure returns (bool) { + return _proxy == Predeploys.SEQUENCER_FEE_WALLET || _proxy == Predeploys.BASE_FEE_VAULT + || _proxy == Predeploys.L1_FEE_VAULT || _proxy == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY + || _proxy == Predeploys.PROXY_ADMIN; + } + + /// @notice Adds predeploys with constructor arguments to the deployment list. + /// @param _input The input struct containing configuration parameters. + function _addPredeploysWithArgs(TransactionGeneration.Input memory _input) internal { + // Add SequencerFeeVault + _addFeeVault( + Predeploys.SEQUENCER_FEE_WALLET, + _input.sequencerFeeVaultRecipient, + _input.sequencerFeeVaultMinimumWithdrawalAmount, + _input.sequencerFeeVaultWithdrawalNetwork + ); + // Add BaseFeeVault + _addFeeVault( + Predeploys.BASE_FEE_VAULT, + _input.baseFeeVaultRecipient, + _input.baseFeeVaultMinimumWithdrawalAmount, + _input.baseFeeVaultWithdrawalNetwork + ); + // Add L1FeeVault + _addFeeVault( + Predeploys.L1_FEE_VAULT, + _input.l1FeeVaultRecipient, + _input.l1FeeVaultMinimumWithdrawalAmount, + _input.l1FeeVaultWithdrawalNetwork + ); + // Add OptimismMintableERC721Factory + _addOptimismMintableERC721Factory(_input); + } + + /// @notice Adds a fee vault to the deployment list with its constructor arguments. + /// @param _feeVault The address of the fee vault contract to add. + /// @param _feeVaultRecipient The recipient of the fee vault. + /// @param _feeVaultMinimumWithdrawalAmount The minimum withdrawal amount for the fee vault. + /// @param _feeVaultWithdrawalNetwork The withdrawal network for the fee vault. + function _addFeeVault( + address _feeVault, + address _feeVaultRecipient, + uint256 _feeVaultMinimumWithdrawalAmount, + uint256 _feeVaultWithdrawalNetwork + ) + internal + { + _addPredeploy( + _feeVault, abi.encode(_feeVaultRecipient, _feeVaultMinimumWithdrawalAmount, _feeVaultWithdrawalNetwork) + ); + } + + /// @notice Adds the OptimismMintableERC721Factory predeploy with its constructor arguments + /// @param _input The input struct containing configuration parameters + function _addOptimismMintableERC721Factory(TransactionGeneration.Input memory _input) internal { + _addPredeploy( + Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY, abi.encode(_input.l1ERC721BridgeProxy, _input.l2ChainID) + ); + } +} diff --git a/packages/contracts-bedrock/scripts/deploy/TransactionGeneration.s.sol b/packages/contracts-bedrock/scripts/deploy/TransactionGeneration.s.sol new file mode 100644 index 00000000000..f8646765ef2 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/TransactionGeneration.s.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Script } from "forge-std/Script.sol"; +import { NetworkUpgradeTxns } from "src/libraries/NetworkUpgradeTxns.sol"; +import { L2ContractsManager } from "src/L2/L2ContractsManager.sol"; +import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; +import { Config, Fork } from "scripts/libraries/Config.sol"; +import { console2 as console } from "forge-std/console2.sol"; +import { PredeployHelper } from "scripts/deploy/PredeployHelper.sol"; +import { Preinstalls } from "src/libraries/Preinstalls.sol"; +import { ICreate2Deployer } from "interfaces/preinstalls/ICreate2Deployer.sol"; +import { L2ImplementationsDeployer } from "src/L2/L2ImplementationsDeployer.sol"; + +/// @title TransactionGenerationScript +/// @notice Script that generates Network Upgrade Transactions (NUTs) for deploying L2 contracts during a hard fork. +/// This script creates a sequence of transactions that deploy Predeploy contracts using CREATE2 and execute +/// and the L2ContractsManager. The last transaction is the execution of the L2ContractsManager. +contract TransactionGeneration is Script { + /// @notice Address of the Create2Deployer predeploy. + address payable immutable CREATE2_DEPLOYER = payable(Preinstalls.Create2Deployer); + + /// @notice Array of Network Upgrade Transactions. + NetworkUpgradeTxns.NetworkUpgradeTxn[] private txns; + + /// @notice Helper for managing predeploy configurations. + PredeployHelper internal helper; + + /// @notice Address of the L2ImplementationsDeployer contract. + address private l2ImplDeployerAddress; + + /// @notice Input struct for the script. + /// @param l2ChainID The ID of the L2 chain. + /// @param l1ChainID The ID of the L1 chain. + /// @param l1CrossDomainMessengerProxy The address of the L1 Cross Domain Messenger proxy. + /// @param l1StandardBridgeProxy The address of the L1 Standard Bridge proxy. + /// @param l1ERC721BridgeProxy The address of the L1 ERC721 Bridge proxy. + /// @param opChainProxyAdminOwner The address of the OP Chain Proxy Admin owner. + /// @param sequencerFeeVaultRecipient The address of the Sequencer Fee Vault recipient. + /// @param sequencerFeeVaultMinimumWithdrawalAmount The minimum withdrawal amount for the Sequencer Fee Vault. + /// @param sequencerFeeVaultWithdrawalNetwork The withdrawal network for the Sequencer Fee Vault. + /// @param baseFeeVaultRecipient The address of the Base Fee Vault recipient. + /// @param baseFeeVaultMinimumWithdrawalAmount The minimum withdrawal amount for the Base Fee Vault. + /// @param baseFeeVaultWithdrawalNetwork The withdrawal network for the Base Fee Vault. + /// @param l1FeeVaultRecipient The address of the L1 Fee Vault recipient. + /// @param l1FeeVaultMinimumWithdrawalAmount The minimum withdrawal amount for the L1 Fee Vault. + /// @param l1FeeVaultWithdrawalNetwork The withdrawal network for the L1 Fee Vault. + /// @param l2ImplDeployerAddress The address of the already-deployed L2ImplementationsDeployer. + /// @param l2cmName The name of the L2 Contracts Manager. + struct Input { + uint256 l2ChainID; + uint256 l1ChainID; + address payable l1CrossDomainMessengerProxy; + address payable l1StandardBridgeProxy; + address payable l1ERC721BridgeProxy; + address opChainProxyAdminOwner; + address sequencerFeeVaultRecipient; + uint256 sequencerFeeVaultMinimumWithdrawalAmount; + uint256 sequencerFeeVaultWithdrawalNetwork; + address baseFeeVaultRecipient; + uint256 baseFeeVaultMinimumWithdrawalAmount; + uint256 baseFeeVaultWithdrawalNetwork; + address l1FeeVaultRecipient; + uint256 l1FeeVaultMinimumWithdrawalAmount; + uint256 l1FeeVaultWithdrawalNetwork; + address l2ImplDeployerAddress; + string l2cmName; + } + + /// @notice Output struct for the script + /// @param txns Array of Network Upgrade Transactions generated + /// @param l2cmAddress Address where the L2ContractsManager is deployed + /// @param predeploys Array of predeploys that were changed + struct Output { + NetworkUpgradeTxns.NetworkUpgradeTxn[] txns; + address l2cmAddress; + PredeployHelper.Predeploy[] predeploys; + } + + /// @notice Generates Network Upgrade Transactions for deploying L2 contracts during a hard fork + /// @dev Creates a sequence of transactions that: + /// 1. Deploy new predeploy implementations via L2ImplementationsDeployer + /// 2. Deploy the L2ContractsManager via CREATE2 + /// 3. Execute the L2ContractsManager to upgrade all predeploy proxies + /// The final artifact is written to deployments/nut-xfork-upgrade-transactions.json + /// @param _input The input struct containing chain configuration and deployment parameters + /// @return Output struct containing the generated transactions, L2CM address, and changed predeploys + function run(Input memory _input) external returns (Output memory) { + helper = new PredeployHelper(uint256(Config.fork()), Config.fork() >= Fork.INTEROP); + + // Set the L2ImplementationsDeployer address from input + l2ImplDeployerAddress = _input.l2ImplDeployerAddress; + + // Get all changed predeploy implementations + PredeployHelper.Predeploy[] memory predeploys = helper.getPredeploys(_input); + + // Generate deployment transactions for each changed predeploy implementations + generateDeploymentTransactions(predeploys); + + // Generate the L2ContractsManager deployment transaction + generateL2ContractsManagerDeploymentTransaction(_input.l2cmName); + + // Generate L2ContractsManager execute transaction + address l2cmAddress = ICreate2Deployer(CREATE2_DEPLOYER).computeAddress( + keccak256(abi.encode(_input.l2cmName)), keccak256(vm.getCode(_input.l2cmName)) + ); + generateL2ContractsManagerExecuteTransaction(_input.l2cmName, l2cmAddress, predeploys); + + // Write all transactions to JSON artifact file + NetworkUpgradeTxns.writeArtifact(txns, "deployments/nut-xfork-upgrade-transactions.json"); + + return Output({ txns: txns, l2cmAddress: l2cmAddress, predeploys: predeploys }); + } + + /// @notice Generates deployment transactions for all changed predeploys using L2ImplementationsDeployer + /// @dev Each predeploy is deployed via the L2ImplementationsDeployer with a salt derived from its name + /// @param predeploys Array of predeploys that need to be deployed + function generateDeploymentTransactions(PredeployHelper.Predeploy[] memory predeploys) internal { + for (uint256 i = 0; i < predeploys.length; i++) { + txns.push( + NetworkUpgradeTxns.newTx({ + intent: string.concat("XFork: ", predeploys[i].name, " Deployment"), + from: address(0), + to: l2ImplDeployerAddress, + mint: 0, + value: 0, + gas: 1_000_000_000, + isSystemTransaction: false, + data: abi.encodeCall( + L2ImplementationsDeployer.deploy, + (0, keccak256(abi.encode(predeploys[i].name)), predeploys[i].initCode) + ) + }) + ); + } + } + + /// @notice Generates a deployment transaction for the L2ContractsManager using CREATE2 + /// @dev The L2ContractsManager is deployed via the Create2Deployer preinstall with a salt derived from its name + /// @param _l2cmName The name of the L2ContractsManager contract to deploy + function generateL2ContractsManagerDeploymentTransaction(string memory _l2cmName) internal { + // Generate the L2ContractsManager deployment transaction + txns.push( + NetworkUpgradeTxns.newTx({ + intent: string.concat("XFork: ", _l2cmName, " Deployment"), + from: address(0), + to: CREATE2_DEPLOYER, + mint: 0, + value: 0, + gas: 1_000_000, + isSystemTransaction: false, + data: abi.encodeCall(ICreate2Deployer.deploy, (0, keccak256(abi.encode(_l2cmName)), vm.getCode(_l2cmName))) + }) + ); + } + + /// @notice Generates a transaction that executes the L2ContractsManager via ProxyAdmin to upgrade predeploys + /// @dev The transaction calls ProxyAdmin.performDelegateCall to delegatecall into the L2ContractsManager, + /// which upgrades all predeploy proxies to their new implementations + /// @param _l2cmName The name of the L2ContractsManager contract + /// @param _l2cmAddress The address where the L2ContractsManager is deployed + /// @param predeploys Array of predeploys that were deployed and need to be upgraded + function generateL2ContractsManagerExecuteTransaction( + string memory _l2cmName, + address _l2cmAddress, + PredeployHelper.Predeploy[] memory predeploys + ) + internal + { + // Build the ProxyUpgrade array for the L2ContractsManager + L2ContractsManager.ProxyUpgrade[] memory proxyUpgrades = + new L2ContractsManager.ProxyUpgrade[](predeploys.length); + for (uint256 i = 0; i < predeploys.length; i++) { + proxyUpgrades[i] = L2ContractsManager.ProxyUpgrade({ + proxy: predeploys[i].proxy, + implementation: predeploys[i].implementation + }); + } + + // Create transaction that calls execute() on the deployed L2ContractsManager + txns.push( + NetworkUpgradeTxns.newTx({ + intent: string.concat("XFork: ", _l2cmName, " Execute"), + from: Constants.DEPOSITOR_ACCOUNT, + to: Predeploys.PROXY_ADMIN, + mint: 0, + value: 0, + gas: type(uint64).max, + isSystemTransaction: false, + data: abi.encodeCall(ProxyAdmin.performDelegateCall, (_l2cmAddress, proxyUpgrades)) + }) + ); + } +} diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index fffce12bb51..31e54a1a377 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -64,9 +64,9 @@ contract L1Block is ISemver { /// @notice The DA footprint gas scalar. uint16 public daFootprintGasScalar; - /// @custom:semver 1.7.0 + /// @custom:semver 1.8.0 function version() public pure virtual returns (string memory) { - return "1.7.0"; + return "1.8.0"; } /// @notice Returns the gas paying token, its decimals, name and symbol. diff --git a/packages/contracts-bedrock/src/L2/L2ContractsManager.sol b/packages/contracts-bedrock/src/L2/L2ContractsManager.sol new file mode 100644 index 00000000000..904f289a024 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2ContractsManager.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; +import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +/// @notice Base contract for L2 Contracts Manager, responsible for orquestrating the upgrades +/// of the L2 contracts during hardforks. +abstract contract L2ContractsManager { + /// @notice Struct representing the data for an upgrade. + struct ProxyUpgrade { + address proxy; + address implementation; + } + + /// @notice Executes the NUT with before/after hooks. + /// @param proxyUpgrades Data for the proxy upgrades. + /// @return Return data from the execution. + function execute(ProxyUpgrade[] memory proxyUpgrades) external returns (bytes memory) { + _beforeExecution(); + bytes memory returnData = _performUpgrades(proxyUpgrades); + _afterExecution(returnData); + return returnData; + } + + /// @notice Hook called before execution. + function _beforeExecution() internal virtual; + + /// @notice Hook called after execution. + /// @param returnData Data returned from execution. + function _afterExecution(bytes memory returnData) internal virtual; + + /// @notice Performs the proxy upgrades logic. + /// @param proxyUpgrades Data for the proxy upgrades. + /// @return returnData Return data from the execution. + function _performUpgrades(ProxyUpgrade[] memory proxyUpgrades) internal virtual returns (bytes memory returnData) { + for (uint256 i = 0; i < proxyUpgrades.length; i++) { + IProxy(payable(proxyUpgrades[i].proxy)).upgradeTo(proxyUpgrades[i].implementation); + } + + return abi.encode(true); + } +} diff --git a/packages/contracts-bedrock/src/L2/L2ImplementationsDeployer.sol b/packages/contracts-bedrock/src/L2/L2ImplementationsDeployer.sol new file mode 100644 index 00000000000..36024c20098 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2ImplementationsDeployer.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 L2ImplementationsDeployer +/// @notice Intermediary contract for deploying predeploy implementations during network upgrades. +contract L2ImplementationsDeployer { + /// @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/src/L2/XForkContractsManager.sol b/packages/contracts-bedrock/src/L2/XForkContractsManager.sol new file mode 100644 index 00000000000..d63187762ff --- /dev/null +++ b/packages/contracts-bedrock/src/L2/XForkContractsManager.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { L2ContractsManager } from "src/L2/L2ContractsManager.sol"; + +/// @title XForkContractsManager +/// @notice The XForkContractsManager is responsible for orquestrating the upgrades of the L2 contracts during xFork +/// hardforks. +contract XForkContractsManager is L2ContractsManager { + /// @notice Hook called before execution. + function _beforeExecution() internal override { } + + /// @notice Hook called after execution. + function _afterExecution(bytes memory returnData) internal override { } +} diff --git a/packages/contracts-bedrock/src/universal/ProxyAdmin.sol b/packages/contracts-bedrock/src/universal/ProxyAdmin.sol index 9e7cd908242..c78ba162011 100644 --- a/packages/contracts-bedrock/src/universal/ProxyAdmin.sol +++ b/packages/contracts-bedrock/src/universal/ProxyAdmin.sol @@ -13,6 +13,7 @@ import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IStaticERC1967Proxy } from "interfaces/universal/IStaticERC1967Proxy.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { L2ContractsManager } from "src/L2/L2ContractsManager.sol"; /// @title ProxyAdmin /// @notice This is an auxiliary contract meant to be assigned as the admin of an ERC1967 Proxy, @@ -191,4 +192,22 @@ contract ProxyAdmin is Ownable { require(success, "ProxyAdmin: call to proxy after upgrade failed"); } } + + /// @notice Performs a delegate call to the target contract. + /// @param _target Address of the target contract. + /// @param proxyUpgrades Data for the proxy upgrades. + function performDelegateCall( + address _target, + L2ContractsManager.ProxyUpgrade[] memory proxyUpgrades + ) + external + payable + returns (bytes memory) + { + require(msg.sender == Constants.DEPOSITOR_ACCOUNT || msg.sender == owner(), "not allowed"); + (bool success, bytes memory returnData) = + _target.delegatecall(abi.encodeCall(L2ContractsManager.execute, (proxyUpgrades))); + require(success, "ProxyAdmin: delegatecall to target failed"); + return returnData; + } } diff --git a/packages/contracts-bedrock/test/L2/TransactionGeneration.t.sol b/packages/contracts-bedrock/test/L2/TransactionGeneration.t.sol new file mode 100644 index 00000000000..32a086cadac --- /dev/null +++ b/packages/contracts-bedrock/test/L2/TransactionGeneration.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { TransactionGeneration } from "scripts/deploy/TransactionGeneration.s.sol"; +import { Config } from "scripts/libraries/Config.sol"; +import { NetworkUpgradeTxns } from "src/libraries/NetworkUpgradeTxns.sol"; +import { L1Block } from "src/L2/L1Block.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { Constants } from "src/libraries/Constants.sol"; +import { XForkContractsManager } from "src/L2/XForkContractsManager.sol"; +import { L2ContractsManager } from "src/L2/L2ContractsManager.sol"; +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; + +contract TransactionGenerationTest is Test { + TransactionGeneration public transactionGeneration; + + /// @notice Address where L2ImplementationsDeployer is etched + address constant L2_IMPLEMENTATIONS_DEPLOYER = 0x4200000000000000000000000000000000000420; + + function setUp() public { + vm.createSelectFork(Config.forkRpcUrl()); + transactionGeneration = new TransactionGeneration(); + + // etch the L2ProxyAdmin + vm.etch(Predeploys.PROXY_ADMIN, vm.getDeployedCode("ProxyAdmin.sol:ProxyAdmin")); + + // etch the L2ImplementationsDeployer + vm.etch( + L2_IMPLEMENTATIONS_DEPLOYER, vm.getDeployedCode("L2ImplementationsDeployer.sol:L2ImplementationsDeployer") + ); + } + + function _getInput() internal view returns (TransactionGeneration.Input memory) { + return TransactionGeneration.Input({ + l2ChainID: block.chainid, + l1ChainID: 1, + l1CrossDomainMessengerProxy: payable(0x42000000000000000000000000000000000000F9), + l1StandardBridgeProxy: payable(0x42000000000000000000000000000000000000f8), + l1ERC721BridgeProxy: payable(0x4200000000000000000000000000000000000060), + opChainProxyAdminOwner: 0x0000000000000000000000000000000000000222, + sequencerFeeVaultRecipient: 0x42000000000000000000000000000000000000F7, + sequencerFeeVaultMinimumWithdrawalAmount: 0x8ac7230489e80000, + sequencerFeeVaultWithdrawalNetwork: 1, + baseFeeVaultRecipient: 0x42000000000000000000000000000000000000f5, + baseFeeVaultMinimumWithdrawalAmount: 0x8ac7230489e80000, + baseFeeVaultWithdrawalNetwork: 0, + l1FeeVaultRecipient: 0x42000000000000000000000000000000000000f6, + l1FeeVaultMinimumWithdrawalAmount: 0x8ac7230489e80000, + l1FeeVaultWithdrawalNetwork: 1, + l2ImplDeployerAddress: L2_IMPLEMENTATIONS_DEPLOYER, + l2cmName: "XForkContractsManager" + }); + } + + /// @notice Test that the upgrade transactions defined in the XForkContractsManager succeed. + function test_upgradeTransactions_succeeds() public { + TransactionGeneration.Output memory output = transactionGeneration.run(_getInput()); + + // The test expects at least 3 transactions: + // 1+ predeploy deployments + // 1 L2ContractsManager deployment + // 1 L2ContractsManager execution + assertGe(output.txns.length, 3, "Should have at least 3 transactions"); + + // Execute all transactions + for (uint256 i = 0; i < output.txns.length; i++) { + vm.prank(output.txns[i].from); + (bool success,) = + output.txns[i].to.call{ value: output.txns[i].value, gas: output.txns[i].gas }(output.txns[i].data); + assertTrue(success, string.concat("Transaction ", vm.toString(i), " should succeed")); + } + + // At this point the L1Block should have been upgraded to v1.8.0 + assertEq(L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).version(), "1.8.0"); + } + + /// @notice Test that the upgrade transaction structure is correct. + function test_upgradeTransactions_transactionStructure_succeeds() public { + TransactionGeneration.Output memory output = transactionGeneration.run(_getInput()); + + // Verify we have at least 3 transactions + assertGe(output.txns.length, 3, "Should have at least 3 transactions"); + + _verifyL2CMDeployment(output.txns); + _verifyExecuteTransaction(output.txns); + _verifyPredeployTransactions(output.txns); + _verifyProxyUpgradesMatch(output.txns); + } + + function _verifyL2CMDeployment(NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns) internal pure { + // Second to last transaction: L2ContractsManager deployment via CREATE2 + uint256 l2cmIndex = txns.length - 2; + assertEq(txns[l2cmIndex].from, address(0), "L2ContractsManager deployment should be from address(0)"); + assertEq(txns[l2cmIndex].value, 0, "L2ContractsManager deployment should have 0 value"); + assertEq(txns[l2cmIndex].mint, 0, "L2ContractsManager deployment should have 0 mint"); + assertFalse(txns[l2cmIndex].isSystemTransaction, "L2ContractsManager deployment should not be a system tx"); + } + + function _verifyExecuteTransaction(NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns) internal pure { + // Last transaction: Execute upgrade via ProxyAdmin + uint256 lastIndex = txns.length - 1; + assertEq(txns[lastIndex].from, Constants.DEPOSITOR_ACCOUNT, "Execute should be from DEPOSITOR_ACCOUNT"); + assertEq(txns[lastIndex].to, Predeploys.PROXY_ADMIN, "Execute should target PROXY_ADMIN"); + assertEq(txns[lastIndex].value, 0, "Execute should have 0 value"); + assertEq(txns[lastIndex].mint, 0, "Execute should have 0 mint"); + assertFalse(txns[lastIndex].isSystemTransaction, "Execute should not be a system tx"); + } + + function _verifyPredeployTransactions(NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns) internal pure { + // All predeploy deployment transactions (all except last 2) should follow the same pattern + // Index 0 to length-3: Predeploy deployments + // Index length-2: L2ContractsManager deployment + // Index length-1: Execute transaction + for (uint256 i = 0; i < txns.length - 2; i++) { + assertEq(txns[i].from, address(0), "Predeploy deployment should be from address(0)"); + assertEq(txns[i].value, 0, "Predeploy deployment should have 0 value"); + assertEq(txns[i].mint, 0, "Predeploy deployment should have 0 mint"); + assertFalse(txns[i].isSystemTransaction, "Predeploy deployment should not be a system tx"); + } + } + + function _verifyProxyUpgradesMatch(NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns) internal pure { + bytes memory callData = txns[txns.length - 1].data; + + // Extract function selector (first 4 bytes) + bytes4 selector; + assembly { + selector := mload(add(callData, 32)) + } + + // Verify the function selector is correct + assertEq(selector, ProxyAdmin.performDelegateCall.selector); + + // Decode the parameters + (, L2ContractsManager.ProxyUpgrade[] memory proxyUpgrades) = _decodeProxyUpgrades(callData); + + // Assert that counts match (subtract 2: L2ContractsManager, Execute) + assertEq( + txns.length - 2, + proxyUpgrades.length, + "Number of predeploy deployments should match ProxyUpgrade array length" + ); + } + + function _decodeProxyUpgrades(bytes memory callData) + internal + pure + returns (address, L2ContractsManager.ProxyUpgrade[] memory) + { + // Create new bytes array without selector for decoding + bytes memory params = new bytes(callData.length - 4); + for (uint256 i = 0; i < params.length; i++) { + params[i] = callData[i + 4]; + } + + return abi.decode(params, (address, L2ContractsManager.ProxyUpgrade[])); + } +}