Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
Expand All @@ -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();

Expand Down Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down
144 changes: 142 additions & 2 deletions packages/contracts-bedrock/test/scripts/GenerateNUTBundle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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 {
Comment thread
0xniha marked this conversation as resolved.
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);
}
}
Loading