diff --git a/l1-contracts/contracts/common/L1ContractErrors.sol b/l1-contracts/contracts/common/L1ContractErrors.sol index 73ff72cc9..5642c07e2 100644 --- a/l1-contracts/contracts/common/L1ContractErrors.sol +++ b/l1-contracts/contracts/common/L1ContractErrors.sol @@ -51,6 +51,10 @@ error DiamondAlreadyFrozen(); error DiamondFreezeIncorrectState(); // 0xa7151b9a error DiamondNotFrozen(); +// +error DiamondFrozenByAdmin(); +// +error DiamondNotFrozenByAdmin(); // 0xfc7ab1d3 error EmptyBlobVersionHash(uint256 index); // 0x95b66fe9 diff --git a/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol b/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol index a06921fdb..340c0a915 100644 --- a/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol +++ b/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol @@ -151,4 +151,6 @@ struct ZkSyncHyperchainStorage { uint128 baseTokenGasPriceMultiplierDenominator; /// @dev The optional address of the contract that has to be used for transaction filtering/whitelisting address transactionFilterer; + /// @dev Boolean for tracking when the hyperchain has been frozen by chain admin + bool frozenByAdmin; } diff --git a/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol b/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol index ff32db040..cd8e5d479 100644 --- a/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol +++ b/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol @@ -8,7 +8,7 @@ import {MAX_GAS_PER_TRANSACTION} from "../../../common/Config.sol"; import {FeeParams, PubdataPricingMode} from "../ZkSyncHyperchainStorage.sol"; import {ZkSyncHyperchainBase} from "./ZkSyncHyperchainBase.sol"; import {IStateTransitionManager} from "../../IStateTransitionManager.sol"; -import {Unauthorized, TooMuchGas, PriorityTxPubdataExceedsMaxPubDataPerBatch, InvalidPubdataPricingMode, ProtocolIdMismatch, ChainAlreadyLive, HashMismatch, ProtocolIdNotGreater, DenominatorIsZero, DiamondAlreadyFrozen, DiamondNotFrozen} from "../../../common/L1ContractErrors.sol"; +import {Unauthorized, TooMuchGas, PriorityTxPubdataExceedsMaxPubDataPerBatch, InvalidPubdataPricingMode, ProtocolIdMismatch, ChainAlreadyLive, HashMismatch, ProtocolIdNotGreater, DenominatorIsZero, DiamondAlreadyFrozen, DiamondNotFrozen, DiamondFrozenByAdmin, DiamondNotFrozenByAdmin} from "../../../common/L1ContractErrors.sol"; // While formally the following import is not used, it is needed to inherit documentation from it import {IZkSyncHyperchainBase} from "../../chain-interfaces/IZkSyncHyperchainBase.sol"; @@ -164,19 +164,35 @@ contract AdminFacet is ZkSyncHyperchainBase, IAdmin { } diamondStorage.isFrozen = true; + if (msg.sender == s.admin) { + s.frozenByAdmin = true; + } + emit Freeze(); } /// @inheritdoc IAdmin function unfreezeDiamond() external onlyStateTransitionManager { Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage(); - // diamond proxy is not frozen if (!diamondStorage.isFrozen) { revert DiamondNotFrozen(); } + // diamond proxy is frozen by admin + if (s.frozenByAdmin) { + revert DiamondFrozenByAdmin(); + } diamondStorage.isFrozen = false; emit Unfreeze(); } + + /// @inheritdoc IAdmin + function allowUnfreezeDiamond() external onlyAdmin { + if (!s.frozenByAdmin) { + // diamond proxy needs to be previously frozen by admin + revert DiamondNotFrozenByAdmin(); + } + s.frozenByAdmin = false; + } } diff --git a/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol b/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol index 79c034f36..4d124b661 100644 --- a/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol +++ b/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol @@ -61,9 +61,13 @@ interface IAdmin is IZkSyncHyperchainBase { function freezeDiamond() external; /// @notice Unpause the functionality of all freezable facets & their selectors - /// @dev Both the admin and the STM can unfreeze Diamond Proxy + /// @dev Only the STM can unfreeze Diamond Proxy function unfreezeDiamond() external; + /// @notice Allow to call unfreeze diamond if it was previously frozen by admin + /// @dev Only the current admin can use this function + function allowUnfreezeDiamond() external; + /// @notice Porter availability status changes event IsPorterAvailableStatusUpdate(bool isPorterAvailable); diff --git a/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol b/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol index 1260334fd..3d0c8efa0 100644 --- a/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol +++ b/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol @@ -257,7 +257,7 @@ library Utils { } function getUtilsFacetSelectors() public pure returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](41); + bytes4[] memory selectors = new bytes4[](43); selectors[0] = UtilsFacet.util_setChainId.selector; selectors[1] = UtilsFacet.util_getChainId.selector; selectors[2] = UtilsFacet.util_setBridgehub.selector; @@ -299,6 +299,8 @@ library Utils { selectors[38] = UtilsFacet.util_setTotalBatchesExecuted.selector; selectors[39] = UtilsFacet.util_setL2LogsRootHash.selector; selectors[40] = UtilsFacet.util_setBaseTokenGasPriceMultiplierNominator.selector; + selectors[41] = UtilsFacet.util_setFrozenByAdmin.selector; + selectors[42] = UtilsFacet.util_getFrozenByAdmin.selector; return selectors; } diff --git a/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol b/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol index ce9e659a0..fe41e6ab8 100644 --- a/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol +++ b/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol @@ -174,6 +174,14 @@ contract UtilsFacet is ZkSyncHyperchainBase { s.baseTokenGasPriceMultiplierNominator = _nominator; } + function util_setFrozenByAdmin(bool _frozenByAdmin) external { + s.frozenByAdmin = _frozenByAdmin; + } + + function util_getFrozenByAdmin() external view returns (bool) { + return s.frozenByAdmin; + } + // add this to be excluded from coverage report function test() internal virtual {} } diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/FreezeDiamond.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/FreezeDiamond.t.sol index 77baed0ef..472522a45 100644 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/FreezeDiamond.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/FreezeDiamond.t.sol @@ -8,11 +8,12 @@ import {Unauthorized} from "contracts/common/L1ContractErrors.sol"; contract FreezeDiamondTest is AdminTest { event Freeze(); - function test_revertWhen_calledByNonStateTransitionManager() public { + function test_revertWhen_calledByNonStateTransitionManagerOrAdmin() public { address nonStateTransitionManager = makeAddr("nonStateTransitionManager"); vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, nonStateTransitionManager)); + vm.startPrank(nonStateTransitionManager); adminFacet.freezeDiamond(); } diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/UnfreezeDiamond.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/UnfreezeDiamond.t.sol index e0da9d6dc..7d412af40 100644 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/UnfreezeDiamond.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/UnfreezeDiamond.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.24; import {AdminTest} from "./_Admin_Shared.t.sol"; -import {Unauthorized, DiamondFreezeIncorrectState, DiamondNotFrozen} from "contracts/common/L1ContractErrors.sol"; +import {Unauthorized, DiamondFreezeIncorrectState, DiamondNotFrozen, DiamondFrozenByAdmin} from "contracts/common/L1ContractErrors.sol"; contract UnfreezeDiamondTest is AdminTest { event Unfreeze(); @@ -26,4 +26,15 @@ contract UnfreezeDiamondTest is AdminTest { vm.startPrank(admin); adminFacet.unfreezeDiamond(); } + + function test_revertWhen_diamondIsNotAllowedTobeUnfrozen() public { + address admin = utilsFacet.util_getStateTransitionManager(); + utilsFacet.util_setIsFrozen(true); + utilsFacet.util_setFrozenByAdmin(true); + + vm.expectRevert(DiamondFrozenByAdmin.selector); + + vm.startPrank(admin); + adminFacet.unfreezeDiamond(); + } } diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol index a4419a342..cb10977fe 100644 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol @@ -38,13 +38,13 @@ contract AdminTest is Test { facetCuts[0] = Diamond.FacetCut({ facet: address(new AdminFacet()), action: Diamond.Action.Add, - isFreezable: true, + isFreezable: false, selectors: getAdminSelectors() }); facetCuts[1] = Diamond.FacetCut({ facet: address(new UtilsFacet()), action: Diamond.Action.Add, - isFreezable: true, + isFreezable: false, selectors: Utils.getUtilsFacetSelectors() });