diff --git a/l1-contracts/contracts/chain-registrar/ChainRegistrar.sol b/l1-contracts/contracts/chain-registrar/ChainRegistrar.sol new file mode 100644 index 000000000..1faad4c8b --- /dev/null +++ b/l1-contracts/contracts/chain-registrar/ChainRegistrar.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IBridgehub} from "../bridgehub/IBridgehub.sol"; +import {PubdataPricingMode} from "../state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import {IStateTransitionManager} from "../state-transition/IStateTransitionManager.sol"; +import {ETH_TOKEN_ADDRESS} from "../common/Config.sol"; +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; +import {IGetters} from "../state-transition/chain-interfaces/IGetters.sol"; +import {SafeERC20} from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; + +/// @title ChainRegistrar Contract +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice This contract is used as a public registry where anyone can propose new chain registration in ZKsync ecosystem. +/// @notice It also helps chain administrators retrieve all necessary L1 information about their chain. +/// @notice Additionally, it assists ZKsync ecosystem admin in verifying the correctness of registration transactions. +/// @dev ChainRegistrar is designed for use with a proxy for upgradability. +/// @dev It interacts with the Bridgehub for getting chain registration results. +/// @dev This contract does not make write calls to the Bridgehub itself for security reasons. +contract ChainRegistrar is Ownable2StepUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Address that will be used for deploying L2 contracts. + /// @dev During the chain proposal, some base tokens must be transferred to this address. + address public l2Deployer; + + /// @notice Address of ZKsync Bridgehub. + IBridgehub public bridgehub; + + /// @notice Mapping of proposed chains by author and chain ID. + /// @notice Stores chain proposals made by users, where each address can propose a chain with a unique chain ID. + mapping(address => mapping(uint256 => ChainConfig)) public proposedChains; + + /// @dev Thrown when trying to propose a chain that is already proposed. + error ChainIsAlreadyProposed(); + + /// @dev Thrown when trying to register a chain that is already deployed. + error ChainIsAlreadyDeployed(); + + /// @dev Thrown when querying information about a chain that is not yet deployed. + error ChainIsNotYetDeployed(); + + /// @dev Thrown when the bridge for a chain is not registered. + error BridgeIsNotRegistered(); + + /// @notice Emitted when a new chain registration proposal is made. + /// @param chainId Unique ID of the proposed chain. + /// @param author Address of the proposer. + event NewChainRegistrationProposal(uint256 indexed chainId, address author); + + /// @notice Emitted when the L2 deployer address is changed. + /// @param newDeployer Address of the new L2 deployer. + event L2DeployerChanged(address newDeployer); + + /// @dev Struct for holding the base token configuration of a chain. + /// @param gasPriceMultiplierNominator Gas price multiplier numerator, used to compare the base token price to ether for L1->L2 transactions. + /// @param gasPriceMultiplierDenominator Gas price multiplier denominator, used to compare the base token price to ether for L1->L2 transactions. + /// @param tokenAddress Address of the base token used for gas fees. + /// @param tokenMultiplierSetter Address responsible for setting the token multiplier. + struct BaseToken { + uint128 gasPriceMultiplierNominator; + uint128 gasPriceMultiplierDenominator; + address tokenAddress; + address tokenMultiplierSetter; + } + + /// @dev Struct for holding the configuration of a proposed chain. + /// @param chainId Unique chain ID. + /// @param baseToken Base token configuration for the chain. + /// @param blobOperator Operator responsible for making commit transactions. + /// @param operator Operator responsible for making prove and execute transactions. + /// @param governor Governor of the chain; will receive ownership of the ChainAdmin contract. + /// @param pubdataPricingMode Mode for charging users for pubdata. + // solhint-disable-next-line gas-struct-packing + struct ChainConfig { + uint256 chainId; + BaseToken baseToken; + address blobOperator; + address operator; + address governor; + PubdataPricingMode pubdataPricingMode; + } + + /// @dev Struct for holding the configuration of a fully deployed chain. + /// @param pendingChainAdmin Address of the pending admin for the chain. + /// @param chainAdmin Address of the current admin for the chain. + /// @param diamondProxy Address of the main contract (diamond proxy) for the deployed chain. + /// @param l2BridgeAddress Address of the L2 bridge inside the deployed chain. + // solhint-disable-next-line gas-struct-packing + struct RegisteredChainConfig { + address pendingChainAdmin; + address chainAdmin; + address diamondProxy; + address l2BridgeAddress; + } + + /// @dev Contract is expected to be used as proxy implementation. + constructor() { + // Disable initialization to prevent Parity hack. + _disableInitializers(); + } + + /// @notice Initializes the contract with the given parameters. + /// @dev Can only be called once, during contract deployment. + /// @param _bridgehub Address of the ZKsync Bridgehub. + /// @param _l2Deployer Address of the L2 deployer. + /// @param _owner Address of the contract owner. + function initialize(address _bridgehub, address _l2Deployer, address _owner) external initializer { + bridgehub = IBridgehub(_bridgehub); + l2Deployer = _l2Deployer; + _transferOwnership(_owner); + } + + /// @notice Proposes a new chain to be registered in the ZKsync ecosystem. + /// @dev The proposal will fail if the chain has already been registered. + /// @dev For non-ETH-based chains, either an equivalent of 1 ETH of the base token must be approved or transferred to the L2 deployer. + /// @param _chainId Unique ID of the proposed chain. + /// @param _pubdataPricingMode Mode for charging users for pubdata. + /// @param _blobOperator Address responsible for commit transactions. + /// @param _operator Address responsible for prove and execute transactions. + /// @param _governor Address to receive ownership of the ChainAdmin contract. + /// @param _baseTokenAddress Address of the base token used for gas fees. + /// @param _tokenMultiplierSetter Address responsible for setting the base token multiplier. + /// @param _gasPriceMultiplierNominator Gas price multiplier numerator for L1->L2 transactions. + /// @param _gasPriceMultiplierDenominator Gas price multiplier denominator for L1->L2 transactions. + function proposeChainRegistration( + uint256 _chainId, + PubdataPricingMode _pubdataPricingMode, + address _blobOperator, + address _operator, + address _governor, + address _baseTokenAddress, + address _tokenMultiplierSetter, + uint128 _gasPriceMultiplierNominator, + uint128 _gasPriceMultiplierDenominator + ) external { + ChainConfig memory config = ChainConfig({ + chainId: _chainId, + pubdataPricingMode: _pubdataPricingMode, + blobOperator: _blobOperator, + operator: _operator, + governor: _governor, + baseToken: BaseToken({ + tokenAddress: _baseTokenAddress, + tokenMultiplierSetter: _tokenMultiplierSetter, + gasPriceMultiplierNominator: _gasPriceMultiplierNominator, + gasPriceMultiplierDenominator: _gasPriceMultiplierDenominator + }) + }); + + if (bridgehub.stateTransitionManager(config.chainId) != address(0)) { + revert ChainIsAlreadyDeployed(); + } + + ChainConfig memory existingConfig = proposedChains[msg.sender][_chainId]; + + // Check if the chain has already been proposed. This prevents situations where the chain author tries to modify parameters after the initial proposal, ensuring that ZKsync administrators are aware of any changes. + if (existingConfig.chainId != 0) { + revert ChainIsAlreadyProposed(); + } + + proposedChains[msg.sender][_chainId] = config; + + // Handle base token transfer for non-ETH-based networks. + if (config.baseToken.tokenAddress != ETH_TOKEN_ADDRESS) { + uint256 amount = (1 ether * config.baseToken.gasPriceMultiplierNominator) / + config.baseToken.gasPriceMultiplierDenominator; + if (IERC20(config.baseToken.tokenAddress).balanceOf(l2Deployer) < amount) { + IERC20(config.baseToken.tokenAddress).safeTransferFrom(msg.sender, l2Deployer, amount); + } + } + + emit NewChainRegistrationProposal(config.chainId, msg.sender); + } + + /// @notice Changes the address of the L2 deployer. + /// @param _newDeployer New address of the L2 deployer. + function changeDeployer(address _newDeployer) external onlyOwner { + l2Deployer = _newDeployer; + emit L2DeployerChanged(l2Deployer); + } + + /// @notice Retrieves the configuration of a registered chain by its ID. + /// @param _chainId ID of the chain. + /// @return The configuration of the registered chain. + function getRegisteredChainConfig(uint256 _chainId) external view returns (RegisteredChainConfig memory) { + address stm = bridgehub.stateTransitionManager(_chainId); + if (stm == address(0)) { + revert ChainIsNotYetDeployed(); + } + + address diamondProxy = IStateTransitionManager(stm).getHyperchain(_chainId); + address pendingChainAdmin = IGetters(diamondProxy).getPendingAdmin(); + address chainAdmin = IGetters(diamondProxy).getAdmin(); + address l2BridgeAddress = bridgehub.sharedBridge().l2BridgeAddress(_chainId); + if (l2BridgeAddress == address(0)) { + revert BridgeIsNotRegistered(); + } + + RegisteredChainConfig memory config = RegisteredChainConfig({ + pendingChainAdmin: pendingChainAdmin, + chainAdmin: chainAdmin, + diamondProxy: diamondProxy, + l2BridgeAddress: l2BridgeAddress + }); + + return config; + } +} diff --git a/l1-contracts/contracts/dev-contracts/test/DummyHyperchain.sol b/l1-contracts/contracts/dev-contracts/test/DummyHyperchain.sol index 11be65a2b..5e85e97e8 100644 --- a/l1-contracts/contracts/dev-contracts/test/DummyHyperchain.sol +++ b/l1-contracts/contracts/dev-contracts/test/DummyHyperchain.sol @@ -5,6 +5,7 @@ import {MailboxFacet} from "../../state-transition/chain-deps/facets/Mailbox.sol import {FeeParams, PubdataPricingMode} from "../../state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; contract DummyHyperchain is MailboxFacet { + address public admin; constructor(address bridgeHubAddress, uint256 _eraChainId) MailboxFacet(_eraChainId) { s.bridgehub = bridgeHubAddress; } @@ -32,6 +33,18 @@ contract DummyHyperchain is MailboxFacet { s.priorityTxMaxGasLimit = type(uint256).max; } + function initialize(address _admin) external { + admin = _admin; + } + + function getAdmin() external view returns (address) { + return admin; + } + + function getPendingAdmin() external view returns (address) { + return admin; + } + function _randomFeeParams() internal pure returns (FeeParams memory) { return FeeParams({ diff --git a/l1-contracts/deploy-scripts/DeployL1.s.sol b/l1-contracts/deploy-scripts/DeployL1.s.sol index 416ffd6a3..6213f3bb9 100644 --- a/l1-contracts/deploy-scripts/DeployL1.s.sol +++ b/l1-contracts/deploy-scripts/DeployL1.s.sol @@ -34,6 +34,7 @@ import {L1SharedBridge} from "contracts/bridge/L1SharedBridge.sol"; import {L1ERC20Bridge} from "contracts/bridge/L1ERC20Bridge.sol"; import {DiamondProxy} from "contracts/state-transition/chain-deps/DiamondProxy.sol"; import {AddressHasNoCode} from "./ZkSyncScriptErrors.sol"; +import {ChainRegistrar} from "../contracts/chain-registrar/ChainRegistrar.sol"; contract DeployL1Script is Script { using stdToml for string; @@ -52,6 +53,7 @@ contract DeployL1Script is Script { address blobVersionedHashRetriever; address validatorTimelock; address create2Factory; + address chainRegistrar; } // solhint-disable-next-line gas-struct-packing @@ -88,6 +90,7 @@ contract DeployL1Script is Script { uint256 l1ChainId; uint256 eraChainId; address deployerAddress; + address l2Deployer; address ownerAddress; bool testnetVerifier; ContractsConfig contracts; @@ -157,6 +160,7 @@ contract DeployL1Script is Script { deployErc20BridgeImplementation(); deployErc20BridgeProxy(); updateSharedBridge(); + deployChainRegistrar(); updateOwners(); @@ -176,6 +180,7 @@ contract DeployL1Script is Script { // https://book.getfoundry.sh/cheatcodes/parse-toml config.eraChainId = toml.readUint("$.era_chain_id"); config.ownerAddress = toml.readAddress("$.owner_address"); + config.l2Deployer = toml.readAddress("$.l2_deployer"); config.testnetVerifier = toml.readBool("$.testnet_verifier"); config.contracts.governanceSecurityCouncilAddress = toml.readAddress( @@ -301,6 +306,27 @@ contract DeployL1Script is Script { addresses.governance = contractAddress; } + function deployChainRegistrar() internal { + bytes memory bytecodeImplementation = abi.encodePacked(type(ChainRegistrar).creationCode); + address chainRegistrarImplementation = deployViaCreate2(bytecodeImplementation); + console.log("Chain Registrar implementation deployed at:", chainRegistrarImplementation); + + bytes memory bytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode( + chainRegistrarImplementation, + addresses.transparentProxyAdmin, + abi.encodeCall( + ChainRegistrar.initialize, + (addresses.bridgehub.bridgehubProxy, config.l2Deployer, config.ownerAddress) + ) + ) + ); + address chainRegistrar = deployViaCreate2(bytecode); + console.log("Chain Registrar deployed at:", chainRegistrar); + addresses.chainRegistrar = chainRegistrar; + } + function deployChainAdmin() internal { bytes memory bytecode = abi.encodePacked( type(ChainAdmin).creationCode, @@ -715,6 +741,7 @@ contract DeployL1Script is Script { vm.serializeAddress("deployed_addresses", "chain_admin", addresses.chainAdmin); vm.serializeString("deployed_addresses", "bridgehub", bridgehub); vm.serializeString("deployed_addresses", "state_transition", stateTransition); + vm.serializeAddress("deployed_addresses", "chain_registrar", addresses.chainRegistrar); string memory deployedAddresses = vm.serializeString("deployed_addresses", "bridges", bridges); vm.serializeAddress("root", "create2_factory_addr", addresses.create2Factory); diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index 899cc9263..42e64cdd0 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.21; -import {Script} from "forge-std/Script.sol"; +import {Script, console2 as console} from "forge-std/Script.sol"; import {stdToml} from "forge-std/StdToml.sol"; import {Utils} from "./Utils.sol"; import {L2ContractHelper} from "contracts/common/libraries/L2ContractHelper.sol"; import {AddressAliasHelper} from "contracts/vendor/AddressAliasHelper.sol"; import {L1SharedBridge} from "contracts/bridge/L1SharedBridge.sol"; +import {ChainRegistrar} from "contracts/chain-registrar/ChainRegistrar.sol"; contract DeployL2Script is Script { using stdToml for string; @@ -22,6 +23,8 @@ contract DeployL2Script is Script { address l1SharedBridgeProxy; address governance; address erc20BridgeProxy; + address chainRegistrar; + address proposalAuthor; // The owner of the contract sets the validator/attester weights. // Can be the developer multisig wallet on mainnet. address consensusRegistryOwner; @@ -184,6 +187,8 @@ contract DeployL2Script is Script { config.l1SharedBridgeProxy = toml.readAddress("$.l1_shared_bridge"); config.erc20BridgeProxy = toml.readAddress("$.erc20_bridge"); config.consensusRegistryOwner = toml.readAddress("$.consensus_registry_owner"); + config.chainRegistrar = toml.readAddress("$.chain_registrar"); + config.proposalAuthor = toml.readAddress("$.proposal_author"); config.chainId = toml.readUint("$.chain_id"); config.eraChainId = toml.readUint("$.era_chain_id"); } diff --git a/l1-contracts/deploy-scripts/ProposeChainRegistration.s.sol b/l1-contracts/deploy-scripts/ProposeChainRegistration.s.sol new file mode 100644 index 000000000..0676a056b --- /dev/null +++ b/l1-contracts/deploy-scripts/ProposeChainRegistration.s.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Script, console2 as console} from "forge-std/Script.sol"; +import {stdToml} from "forge-std/StdToml.sol"; +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import {ChainRegistrar} from "contracts/chain-registrar/ChainRegistrar.sol"; +import {PubdataPricingMode} from "contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; + +contract ProposeChainRegistration is Script { + using stdToml for string; + address internal constant ADDRESS_ONE = 0x0000000000000000000000000000000000000001; + + // solhint-disable-next-line gas-struct-packing + struct Config { + address chainRegistrar; + ChainRegistrar.ChainConfig chainConfig; + } + + Config config; + + function run() external { + initializeConfig(); + approveBaseToken(); + proposeRegistration(); + } + + function initializeConfig() internal { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/script-config/config-propose-chain-registration.toml"); + string memory toml = vm.readFile(path); + + // Config file must be parsed key by key, otherwise values returned + // are parsed alfabetically and not by key. + // https://book.getfoundry.sh/cheatcodes/parse-toml + config.chainRegistrar = toml.readAddress("$.chain_registrar"); + + config.chainConfig.chainId = toml.readUint("$.chain.chain_id"); + config.chainConfig.operator = toml.readAddress("$.chain.operator"); + config.chainConfig.blobOperator = toml.readAddress("$.chain.blob_operator"); + config.chainConfig.governor = toml.readAddress("$.chain.governor"); + config.chainConfig.pubdataPricingMode = PubdataPricingMode(toml.readUint("$.chain.pubdata_pricing_mode")); + + config.chainConfig.baseToken.tokenMultiplierSetter = toml.readAddress( + "$.chain.base_token.token_multiplier_setter" + ); + config.chainConfig.baseToken.tokenAddress = toml.readAddress("$.chain.base_token.address"); + config.chainConfig.baseToken.gasPriceMultiplierNominator = uint128( + toml.readUint("$.chain.base_token.nominator") + ); + config.chainConfig.baseToken.gasPriceMultiplierDenominator = uint128( + toml.readUint("$.chain.base_token.denominator") + ); + } + + function approveBaseToken() internal { + if (config.chainConfig.baseToken.tokenAddress == ADDRESS_ONE) { + return; + } + uint256 amount = (1 ether * config.chainConfig.baseToken.gasPriceMultiplierNominator) / + config.chainConfig.baseToken.gasPriceMultiplierDenominator; + + vm.broadcast(); + IERC20(config.chainConfig.baseToken.tokenAddress).approve(config.chainRegistrar, amount); + } + + function proposeRegistration() internal { + ChainRegistrar chain_registrar = ChainRegistrar(config.chainRegistrar); + vm.broadcast(); + chain_registrar.proposeChainRegistration( + config.chainConfig.chainId, + config.chainConfig.pubdataPricingMode, + config.chainConfig.blobOperator, + config.chainConfig.operator, + config.chainConfig.governor, + config.chainConfig.baseToken.tokenAddress, + config.chainConfig.baseToken.tokenMultiplierSetter, + config.chainConfig.baseToken.gasPriceMultiplierNominator, + config.chainConfig.baseToken.gasPriceMultiplierDenominator + ); + } +} diff --git a/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol b/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol index bbc01226d..0f5ad1b7d 100644 --- a/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol +++ b/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol @@ -4,16 +4,17 @@ pragma solidity 0.8.24; // solhint-disable no-console, gas-custom-errors, reason-string import {Script, console2 as console} from "forge-std/Script.sol"; -import {Vm} from "forge-std/Vm.sol"; import {stdToml} from "forge-std/StdToml.sol"; import {Bridgehub} from "contracts/bridgehub/Bridgehub.sol"; +import {StateTransitionManager} from "contracts/state-transition/StateTransitionManager.sol"; + import {IZkSyncHyperchain} from "contracts/state-transition/chain-interfaces/IZkSyncHyperchain.sol"; import {ValidatorTimelock} from "contracts/state-transition/ValidatorTimelock.sol"; -import {Governance} from "contracts/governance/Governance.sol"; import {ChainAdmin} from "contracts/governance/ChainAdmin.sol"; import {Utils} from "./Utils.sol"; import {PubdataPricingMode} from "contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import {ChainRegistrar} from "contracts/chain-registrar/ChainRegistrar.sol"; contract RegisterHyperchainScript is Script { using stdToml for string; @@ -23,35 +24,28 @@ contract RegisterHyperchainScript is Script { // solhint-disable-next-line gas-struct-packing struct Config { - address deployerAddress; - address ownerAddress; uint256 chainChainId; - bool validiumMode; - uint256 bridgehubCreateNewChainSalt; - address validatorSenderOperatorCommitEth; - address validatorSenderOperatorBlobsEth; - address baseToken; - uint128 baseTokenGasPriceMultiplierNominator; - uint128 baseTokenGasPriceMultiplierDenominator; + address proposalAuthor; + address chainRegistrar; address bridgehub; + uint256 bridgehubCreateNewChainSalt; address stateTransitionProxy; address validatorTimelock; bytes diamondCutData; - address governanceSecurityCouncilAddress; - uint256 governanceMinDelay; address newDiamondProxy; - address governance; address chainAdmin; } + ChainRegistrar internal chainRegistrar; Config internal config; + ChainRegistrar.ChainConfig internal chainConfig; function run() public { console.log("Deploying Hyperchain"); initializeConfig(); + loadChain(); - deployGovernance(); deployChainAdmin(); checkTokenAddress(); registerTokenOnBridgehub(); @@ -69,61 +63,67 @@ contract RegisterHyperchainScript is Script { string memory path = string.concat(root, "/script-config/register-hyperchain.toml"); string memory toml = vm.readFile(path); - config.deployerAddress = msg.sender; - // Config file must be parsed key by key, otherwise values returned // are parsed alfabetically and not by key. // https://book.getfoundry.sh/cheatcodes/parse-toml - config.ownerAddress = toml.readAddress("$.owner_address"); - - config.bridgehub = toml.readAddress("$.deployed_addresses.bridgehub.bridgehub_proxy_addr"); config.stateTransitionProxy = toml.readAddress( "$.deployed_addresses.state_transition.state_transition_proxy_addr" ); - config.validatorTimelock = toml.readAddress("$.deployed_addresses.validator_timelock_addr"); + config.chainRegistrar = toml.readAddress("$.deployed_addresses.chain_registrar"); + chainRegistrar = ChainRegistrar(config.chainRegistrar); + config.bridgehub = address(chainRegistrar.bridgehub()); + config.validatorTimelock = StateTransitionManager(config.stateTransitionProxy).validatorTimelock(); config.diamondCutData = toml.readBytes("$.contracts_config.diamond_cut_data"); config.chainChainId = toml.readUint("$.chain.chain_chain_id"); + config.proposalAuthor = toml.readAddress("$.chain.proposal_author"); config.bridgehubCreateNewChainSalt = toml.readUint("$.chain.bridgehub_create_new_chain_salt"); - config.baseToken = toml.readAddress("$.chain.base_token_addr"); - config.validiumMode = toml.readBool("$.chain.validium_mode"); - config.validatorSenderOperatorCommitEth = toml.readAddress("$.chain.validator_sender_operator_commit_eth"); - config.validatorSenderOperatorBlobsEth = toml.readAddress("$.chain.validator_sender_operator_blobs_eth"); - config.baseTokenGasPriceMultiplierNominator = uint128( - toml.readUint("$.chain.base_token_gas_price_multiplier_nominator") - ); - config.baseTokenGasPriceMultiplierDenominator = uint128( - toml.readUint("$.chain.base_token_gas_price_multiplier_denominator") - ); - config.governanceMinDelay = uint256(toml.readUint("$.chain.governance_min_delay")); - config.governanceSecurityCouncilAddress = toml.readAddress("$.chain.governance_security_council_address"); + } + + function loadChain() internal { + ( + uint256 chainId, + ChainRegistrar.BaseToken memory baseToken, + address blobOperator, + address operator, + address governor, + PubdataPricingMode pubdataPricingMode + ) = chainRegistrar.proposedChains(config.proposalAuthor, config.chainChainId); + chainConfig = ChainRegistrar.ChainConfig({ + chainId: chainId, + baseToken: baseToken, + operator: operator, + blobOperator: blobOperator, + governor: governor, + pubdataPricingMode: pubdataPricingMode + }); } function checkTokenAddress() internal view { - if (config.baseToken == address(0)) { + if (chainConfig.baseToken.tokenAddress == address(0)) { revert("Token address is not set"); } // Check if it's ethereum address - if (config.baseToken == ADDRESS_ONE) { + if (chainConfig.baseToken.tokenAddress == ADDRESS_ONE) { return; } - if (config.baseToken.code.length == 0) { + if (chainConfig.baseToken.tokenAddress.code.length == 0) { revert("Token address is not a contract address"); } - console.log("Using base token address:", config.baseToken); + console.log("Using base token address:", chainConfig.baseToken.tokenAddress); } function registerTokenOnBridgehub() internal { Bridgehub bridgehub = Bridgehub(config.bridgehub); - if (bridgehub.tokenIsRegistered(config.baseToken)) { + if (bridgehub.tokenIsRegistered(chainConfig.baseToken.tokenAddress)) { console.log("Token already registered on Bridgehub"); } else { - bytes memory data = abi.encodeCall(bridgehub.addToken, (config.baseToken)); + bytes memory data = abi.encodeCall(bridgehub.addToken, (chainConfig.baseToken.tokenAddress)); Utils.chainAdminMulticall({ _chainAdmin: bridgehub.admin(), _target: config.bridgehub, @@ -134,20 +134,9 @@ contract RegisterHyperchainScript is Script { } } - function deployGovernance() internal { - vm.broadcast(); - Governance governance = new Governance( - config.ownerAddress, - config.governanceSecurityCouncilAddress, - config.governanceMinDelay - ); - console.log("Governance deployed at:", address(governance)); - config.governance = address(governance); - } - function deployChainAdmin() internal { vm.broadcast(); - ChainAdmin chainAdmin = new ChainAdmin(config.ownerAddress, address(0)); + ChainAdmin chainAdmin = new ChainAdmin(chainConfig.governor, chainConfig.baseToken.tokenMultiplierSetter); console.log("ChainAdmin deployed at:", address(chainAdmin)); config.chainAdmin = address(chainAdmin); } @@ -159,9 +148,9 @@ contract RegisterHyperchainScript is Script { bytes memory data = abi.encodeCall( bridgehub.createNewChain, ( - config.chainChainId, + chainConfig.chainId, config.stateTransitionProxy, - config.baseToken, + chainConfig.baseToken.tokenAddress, config.bridgehubCreateNewChainSalt, msg.sender, config.diamondCutData @@ -171,16 +160,8 @@ contract RegisterHyperchainScript is Script { Utils.chainAdminMulticall({_chainAdmin: bridgehub.admin(), _target: config.bridgehub, _data: data, _value: 0}); console.log("Hyperchain registered"); - // Get new diamond proxy address from emitted events - Vm.Log[] memory logs = vm.getRecordedLogs(); - address diamondProxyAddress; - uint256 logsLength = logs.length; - for (uint256 i = 0; i < logsLength; ++i) { - if (logs[i].topics[0] == STATE_TRANSITION_NEW_CHAIN_HASH) { - diamondProxyAddress = address(uint160(uint256(logs[i].topics[2]))); - break; - } - } + // Get new diamond proxy address from bridgehub + address diamondProxyAddress = bridgehub.getHyperchain(chainConfig.chainId); if (diamondProxyAddress == address(0)) { revert("Diamond proxy address not found"); } @@ -192,8 +173,8 @@ contract RegisterHyperchainScript is Script { ValidatorTimelock validatorTimelock = ValidatorTimelock(config.validatorTimelock); vm.startBroadcast(); - validatorTimelock.addValidator(config.chainChainId, config.validatorSenderOperatorCommitEth); - validatorTimelock.addValidator(config.chainChainId, config.validatorSenderOperatorBlobsEth); + validatorTimelock.addValidator(chainConfig.chainId, chainConfig.blobOperator); + validatorTimelock.addValidator(chainConfig.chainId, chainConfig.operator); vm.stopBroadcast(); console.log("Validators added"); @@ -204,11 +185,11 @@ contract RegisterHyperchainScript is Script { vm.startBroadcast(); hyperchain.setTokenMultiplier( - config.baseTokenGasPriceMultiplierNominator, - config.baseTokenGasPriceMultiplierDenominator + chainConfig.baseToken.gasPriceMultiplierNominator, + chainConfig.baseToken.gasPriceMultiplierDenominator ); - if (config.validiumMode) { + if (chainConfig.pubdataPricingMode == PubdataPricingMode.Validium) { hyperchain.setPubdataPricingMode(PubdataPricingMode.Validium); } @@ -226,8 +207,7 @@ contract RegisterHyperchainScript is Script { function saveOutput() internal { vm.serializeAddress("root", "diamond_proxy_addr", config.newDiamondProxy); - vm.serializeAddress("root", "chain_admin_addr", config.chainAdmin); - string memory toml = vm.serializeAddress("root", "governance_addr", config.governance); + string memory toml = vm.serializeAddress("root", "chain_admin_addr", config.chainAdmin); string memory root = vm.projectRoot(); string memory path = string.concat(root, "/script-out/output-register-hyperchain.toml"); vm.writeToml(toml, path); diff --git a/l1-contracts/test/foundry/unit/concrete/chain-registrator/ChainRegistrar.t.sol b/l1-contracts/test/foundry/unit/concrete/chain-registrator/ChainRegistrar.t.sol new file mode 100644 index 000000000..e76823456 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/chain-registrator/ChainRegistrar.t.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {DummyStateTransitionManagerWBH} from "contracts/dev-contracts/test/DummyStateTransitionManagerWithBridgeHubAddress.sol"; +import {VerifierParams, IVerifier} from "contracts/state-transition/chain-interfaces/IVerifier.sol"; +import {GettersFacet} from "contracts/state-transition/chain-deps/facets/Getters.sol"; + +import "contracts/bridgehub/Bridgehub.sol"; +import "contracts/chain-registrar/ChainRegistrar.sol"; +import {PubdataPricingMode} from "contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import {InitializeDataNewChain as DiamondInitializeDataNewChain} from "contracts/state-transition/chain-interfaces/IDiamondInit.sol"; +import "contracts/dev-contracts/test/DummyBridgehub.sol"; +import "contracts/dev-contracts/test/DummySharedBridge.sol"; +import {L1SharedBridge} from "contracts/bridge/L1SharedBridge.sol"; +import {console2 as console} from "forge-std/Script.sol"; +import {Diamond} from "contracts/state-transition/libraries/Diamond.sol"; +import {ChainCreationParams} from "contracts/state-transition/IStateTransitionManager.sol"; +import {FeeParams} from "contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import "contracts/dev-contracts/test/DummyHyperchain.sol"; +import {TestnetERC20Token} from "contracts/dev-contracts/TestnetERC20Token.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts-v4/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract ChainRegistrarTest is Test { + DummyBridgehub private bridgeHub; + DummyStateTransitionManagerWBH private stm; + address private admin; + address private deployer; + ChainRegistrar private chainRegistrar; + L1SharedBridge private sharedBridge; + bytes diamondCutData; + bytes initCalldata; + + constructor() public { + bridgeHub = new DummyBridgehub(); + stm = new DummyStateTransitionManagerWBH(address(bridgeHub)); + admin = makeAddr("admin"); + deployer = makeAddr("deployer"); + address defaultOwner = bridgeHub.owner(); + vm.prank(defaultOwner); + bridgeHub.transferOwnership(admin); + vm.prank(admin); + bridgeHub.acceptOwnership(); + + sharedBridge = new L1SharedBridge({ + _l1WethAddress: makeAddr("weth"), + _bridgehub: IBridgehub(bridgeHub), + _eraChainId: 270, + _eraDiamondProxy: makeAddr("era") + }); + address defaultOwnerSb = sharedBridge.owner(); + vm.prank(defaultOwnerSb); + sharedBridge.transferOwnership(admin); + vm.startPrank(admin); + sharedBridge.acceptOwnership(); + bridgeHub.setSharedBridge(address(sharedBridge)); + bridgeHub.addStateTransitionManager(address(stm)); + bridgeHub.addToken(ETH_TOKEN_ADDRESS); + + Diamond.FacetCut[] memory facetCuts = new Diamond.FacetCut[](0); + + DiamondInitializeDataNewChain memory initializeData = DiamondInitializeDataNewChain({ + verifier: IVerifier(makeAddr("verifier")), + verifierParams: VerifierParams({ + recursionNodeLevelVkHash: bytes32(0), + recursionLeafLevelVkHash: bytes32(0), + recursionCircuitsSetVksHash: bytes32(0) + }), + l2BootloaderBytecodeHash: bytes32(0), + l2DefaultAccountBytecodeHash: bytes32(0), + priorityTxMaxGasLimit: 10, + feeParams: FeeParams({ + pubdataPricingMode: PubdataPricingMode.Rollup, + batchOverheadL1Gas: 1_000_000, + maxPubdataPerBatch: 110_000, + maxL2GasPerBatch: 80_000_000, + priorityTxMaxPubdata: 99_000, + minimalL2GasPrice: 250_000_000 + }), + blobVersionedHashRetriever: makeAddr("blob") + }); + initCalldata = abi.encode(initializeData); + + Diamond.DiamondCutData memory diamondCutDataStruct = Diamond.DiamondCutData({ + facetCuts: facetCuts, + initAddress: makeAddr("init"), + initCalldata: initCalldata + }); + ChainCreationParams memory chainCreationParams = ChainCreationParams({ + genesisUpgrade: makeAddr("genesis"), + genesisBatchHash: bytes32(uint256(0x01)), + genesisIndexRepeatedStorageChanges: 0x01, + genesisBatchCommitment: bytes32(uint256(0x01)), + diamondCut: diamondCutDataStruct + }); + diamondCutData = abi.encode(diamondCutDataStruct); + vm.stopPrank(); + vm.prank(stm.admin()); + stm.setChainCreationParams(chainCreationParams); + address chainRegistrarImplementation = address(new ChainRegistrar()); + TransparentUpgradeableProxy chainRegistrarProxy = new TransparentUpgradeableProxy( + chainRegistrarImplementation, + admin, + abi.encodeCall(ChainRegistrar.initialize, (address(bridgeHub), deployer, admin)) + ); + chainRegistrar = ChainRegistrar(address(chainRegistrarProxy)); + } + + function test_ChainIsAlreadyProposed() public { + address author = makeAddr("author"); + vm.startPrank(author); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: ETH_TOKEN_ADDRESS, + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 1, + _gasPriceMultiplierDenominator: 1 + }); + + vm.expectRevert(ChainRegistrar.ChainIsAlreadyProposed.selector); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("newGovernor"), + _baseTokenAddress: ETH_TOKEN_ADDRESS, + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 1, + _gasPriceMultiplierDenominator: 1 + }); + vm.stopPrank(); + } + + function test_SuccessfulProposal() public { + address author = makeAddr("author"); + vm.prank(author); + vm.recordLogs(); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: ETH_TOKEN_ADDRESS, + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 1, + _gasPriceMultiplierDenominator: 1 + }); + registerChainAndVerify(author, 1); + } + + function test_CustomBaseToken() public { + address author = makeAddr("author"); + vm.prank(author); + vm.recordLogs(); + TestnetERC20Token token = new TestnetERC20Token("test", "test", 18); + token.mint(author, 100 ether); + vm.prank(author); + token.approve(address(chainRegistrar), 10 ether); + vm.prank(author); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: address(token), + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 10, + _gasPriceMultiplierDenominator: 1 + }); + registerChainAndVerify(author, 1); + } + + function test_PreTransferErc20Token() public { + address author = makeAddr("author"); + vm.startPrank(author); + vm.recordLogs(); + TestnetERC20Token token = new TestnetERC20Token("test", "test", 18); + token.mint(author, 100 ether); + token.transfer(chainRegistrar.l2Deployer(), 10 ether); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: address(token), + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 10, + _gasPriceMultiplierDenominator: 1 + }); + vm.stopPrank(); + registerChainAndVerify(author, 1); + } + + function test_BaseTokenPreTransferIsNotEnough() public { + address author = makeAddr("author"); + vm.startPrank(author); + vm.recordLogs(); + TestnetERC20Token token = new TestnetERC20Token("test", "test", 18); + token.mint(author, 100 ether); + token.transfer(chainRegistrar.l2Deployer(), 1 ether); + vm.expectRevert(bytes("ERC20: insufficient allowance")); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: address(token), + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 10, + _gasPriceMultiplierDenominator: 1 + }); + } + + function test_BaseTokenApproveIsNotEnough() public { + address author = makeAddr("author"); + vm.startPrank(author); + vm.recordLogs(); + TestnetERC20Token token = new TestnetERC20Token("test", "test", 18); + token.mint(author, 100 ether); + token.approve(chainRegistrar.l2Deployer(), 1 ether); + vm.expectRevert(bytes("ERC20: insufficient allowance")); + chainRegistrar.proposeChainRegistration({ + _chainId: 1, + _pubdataPricingMode: PubdataPricingMode.Validium, + _blobOperator: makeAddr("blobOperator"), + _operator: makeAddr("operator"), + _governor: makeAddr("governor"), + _baseTokenAddress: address(token), + _tokenMultiplierSetter: makeAddr("setter"), + _gasPriceMultiplierNominator: 10, + _gasPriceMultiplierDenominator: 1 + }); + } + + function registerChainAndVerify(address author, uint256 chainId) internal { + DummyHyperchain hyperchain = new DummyHyperchain(address(bridgeHub), 270); + hyperchain.initialize(admin); + vm.prank(admin); + stm.setHyperchain(1, address(hyperchain)); + bridgeHub.setStateTransitionManager(chainId, address(stm)); + vm.prank(admin); + sharedBridge.initializeChainGovernance(chainId, makeAddr("l2bridge")); + ChainRegistrar.RegisteredChainConfig memory registeredConfig = chainRegistrar.getRegisteredChainConfig(chainId); + ( + uint256 proposedChainId, + ChainRegistrar.BaseToken memory baseToken, + address blobOperator, + address operator, + address governor, + PubdataPricingMode pubdataPricingMode + ) = chainRegistrar.proposedChains(author, chainId); + require(registeredConfig.diamondProxy != address(0)); + require(registeredConfig.chainAdmin != address(0)); + require(registeredConfig.l2BridgeAddress != address(0)); + require(proposedChainId == chainId); + } +}