diff --git a/packages/contracts-bedrock/scripts/upgrade/GenerateNUTBundle.s.sol b/packages/contracts-bedrock/scripts/upgrade/GenerateNUTBundle.s.sol index 80bf5aa56d7..fa6a37e5418 100644 --- a/packages/contracts-bedrock/scripts/upgrade/GenerateNUTBundle.s.sol +++ b/packages/contracts-bedrock/scripts/upgrade/GenerateNUTBundle.s.sol @@ -30,6 +30,9 @@ contract GenerateNUTBundle is Script { /// @notice Version of the upgrade bundle. string internal constant BUNDLE_VERSION = "1.0.0"; + /// @notice EIP-7825 per-transaction gas limit cap (2 ** 24). + uint256 public constant MAX_TX_GAS_LIMIT = 16777216; + /// @notice Output containing generated transactions. /// @param txns Array of Network Upgrade Transactions to execute. struct Output { @@ -69,7 +72,22 @@ contract GenerateNUTBundle is Script { gasLimits = UpgradeUtils.gasLimits(); } - /// @notice Generates the complete upgrade transaction bundle. + /// @notice Generates the upgrade transaction bundle and writes the artifact to disk. + /// @return output_ Output containing all generated transactions in execution order. + function run() public returns (Output memory output_) { + setUp(); + + output_ = _buildOutput(); + + _assertValidOutput(output_); + + // Write transactions to artifact with metadata + NetworkUpgradeTxns.BundleMetadata memory metadata = + NetworkUpgradeTxns.BundleMetadata({ version: BUNDLE_VERSION }); + NetworkUpgradeTxns.writeArtifact(txns, metadata, Constants.CURRENT_BUNDLE_PATH); + } + + /// @notice Builds the upgrade transaction bundle Output struct. /// @dev Executes 5 phases in fixed order: /// 1. Pre-implementation deployments [CUSTOM] /// 2. Implementation deployments [FIXED] @@ -78,9 +96,7 @@ contract GenerateNUTBundle is Script { /// 5. Upgrade execution [FIXED] /// @dev Only modify phases 1 and 3 for fork-specific logic. Other phases must remain unchanged. /// @return output_ Output containing all generated transactions in execution order. - function run() public returns (Output memory output_) { - setUp(); - + function _buildOutput() internal returns (Output memory output_) { // Build implementation deployment configurations _buildImplementationDeploymentConfigs(); @@ -112,13 +128,6 @@ contract GenerateNUTBundle is Script { for (uint256 i = 0; i < txnsLength; i++) { output_.txns[i] = txns[i]; } - - _assertValidOutput(output_); - - // Write transactions to artifact with metadata - NetworkUpgradeTxns.BundleMetadata memory metadata = - NetworkUpgradeTxns.BundleMetadata({ version: BUNDLE_VERSION }); - NetworkUpgradeTxns.writeArtifact(txns, metadata, Constants.CURRENT_BUNDLE_PATH); } /// @notice Asserts the output is valid. @@ -132,13 +141,16 @@ contract GenerateNUTBundle is Script { require(_output.txns[i].data.length > 0, "GenerateNUTBundle: invalid transaction data"); require(bytes(_output.txns[i].intent).length > 0, "GenerateNUTBundle: invalid transaction intent"); require(_output.txns[i].to != address(0), "GenerateNUTBundle: invalid transaction to"); - require(_output.txns[i].gasLimit > 0, "GenerateNUTBundle: invalid transaction gasLimit"); - - // EIP-7623: op-geth rejects the tx (ErrFloorDataGas) if gasLimit < floorDataGas. + // Lower bound: EIP-7623 calldata floor (op-geth rejects with ErrFloorDataGas below this). + // Upper bound: EIP-7825 per-tx gas cap (2 ** 24). The floor dominates `> 0`, so the + // floor is the only lower bound we need here, assuming every NUT is a CALL, which is + // guaranteed by the `to != address(0)` check above. uint64 floorDataGas = UpgradeUtils.computeFloorDataGas(_output.txns[i].data); require( - _output.txns[i].gasLimit >= floorDataGas, - string.concat("GenerateNUTBundle: gasLimit below EIP-7623 floor for ", _output.txns[i].intent) + _output.txns[i].gasLimit >= floorDataGas && _output.txns[i].gasLimit <= MAX_TX_GAS_LIMIT, + string.concat( + "GenerateNUTBundle: gasLimit outside [EIP-7623 floor, EIP-7825 cap] for ", _output.txns[i].intent + ) ); if (_output.txns[i].from == address(0)) { diff --git a/packages/contracts-bedrock/test/scripts/GenerateNUTBundle.t.sol b/packages/contracts-bedrock/test/scripts/GenerateNUTBundle.t.sol index 951a5ed2538..42b84777b1d 100644 --- a/packages/contracts-bedrock/test/scripts/GenerateNUTBundle.t.sol +++ b/packages/contracts-bedrock/test/scripts/GenerateNUTBundle.t.sol @@ -12,17 +12,32 @@ import { NetworkUpgradeTxns } from "src/libraries/NetworkUpgradeTxns.sol"; import { UpgradeUtils } from "scripts/libraries/UpgradeUtils.sol"; import { Constants } from "src/libraries/Constants.sol"; import { L2ContractsManagerTypes } from "src/libraries/L2ContractsManagerTypes.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @title GenerateNUTBundle_Harness +/// @notice Harness contract that exposes internal functions for testing. +contract GenerateNUTBundle_Harness is GenerateNUTBundle { + /// @notice Builds the upgrade transaction bundle Output struct without writing to disk. + function buildOutput() external returns (Output memory) { + return _buildOutput(); + } + + /// @notice Asserts that the given output is valid. + function assertValidOutput(Output memory _output) external pure { + _assertValidOutput(_output); + } +} /// @title GenerateNUTBundleTest /// @notice Tests that GenerateNUTBundle correctly generates Network Upgrade Transaction bundles /// for L2 hardfork upgrades. contract GenerateNUTBundleTest is Test { - GenerateNUTBundle script; + GenerateNUTBundle_Harness script; uint256 constant TEST_L1_CHAIN_ID = 1; function setUp() public { - script = new GenerateNUTBundle(); + script = new GenerateNUTBundle_Harness(); script.setUp(); } @@ -137,4 +152,129 @@ contract GenerateNUTBundleTest is Test { string[] memory names = UpgradeUtils.getImplementationsNamesToUpgrade(); assertEq(names.length, structFieldCount, "Deployment list must equal Implementations struct field count"); } + + /// @notice Tests that a bundle with an incorrect number of transactions is rejected. + /// @dev Builds a valid bundle, then mutates the array length to trigger the assertion. + function testFuzz_assertValidOutput_transactionCountMismatch_reverts(uint256 _newLength) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _newLength = bound(_newLength, 0, output.txns.length - 1); + NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = output.txns; + assembly { + mstore(txns, _newLength) + } + + vm.expectRevert("GenerateNUTBundle: invalid transaction count"); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction with empty data is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_emptyData_reverts(uint256 _index) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + output.txns[_index].data = new bytes(0); + + vm.expectRevert("GenerateNUTBundle: invalid transaction data"); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction with an empty intent is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_emptyIntent_reverts(uint256 _index) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + output.txns[_index].intent = ""; + + vm.expectRevert("GenerateNUTBundle: invalid transaction intent"); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction with a zero destination address is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_zeroTo_reverts(uint256 _index) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + output.txns[_index].to = address(0); + + vm.expectRevert("GenerateNUTBundle: invalid transaction to"); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction exceeding the EIP-7825 per-tx gas limit cap is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_gasLimitExceedsMax_reverts(uint256 _index, uint64 _gasLimit) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + _gasLimit = uint64(bound(_gasLimit, script.MAX_TX_GAS_LIMIT() + 1, type(uint64).max)); + output.txns[_index].gasLimit = _gasLimit; + + vm.expectRevert( + bytes( + string.concat( + "GenerateNUTBundle: gasLimit outside [EIP-7623 floor, EIP-7825 cap] for ", + output.txns[_index].intent + ) + ) + ); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction with a zero gas limit is rejected by the EIP-7623 floor. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_zeroGasLimit_reverts(uint256 _index) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + output.txns[_index].gasLimit = 0; + + vm.expectRevert( + bytes( + string.concat( + "GenerateNUTBundle: gasLimit outside [EIP-7623 floor, EIP-7825 cap] for ", + output.txns[_index].intent + ) + ) + ); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction whose gasLimit is one below the EIP-7623 floor is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_gasLimitBelowFloor_reverts(uint256 _index) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + _index = bound(_index, 0, output.txns.length - 1); + uint64 floor = UpgradeUtils.computeFloorDataGas(output.txns[_index].data); + output.txns[_index].gasLimit = floor - 1; + + vm.expectRevert( + bytes( + string.concat( + "GenerateNUTBundle: gasLimit outside [EIP-7623 floor, EIP-7825 cap] for ", + output.txns[_index].intent + ) + ) + ); + script.assertValidOutput(output); + } + + /// @notice Tests that a transaction with a zero sender and a non-privileged destination is rejected. + /// @dev Builds a valid bundle, then mutates one transaction to trigger the assertion. + function testFuzz_assertValidOutput_zeroFromNonPrivilegedTo_reverts(uint256 _index, address _to) public { + GenerateNUTBundle.Output memory output = script.buildOutput(); + + vm.assume(_to != address(0)); + vm.assume(_to != Predeploys.PROXY_ADMIN && _to != Predeploys.CONDITIONAL_DEPLOYER); + _index = bound(_index, 0, output.txns.length - 1); + output.txns[_index].from = address(0); + output.txns[_index].to = _to; + + vm.expectRevert("GenerateNUTBundle: invalid transaction from"); + script.assertValidOutput(output); + } }