diff --git a/packages/contracts-bedrock/scripts/Config.sol b/packages/contracts-bedrock/scripts/Config.sol index eafb8bfe983bd..041a96a8964bb 100644 --- a/packages/contracts-bedrock/scripts/Config.sol +++ b/packages/contracts-bedrock/scripts/Config.sol @@ -52,9 +52,9 @@ library Config { /// @notice Returns the path that the state dump file should be written to or read from /// on the local filesystem. - function stateDumpPath(string memory _name) internal view returns (string memory _env) { + function stateDumpPath() internal view returns (string memory _env) { _env = vm.envOr( - "STATE_DUMP_PATH", string.concat(vm.projectRoot(), "/", _name, "-", vm.toString(block.chainid), ".json") + "STATE_DUMP_PATH", string.concat(vm.projectRoot(), "/state-dump-", vm.toString(block.chainid), ".json") ); } diff --git a/packages/contracts-bedrock/scripts/Deploy.s.sol b/packages/contracts-bedrock/scripts/Deploy.s.sol index bcba49298b233..3552e1466942a 100644 --- a/packages/contracts-bedrock/scripts/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/Deploy.s.sol @@ -259,8 +259,7 @@ contract Deploy is Deployer { function runWithStateDump() public { _run(); - - vm.dumpState(Config.stateDumpPath(name())); + vm.dumpState(Config.stateDumpPath()); } /// @notice Deploy all L1 contracts and write the state diff to a file. diff --git a/packages/contracts-bedrock/scripts/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/DeployConfig.s.sol index 5921a57a2a548..95ca657219145 100644 --- a/packages/contracts-bedrock/scripts/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/DeployConfig.s.sol @@ -39,8 +39,10 @@ contract DeployConfig is Script { address public proxyAdminOwner; address public baseFeeVaultRecipient; uint256 public baseFeeVaultMinimumWithdrawalAmount; + uint256 public baseFeeVaultWithdrawalNetwork; address public l1FeeVaultRecipient; uint256 public l1FeeVaultMinimumWithdrawalAmount; + uint256 public l1FeeVaultWithdrawalNetwork; address public sequencerFeeVaultRecipient; uint256 public sequencerFeeVaultMinimumWithdrawalAmount; uint256 public sequencerFeeVaultWithdrawalNetwork; @@ -106,8 +108,10 @@ contract DeployConfig is Script { proxyAdminOwner = stdJson.readAddress(_json, "$.proxyAdminOwner"); baseFeeVaultRecipient = stdJson.readAddress(_json, "$.baseFeeVaultRecipient"); baseFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.baseFeeVaultMinimumWithdrawalAmount"); + baseFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.baseFeeVaultWithdrawalNetwork"); l1FeeVaultRecipient = stdJson.readAddress(_json, "$.l1FeeVaultRecipient"); l1FeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.l1FeeVaultMinimumWithdrawalAmount"); + l1FeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.l1FeeVaultWithdrawalNetwork"); sequencerFeeVaultRecipient = stdJson.readAddress(_json, "$.sequencerFeeVaultRecipient"); sequencerFeeVaultMinimumWithdrawalAmount = stdJson.readUint(_json, "$.sequencerFeeVaultMinimumWithdrawalAmount"); sequencerFeeVaultWithdrawalNetwork = stdJson.readUint(_json, "$.sequencerFeeVaultWithdrawalNetwork"); diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 5f5f8a198bb94..e64d72ff26803 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -3,62 +3,41 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; import { console2 as console } from "forge-std/console2.sol"; +import { Deployer } from "scripts/Deployer.sol"; +import { Config } from "scripts/Config.sol"; import { Artifacts } from "scripts/Artifacts.s.sol"; import { DeployConfig } from "scripts/DeployConfig.s.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { L1StandardBridge } from "src/L1/L1StandardBridge.sol"; -import { L1CrossDomainMessenger } from "src/L1/L1CrossDomainMessenger.sol"; -import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; import { L2CrossDomainMessenger } from "src/L2/L2CrossDomainMessenger.sol"; +import { L2StandardBridge } from "src/L2/L2StandardBridge.sol"; +import { L2ERC721Bridge } from "src/L2/L2ERC721Bridge.sol"; import { SequencerFeeVault } from "src/L2/SequencerFeeVault.sol"; -import { FeeVault } from "src/universal/FeeVault.sol"; import { OptimismMintableERC20Factory } from "src/universal/OptimismMintableERC20Factory.sol"; -import { L1Block } from "src/L2/L1Block.sol"; +import { OptimismMintableERC721Factory } from "src/universal/OptimismMintableERC721Factory.sol"; +import { BaseFeeVault } from "src/L2/BaseFeeVault.sol"; +import { L1FeeVault } from "src/L2/L1FeeVault.sol"; import { GovernanceToken } from "src/governance/GovernanceToken.sol"; +import { L1CrossDomainMessenger } from "src/L1/L1CrossDomainMessenger.sol"; +import { L1StandardBridge } from "src/L1/L1StandardBridge.sol"; +import { FeeVault } from "src/universal/FeeVault.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; interface IInitializable { function initialize(address _addr) external; } -/// @dev The general flow of adding a predeploy is: -/// 1. _setPredeployProxies uses vm.etch to set the Proxy.sol deployed bytecode for proxy address `0x420...000` to -/// `0x420...000 + PROXY_COUNT - 1`. -/// Additionally, the PROXY_ADMIN_ADDRESS and PROXY_IMPLEMENTATION_ADDRESS storage slots are set for the proxy -/// address. -/// 2. `vm.etch` sets the deployed bytecode for each predeploy at the implementation address (i.e. `0xc0d3` -/// namespace). -/// 3. The `initialize` method is called at the implementation address with zero/dummy vaules if the method exists. -/// 4. The `initialize` method is called at the proxy address with actual vaules if the method exists. -/// 5. A `require` check to verify the expected implementation address is set for the proxy. -/// @notice The following safety invariants are used when setting state: +/// @title L2Genesis +/// @notice Generates the genesis state for the L2 network. +/// The following safety invariants are used when setting state: /// 1. `vm.getDeployedBytecode` can only be used with `vm.etch` when there are no side /// effects in the constructor and no immutables in the bytecode. /// 2. A contract must be deployed using the `new` syntax if there are immutables in the code. /// Any other side effects from the init code besides setting the immutables must be cleaned up afterwards. -/// 3. A contract is deployed using the `new` syntax, however it's not proxied and is still expected to exist at -/// a -/// specific implementation address (i.e. `0xc0d3` namespace). In this case we deploy an instance of the -/// contract -/// using `new` syntax, use `contract.code` to retrieve it's deployed bytecode, `vm.etch` the bytecode at the -/// expected implementation address, and `vm.store` to set any storage slots that are -/// expected to be set after a new deployment. Lastly, we reset the account code and storage slots the contract -/// was initially deployed to so it's not included in the `vm.dumpState`. -contract L2Genesis is Script, Artifacts { - uint256 constant PROXY_COUNT = 2048; - uint256 constant PRECOMPILE_COUNT = 256; - DeployConfig public constant cfg = - DeployConfig(address(uint160(uint256(keccak256(abi.encode("optimism.deployconfig")))))); - - /// @notice The storage slot that holds the address of a proxy implementation. - /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` - bytes32 internal constant PROXY_IMPLEMENTATION_ADDRESS = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /// @notice The storage slot that holds the address of the owner. - /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` - bytes32 internal constant PROXY_ADMIN_ADDRESS = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; +contract L2Genesis is Deployer { + uint256 constant public PREDEPLOY_COUNT = 2048; + uint256 constant public PRECOMPILE_COUNT = 256; + uint80 internal constant DEV_ACCOUNT_FUND_AMT = 10_000 ether; /// @notice Default Anvil dev accounts. Only funded if `cfg.fundDevAccounts == true`. address[10] internal devAccounts = [ @@ -74,46 +53,65 @@ contract L2Genesis is Script, Artifacts { 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 ]; - string internal outfile; + + mapping(address => string) internal names; + + function name() public pure override returns (string memory) { + return "L2Genesis"; + } /// @dev Reads the deploy config, sets `outfile` which is where the `vm.dumpState` will be saved to, and /// loads in the addresses for the L1 contract deployments. function setUp() public override { - Artifacts.setUp(); - - string memory path = string.concat(vm.projectRoot(), "/deploy-config/", deploymentContext, ".json"); - vm.etch(address(cfg), vm.getDeployedCode("DeployConfig.s.sol:DeployConfig")); - vm.label(address(cfg), "DeployConfig"); - vm.allowCheatcodes(address(cfg)); - cfg.read(path); + super.setUp(); - outfile = string.concat(vm.projectRoot(), "/deployments/", deploymentContext, "/genesis-l2.json"); + // TODO: modularize this setNames into own contract + _setNames(); + } - _loadAddresses(string.concat(vm.projectRoot(), "/deployments/", deploymentContext, "/.deploy")); + /// @dev Creates a mapping of predeploy addresses to their names. This needs to be updated + /// any time there is a new predeploy added. + function _setNames() internal { + names[Predeploys.L2_TO_L1_MESSAGE_PASSER] = "L2ToL1MessagePasser"; + names[Predeploys.L2_CROSS_DOMAIN_MESSENGER] = "L2CrossDomainMessenger"; + names[Predeploys.L2_STANDARD_BRIDGE] = "L2StandardBridge"; + names[Predeploys.L2_ERC721_BRIDGE] = "L2ERC721Bridge"; + names[Predeploys.SEQUENCER_FEE_WALLET] = "SequencerFeeVault"; + names[Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY] = "OptimismMintableERC20Factory"; + names[Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY] = "OptimismMintableERC721Factory"; + names[Predeploys.L1_BLOCK_ATTRIBUTES] = "L1Block"; + names[Predeploys.GAS_PRICE_ORACLE] = "GasPriceOracle"; + names[Predeploys.L1_MESSAGE_SENDER] = "L1MessageSender"; + names[Predeploys.DEPLOYER_WHITELIST] = "DeployerWhitelist"; + names[Predeploys.WETH9] = "WETH9"; + names[Predeploys.LEGACY_ERC20_ETH] = "LegacyERC20ETH"; + names[Predeploys.L1_BLOCK_NUMBER] = "L1BlockNumber"; + names[Predeploys.LEGACY_MESSAGE_PASSER] = "LegacyMessagePasser"; + names[Predeploys.PROXY_ADMIN] = "ProxyAdmin"; + names[Predeploys.BASE_FEE_VAULT] = "BaseFeeVault"; + names[Predeploys.L1_FEE_VAULT] = "L1FeeVault"; + names[Predeploys.GOVERNANCE_TOKEN] = "GovernanceToken"; + names[Predeploys.SCHEMA_REGISTRY] = "SchemaRegistry"; + names[Predeploys.EAS] = "EAS"; } /// @dev Sets the precompiles, proxies, and the implementation accounts to be `vm.dumpState` /// to generate a L2 genesis alloc. /// @notice The alloc object is sorted numerically by address. function run() public { - _dealEthToPrecompiles(); - _setPredeployProxies(); - _setPredeployImplementations(); + dealEthToPrecompiles(); + setPredeployProxies(); + setPredeployImplementations(); if (cfg.fundDevAccounts()) { - _fundDevAccounts(); + fundDevAccounts(); } - /// Reset so its not included state dump - vm.etch(address(cfg), ""); - - vm.dumpState(outfile); - _sortJsonByKeys(outfile); + writeStateDump(); } - /// @notice Give all of the precompiles 1 wei so that they are - /// not considered empty accounts. - function _dealEthToPrecompiles() internal { + /// @notice Give all of the precompiles 1 wei + function dealEthToPrecompiles() internal { for (uint256 i; i < PRECOMPILE_COUNT; i++) { vm.deal(address(uint160(i)), 1); } @@ -123,66 +121,165 @@ contract L2Genesis is Script, Artifacts { /// The Proxy bytecode should be set. All proxied predeploys should have /// the 1967 admin slot set to the ProxyAdmin predeploy. All defined predeploys /// should have their implementations set. - function _setPredeployProxies() internal { + function setPredeployProxies() public { bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy"); uint160 prefix = uint160(0x420) << 148; console.log( "Setting proxy deployed bytecode for addresses in range %s through %s", address(prefix | uint160(0)), - address(prefix | uint160(PROXY_COUNT - 1)) + address(prefix | uint160(PREDEPLOY_COUNT - 1)) ); - for (uint256 i = 0; i < PROXY_COUNT; i++) { + for (uint256 i = 0; i < PREDEPLOY_COUNT; i++) { address addr = address(prefix | uint160(i)); if (_notProxied(addr)) { + console.log("Skipping proxy at %s", addr); continue; } vm.etch(addr, code); - vm.store(addr, PROXY_ADMIN_ADDRESS, bytes32(uint256(uint160(Predeploys.PROXY_ADMIN)))); + EIP1967Helper.setAdmin(addr, Predeploys.PROXY_ADMIN); if (_isDefinedPredeploy(addr)) { - address implementation = _predeployToCodeNamespace(addr); + address implementation = predeployToCodeNamespace(addr); console.log("Setting proxy %s implementation: %s", addr, implementation); - vm.store(addr, PROXY_IMPLEMENTATION_ADDRESS, bytes32(uint256(uint160(implementation)))); + EIP1967Helper.setImplementation(addr, implementation); } } } - /// @notice LEGACY_ERC20_ETH is not being predeployed since it's been deprecated. /// @dev Sets all the implementations for the predeploy proxies. For contracts without proxies, /// sets the deployed bytecode at their expected predeploy address. - function _setPredeployImplementations() internal { - _setLegacyMessagePasser(); - _setDeployerWhitelist(); - _setWETH9(); - _setL2StandardBridge(); - _setL2CrossDomainMessenger(); - _setSequencerFeeVault(); - _setOptimismMintableERC20Factory(); - _setL1BlockNumber(); - _setGasPriceOracle(); - _setGovernanceToken(); - _setL1Block(); + /// LEGACY_ERC20_ETH and L1_MESSAGE_SENDER are deprecated and are not set. + function setPredeployImplementations() internal { + setL2ToL1MessagePasser(); + setL2CrossDomainMessenger(); + setL2StandardBridge(); + setL2ERC721Bridge(); + setSequencerFeeVault(); + setOptimismMintableERC20Factory(); + setOptimismMintableERC721Factory(); + setL1Block(); + setGasPriceOracle(); + setDeployerWhitelist(); + setWETH9(); + setL1BlockNumber(); + setLegacyMessagePasser(); + setBaseFeeVault(); + setL1FeeVault(); + setGovernanceToken(); + setSchemaRegistry(); + setEAS(); + } + + function setL2ToL1MessagePasser() public { + _setImplementationCode(Predeploys.L2_TO_L1_MESSAGE_PASSER); } /// @notice This predeploy is following the saftey invariant #1. - function _setLegacyMessagePasser() internal { - _setImplementationCode(Predeploys.LEGACY_MESSAGE_PASSER, "LegacyMessagePasser"); + function setL2CrossDomainMessenger() public { + address impl = _setImplementationCode(Predeploys.L2_CROSS_DOMAIN_MESSENGER); + + L2CrossDomainMessenger(impl).initialize({ + _l1CrossDomainMessenger: L1CrossDomainMessenger(address(0)) + }); + + L2CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER).initialize({ + _l1CrossDomainMessenger: L1CrossDomainMessenger(mustGetAddress("L1CrossDomainMessengerProxy")) + }); } /// @notice This predeploy is following the saftey invariant #1. - function _setDeployerWhitelist() internal { - _setImplementationCode(Predeploys.DEPLOYER_WHITELIST, "DeployerWhitelist"); + function setL2StandardBridge() public { + address impl = _setImplementationCode(Predeploys.L2_STANDARD_BRIDGE); + + L2StandardBridge(payable(impl)).initialize({ + _otherBridge: L1StandardBridge(payable(address(0))) + }); + + L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).initialize({ + _otherBridge: L1StandardBridge(mustGetAddress("L1StandardBridgeProxy")) + }); } /// @notice This predeploy is following the saftey invariant #1. - /// Contract metadata hash appended to deployed bytecode will differ - /// from previous L2 genesis output. - /// This contract is NOT proxied. - /// @dev We're manually setting storage slots because we need to deployment to be at - /// the address `Predeploys.WETH9`, so we can't just deploy a new instance of `WETH9`. - function _setWETH9() internal { + function setL2ERC721Bridge() public { + address impl = _setImplementationCode(Predeploys.L2_ERC721_BRIDGE); + + L2ERC721Bridge(impl).initialize({ + _l1ERC721Bridge: payable(address(0)) + }); + + L2ERC721Bridge(Predeploys.L2_ERC721_BRIDGE).initialize({ + _l1ERC721Bridge: payable(mustGetAddress("L1ERC721BridgeProxy")) + }); + } + + /// @notice This predeploy is following the saftey invariant #2, + function setSequencerFeeVault() public { + SequencerFeeVault vault = new SequencerFeeVault({ + _recipient: cfg.sequencerFeeVaultRecipient(), + _minWithdrawalAmount: cfg.sequencerFeeVaultMinimumWithdrawalAmount(), + _withdrawalNetwork: FeeVault.WithdrawalNetwork(cfg.sequencerFeeVaultWithdrawalNetwork()) + }); + + address impl = predeployToCodeNamespace(Predeploys.SEQUENCER_FEE_WALLET); + console.log("Setting %s implementation at: %s", "SequencerFeeVault", impl); + vm.etch(impl, address(vault).code); + + /// Reset so its not included state dump + vm.etch(address(vault), ""); + vm.resetNonce(address(vault)); + } + + /// @notice This predeploy is following the saftey invariant #1. + function setOptimismMintableERC20Factory() public { + address impl = _setImplementationCode(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY); + + OptimismMintableERC20Factory(impl).initialize({ + _bridge: address(0) + }); + + OptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).initialize({ + _bridge: Predeploys.L2_STANDARD_BRIDGE + }); + } + + /// @notice This predeploy is following the saftey invariant #2, + function setOptimismMintableERC721Factory() public { + OptimismMintableERC721Factory factory = new OptimismMintableERC721Factory({ + _bridge: Predeploys.L2_ERC721_BRIDGE, + _remoteChainId: cfg.l1ChainID() + }); + + address impl = predeployToCodeNamespace(Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY); + console.log("Setting %s implementation at: %s", "OptimismMintableERC721Factory", impl); + vm.etch(impl, address(factory).code); + + /// Reset so its not included state dump + vm.etch(address(factory), ""); + vm.resetNonce(address(factory)); + } + + /// @notice This predeploy is following the saftey invariant #1. + function setL1Block() public { + _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); + } + + /// @notice This predeploy is following the saftey invariant #1. + function setGasPriceOracle() public { + _setImplementationCode(Predeploys.GAS_PRICE_ORACLE); + } + + /// @notice This predeploy is following the saftey invariant #1. + function setDeployerWhitelist() public { + _setImplementationCode(Predeploys.DEPLOYER_WHITELIST); + } + + /// @notice This predeploy is following the saftey invariant #1. + /// This contract is NOT proxied and the state that is set + /// in the constructor is set manually. + function setWETH9() public { console.log("Setting %s implementation at: %s", "WETH9", Predeploys.WETH9); vm.etch(Predeploys.WETH9, vm.getDeployedCode("WETH9.sol:WETH9")); @@ -210,92 +307,51 @@ contract L2Genesis is Script, Artifacts { } /// @notice This predeploy is following the saftey invariant #1. - /// We're initializing the implementation with `address(0)` so - /// it's not left uninitialized. After `initialize` is called on the - /// proxy to set the storage slot with the expected value. - function _setL2StandardBridge() internal { - address impl = _setImplementationCode(Predeploys.L2_STANDARD_BRIDGE, "L2StandardBridge"); - - L2StandardBridge(payable(impl)).initialize(L1StandardBridge(payable(address(0)))); - - L2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).initialize( - L1StandardBridge(mustGetAddress("L1StandardBridgeProxy")) - ); - - _checkL2StandardBridge(impl); + function setL1BlockNumber() public { + _setImplementationCode(Predeploys.L1_BLOCK_NUMBER); } /// @notice This predeploy is following the saftey invariant #1. - /// We're initializing the implementation with `address(0)` so - /// it's not left uninitialized. After `initialize` is called on the - /// proxy to set the storage slot with the expected value. - function _setL2CrossDomainMessenger() internal { - address impl = _setImplementationCode(Predeploys.L2_CROSS_DOMAIN_MESSENGER, "L2CrossDomainMessenger"); - - L2CrossDomainMessenger(impl).initialize(L1CrossDomainMessenger(address(0))); - - L2CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER).initialize( - L1CrossDomainMessenger(mustGetAddress("L1CrossDomainMessengerProxy")) - ); - - _checkL2CrossDomainMessenger(impl); + function setLegacyMessagePasser() public { + _setImplementationCode(Predeploys.LEGACY_MESSAGE_PASSER); } - /// @notice This predeploy is following the saftey invariant #2, - /// because the constructor args are non-static L1 contract - /// addresses that are being read from the deploy config - /// that are set as immutables. - /// @dev Because the constructor args are stored as immutables, - /// we don't have to worry about setting storage slots. - function _setSequencerFeeVault() internal { - SequencerFeeVault vault = new SequencerFeeVault({ - _recipient: cfg.sequencerFeeVaultRecipient(), - _minWithdrawalAmount: cfg.sequencerFeeVaultMinimumWithdrawalAmount(), - _withdrawalNetwork: FeeVault.WithdrawalNetwork(cfg.sequencerFeeVaultWithdrawalNetwork()) + /// @notice This predeploy is following the saftey invariant #2. + function setBaseFeeVault() public { + BaseFeeVault vault = new BaseFeeVault({ + _recipient: cfg.baseFeeVaultRecipient(), + _minWithdrawalAmount: cfg.baseFeeVaultMinimumWithdrawalAmount(), + _withdrawalNetwork: FeeVault.WithdrawalNetwork(cfg.baseFeeVaultWithdrawalNetwork()) }); - address impl = _predeployToCodeNamespace(Predeploys.SEQUENCER_FEE_WALLET); - console.log("Setting %s implementation at: %s", "SequencerFeeVault", impl); + address impl = predeployToCodeNamespace(Predeploys.BASE_FEE_VAULT); + console.log("Setting %s implementation at: %s", "BaseFeeVault", impl); vm.etch(impl, address(vault).code); /// Reset so its not included state dump vm.etch(address(vault), ""); vm.resetNonce(address(vault)); - - _checkSequencerFeeVault(impl); } - /// @notice This predeploy is following the saftey invariant #1. - /// We're initializing the implementation with `address(0)` so - /// it's not left uninitialized. After `initialize` is called on the - /// proxy to set the storage slot with the expected value. - function _setOptimismMintableERC20Factory() internal { - address impl = - _setImplementationCode(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY, "OptimismMintableERC20Factory"); - - OptimismMintableERC20Factory(impl).initialize(address(0)); - - OptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).initialize( - Predeploys.L2_STANDARD_BRIDGE - ); - - _checkOptimismMintableERC20Factory(impl); - } + /// @notice This predeploy is following the saftey invariant #2. + function setL1FeeVault() public { + L1FeeVault vault = new L1FeeVault({ + _recipient: cfg.l1FeeVaultRecipient(), + _minWithdrawalAmount: cfg.l1FeeVaultMinimumWithdrawalAmount(), + _withdrawalNetwork: FeeVault.WithdrawalNetwork(cfg.l1FeeVaultWithdrawalNetwork()) + }); - /// @notice This predeploy is following the saftey invariant #1. - /// This contract has no initializer. - function _setL1BlockNumber() internal { - _setImplementationCode(Predeploys.L1_BLOCK_NUMBER, "L1BlockNumber"); - } + address impl = predeployToCodeNamespace(Predeploys.L1_FEE_VAULT); + console.log("Setting %s implementation at: %s", "L1FeeVault", impl); + vm.etch(impl, address(vault).code); - /// @notice This predeploy is following the saftey invariant #1. - /// This contract has no initializer. - function _setGasPriceOracle() internal { - _setImplementationCode(Predeploys.GAS_PRICE_ORACLE, "GasPriceOracle"); + /// Reset so its not included state dump + vm.etch(address(vault), ""); + vm.resetNonce(address(vault)); } - /// @notice This predeploy is following the saftey invariant #3. - function _setGovernanceToken() internal { + /// @notice This predeploy is following the saftey invariant #2. + function setGovernanceToken() public { if (!cfg.enableGovernance()) { console.log("Governance not enabled, skipping setting governanace token"); return; @@ -319,19 +375,37 @@ contract L2Genesis is Script, Artifacts { } /// @notice This predeploy is following the saftey invariant #1. - /// This contract has no initializer. - /// @dev Previously the initial L1 attributes was set at genesis, to simplify, - /// they no longer are so the resulting storage slots are no longer set. - function _setL1Block() internal { - _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES, "L1Block"); + function setSchemaRegistry() public { + _setImplementationCode(Predeploys.SCHEMA_REGISTRY); + } + + /// @notice This predeploy is following the saftey invariant #2, + /// It uses low level create to deploy the contract due to the code + /// having immutables and being a different compiler version. + function setEAS() public { + string memory cname = names[Predeploys.EAS]; + address impl = predeployToCodeNamespace(Predeploys.EAS); + bytes memory code = vm.getCode(string.concat(cname, ".sol:", cname)); + + address eas; + assembly { + eas := create(0, add(code, 0x20), mload(code)) + } + + console.log("Setting %s implementation at: %s", cname, impl); + vm.etch(impl, eas.code); + + /// Reset so its not included state dump + vm.etch(address(eas), ""); + vm.resetNonce(address(eas)); } - /// @dev Returns true if the address is not proxied. + /// @notice Returns true if the address is not proxied. function _notProxied(address _addr) internal pure returns (bool) { return _addr == Predeploys.GOVERNANCE_TOKEN || _addr == Predeploys.WETH9; } - /// @dev Returns true if the address is a predeploy. + /// @notice Returns true if the address is a predeploy. function _isDefinedPredeploy(address _addr) internal pure returns (bool) { return _addr == Predeploys.L2_TO_L1_MESSAGE_PASSER || _addr == Predeploys.L2_CROSS_DOMAIN_MESSENGER || _addr == Predeploys.L2_STANDARD_BRIDGE || _addr == Predeploys.L2_ERC721_BRIDGE @@ -343,42 +417,39 @@ contract L2Genesis is Script, Artifacts { || _addr == Predeploys.GOVERNANCE_TOKEN || _addr == Predeploys.SCHEMA_REGISTRY || _addr == Predeploys.EAS; } - /// @dev Function to compute the expected address of the predeploy implementation - /// in the genesis state. - function _predeployToCodeNamespace(address _addr) internal pure returns (address) { + /// @notice Function to compute the expected address of the predeploy implementation + /// in the genesis state. + function predeployToCodeNamespace(address _addr) public pure returns (address) { return address( uint160(uint256(uint160(_addr)) & 0xffff | uint256(uint160(0xc0D3C0d3C0d3C0D3c0d3C0d3c0D3C0d3c0d30000))) ); } - function _setImplementationCode(address _addr, string memory _name) internal returns (address) { - address impl = _predeployToCodeNamespace(_addr); - console.log("Setting %s implementation at: %s", _name, impl); - vm.etch(impl, vm.getDeployedCode(string.concat(_name, ".sol:", _name))); - - _verifyProxyImplementationAddress(_addr, impl); - + /// @notice Sets the bytecode in state + function _setImplementationCode(address _addr) internal returns (address) { + string memory cname = names[_addr]; + address impl = predeployToCodeNamespace(_addr); + console.log("Setting %s implementation at: %s", cname, impl); + vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); return impl; } - /// @dev Function to verify the expected implementation address is set for the respective proxy. - function _verifyProxyImplementationAddress(address _proxy, address _impl) internal view { - require( - EIP1967Helper.getImplementation(_proxy) == _impl, - "Expected different address at Proxys PROXY_IMPLEMENTATION_ADDRESS storage slot" - ); - } - - /// @dev Function to verify that a contract was initialized, and can't be reinitialized. - /// @notice There isn't a good way to know if the resulting revering is due to abi mismatch - /// or because it's already been initialized - function _verifyCantReinitialize(address _contract, address _arg) internal { - vm.expectRevert("Initializable: contract is already initialized"); - IInitializable(_contract).initialize(_arg); + /// @notice Writes the state dump to disk + function writeStateDump() public { + /// Reset so its not included state dump + vm.etch(address(cfg), ""); + vm.etch(msg.sender, ""); + vm.resetNonce(msg.sender); + vm.deal(msg.sender, 0); + + string memory path = Config.stateDumpPath(); + console.log("Writing state dump to: %s", path); + vm.dumpState(path); + sortJsonByKeys(path); } - /// @dev Helper function to sort the genesis alloc numerically by address. - function _sortJsonByKeys(string memory _path) internal { + /// @notice Sorts the allocs by address + function sortJsonByKeys(string memory _path) internal { string[] memory commands = new string[](3); commands[0] = "bash"; commands[1] = "-c"; @@ -386,44 +457,11 @@ contract L2Genesis is Script, Artifacts { vm.ffi(commands); } - function _fundDevAccounts() internal { + /// @notice Funds the default dev accounts with ether + function fundDevAccounts() internal { for (uint256 i; i < devAccounts.length; i++) { console.log("Funding dev account %s with %s ETH", devAccounts[i], DEV_ACCOUNT_FUND_AMT / 1e18); vm.deal(devAccounts[i], DEV_ACCOUNT_FUND_AMT); } - - _checkDevAccountsFunded(); - } - - ////////////////////////////////////////////////////// - /// Post Checks - ////////////////////////////////////////////////////// - function _checkL2StandardBridge(address _impl) internal { - _verifyCantReinitialize(_impl, address(0)); - _verifyCantReinitialize(Predeploys.L2_STANDARD_BRIDGE, mustGetAddress("L1StandardBridgeProxy")); - } - - function _checkL2CrossDomainMessenger(address _impl) internal { - _verifyCantReinitialize(_impl, address(0)); - _verifyCantReinitialize(Predeploys.L2_CROSS_DOMAIN_MESSENGER, mustGetAddress("L1CrossDomainMessengerProxy")); - } - - function _checkSequencerFeeVault(address _impl) internal view { - _verifyProxyImplementationAddress(Predeploys.SEQUENCER_FEE_WALLET, _impl); - } - - function _checkOptimismMintableERC20Factory(address _impl) internal { - _verifyCantReinitialize(_impl, address(0)); - _verifyCantReinitialize(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY, Predeploys.L2_STANDARD_BRIDGE); - } - - function _checkDevAccountsFunded() internal view { - for (uint256 i; i < devAccounts.length; i++) { - if (devAccounts[i].balance != DEV_ACCOUNT_FUND_AMT) { - revert( - string.concat("Dev account not funded with expected amount of ETH: ", vm.toString(devAccounts[i])) - ); - } - } } } diff --git a/packages/contracts-bedrock/src/libraries/Constants.sol b/packages/contracts-bedrock/src/libraries/Constants.sol index 933250d1ecda5..2c7b337a745f0 100644 --- a/packages/contracts-bedrock/src/libraries/Constants.sol +++ b/packages/contracts-bedrock/src/libraries/Constants.sol @@ -44,3 +44,4 @@ library Constants { return config; } } + diff --git a/packages/contracts-bedrock/test/L2Genesis.t.sol b/packages/contracts-bedrock/test/L2Genesis.t.sol new file mode 100644 index 0000000000000..4a24f9d5b461d --- /dev/null +++ b/packages/contracts-bedrock/test/L2Genesis.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { L2Genesis } from "scripts/L2Genesis.s.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import { LibString } from "solady/utils/LibString.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +contract L2GenesisTest is Test { + L2Genesis genesis; + + function setUp() public { + vm.setEnv("CONTRACT_ADDRESSES_PATH", string.concat(vm.projectRoot(), "/test/mocks/addresses.json")); + + genesis = new L2Genesis(); + genesis.setUp(); + } + + function tmpfile() internal returns (string memory) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = "mktemp"; + bytes memory result = vm.ffi(commands); + return string(result); + } + + function deleteFile(string memory path) internal { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("rm ", path); + vm.ffi(commands); + } + + function readJSON(string memory path) internal returns (string memory) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq < ", path); + return string(vm.ffi(commands)); + } + + function getJSONKeyCount(string memory path) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq 'keys | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + // can this become a modifier? + function withTempDump(function (string memory) internal f) internal { + string memory path = tmpfile(); + vm.setEnv("STATE_DUMP_PATH", path); + f(path); + deleteFile(path); + } + + function getAccount(string memory path, string memory key) internal returns (string memory) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq '.[\"", key, "\"]' < ", path); + return string(vm.ffi(commands)); + } + + // this is slower.. + function getBalance(string memory account) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("echo '", account, "' | jq -r '.balance'"); + return vm.parseUint(string(vm.ffi(commands))); + } + + function getAccountCountWithNoCodeAndNoBalance(string memory path) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq 'map_values(select(.nonce == \"0x0\" and .balance == \"0x0\")) | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + // Go from keys + function getCode(string memory account) internal returns (bytes memory) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("echo '", account, "' | jq -r '.code'"); + return bytes(vm.ffi(commands)); + } + + /// @notice Returns the number of accounts that contain particular code at a given path to a genesis file. + function getCodeCount(string memory path, string memory name) internal returns (uint256) { + bytes memory code = vm.getDeployedCode(name); + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq -r 'map_values(select(.code == \"", vm.toString(code), "\")) | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + function getPredeployCountWithStorage(string memory path, uint256 count) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq 'map_values(.storage | select(length == ", vm.toString(count), ")) | keys | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + function getPredeployCountWithSlotSet(string memory path, bytes32 slot) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq 'map_values(.storage | select(has(\"", vm.toString(slot), "\"))) | keys | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + function getPredeployCountWithSlotSetToValue(string memory path, bytes32 slot, bytes32 value) internal returns (uint256) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + // jq 'map_values(.storage | select(."0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103" == "0x0000000000000000000000004200000000000000000000000000000000000018"))' + commands[2] = string.concat("jq 'map_values(.storage | select(.\"", vm.toString(slot), "\" == \"", vm.toString(value), "\")) | length' < ", path, " | xargs cast abi-encode 'f(uint256)'"); + return abi.decode(vm.ffi(commands), (uint256)); + } + + function getImplementationAtAPath(string memory path, address addr) internal returns (address) { + string[] memory commands = new string[](3); + commands[0] = "bash"; + commands[1] = "-c"; + commands[2] = string.concat("jq -r '.\"", vm.toString(addr), "\".storage.\"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc\"' < ", path); + return address(uint160(uint256(abi.decode(vm.ffi(commands), (bytes32))))); + } + + function testPredeployProxies() external { + withTempDump(_testPredeployProxies); + } + + // TODO: there are 2 addresses that dont work + function _testPredeployProxies(string memory path) internal { + // Set the predeploy proxies into state + genesis.setPredeployProxies(); + genesis.writeStateDump(); + + // 2 predeploys do not have proxies + assertEq(getCodeCount(path, "Proxy.sol:Proxy"), genesis.PREDEPLOY_COUNT() - 2); + + // 17 proxies have the implementation set + assertEq(getPredeployCountWithSlotSet(path, Constants.PROXY_IMPLEMENTATION_ADDRESS), 17); + + // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin + assertEq(getPredeployCountWithSlotSetToValue(path, Constants.PROXY_OWNER_ADDRESS, bytes32(uint256(uint160(Predeploys.PROXY_ADMIN)))), genesis.PREDEPLOY_COUNT() - 2); + + // For each predeploy + assertEq(getImplementationAtAPath(path, Predeploys.L2_TO_L1_MESSAGE_PASSER), 0xC0D3C0d3C0d3c0d3C0d3C0D3c0D3c0d3c0D30016); + assertEq(getImplementationAtAPath(path, Predeploys.L2_CROSS_DOMAIN_MESSENGER), 0xC0d3c0d3c0D3c0D3C0d3C0D3C0D3c0d3c0d30007); + assertEq(getImplementationAtAPath(path, Predeploys.L2_STANDARD_BRIDGE), 0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010); + assertEq(getImplementationAtAPath(path, Predeploys.L2_ERC721_BRIDGE), 0xC0D3c0d3c0d3c0d3c0D3C0d3C0D3C0D3c0d30014); + assertEq(getImplementationAtAPath(path, Predeploys.SEQUENCER_FEE_WALLET), 0xC0D3C0d3c0d3c0d3C0D3c0d3C0D3c0d3c0D30011); + assertEq(getImplementationAtAPath(path, Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY), 0xc0D3c0d3C0d3c0d3c0D3c0d3c0D3c0D3c0D30012); + assertEq(getImplementationAtAPath(path, Predeploys.OPTIMISM_MINTABLE_ERC721_FACTORY), 0xc0d3C0d3C0d3C0d3C0d3c0d3C0D3C0d3C0D30017); + assertEq(getImplementationAtAPath(path, Predeploys.L1_BLOCK_ATTRIBUTES), 0xc0d3C0D3C0D3c0D3C0D3C0d3C0D3c0D3c0d30015); + //assertEq(getImplementationAtAPath(path, Predeploys.GAS_PRICE_ORACLE), 0xc0d3C0d3C0d3c0D3C0D3C0d3C0d3C0D3C0D3000f); + assertEq(getImplementationAtAPath(path, Predeploys.DEPLOYER_WHITELIST), 0xc0d3c0d3C0d3c0D3c0d3C0D3c0d3C0d3c0D30002); + assertEq(getImplementationAtAPath(path, Predeploys.L1_BLOCK_NUMBER), 0xC0D3C0d3C0D3c0D3C0d3c0D3C0d3c0d3C0d30013); + assertEq(getImplementationAtAPath(path, Predeploys.LEGACY_MESSAGE_PASSER), 0xc0D3C0d3C0d3C0D3c0d3C0d3c0D3C0d3c0d30000); + assertEq(getImplementationAtAPath(path, Predeploys.PROXY_ADMIN), 0xC0d3C0D3c0d3C0d3c0d3c0D3C0D3C0d3C0D30018); + assertEq(getImplementationAtAPath(path, Predeploys.BASE_FEE_VAULT), 0xC0d3c0D3c0d3C0D3C0D3C0d3c0D3C0D3c0d30019); + //assertEq(getImplementationAtAPath(path, Predeploys.L1_FEE_VAULT), 0xc0D3c0D3C0d3c0d3c0d3C0d3c0d3C0d3C0D3001A); + assertEq(getImplementationAtAPath(path, Predeploys.SCHEMA_REGISTRY), 0xc0d3c0d3c0d3C0d3c0d3C0D3C0D3c0d3C0D30020); + assertEq(getImplementationAtAPath(path, Predeploys.EAS), 0xC0D3c0D3C0d3c0D3c0D3C0D3c0D3c0d3c0d30021); + } +} diff --git a/packages/contracts-bedrock/test/mocks/EIP1967Helper.sol b/packages/contracts-bedrock/test/mocks/EIP1967Helper.sol index a9330890d112a..8717c7a2f072c 100644 --- a/packages/contracts-bedrock/test/mocks/EIP1967Helper.sol +++ b/packages/contracts-bedrock/test/mocks/EIP1967Helper.sol @@ -8,20 +8,28 @@ import { Vm } from "forge-std/Vm.sol"; library EIP1967Helper { /// @notice The storage slot that holds the address of a proxy implementation. /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` - bytes32 internal constant PROXY_IMPLEMENTATION_ADDRESS = + bytes32 internal constant PROXY_IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /// @notice The storage slot that holds the address of the owner. /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` - bytes32 internal constant PROXY_OWNER_ADDRESS = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant PROXY_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function getAdmin(address _proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(address(_proxy), PROXY_OWNER_ADDRESS)))); + return address(uint160(uint256(vm.load(address(_proxy), PROXY_ADMIN_SLOT)))); + } + + function setAdmin(address _addr, address _admin) internal { + vm.store(_addr, PROXY_ADMIN_SLOT, bytes32(uint256(uint160(_admin)))); } function getImplementation(address _proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(address(_proxy), PROXY_IMPLEMENTATION_ADDRESS)))); + return address(uint160(uint256(vm.load(address(_proxy), PROXY_IMPLEMENTATION_SLOT)))); + } + + function setImplementation(address _addr, address _admin) internal { + vm.store(_addr, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(_admin)))); } } diff --git a/packages/contracts-bedrock/test/mocks/addresses.json b/packages/contracts-bedrock/test/mocks/addresses.json new file mode 100644 index 0000000000000..f3b319c9042d8 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/addresses.json @@ -0,0 +1,30 @@ +{ + "AddressManager": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", + "DisputeGameFactory": "0x443d8fa004E7Ab97594A93ecC0b84b27dC595FED", + "DisputeGameFactoryProxy": "0x756e0562323ADcDA4430d6cb456d9151f605290B", + "L1CrossDomainMessenger": "0x71fA82Ea96672797954C28032b337aA40AAFC99f", + "L1CrossDomainMessengerProxy": "0x13aa49bAc059d709dd0a18D6bb63290076a702D7", + "L1ERC721Bridge": "0x44637A4292E0CD2B17A55d5F6B2F05AFcAcD0586", + "L1ERC721BridgeProxy": "0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f", + "L1StandardBridge": "0x0Da314776B267D898dEE57F6Ede357ae28b3b83c", + "L1StandardBridgeProxy": "0x96d3F6c20EEd2697647F543fE6C08bC2Fbf39758", + "L2OutputOracle": "0x19652082F846171168Daf378C4fD3ee85a0D4A60", + "L2OutputOracleProxy": "0x1aF7f588A501EA2B5bB3feeFA744892aA2CF00e6", + "Mips": "0xc9dCf0B40e4229049402025091A809a967840FA2", + "OptimismMintableERC20Factory": "0x39Aea2Dd53f2d01c15877aCc2791af6BDD7aD567", + "OptimismMintableERC20FactoryProxy": "0xDB25A7b768311dE128BBDa7B8426c3f9C74f3240", + "OptimismPortal": "0x8887E7568E81405c4E0D4cAaabdda949e3B9d4E4", + "OptimismPortal2": "0x9551B6e27B135D3929Feaa8088c92E8b5d6dFbf0", + "OptimismPortalProxy": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", + "PreimageOracle": "0x4807Ac1C24f0f415466E892C1de4FAB06b0390Ac", + "ProtocolVersions": "0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F", + "ProtocolVersionsProxy": "0x15cF58144EF33af1e14b5208015d11F9143E27b9", + "ProxyAdmin": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", + "SafeProxyFactory": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f", + "SafeSingleton": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "SuperchainConfig": "0x068E44eB31e111028c41598E4535be7468674D0A", + "SuperchainConfigProxy": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c", + "SystemConfig": "0xffbA8944650e26653823658d76A122946F27e2f2", + "SystemConfigProxy": "0xD16d567549A2a2a2005aEACf7fB193851603dd70", + "SystemOwnerSafe": "0xEC0Ed5dAF1E398eC507e7041Ca8702D85D770bb8" +} \ No newline at end of file