diff --git a/contracts/src/security/pausing/PauseManager.sol b/contracts/src/security/pausing/PauseManager.sol index 42e11efec..d465bae6c 100644 --- a/contracts/src/security/pausing/PauseManager.sol +++ b/contracts/src/security/pausing/PauseManager.sol @@ -16,7 +16,33 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { /// @notice This is used to unpause all unpausable functions. bytes32 public constant UNPAUSE_ALL_ROLE = keccak256("UNPAUSE_ALL_ROLE"); - // @dev DEPRECATED. USE _pauseTypeStatusesBitMap INSTEAD + /// @notice This is used to unpause all unpausable functions. + bytes32 public constant SECURITY_COUNCIL_ROLE = keccak256("SECURITY_COUNCIL_ROLE"); + + /// @notice Duration of cooldown period after a pause expires, in which no further + /// @dev SECURITY_COUNCIL_ROLE role will ignore cooldown + uint256 public constant COOLDOWN_DURATION = 24 hours; + + /// @notice Duration of pauses + /// @dev All pauses have a limited duration, except for pauses enacted by the SECURITY_COUNCIL_ROLE + uint256 public constant PAUSE_DURATION = 72 hours; + + /// @dev custom:storage-location erc7201:linea.storage.PauseManager + struct PauseManagerStorage { + uint256 _pauseExpiry; + } + + /// @notice ERC-7201 Namespaced storage slot + /// @dev keccak256(abi.encode(uint256(keccak256("linea.storage.PauseManager")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant PauseManagerStorageLocation = 0x477c83544ba7ee7a035c74dbc9991f7ec4e921343919039e273014d568e17a00; + + function _getPauseManagerStorage() private pure returns (PauseManagerStorage storage $) { + assembly { + $.slot := PauseManagerStorageLocation + } + } + + /// @dev DEPRECATED. USE _pauseTypeStatusesBitMap INSTEAD mapping(bytes32 pauseType => bool pauseStatus) public pauseTypeStatuses; /// @dev The bitmap containing the pause statuses mapped by type. @@ -28,10 +54,9 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { /// @dev This maps the unpause type to the role that is allowed to unpause it. mapping(PauseType unPauseType => bytes32 role) private _unPauseTypeRoles; - /// @dev Total contract storage is 11 slots with the gap below. - /// @dev Keep 7 free storage slots for future implementation updates to avoid storage collision. - /// @dev Note: This was reduced previously to cater for new functionality. - uint256[7] private __gap; + /// @dev Do not add any new storage variables to the contract. Instead add new variables to the PauseManagerStorage struct. + /// @dev __gap is deprecated in favour of the ERC-7201 Namespaced Storage pattern + uint256[7] private __gap_DEPRECATED; /** * @dev Modifier to prevent usage of unused PauseType. @@ -133,27 +158,65 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { revert IsPaused(_pauseType); } + PauseManagerStorage storage $ = _getPauseManagerStorage(); + // TO-DISCUSS - Introduces duplicate SLOAD, but the alternative is to create a dedicated function for the security council. Perhaps is ok for an emergency function. + bool isSecurityCouncil = hasRole(SECURITY_COUNCIL_ROLE, _msgSender()); + if (isSecurityCouncil) { + $._pauseExpiry = type(uint256).max; + } else { + uint256 cooldownEnd = $._pauseExpiry + COOLDOWN_DURATION; + if (block.timestamp < cooldownEnd) { + revert PauseUnavailableDueToCooldown(cooldownEnd); + } + $._pauseExpiry = block.timestamp + PAUSE_DURATION; + } + _pauseTypeStatusesBitMap |= 1 << uint256(_pauseType); emit Paused(_msgSender(), _pauseType); } /** * @notice Unpauses functionality by specific type. - * @dev Throws if UNUSED pause type is used. + * @dev Throws if UNUSED pause type is used, or is not called by SECURITY_COUNCIL_ROLE * @dev Requires the role mapped in `_unPauseTypeRoles` for the pauseType. * @param _pauseType The pause type value. */ function unPauseByType( PauseType _pauseType - ) external onlyUsedPausedTypes(_pauseType) onlyRole(_unPauseTypeRoles[_pauseType]) { + ) external onlyUsedPausedTypes(_pauseType) onlyRole(SECURITY_COUNCIL_ROLE) { if (!isPaused(_pauseType)) { revert IsNotPaused(_pauseType); } + PauseManagerStorage storage $ = _getPauseManagerStorage(); + $._pauseExpiry = block.timestamp - COOLDOWN_DURATION; _pauseTypeStatusesBitMap &= ~(1 << uint256(_pauseType)); emit UnPaused(_msgSender(), _pauseType); } + /** + * @notice Unpauses functionality by specific type when pause period has expired. + * @dev Throws if UNUSED pause type is used, or the pause expiry period has not passed. + * @dev Requires the role mapped in `_unPauseTypeRoles` for the pauseType. + * @param _pauseType The pause type value. + */ + function unPauseDueToExpiry( + PauseType _pauseType + ) external onlyUsedPausedTypes(_pauseType) { + if (!isPaused(_pauseType)) { + revert IsNotPaused(_pauseType); + } + + PauseManagerStorage storage $ = _getPauseManagerStorage(); + uint256 pauseExpiry = $._pauseExpiry; + if (block.timestamp < pauseExpiry) { + revert PauseNotExpired(pauseExpiry); + } + + _pauseTypeStatusesBitMap &= ~(1 << uint256(_pauseType)); + emit UnPausedDueToExpiry(_pauseType); + } + /** * @notice Check if a pause type is enabled. * @param _pauseType The pause type value. @@ -167,14 +230,14 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { * @notice Update the pause type role mapping. * @dev Throws if UNUSED pause type is used. * @dev Throws if role not different. - * @dev PAUSE_ALL_ROLE role is required to execute this function. + * @dev SECURITY_COUNCIL_ROLE role is required to execute this function. * @param _pauseType The pause type value to update. * @param _newRole The role to update to. */ function updatePauseTypeRole( PauseType _pauseType, bytes32 _newRole - ) external onlyUsedPausedTypes(_pauseType) onlyRole(PAUSE_ALL_ROLE) { + ) external onlyUsedPausedTypes(_pauseType) onlyRole(SECURITY_COUNCIL_ROLE) { bytes32 previousRole = _pauseTypeRoles[_pauseType]; if (previousRole == _newRole) { revert RolesNotDifferent(); @@ -188,14 +251,14 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { * @notice Update the unpause type role mapping. * @dev Throws if UNUSED pause type is used. * @dev Throws if role not different. - * @dev UNPAUSE_ALL_ROLE role is required to execute this function. + * @dev SECURITY_COUNCIL_ROLE role is required to execute this function. * @param _pauseType The pause type value to update. * @param _newRole The role to update to. */ function updateUnpauseTypeRole( PauseType _pauseType, bytes32 _newRole - ) external onlyUsedPausedTypes(_pauseType) onlyRole(UNPAUSE_ALL_ROLE) { + ) external onlyUsedPausedTypes(_pauseType) onlyRole(SECURITY_COUNCIL_ROLE) { bytes32 previousRole = _unPauseTypeRoles[_pauseType]; if (previousRole == _newRole) { revert RolesNotDifferent(); @@ -204,4 +267,13 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { _unPauseTypeRoles[_pauseType] = _newRole; emit UnPauseTypeRoleUpdated(_pauseType, _newRole, previousRole); } + + /** + * @notice Returns the Unix timestamp for the pause expiry. + * @return pauseExpiry Unix timestamp for the pause expiry. + */ + function getPauseExpiry() external view returns (uint256 pauseExpiry) { + PauseManagerStorage storage $ = _getPauseManagerStorage(); + return $._pauseExpiry; + } } diff --git a/contracts/src/security/pausing/interfaces/IPauseManager.sol b/contracts/src/security/pausing/interfaces/IPauseManager.sol index f78dd757c..aeeb95c50 100644 --- a/contracts/src/security/pausing/interfaces/IPauseManager.sol +++ b/contracts/src/security/pausing/interfaces/IPauseManager.sol @@ -50,6 +50,12 @@ interface IPauseManager { */ event UnPaused(address messageSender, PauseType indexed pauseType); + /** + * @notice Emitted when a pause type is unpaused due to pause expiry period elapsing. + * @param pauseType The indexed pause type that was unpaused. + */ + event UnPausedDueToExpiry(PauseType pauseType); + /** * @notice Emitted when a pause type and its associated role are set in the `_pauseTypeRoles` mapping. * @param pauseType The indexed type of pause. @@ -85,11 +91,21 @@ interface IPauseManager { */ error IsPaused(PauseType pauseType); + /** + * @dev Thrown when unpauseDueToExpiry is attempted before a pause has expired. + */ + error PauseNotExpired(uint256 expiryEnd); + /** * @dev Thrown when a specific pause type is not paused and expected to be. */ error IsNotPaused(PauseType pauseType); + /** + * @dev Thrown when pausing is attempted during the cooldown period. + */ + error PauseUnavailableDueToCooldown(uint256 cooldownEnd); + /** * @dev Thrown when the unused paused type is used. */ @@ -116,6 +132,14 @@ interface IPauseManager { */ function unPauseByType(PauseType _pauseType) external; + /** + * @notice Unpauses functionality by specific type when pause period has expired. + * @dev Throws if UNUSED pause type is used, or the pause expiry period has not passed. + * @dev Requires the role mapped in unPauseTypeRoles for the pauseType. + * @param _pauseType The pause type value. + */ + function unPauseDueToExpiry(PauseType _pauseType) external; + /** * @notice Check if a pause type is enabled. * @param _pauseType The pause type value. @@ -142,4 +166,10 @@ interface IPauseManager { * @param _newRole The role to update to. */ function updateUnpauseTypeRole(PauseType _pauseType, bytes32 _newRole) external; + + /** + * @notice Returns the Unix timestamp for the pause expiry. + * @return pauseExpiry Unix timestamp for the pause expiry. + */ + function getPauseExpiry() external view returns (uint256 pauseExpiry); } diff --git a/contracts/test/hardhat/common/constants/roles.ts b/contracts/test/hardhat/common/constants/roles.ts index c885694c4..3ea26507b 100644 --- a/contracts/test/hardhat/common/constants/roles.ts +++ b/contracts/test/hardhat/common/constants/roles.ts @@ -33,6 +33,7 @@ export const VERIFIER_SETTER_ROLE = generateKeccak256(["string"], ["VERIFIER_SET export const VERIFIER_UNSETTER_ROLE = generateKeccak256(["string"], ["VERIFIER_UNSETTER_ROLE"], true); export const L1_MERKLE_ROOTS_SETTER_ROLE = generateKeccak256(["string"], ["L1_MERKLE_ROOTS_SETTER_ROLE"], true); export const L2_MERKLE_ROOTS_SETTER_ROLE = generateKeccak256(["string"], ["L2_MERKLE_ROOTS_SETTER_ROLE"], true); +export const SECURITY_COUNCIL_ROLE = generateKeccak256(["string"], ["SECURITY_COUNCIL_ROLE"], true); export const BAD_STARTING_HASH = generateKeccak256(["string"], ["BAD_STARTING_HASH"], true); // TokenBridge roles