Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/contracts-bedrock/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ deployments/kontrol.jsonReversed
deployments/kontrol-fp.json
deployments/kontrol-fp.jsonReversed
deployments/1-deploy.json
deployments/10-deploy.json



Expand Down
14 changes: 14 additions & 0 deletions packages/contracts-bedrock/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,20 @@ coverage-lcov-all *ARGS:
just coverage-lcov-upgrade --match-contract "OPContractsManager.*_Upgrade_Test" {{ARGS}} && \
lcov -a lcov.info -a lcov-upgrade.info -o lcov-all.info

# Runs L2 fork upgrade tests against a specified L2 chain.
# Usage: just test-l2-fork-upgrade <L2_RPC_URL> <BLOCK_NUMBER> [ARGS]
test-l2-fork-upgrade L2_RPC BLOCK *ARGS: build-go-ffi
#!/bin/bash
set -euo pipefail
# Generate NUT bundle
forge script scripts/upgrade/GenerateNUTBundle.s.sol:GenerateNUTBundle --sig "run()" 1
# Run L2 fork upgrade test
L2_FORK_TEST=true \
L2_FORK_RPC_URL={{L2_RPC}} \
L2_FORK_BLOCK_NUMBER={{BLOCK}} \
DEV_FEATURE__L2CM=true \
forge test --match-contract L2ForkUpgrade {{ARGS}}

########################################################
# DEPLOY #
########################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ src_validation = [
"test/vendor/InitializableOZv5.t.sol", # Tests external vendor code
"test/L2/LegacyFeeSplitter.t.sol", # Tests legacy fee splitter with updated vaults interface
"test/L2/FeeSplitterVaults.t.sol", # Tests FeeSplitter with vault-specific scenarios using test helper
"test/L2/L2ForkUpgrade.t.sol", # Tests L2 fork upgrade workflow
]

# PATHS EXCLUDED FROM CONTRACT NAME FILE PATH VALIDATION:
Expand Down
20 changes: 20 additions & 0 deletions packages/contracts-bedrock/scripts/libraries/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,26 @@ library Config {
return vm.envOr("FORK_TEST", false);
}

/// @notice Returns true if this is an L2 fork test.
function l2ForkTest() internal view returns (bool) {
return vm.envOr("L2_FORK_TEST", false);
}

/// @notice Returns the L2 RPC URL for forking.
function l2ForkRpcUrl() internal view returns (string memory) {
return vm.envString("L2_FORK_RPC_URL");
}

/// @notice Returns the L2 block number to fork at.
function l2ForkBlockNumber() internal view returns (uint256) {
return vm.envUint("L2_FORK_BLOCK_NUMBER");
}

/// @notice Returns the L2 chain identifier (e.g., "op", "base", "mode").
function l2ForkChain() internal view returns (string memory) {
return vm.envOr("L2_FORK_CHAIN", string("op"));
}

/// @notice Returns true if the development feature interop is enabled.
function devFeatureInterop() internal view returns (bool) {
return vm.envOr("DEV_FEATURE__OPTIMISM_PORTAL_INTEROP", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ library UpgradeUtils {
uint256 internal constant IMPLEMENTATION_COUNT = 28;

/// @notice The default gas limit for a deployment transaction.
uint64 internal constant DEFAULT_DEPLOYMENT_GAS = 375_000;
uint64 internal constant DEFAULT_DEPLOYMENT_GAS = 1_000_000_000;

/// @notice The default gas limit for an upgrade transaction.
uint64 internal constant DEFAULT_UPGRADE_GAS = 50_000;
uint64 internal constant DEFAULT_UPGRADE_GAS = 1_000_000;

/// @notice Gas limits for different types of upgrade transactions.
/// @param l2cmDeployment Gas for deploying L2ContractsManager
Expand Down
109 changes: 109 additions & 0 deletions packages/contracts-bedrock/scripts/upgrade/ExecuteNUTBundle.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

// Scripts
import { Script } from "forge-std/Script.sol";

// Libraries
import { NetworkUpgradeTxns } from "src/libraries/NetworkUpgradeTxns.sol";
import { console2 as console } from "forge-std/console2.sol";

/// @title ExecuteNUTBundle
/// @notice Executes Network Upgrade Transaction (NUT) bundles on a forked L2 chain.
/// @dev This script is designed to be used in fork testing to execute upgrade bundles
/// generated by GenerateNUTBundle stored in the current bundle json file.
contract ExecuteNUTBundle is Script {
/// @notice Current bundle artifact path.
string public constant CURRENT_BUNDLE_PATH = "snapshots/upgrades/current-upgrade-bundle.json";

/// @notice Executes transactions from the current bundle artifact.
function execute() public {
console.log("ExecuteNUTBundle: Reading bundle from", CURRENT_BUNDLE_PATH);
NetworkUpgradeTxns.NetworkUpgradeTxn[] memory txns = NetworkUpgradeTxns.readArtifact(CURRENT_BUNDLE_PATH);
console.log("ExecuteNUTBundle: Loaded", txns.length, "transactions");
_executeAll(txns);
}

/// @notice Executes all transactions in the bundle sequentially.
/// @dev Each transaction is executed with:
/// - The correct sender via vm.prank()
/// - Sufficient ETH balance via vm.deal()
/// - The specified gas limit
/// Failures are reported with the transaction intent for debugging.
/// @param _txns Array of Network Upgrade Transactions to execute.
function _executeAll(NetworkUpgradeTxns.NetworkUpgradeTxn[] memory _txns) internal {
console.log("ExecuteNUTBundle: Executing", _txns.length, "transactions");

for (uint256 i = 0; i < _txns.length; i++) {
NetworkUpgradeTxns.NetworkUpgradeTxn memory txn = _txns[i];

console.log("");
console.log("ExecuteNUTBundle: [%s/%s] %s", i + 1, _txns.length, txn.intent);
console.log(" from:", txn.from);
console.log(" to:", txn.to);
console.log(" gasLimit:", txn.gasLimit);

// Ensure sender has sufficient balance
vm.deal(txn.from, 100 ether);

// Execute transaction as the specified sender
vm.prank(txn.from);
(bool success, bytes memory returnData) = txn.to.call{ gas: txn.gasLimit }(txn.data);

if (!success) {
// Decode revert reason if available
string memory revertReason = _getRevertReason(returnData);
console.log(" FAILED:", revertReason);
revert(string.concat("ExecuteNUTBundle: Transaction failed - ", txn.intent, " - ", revertReason));
}

console.log(" SUCCESS");
}

console.log("");
console.log("ExecuteNUTBundle: All transactions executed successfully");
}

/// @notice Extracts a revert reason from return data.
/// @param _returnData The return data from a failed call.
/// @return reason_ The revert reason string, or a default message if unavailable.
function _getRevertReason(bytes memory _returnData) internal pure returns (string memory reason_) {
// If the return data is at least 68 bytes, it might contain a revert reason
// 4 bytes for Error(string) selector + 32 bytes for offset + 32 bytes for length
if (_returnData.length >= 68) {
// Check if it's an Error(string) revert
bytes4 errorSelector = bytes4(_returnData);
if (errorSelector == 0x08c379a0) {
// Decode the string
assembly {
// Skip the first 68 bytes (4 byte selector + 32 byte offset + 32 byte length)
// to get to the actual string data
reason_ := add(_returnData, 0x44)
}
return reason_;
}
}

// If we can't decode a revert reason, return hex representation
if (_returnData.length > 0) {
return string(abi.encodePacked("0x", _toHexString(_returnData)));
}

return "Unknown error";
}

/// @notice Converts bytes to hex string.
/// @param _data The bytes to convert.
/// @return hex_ The hex string representation.
function _toHexString(bytes memory _data) internal pure returns (string memory hex_) {
bytes memory hexChars = "0123456789abcdef";
bytes memory result = new bytes(_data.length * 2);

for (uint256 i = 0; i < _data.length; i++) {
result[i * 2] = hexChars[uint8(_data[i] >> 4)];
result[i * 2 + 1] = hexChars[uint8(_data[i] & 0x0f)];
}

return string(result);
}
}
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 Current bundle artifact path.
string public constant CURRENT_BUNDLE_PATH = "snapshots/upgrades/current-upgrade-bundle.json";

/// @notice Output containing generated transactions.
/// @param txns Array of Network Upgrade Transactions to execute.
struct Output {
Expand All @@ -56,7 +59,7 @@ contract GenerateNUTBundle is Script {
L2ContractsManagerTypes.Implementations internal implementations;

/// @notice Implementation configurations.
mapping(string => ImplementationConfig) internal implementationConfigs;
mapping(string => ImplementationConfig) public implementationConfigs;

/// @notice Array of generated transactions.
NetworkUpgradeTxns.NetworkUpgradeTxn[] internal txns;
Expand Down Expand Up @@ -118,7 +121,7 @@ contract GenerateNUTBundle is Script {
// Write transactions to artifact with metadata
NetworkUpgradeTxns.BundleMetadata memory metadata =
NetworkUpgradeTxns.BundleMetadata({ version: BUNDLE_VERSION });
NetworkUpgradeTxns.writeArtifact(txns, metadata, upgradeBundlePath());
NetworkUpgradeTxns.writeArtifact(txns, metadata, CURRENT_BUNDLE_PATH);
}

/// @notice Asserts the output is valid.
Expand Down Expand Up @@ -306,11 +309,6 @@ contract GenerateNUTBundle is Script {
// HELPERS
// ========================================

/// @notice Returns the path to the upgrade bundle.
function upgradeBundlePath() public pure returns (string memory) {
return string.concat("snapshots/upgrades/current-upgrade-bundle.json");
}

/// @notice Retrieves all expected implementation addresses for the upgrade.
/// @dev All addresses are looked up from the implementationConfigs mapping, which contains
/// deterministically computed CREATE2 addresses using the hardcoded salt. This ensures
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts-bedrock/snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@
"sourceCodeHash": "0x7e438cbbe9a8248887b8c21f68c811f90a5cae4902cbbf7b0a1f6cd644dc42d9"
},
"src/L2/L2ContractsManager.sol:L2ContractsManager": {
"initCodeHash": "0xf16ab1061ba6d4205583c0136fedecd7db78740c2376b68d5123a7a001d89d6b",
"sourceCodeHash": "0x5f076c6770a0ef56921a7ba347965d3d7f85daa3a6f9d5737823ec22283fbbe5"
"initCodeHash": "0x88e09457150afc6b5674e2a1e4ecb41534b2f8695f36e33fd4e1b80aba960b76",
"sourceCodeHash": "0xcc9eb48e7478d29c0c59715bd5ef0bffa0d3410e6b8c9d24e52d28db9ec91c4f"
},
"src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": {
"initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61",
Expand Down

Large diffs are not rendered by default.

31 changes: 22 additions & 9 deletions packages/contracts-bedrock/src/L2/L2ContractsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -168,28 +168,34 @@ contract L2ContractsManager is ISemver {

// L2CrossDomainMessenger
fullConfig_.crossDomainMessenger = L2ContractsManagerTypes.CrossDomainMessengerConfig({
otherMessenger: ICrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER).otherMessenger()
// TODO(#19468): Remove legacy getter after Karst upgrade.
otherMessenger: ICrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER).OTHER_MESSENGER()
});

// L2StandardBridge
fullConfig_.standardBridge = L2ContractsManagerTypes.StandardBridgeConfig({
otherBridge: IStandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).otherBridge()
// TODO(#19468): Remove legacy getter after Karst upgrade.
otherBridge: IStandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).OTHER_BRIDGE()
});

// L2ERC721Bridge
fullConfig_.erc721Bridge = L2ContractsManagerTypes.ERC721BridgeConfig({
otherBridge: IERC721Bridge(Predeploys.L2_ERC721_BRIDGE).otherBridge()
// TODO(#19468): Remove legacy getter after Karst upgrade.
otherBridge: IERC721Bridge(Predeploys.L2_ERC721_BRIDGE).OTHER_BRIDGE()
});

// OptimismMintableERC20Factory
fullConfig_.mintableERC20Factory = L2ContractsManagerTypes.MintableERC20FactoryConfig({
bridge: IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).bridge()
// TODO(#19468): Remove legacy getter after Karst upgrade.
bridge: IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).BRIDGE()
});

// OptimismMintableERC721Factory
fullConfig_.mintableERC721Factory = L2ContractsManagerTypes.MintableERC721FactoryConfig({
bridge: IOptimismMintableERC721Factory(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY).bridge(),
remoteChainID: IOptimismMintableERC721Factory(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY).remoteChainID()
// TODO(#19468): Remove legacy getter after Karst upgrade.
bridge: IOptimismMintableERC721Factory(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY).BRIDGE(),
// TODO(#19468): Remove legacy getter after Karst upgrade.
remoteChainID: IOptimismMintableERC721Factory(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY).REMOTE_CHAIN_ID()
});

// SequencerFeeVault
Expand All @@ -215,9 +221,16 @@ contract L2ContractsManager is ISemver {
}

// FeeSplitter
fullConfig_.feeSplitter = L2ContractsManagerTypes.FeeSplitterConfig({
sharesCalculator: IFeeSplitter(payable(Predeploys.FEE_SPLITTER)).sharesCalculator()
});
ISharesCalculator sharesCalculator;
// eip150-safe
try IFeeSplitter(payable(Predeploys.FEE_SPLITTER)).sharesCalculator() returns (
ISharesCalculator sharesCalculator_
) {
sharesCalculator = sharesCalculator_;
} catch {
sharesCalculator = ISharesCalculator(address(0));
}
fullConfig_.feeSplitter = L2ContractsManagerTypes.FeeSplitterConfig({ sharesCalculator: sharesCalculator });
}

/// @notice Upgrades each of the predeploys to its corresponding new implementation. Applies the appropriate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.0;
import { L2ContractsManagerTypes } from "src/libraries/L2ContractsManagerTypes.sol";
import { SemverComp } from "src/libraries/SemverComp.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { Types } from "src/libraries/Types.sol";

// Contracts
import { L2ProxyAdmin } from "src/L2/L2ProxyAdmin.sol";
Expand Down Expand Up @@ -60,14 +61,23 @@ library L2ContractsManagerUtils {
view
returns (L2ContractsManagerTypes.FeeVaultConfig memory config_)
{
// Try to read the withdrawal network from the FeeVault. If it fails, use the default value.
Types.WithdrawalNetwork withdrawalNetwork;
// eip150-safe
try IFeeVault(payable(_feeVault)).WITHDRAWAL_NETWORK() returns (Types.WithdrawalNetwork withdrawalNetwork_) {
withdrawalNetwork = withdrawalNetwork_;
} catch {
withdrawalNetwork = Types.WithdrawalNetwork.L1;
}

// Note: We are intentionally using legacy deprecated getters for this 1.0.0 version of the L2ContractsManager.
// Subsequent versions should use the new getters as L2ContractsManager should ensure that the new current
// version of the FeeVault is used.
IFeeVault feeVault = IFeeVault(payable(_feeVault));
config_ = L2ContractsManagerTypes.FeeVaultConfig({
recipient: feeVault.RECIPIENT(),
minWithdrawalAmount: feeVault.MIN_WITHDRAWAL_AMOUNT(),
withdrawalNetwork: feeVault.WITHDRAWAL_NETWORK()
withdrawalNetwork: withdrawalNetwork
});
}

Expand Down
Loading
Loading