diff --git a/packages/contracts-bedrock/.gitignore b/packages/contracts-bedrock/.gitignore index 1e2b6f844e2..bf0d1590660 100644 --- a/packages/contracts-bedrock/.gitignore +++ b/packages/contracts-bedrock/.gitignore @@ -35,6 +35,7 @@ deployments/kontrol.jsonReversed deployments/kontrol-fp.json deployments/kontrol-fp.jsonReversed deployments/1-deploy.json +deployments/nut-*.json 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..5e2e356c01d --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/PredeployHelper.sol @@ -0,0 +1,165 @@ +// 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[17] private predeploys; + + /// @notice Constructor for the PredeployHelper. + constructor() { + predeploys[0].proxy = Predeploys.LEGACY_MESSAGE_PASSER; // 0: LegacyMessagePasser + predeploys[1].proxy = Predeploys.DEPLOYER_WHITELIST; // 1: DeployerWhitelist + predeploys[2].proxy = Predeploys.L2_CROSS_DOMAIN_MESSENGER; // 2: L2CrossDomainMessenger + predeploys[3].proxy = Predeploys.GAS_PRICE_ORACLE; // 3: GasPriceOracle + predeploys[4].proxy = Predeploys.L2_STANDARD_BRIDGE; // 4: L2StandardBridge + predeploys[5].proxy = Predeploys.SEQUENCER_FEE_WALLET; // 5: SequencerFeeWallet + predeploys[6].proxy = Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY; // 6: OptimismMintableERC20Factory + predeploys[7].proxy = Predeploys.L1_BLOCK_NUMBER; // 7: L1BlockNumber + predeploys[8].proxy = Predeploys.L2_ERC721_BRIDGE; // 8: L2ERC721Bridge + predeploys[9].proxy = Predeploys.L1_BLOCK_ATTRIBUTES; // 9: L1BlockAttributes + predeploys[10].proxy = Predeploys.L2_TO_L1_MESSAGE_PASSER; // 10: L2ToL1MessagePasser + predeploys[11].proxy = Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY; // 11: OptimismMintableERC721Factory + predeploys[12].proxy = Predeploys.BASE_FEE_VAULT; // 12: BaseFeeVault + predeploys[13].proxy = Predeploys.L1_FEE_VAULT; // 13: L1FeeVault + predeploys[14].proxy = Predeploys.OPERATOR_FEE_VAULT; // 14: OperatorFeeVault + predeploys[15].proxy = Predeploys.SCHEMA_REGISTRY; // 15: SchemaRegistry + predeploys[16].proxy = Predeploys.EAS; // 16: EAS + } + + /// @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) { + for (uint256 i = 0; i < predeploys.length; i++) { + // Skip if not supported or not proxied or needs constructor args + if (_needsConstructorArgs(predeploys[i].proxy)) { + _addPredeploysWithArgs(_input, i, predeploys[i].proxy); + } else { + _addPredeploy(i, predeploys[i].proxy, bytes("")); + } + } + + // 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(uint256 _index, address _addr, bytes memory _args) internal { + 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[_index] = + 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; + } + + /// @notice Adds predeploys with constructor arguments to the deployment list. + /// @param _input The input struct containing configuration parameters. + function _addPredeploysWithArgs( + TransactionGeneration.Input memory _input, + uint256 _index, + address _proxy + ) + internal + { + if (_proxy == Predeploys.SEQUENCER_FEE_WALLET) { + _addFeeVault( + _index, + Predeploys.SEQUENCER_FEE_WALLET, + _input.sequencerFeeVaultRecipient, + _input.sequencerFeeVaultMinimumWithdrawalAmount, + _input.sequencerFeeVaultWithdrawalNetwork + ); + } else if (_proxy == Predeploys.BASE_FEE_VAULT) { + _addFeeVault( + _index, + Predeploys.BASE_FEE_VAULT, + _input.baseFeeVaultRecipient, + _input.baseFeeVaultMinimumWithdrawalAmount, + _input.baseFeeVaultWithdrawalNetwork + ); + } else if (_proxy == Predeploys.L1_FEE_VAULT) { + _addFeeVault( + _index, + Predeploys.L1_FEE_VAULT, + _input.l1FeeVaultRecipient, + _input.l1FeeVaultMinimumWithdrawalAmount, + _input.l1FeeVaultWithdrawalNetwork + ); + } else if (_proxy == Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY) { + _addOptimismMintableERC721Factory(_index, _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( + uint256 _index, + address _feeVault, + address _feeVaultRecipient, + uint256 _feeVaultMinimumWithdrawalAmount, + uint256 _feeVaultWithdrawalNetwork + ) + internal + { + _addPredeploy( + _index, + _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(uint256 _index, TransactionGeneration.Input memory _input) internal { + _addPredeploy( + _index, + 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..28374c74941 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/TransactionGeneration.s.sol @@ -0,0 +1,231 @@ +// 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; + string hardForkName; + } + + /// @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(); + + // 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(_input.hardForkName, predeploys); + + address[] memory predeploysAddresses = new address[](predeploys.length); + for (uint256 i = 0; i < predeploysAddresses.length; i++) { + predeploysAddresses[i] = predeploys[i].implementation; + } + // Generate the L2ContractsManager deployment transaction + address l2cmAddress = generateL2ContractsManagerDeploymentTransaction(_input, predeploysAddresses); + + // Generate L2ContractsManager execute transaction + generateL2ContractsManagerExecuteTransaction(_input.hardForkName, _input.l2cmName, l2cmAddress, predeploys); + + // Write all transactions to JSON artifact file + NetworkUpgradeTxns.writeArtifact( + txns, string.concat("deployments/nut-", _input.hardForkName, "-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( + string memory _hardForkName, + PredeployHelper.Predeploy[] memory predeploys + ) + internal + { + for (uint256 i = 0; i < predeploys.length; i++) { + txns.push( + NetworkUpgradeTxns.newTx({ + intent: string.concat(_hardForkName, 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 L2ImplementationsDeployer + /// @dev The L2ContractsManager is deployed via the L2ImplementationsDeployer with a salt derived from its name + function generateL2ContractsManagerDeploymentTransaction( + TransactionGeneration.Input memory _input, + address[] memory predeploysAddresses + ) + internal + returns (address l2cmAddress) + { + bytes memory constructorArgs = _encodeL2CMConstructorArgs(predeploysAddresses); + bytes memory initCode = abi.encodePacked(vm.getCode(_input.l2cmName), constructorArgs); + bytes32 salt = keccak256(abi.encode(_input.hardForkName, _input.l2cmName)); + + l2cmAddress = ICreate2Deployer(CREATE2_DEPLOYER).computeAddress(salt, keccak256(initCode)); + + // Generate the L2ContractsManager deployment transaction + txns.push( + NetworkUpgradeTxns.newTx({ + intent: string.concat(_input.hardForkName, ": ", _input.l2cmName, " Deployment"), + from: address(0), + to: l2ImplDeployerAddress, + mint: 0, + value: 0, + gas: 1_000_000, + isSystemTransaction: false, + data: abi.encodeCall(L2ImplementationsDeployer.deploy, (0, salt, initCode)) + }) + ); + } + + /// @notice Helper function to encode constructor arguments for L2ContractsManager + /// @dev Uses assembly to avoid stack too deep errors + function _encodeL2CMConstructorArgs(address[] memory addrs) internal pure returns (bytes memory result) { + result = new bytes(32 * 17); + assembly { + let ptr := add(result, 32) + for { let i := 0 } lt(i, 17) { i := add(i, 1) } { + let addr := mload(add(add(addrs, 32), mul(i, 32))) + mstore(add(ptr, mul(i, 32)), addr) + } + } + } + + /// @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 _hardForkName The name of the hard fork + /// @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 _hardForkName, + 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(_hardForkName, ": ", _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)) + }) + ); + } +} diff --git a/packages/contracts-bedrock/src/L2/L2ContractsManager.sol b/packages/contracts-bedrock/src/L2/L2ContractsManager.sol new file mode 100644 index 00000000000..5d48cc22b4d --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2ContractsManager.sol @@ -0,0 +1,34 @@ +// 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. + function execute() external { + _beforeExecution(); + _performUpgrades(); + _afterExecution(); + } + + /// @notice Hook called before execution. + function _beforeExecution() internal virtual; + + /// @notice Hook called after execution. + function _afterExecution() internal virtual; + + /// @notice Performs the proxy upgrades logic. + function _performUpgrades() internal virtual { } +} 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..2edf5981988 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/XForkContractsManager.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { L2ContractsManager } from "src/L2/L2ContractsManager.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IProxy } from "interfaces/universal/IProxy.sol"; + +/// @title XForkContractsManager +/// @notice The XForkContractsManager is responsible for orquestrating the upgrades of the L2 contracts during xFork +/// hardforks. +contract XForkContractsManager is L2ContractsManager { + /// @notice Configuration for all L2 predeploy implementation addresses. + /// @param legacyMessagePasserImplementation Implementation for LegacyMessagePasser. + /// @param deployerWhitelistImplementation Implementation for DeployerWhitelist. + /// @param l2CrossDomainMessengerImplementation Implementation for L2CrossDomainMessenger. + /// @param gasPriceOracleImplementation Implementation for GasPriceOracle. + /// @param l2StandardBridgeImplementation Implementation for L2StandardBridge. + /// @param sequencerFeeWalletImplementation Implementation for SequencerFeeWallet. + /// @param optimismMintableERC20FactoryImplementation Implementation for OptimismMintableERC20Factory. + /// @param l1BlockNumberImplementation Implementation for L1BlockNumber. + /// @param l2ERC721BridgeImplementation Implementation for L2ERC721Bridge. + /// @param l1BlockAttributesImplementation Implementation for L1Block. + /// @param l2ToL1MessagePasserImplementation Implementation for L2ToL1MessagePasser. + /// @param optimismMintableERC721FactoryImplementation Implementation for OptimismMintableERC721Factory. + /// @param baseFeeVaultImplementation Implementation for BaseFeeVault. + /// @param l1FeeVaultImplementation Implementation for L1FeeVault. + /// @param operatorFeeVaultImplementation Implementation for OperatorFeeVault. + /// @param schemaRegistryImplementation Implementation for SchemaRegistry. + /// @param easImplementation Implementation for EAS. + struct Input { + address legacyMessagePasserImplementation; + address deployerWhitelistImplementation; + address l2CrossDomainMessengerImplementation; + address gasPriceOracleImplementation; + address l2StandardBridgeImplementation; + address sequencerFeeWalletImplementation; + address optimismMintableERC20FactoryImplementation; + address l1BlockNumberImplementation; + address l2ERC721BridgeImplementation; + address l1BlockAttributesImplementation; + address l2ToL1MessagePasserImplementation; + address optimismMintableERC721FactoryImplementation; + address baseFeeVaultImplementation; + address l1FeeVaultImplementation; + address operatorFeeVaultImplementation; + address schemaRegistryImplementation; + address easImplementation; + } + + /// @notice Implementation address for LegacyMessagePasser. + address internal immutable LEGACY_MESSAGE_PASSER_IMPLEMENTATION; + + /// @notice Implementation address for DeployerWhitelist. + address internal immutable DEPLOYER_WHITELIST_IMPLEMENTATION; + + /// @notice Implementation address for L2CrossDomainMessenger. + address internal immutable L2_CROSS_DOMAIN_MESSENGER_IMPLEMENTATION; + + /// @notice Implementation address for GasPriceOracle. + address internal immutable GAS_PRICE_ORACLE_IMPLEMENTATION; + + /// @notice Implementation address for L2StandardBridge. + address internal immutable L2_STANDARD_BRIDGE_IMPLEMENTATION; + + /// @notice Implementation address for SequencerFeeWallet. + address internal immutable SEQUENCER_FEE_WALLET_IMPLEMENTATION; + + /// @notice Implementation address for OptimismMintableERC20Factory. + address internal immutable OPTIMISM_MINTABLE_ERC20_FACTORY_IMPLEMENTATION; + + /// @notice Implementation address for L1BlockNumber. + address internal immutable L1_BLOCK_NUMBER_IMPLEMENTATION; + + /// @notice Implementation address for L2ERC721Bridge. + address internal immutable L2_ERC721_BRIDGE_IMPLEMENTATION; + + /// @notice Implementation address for L1BlockAttributes. + address internal immutable L1_BLOCK_ATTRIBUTES_IMPLEMENTATION; + + /// @notice Implementation address for L2ToL1MessagePasser. + address internal immutable L2_TO_L1_MESSAGE_PASSER_IMPLEMENTATION; + + /// @notice Implementation address for OptimismMintableERC721Factory. + address internal immutable OPTIMISM_MINTABLE_ERC721_FACTORY_IMPLEMENTATION; + + /// @notice Implementation address for BaseFeeVault. + address internal immutable BASE_FEE_VAULT_IMPLEMENTATION; + + /// @notice Implementation address for L1FeeVault. + address internal immutable L1_FEE_VAULT_IMPLEMENTATION; + + /// @notice Implementation address for OperatorFeeVault. + address internal immutable OPERATOR_FEE_VAULT_IMPLEMENTATION; + + /// @notice Implementation address for SchemaRegistry. + address internal immutable SCHEMA_REGISTRY_IMPLEMENTATION; + + /// @notice Implementation address for EAS. + address internal immutable EAS_IMPLEMENTATION; + + /// @notice Constructs the XForkContractsManager with implementation addresses. + /// @dev Reverts if any implementation address is zero. + /// @param _input Configuration containing all implementation addresses. + constructor(Input memory _input) { + LEGACY_MESSAGE_PASSER_IMPLEMENTATION = _input.legacyMessagePasserImplementation; + DEPLOYER_WHITELIST_IMPLEMENTATION = _input.deployerWhitelistImplementation; + L2_CROSS_DOMAIN_MESSENGER_IMPLEMENTATION = _input.l2CrossDomainMessengerImplementation; + GAS_PRICE_ORACLE_IMPLEMENTATION = _input.gasPriceOracleImplementation; + L2_STANDARD_BRIDGE_IMPLEMENTATION = _input.l2StandardBridgeImplementation; + SEQUENCER_FEE_WALLET_IMPLEMENTATION = _input.sequencerFeeWalletImplementation; + OPTIMISM_MINTABLE_ERC20_FACTORY_IMPLEMENTATION = _input.optimismMintableERC20FactoryImplementation; + L1_BLOCK_NUMBER_IMPLEMENTATION = _input.l1BlockNumberImplementation; + L2_ERC721_BRIDGE_IMPLEMENTATION = _input.l2ERC721BridgeImplementation; + L1_BLOCK_ATTRIBUTES_IMPLEMENTATION = _input.l1BlockAttributesImplementation; + L2_TO_L1_MESSAGE_PASSER_IMPLEMENTATION = _input.l2ToL1MessagePasserImplementation; + OPTIMISM_MINTABLE_ERC721_FACTORY_IMPLEMENTATION = _input.optimismMintableERC721FactoryImplementation; + BASE_FEE_VAULT_IMPLEMENTATION = _input.baseFeeVaultImplementation; + L1_FEE_VAULT_IMPLEMENTATION = _input.l1FeeVaultImplementation; + OPERATOR_FEE_VAULT_IMPLEMENTATION = _input.operatorFeeVaultImplementation; + SCHEMA_REGISTRY_IMPLEMENTATION = _input.schemaRegistryImplementation; + EAS_IMPLEMENTATION = _input.easImplementation; + } + + /// @notice Hook called before execution. + function _beforeExecution() internal override { } + + /// @notice Hook called after execution. + function _afterExecution() internal override { } + + /// @notice Performs upgrades for all L2 predeploy contracts. + function _performUpgrades() internal override { + IProxy(payable(Predeploys.LEGACY_MESSAGE_PASSER)).upgradeTo(LEGACY_MESSAGE_PASSER_IMPLEMENTATION); + IProxy(payable(Predeploys.DEPLOYER_WHITELIST)).upgradeTo(DEPLOYER_WHITELIST_IMPLEMENTATION); + IProxy(payable(Predeploys.L2_CROSS_DOMAIN_MESSENGER)).upgradeTo(L2_CROSS_DOMAIN_MESSENGER_IMPLEMENTATION); + IProxy(payable(Predeploys.GAS_PRICE_ORACLE)).upgradeTo(GAS_PRICE_ORACLE_IMPLEMENTATION); + IProxy(payable(Predeploys.L2_STANDARD_BRIDGE)).upgradeTo(L2_STANDARD_BRIDGE_IMPLEMENTATION); + IProxy(payable(Predeploys.SEQUENCER_FEE_WALLET)).upgradeTo(SEQUENCER_FEE_WALLET_IMPLEMENTATION); + IProxy(payable(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY)).upgradeTo( + OPTIMISM_MINTABLE_ERC20_FACTORY_IMPLEMENTATION + ); + IProxy(payable(Predeploys.L1_BLOCK_NUMBER)).upgradeTo(L1_BLOCK_NUMBER_IMPLEMENTATION); + IProxy(payable(Predeploys.L2_ERC721_BRIDGE)).upgradeTo(L2_ERC721_BRIDGE_IMPLEMENTATION); + IProxy(payable(Predeploys.L1_BLOCK_ATTRIBUTES)).upgradeTo(L1_BLOCK_ATTRIBUTES_IMPLEMENTATION); + IProxy(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)).upgradeTo(L2_TO_L1_MESSAGE_PASSER_IMPLEMENTATION); + IProxy(payable(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY)).upgradeTo( + OPTIMISM_MINTABLE_ERC721_FACTORY_IMPLEMENTATION + ); + IProxy(payable(Predeploys.BASE_FEE_VAULT)).upgradeTo(BASE_FEE_VAULT_IMPLEMENTATION); + IProxy(payable(Predeploys.L1_FEE_VAULT)).upgradeTo(L1_FEE_VAULT_IMPLEMENTATION); + IProxy(payable(Predeploys.OPERATOR_FEE_VAULT)).upgradeTo(OPERATOR_FEE_VAULT_IMPLEMENTATION); + IProxy(payable(Predeploys.SCHEMA_REGISTRY)).upgradeTo(SCHEMA_REGISTRY_IMPLEMENTATION); + IProxy(payable(Predeploys.EAS)).upgradeTo(EAS_IMPLEMENTATION); + } +} diff --git a/packages/contracts-bedrock/src/libraries/NetworkUpgradeTxns.sol b/packages/contracts-bedrock/src/libraries/NetworkUpgradeTxns.sol new file mode 100644 index 00000000000..073071a8cc8 --- /dev/null +++ b/packages/contracts-bedrock/src/libraries/NetworkUpgradeTxns.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Vm } from "forge-std/Vm.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import { console } from "forge-std/console.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; + +/// @title NetworkUpgradeTxns +/// @notice Standard library for generating Network Upgrade Transaction (NUT) artifacts. +/// Provides minimal interface to create DepositTx-compatible transaction metadata. +library NetworkUpgradeTxns { + using stdJson for string; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + /// @notice Source domain for upgrade transactions + uint64 internal constant UPGRADE_DEPOSIT_SOURCE_DOMAIN = 2; + + /// @notice Represents a single Network Upgrade Transaction + /// Maps to the fields of the `DepositTx` struct defined in + /// https://github.com/ethereum-optimism/op-geth/blob/optimism/core/types/deposit_tx.go + struct NetworkUpgradeTxn { + bytes data; + address from; + uint64 gas; + bool isSystemTransaction; + uint256 mint; + bytes32 sourceHash; + address to; + uint256 value; + } + + /// @notice Create an upgrade transaction + /// @param intent Human-readable intent + /// @param from Sender address + /// @param to Target address + /// @param mint Mint amount + /// @param value Value to send + /// @param gas Gas limit + /// @param isSystemTransaction Whether this is a system transaction + /// @param data Transaction data + /// @return Upgrade transaction struct + function newTx( + string memory intent, + address from, + address to, + uint256 mint, + uint256 value, + uint64 gas, + bool isSystemTransaction, + bytes memory data + ) + internal + pure + returns (NetworkUpgradeTxn memory) + { + return NetworkUpgradeTxn({ + sourceHash: sourceHash(intent), + from: from, + to: to, + mint: mint, + value: value, + gas: gas, + isSystemTransaction: isSystemTransaction, + data: data + }); + } + + /// @notice Calculate source hash for an upgrade transaction + /// @param intent Human-readable intent string + /// @return Source hash + function sourceHash(string memory intent) internal pure returns (bytes32) { + bytes32 intentHash = keccak256(bytes(intent)); + bytes memory domainInput = new bytes(64); + + assembly { + mstore(add(domainInput, 56), shl(192, UPGRADE_DEPOSIT_SOURCE_DOMAIN)) + mstore(add(domainInput, 64), intentHash) + } + + return keccak256(domainInput); + } + + /// @notice Write transactions array to JSON file + /// @param txns Array of upgrade transactions + /// @param outputPath File path for output JSON + function writeArtifact(NetworkUpgradeTxn[] memory txns, string memory outputPath) internal { + string memory finalJson = "["; + + for (uint256 i = 0; i < txns.length; i++) { + string memory txnJson = serializeTxn(txns[i], i); + finalJson = string.concat(finalJson, txnJson); + if (i < txns.length - 1) { + finalJson = string.concat(finalJson, ","); + } + } + + finalJson = string.concat(finalJson, "]"); + + // Write the final serialized JSON array to file + vm.writeJson(finalJson, outputPath); + } + + /// @notice Serialize a single transaction to JSON + /// @param txn Transaction to serialize + /// @param index Transaction index + /// @return JSON string + function serializeTxn(NetworkUpgradeTxn memory txn, uint256 index) internal returns (string memory) { + string memory key = vm.toString(index); + + vm.serializeBytes32(key, "sourceHash", txn.sourceHash); + vm.serializeAddress(key, "from", txn.from); + vm.serializeAddress(key, "to", txn.to); + vm.serializeUint(key, "mint", txn.mint); + vm.serializeUint(key, "value", txn.value); + vm.serializeUint(key, "gas", uint256(txn.gas)); + vm.serializeBool(key, "isSystemTransaction", txn.isSystemTransaction); + return vm.serializeBytes(key, "data", txn.data); + } + + /// @notice Helper function to read upgrade transactions from JSON file + /// @param _inputPath File path for input JSON + /// @return Array of upgrade transactions + function readArtifact(string memory _inputPath) + internal + view + returns (NetworkUpgradeTxns.NetworkUpgradeTxn[] memory) + { + string memory json = vm.readFile(_inputPath); + bytes memory parsedData = vm.parseJson(json); + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = + abi.decode(parsedData, (NetworkUpgradeTxns.NetworkUpgradeTxn[])); + return txns; + } +} diff --git a/packages/contracts-bedrock/src/universal/ProxyAdmin.sol b/packages/contracts-bedrock/src/universal/ProxyAdmin.sol index 9e7cd908242..50210c3287e 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,12 @@ 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. + function performDelegateCall(address _target) external payable { + require(msg.sender == Constants.DEPOSITOR_ACCOUNT || msg.sender == owner(), "not allowed"); + (bool success,) = _target.delegatecall(abi.encodeCall(L2ContractsManager.execute, ())); + require(success, "ProxyAdmin: delegatecall to target failed"); + } } diff --git a/packages/contracts-bedrock/test/libraries/NetworkUpgradeTxns.t.sol b/packages/contracts-bedrock/test/libraries/NetworkUpgradeTxns.t.sol new file mode 100644 index 00000000000..10e0f33d670 --- /dev/null +++ b/packages/contracts-bedrock/test/libraries/NetworkUpgradeTxns.t.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Interfaces +import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Libraries +import { NetworkUpgradeTxns } from "src/libraries/NetworkUpgradeTxns.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @title NetworkUpgradeTxns_TestInit +/// @notice Reusable test initialization for `NetworkUpgradeTxns` tests. +abstract contract NetworkUpgradeTxns_TestInit is Test { + // Test constants matching Go implementation + address constant L1_BLOCK_DEPLOYER = 0x4210000000000000000000000000000000000000; + address constant GAS_PRICE_ORACLE_DEPLOYER = 0x4210000000000000000000000000000000000001; + address constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + + // Known source hashes from Ecotone upgrade (ecotone_upgrade_transactions_test.go:14-45) + bytes32 constant DEPLOY_L1_BLOCK_HASH = 0x877a6077205782ea15a6dc8699fa5ebcec5e0f4389f09cb8eda09488231346f8; + bytes32 constant DEPLOY_GAS_PRICE_ORACLE_HASH = 0xa312b4510adf943510f05fcc8f15f86995a5066bd83ce11384688ae20e6ecf42; + bytes32 constant UPDATE_L1_BLOCK_PROXY_HASH = 0x18acb38c5ff1c238a7460ebc1b421fa49ec4874bdf1e0a530d234104e5e67dbc; + bytes32 constant UPDATE_GAS_PRICE_ORACLE_HASH = 0xee4f9385eceef498af0be7ec5862229f426dec41c8d42397c7257a5117d9230a; + bytes32 constant ENABLE_ECOTONE_HASH = 0x0c1cb38e99dbc9cbfab3bb80863380b0905290b37eb3d6ab18dc01c1f3e75f93; + bytes32 constant BEACON_ROOTS_HASH = 0x69b763c48478b9dc2f65ada09b3d92133ec592ea715ec65ad6e7f3dc519dc00c; + + // Intent strings from Ecotone upgrade (ecotone_upgrade_transactions.go:27-32) + string constant INTENT_DEPLOY_L1_BLOCK = "Ecotone: L1 Block Deployment"; + string constant INTENT_DEPLOY_GAS_PRICE_ORACLE = "Ecotone: Gas Price Oracle Deployment"; + string constant INTENT_UPDATE_L1_BLOCK_PROXY = "Ecotone: L1 Block Proxy Update"; + string constant INTENT_UPDATE_GAS_PRICE_ORACLE = "Ecotone: Gas Price Oracle Proxy Update"; + string constant INTENT_ENABLE_ECOTONE = "Ecotone: Gas Price Oracle Set Ecotone"; + string constant INTENT_BEACON_ROOTS = "Ecotone: beacon block roots contract deployment"; +} + +/// @title NetworkUpgradeTxns_SourceHash_Test +/// @notice Tests the `sourceHash` function matches Go implementation. +contract NetworkUpgradeTxns_SourceHash_Test is NetworkUpgradeTxns_TestInit { + /// @notice Test sourceHash for L1Block deployment matches Go test vector + function test_sourceHash_deployL1Block_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_DEPLOY_L1_BLOCK); + assertEq(hash, DEPLOY_L1_BLOCK_HASH, "L1Block deployment hash mismatch"); + } + + /// @notice Test sourceHash for GasPriceOracle deployment matches Go test vector + function test_sourceHash_deployGasPriceOracle_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_DEPLOY_GAS_PRICE_ORACLE); + assertEq(hash, DEPLOY_GAS_PRICE_ORACLE_HASH, "GasPriceOracle deployment hash mismatch"); + } + + /// @notice Test sourceHash for L1Block proxy update matches Go test vector + function test_sourceHash_updateL1BlockProxy_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_UPDATE_L1_BLOCK_PROXY); + assertEq(hash, UPDATE_L1_BLOCK_PROXY_HASH, "L1Block proxy update hash mismatch"); + } + + /// @notice Test sourceHash for GasPriceOracle proxy update matches Go test vector + function test_sourceHash_updateGasPriceOracleProxy_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_UPDATE_GAS_PRICE_ORACLE); + assertEq(hash, UPDATE_GAS_PRICE_ORACLE_HASH, "GasPriceOracle proxy update hash mismatch"); + } + + /// @notice Test sourceHash for enable Ecotone matches Go test vector + function test_sourceHash_enableEcotone_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_ENABLE_ECOTONE); + assertEq(hash, ENABLE_ECOTONE_HASH, "Enable Ecotone hash mismatch"); + } + + /// @notice Test sourceHash for beacon roots matches Go test vector + function test_sourceHash_beaconRoots_succeeds() public pure { + bytes32 hash = NetworkUpgradeTxns.sourceHash(INTENT_BEACON_ROOTS); + assertEq(hash, BEACON_ROOTS_HASH, "Beacon roots hash mismatch"); + } +} + +/// @title NetworkUpgradeTxns_NewTx_Test +/// @notice Tests the `newTx` function. +contract NetworkUpgradeTxns_NewTx_Test is NetworkUpgradeTxns_TestInit { + /// @notice Test newTx creates transaction with correct fields + function test_newTx_allFields_succeeds( + string memory _intent, + address _from, + address _to, + uint256 _mint, + uint256 _value, + uint64 _gas, + bool _isSystemTransaction, + bytes memory _data + ) + public + pure + { + NetworkUpgradeTxns.NetworkUpgradeTxn memory txn = NetworkUpgradeTxns.newTx({ + intent: _intent, + from: _from, + to: _to, + mint: _mint, + value: _value, + gas: _gas, + isSystemTransaction: _isSystemTransaction, + data: _data + }); + + assertEq(txn.sourceHash, NetworkUpgradeTxns.sourceHash(_intent), "sourceHash mismatch"); + assertEq(txn.from, _from, "from mismatch"); + assertEq(txn.to, _to, "to mismatch"); + assertEq(txn.mint, _mint, "mint mismatch"); + assertEq(txn.value, _value, "value mismatch"); + assertEq(txn.gas, _gas, "gas mismatch"); + assertEq(txn.isSystemTransaction, _isSystemTransaction, "isSystemTransaction mismatch"); + assertEq(txn.data, _data, "data mismatch"); + } +} + +/// @title NetworkUpgradeTxns_WriteArtifact_Test +/// @notice Tests the `writeArtifact` function. +contract NetworkUpgradeTxns_WriteArtifact_Test is NetworkUpgradeTxns_TestInit { + /// @notice Test writeArtifact with empty array + function test_writeArtifact_emptyArray() public { + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = new NetworkUpgradeTxns.NetworkUpgradeTxn[](0); + string memory outputPath = "deployments/nut-test-empty.json"; + NetworkUpgradeTxns.writeArtifact(txns, outputPath); + } + + /// @notice Test writeArtifact with single Predeploy deployment + function test_writeArtifact_singleDeployment() public { + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = new NetworkUpgradeTxns.NetworkUpgradeTxn[](1); + + txns[0] = NetworkUpgradeTxns.newTx({ + intent: INTENT_DEPLOY_L1_BLOCK, + from: L1_BLOCK_DEPLOYER, + to: address(0), + mint: 0, + value: 0, + gas: 375_000, + isSystemTransaction: false, + data: vm.getCode("L1Block.sol:L1Block") + }); + string memory outputPath = "deployments/nut-test-single.json"; + NetworkUpgradeTxns.writeArtifact(txns, outputPath); + } + + /// @notice Test writeArtifact creates valid JSON file + function test_writeArtifact_succeeds() public { + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = new NetworkUpgradeTxns.NetworkUpgradeTxn[](2); + + txns[0] = NetworkUpgradeTxns.newTx({ + intent: INTENT_DEPLOY_L1_BLOCK, + from: L1_BLOCK_DEPLOYER, + to: address(0), + mint: 0, + value: 0, + gas: 375_000, + isSystemTransaction: false, + data: vm.getCode("L1Block.sol:L1Block") + }); + + txns[1] = NetworkUpgradeTxns.newTx({ + intent: INTENT_ENABLE_ECOTONE, + from: DEPOSITOR_ACCOUNT, + to: Predeploys.GAS_PRICE_ORACLE, + mint: 0, + value: 0, + gas: 50_000, + isSystemTransaction: false, + data: abi.encodeCall(IGasPriceOracle.setEcotone, ()) + }); + + string memory outputPath = "deployments/nut-test.json"; + NetworkUpgradeTxns.writeArtifact(txns, outputPath); + + // Read json file and validate the transactions + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory readTxns = NetworkUpgradeTxns.readArtifact(outputPath); + assertEq(readTxns.length, txns.length, "Transaction count mismatch"); + for (uint256 i = 0; i < txns.length; i++) { + assertEq(readTxns[i].sourceHash, txns[i].sourceHash, "'sourceHash' doesn't match"); + assertEq(readTxns[i].from, txns[i].from, "'from' doesn't match"); + assertEq(readTxns[i].to, txns[i].to, "'to' doesn't match"); + assertEq(readTxns[i].mint, txns[i].mint, "'mint' doesn't match"); + } + } +} + +/// @title NetworkUpgradeTxns_EcotoneUpgrade_Test +/// @notice Tests that the artifact produced by the library matches the expected values. +contract NetworkUpgradeTxns_EcotoneUpgrade_Test is NetworkUpgradeTxns_TestInit { + /// @notice EIP-4788 beacon roots contract deployment data from EIP spec + /// Obtained from https://eips.ethereum.org/EIPS/eip-4788#deployment + bytes constant EIP4788_CREATION_DATA = + hex"60618060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"; + + /// @notice Test constructing Ecotone upgrade transactions, writing to file and reading back. + function test_ecotoneUpgrade_roundtrip_succeeds() public { + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = new NetworkUpgradeTxns.NetworkUpgradeTxn[](6); + + // 1. Deploy L1Block + // ecotone_upgrade_transactions.go:47 + txns[0] = NetworkUpgradeTxns.newTx({ + intent: INTENT_DEPLOY_L1_BLOCK, + from: L1_BLOCK_DEPLOYER, + to: address(0), + mint: 0, + value: 0, + gas: 375_000, + isSystemTransaction: false, + data: vm.getCode("L1Block.sol:L1Block") + }); + + // 2. Deploy GasPriceOracle + // ecotone_upgrade_transactions.go:64 + txns[1] = NetworkUpgradeTxns.newTx({ + intent: INTENT_DEPLOY_GAS_PRICE_ORACLE, + from: GAS_PRICE_ORACLE_DEPLOYER, + to: address(0), + mint: 0, + value: 0, + gas: 1_000_000, + isSystemTransaction: false, + data: vm.getCode("GasPriceOracle.sol:GasPriceOracle") + }); + + // 3. Update L1Block proxy + // ecotone_upgrade_transactions.go:81 + // Calculate the deployed L1Block address + address newL1BlockAddress = vm.computeCreateAddress(L1_BLOCK_DEPLOYER, 0); + txns[2] = NetworkUpgradeTxns.newTx({ + intent: INTENT_UPDATE_L1_BLOCK_PROXY, + from: address(0), + to: Predeploys.L1_BLOCK_ATTRIBUTES, + mint: 0, + value: 0, + gas: 50_000, + isSystemTransaction: false, + data: abi.encodeCall(IProxy.upgradeTo, (newL1BlockAddress)) + }); + + // 4. Update GasPriceOracle proxy + // ecotone_upgrade_transactions.go:98 + // Calculate the deployed GasPriceOracle address + address newGasPriceOracleAddress = vm.computeCreateAddress(GAS_PRICE_ORACLE_DEPLOYER, 0); + txns[3] = NetworkUpgradeTxns.newTx({ + intent: INTENT_UPDATE_GAS_PRICE_ORACLE, + from: address(0), + to: Predeploys.GAS_PRICE_ORACLE, + mint: 0, + value: 0, + gas: 50_000, + isSystemTransaction: false, + data: abi.encodeCall(IProxy.upgradeTo, (newGasPriceOracleAddress)) + }); + + // 5. Enable Ecotone on GasPriceOracle + // ecotone_upgrade_transactions.go:115 + txns[4] = NetworkUpgradeTxns.newTx({ + intent: INTENT_ENABLE_ECOTONE, + from: DEPOSITOR_ACCOUNT, + to: Predeploys.GAS_PRICE_ORACLE, + mint: 0, + value: 0, + gas: 80_000, + isSystemTransaction: false, + data: abi.encodeCall(IGasPriceOracle.setEcotone, ()) + }); + + // 6. Deploy EIP-4788 beacon block roots contract + // ecotone_upgrade_transactions.go:130 + txns[5] = NetworkUpgradeTxns.newTx({ + intent: INTENT_BEACON_ROOTS, + from: 0x0B799C86a49DEeb90402691F1041aa3AF2d3C875, + to: address(0), // Contract deployment + mint: 0, + value: 0, + gas: 250_000, // hex constant 0x3d090, as defined in EIP-4788 (250_000 in decimal) + isSystemTransaction: false, + data: EIP4788_CREATION_DATA + }); + + // Write transactions to JSON file + string memory outputPath = "deployments/nut-ecotone-upgrade-test.json"; + NetworkUpgradeTxns.writeArtifact(txns, outputPath); + + // Read back the transactions + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory readTxns = NetworkUpgradeTxns.readArtifact(outputPath); + + // Validate array length matches + assertEq(readTxns.length, txns.length, "Transaction count mismatch"); + + // Validate each transaction matches + for (uint256 i = 0; i < txns.length; i++) { + assertEq(readTxns[i].sourceHash, txns[i].sourceHash, "'sourceHash' doesn't match"); + assertEq(readTxns[i].from, txns[i].from, "'from' doesn't match"); + assertEq(readTxns[i].to, txns[i].to, "'to' doesn't match"); + assertEq(readTxns[i].mint, txns[i].mint, "'mint' doesn't match"); + assertEq(readTxns[i].value, txns[i].value, "'value' doesn't match"); + assertEq(readTxns[i].gas, txns[i].gas, "'gas' doesn't match"); + assertEq( + readTxns[i].isSystemTransaction, txns[i].isSystemTransaction, "'isSystemTransaction' doesn't match" + ); + assertEq(readTxns[i].data, txns[i].data, "'data' doesn't match"); + } + } +} diff --git a/packages/contracts-bedrock/test/scripts/TransactionGeneration.t.sol b/packages/contracts-bedrock/test/scripts/TransactionGeneration.t.sol new file mode 100644 index 00000000000..3dbc6c806e5 --- /dev/null +++ b/packages/contracts-bedrock/test/scripts/TransactionGeneration.t.sol @@ -0,0 +1,220 @@ +// 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"; +import { Fork } from "scripts/libraries/Config.sol"; +import { PredeployHelper } from "scripts/deploy/PredeployHelper.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; + +contract TransactionGenerationTest is Test { + TransactionGeneration public transactionGeneration; + + /// @notice Address where L2ImplementationsDeployer is etched + address constant L2_IMPLEMENTATIONS_DEPLOYER = 0x4200000000000000000000000000000000000420; + + event Upgraded(address indexed implementation); + + 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", + hardForkName: "XFork" + }); + } + + /// @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); + if (output.txns[i].to == Predeploys.PROXY_ADMIN) { + for (uint256 k = 0; k < output.predeploys.length; k++) { + vm.expectEmit(output.predeploys[k].proxy); + emit Upgraded(output.predeploys[k].implementation); + } + } + (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")); + } + + for (uint256 i = 0; i < output.predeploys.length; i++) { + assertEq( + ProxyAdmin(Predeploys.PROXY_ADMIN).getProxyImplementation(output.predeploys[i].proxy), + output.predeploys[i].implementation, + string.concat("Predeploy ", output.predeploys[i].name, " should have correct implementation") + ); + if (!_hasConstructor(output.predeploys[i].proxy)) { + assertEq( + output.predeploys[i].implementation.code, + vm.getDeployedCode(output.predeploys[i].name), + string.concat( + "Predeploy", vm.toString(i), " ", output.predeploys[i].name, " should have correct code" + ) + ); + } + } + } + + function _hasConstructor(address _proxy) internal 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.OPERATOR_FEE_VAULT || _proxy == Predeploys.EAS; + } + + /// @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 target address from performDelegateCall + address target = _decodeTarget(callData); + + // Verify target is a valid address + assertTrue(target != address(0), "Target address should not be zero"); + } + + function _decodeTarget(bytes memory callData) internal pure returns (address) { + // 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)); + } + + /// @notice Test that running the upgrade twice results in the same implementations (idempotency). + function test_upgradeTransactions_idempotent_succeeds() public { + TransactionGeneration.Input memory input = _getInput(); + TransactionGeneration.Output memory output = transactionGeneration.run(input); + + // Execute all transactions from first run + 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("First run transaction ", vm.toString(i), " should succeed")); + } + + input.hardForkName = "XFork2"; + TransactionGeneration.Output memory output2 = new TransactionGeneration().run(input); + + // Execute all transactions from second run + for (uint256 i = 0; i < output2.txns.length; i++) { + vm.prank(output2.txns[i].from); + (bool success,) = + output2.txns[i].to.call{ value: output2.txns[i].value, gas: output2.txns[i].gas }(output2.txns[i].data); + assertTrue(success, string.concat("Second run transaction ", vm.toString(i), " should succeed")); + } + + // Verify that the implementations are the same after second upgrade + for (uint256 i = 0; i < output2.predeploys.length; i++) { + assertEq( + ProxyAdmin(Predeploys.PROXY_ADMIN).getProxyImplementation(output2.predeploys[i].proxy), + ProxyAdmin(Predeploys.PROXY_ADMIN).getProxyImplementation(output.predeploys[i].proxy), + string.concat( + "Implementation for ", output2.predeploys[i].name, " should be the same after second upgrade" + ) + ); + } + } +}