Files
linea-monorepo/contracts/src/security/pausing/PauseManager.sol
kyzooghost d05475241d [Feat] Pause cooldown (#723)
* changes to pausemanager

* add space

* working pausemanager tests

* npx hardhat test working for existing suite

* more pausemanager tests

* more tests and comments

* minor typo fix

* revert pauseTypes.ts changes

* fix PauseManager test cases

* small reverts

* more test adjustments

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* Update contracts/src/security/pausing/interfaces/IPauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* fix unchecked

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* more comment fixes

* match interface and contract natspec comments

* add *.mdx changes

* change pauseExpiry to pauseExpiryTimestamp

* doc change

* tests passing with new pause expiry value after security council pause

* add new overflow test to pausemanager

* expand unchecked block

* indent unchecked block

* unPauseDueToExpiry -> unPauseByExpiredType

* Update contracts/src/security/pausing/PauseManager.sol

Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>

* added unpausebytype comment

---------

Signed-off-by: kyzooghost <73516204+kyzooghost@users.noreply.github.com>
Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
2025-03-01 00:46:53 +11:00

261 lines
10 KiB
Solidity

// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.19;
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { IPauseManager } from "./interfaces/IPauseManager.sol";
/**
* @title Contract to manage cross-chain function pausing with limited duration and cooldown mechanic.
* @author ConsenSys Software Inc.
* @custom:security-contact security-report@linea.build
*/
abstract contract PauseManager is IPauseManager, AccessControlUpgradeable {
/// @notice This is used to pause all pausable functions.
bytes32 public constant PAUSE_ALL_ROLE = keccak256("PAUSE_ALL_ROLE");
/// @notice This is used to unpause all unpausable functions.
bytes32 public constant UNPAUSE_ALL_ROLE = keccak256("UNPAUSE_ALL_ROLE");
/// @notice Role assigned to the security council that enables indefinite pausing and bypassing the cooldown period.
/// @dev Is not a pause or unpause role; a specific pause/unpause role is still required for specific pause/unpause types.
bytes32 public constant SECURITY_COUNCIL_ROLE = keccak256("SECURITY_COUNCIL_ROLE");
/// @notice Duration of pauses, after which pauses will expire (except by the SECURITY_COUNCIL_ROLE).
uint256 public constant PAUSE_DURATION = 72 hours;
/// @notice Duration of cooldown after a pause expires, during which no pauses (except by the SECURITY_COUNCIL_ROLE) can be enacted.
/// @dev This prevents indefinite pause chaining by a non-SECURITY_COUNCIL_ROLE.
uint256 public constant COOLDOWN_DURATION = 24 hours;
// @dev DEPRECATED. USE _pauseTypeStatusesBitMap INSTEAD
mapping(bytes32 pauseType => bool pauseStatus) private pauseTypeStatuses;
/// @dev The bitmap containing the pause statuses mapped by type.
uint256 private _pauseTypeStatusesBitMap;
/// @dev This maps the pause type to the role that is allowed to pause it.
mapping(PauseType pauseType => bytes32 role) private _pauseTypeRoles;
/// @dev This maps the unpause type to the role that is allowed to unpause it.
mapping(PauseType unPauseType => bytes32 role) private _unPauseTypeRoles;
/// @notice Unix timestamp of pause expiry.
/// @dev pauseExpiryTimestamp applies to all pause types. Pausing with one pause type blocks other pause types from being enacted (unless the SECURITY_COUNCIL_ROLE is used).
/// @dev This prevents indefinite pause chaining by a non-SECURITY_COUNCIL_ROLE.
uint256 public pauseExpiryTimestamp;
/// @dev Total contract storage is 12 slots with the gap below.
/// @dev Keep 6 free storage slots for future implementation updates to avoid storage collision.
/// @dev Note: This was reduced previously to cater for new functionality.
uint256[6] private __gap;
/**
* @dev Modifier to prevent usage of unused PauseType.
* @param _pauseType The PauseType value being checked.
* Requirements:
*
* - The type must not be UNUSED.
*/
modifier onlyUsedPausedTypes(PauseType _pauseType) {
if (_pauseType == PauseType.UNUSED) {
revert PauseTypeNotUsed();
}
_;
}
/**
* @dev Modifier to make a function callable only when the specific and general types are not paused.
* @param _pauseType The pause type value being checked.
* Requirements:
*
* - The type must not be paused.
*/
modifier whenTypeAndGeneralNotPaused(PauseType _pauseType) {
_requireTypeAndGeneralNotPaused(_pauseType);
_;
}
/**
* @dev Modifier to make a function callable only when the type is not paused.
* @param _pauseType The pause type value being checked.
* Requirements:
*
* - The type must not be paused.
*/
modifier whenTypeNotPaused(PauseType _pauseType) {
_requireTypeNotPaused(_pauseType);
_;
}
/**
* @notice Initializes the pause manager with the given pause and unpause roles.
* @dev This function is called during contract initialization to set up the pause and unpause roles.
* @param _pauseTypeRoleAssignments An array of PauseTypeRole structs defining the pause types and their associated roles.
* @param _unpauseTypeRoleAssignments An array of PauseTypeRole structs defining the unpause types and their associated roles.
*/
function __PauseManager_init(
PauseTypeRole[] calldata _pauseTypeRoleAssignments,
PauseTypeRole[] calldata _unpauseTypeRoleAssignments
) internal onlyInitializing {
for (uint256 i; i < _pauseTypeRoleAssignments.length; i++) {
_pauseTypeRoles[_pauseTypeRoleAssignments[i].pauseType] = _pauseTypeRoleAssignments[i].role;
emit PauseTypeRoleSet(_pauseTypeRoleAssignments[i].pauseType, _pauseTypeRoleAssignments[i].role);
}
for (uint256 i; i < _unpauseTypeRoleAssignments.length; i++) {
_unPauseTypeRoles[_unpauseTypeRoleAssignments[i].pauseType] = _unpauseTypeRoleAssignments[i].role;
emit UnPauseTypeRoleSet(_unpauseTypeRoleAssignments[i].pauseType, _unpauseTypeRoleAssignments[i].role);
}
}
/**
* @dev Throws if the specific or general types are paused.
* @dev Checks the specific and general pause types.
* @param _pauseType The pause type value being checked.
*/
function _requireTypeAndGeneralNotPaused(PauseType _pauseType) internal view virtual {
uint256 pauseBitMap = _pauseTypeStatusesBitMap;
if (pauseBitMap & (1 << uint256(_pauseType)) != 0) {
revert IsPaused(_pauseType);
}
if (pauseBitMap & (1 << uint256(PauseType.GENERAL)) != 0) {
revert IsPaused(PauseType.GENERAL);
}
}
/**
* @dev Throws if the type is paused.
* @dev Checks the specific pause type.
* @param _pauseType The pause type value being checked.
*/
function _requireTypeNotPaused(PauseType _pauseType) internal view virtual {
if (isPaused(_pauseType)) {
revert IsPaused(_pauseType);
}
}
/**
* @notice Pauses functionality by specific type.
* @dev Throws if UNUSED pause type is used.
* @dev Requires the role mapped in `_pauseTypeRoles` for the pauseType.
* @dev Non-SECURITY_COUNCIL_ROLE can only pause after cooldown has passed.
* @dev SECURITY_COUNCIL_ROLE can pause without cooldown or expiry restrictions.
* @param _pauseType The pause type value.
*/
function pauseByType(
PauseType _pauseType
) external onlyUsedPausedTypes(_pauseType) onlyRole(_pauseTypeRoles[_pauseType]) {
if (isPaused(_pauseType)) {
revert IsPaused(_pauseType);
}
unchecked {
if (hasRole(SECURITY_COUNCIL_ROLE, _msgSender())) {
pauseExpiryTimestamp = type(uint256).max - COOLDOWN_DURATION;
} else {
if (block.timestamp < pauseExpiryTimestamp + COOLDOWN_DURATION) {
revert PauseUnavailableDueToCooldown(pauseExpiryTimestamp + COOLDOWN_DURATION);
}
pauseExpiryTimestamp = 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 Requires the role mapped in `_unPauseTypeRoles` for the pauseType.
* @dev SECURITY_COUNCIL_ROLE unpause will reset the cooldown, enabling non-SECURITY_COUNCIL_ROLE pausing.
* @param _pauseType The pause type value.
*/
function unPauseByType(
PauseType _pauseType
) external onlyUsedPausedTypes(_pauseType) onlyRole(_unPauseTypeRoles[_pauseType]) {
if (!isPaused(_pauseType)) {
revert IsNotPaused(_pauseType);
}
if (hasRole(SECURITY_COUNCIL_ROLE, _msgSender())) {
pauseExpiryTimestamp = block.timestamp - COOLDOWN_DURATION;
}
_pauseTypeStatusesBitMap &= ~(1 << uint256(_pauseType));
emit UnPaused(_msgSender(), _pauseType);
}
/**
* @notice Unpauses a specific pause type when the pause has expired.
* @dev Can be called by anyone.
* @dev Throws if UNUSED pause type is used, or the pause expiry period has not passed.
* @param _pauseType The pause type value.
*/
function unPauseByExpiredType(
PauseType _pauseType
) external onlyUsedPausedTypes(_pauseType) {
if (!isPaused(_pauseType)) {
revert IsNotPaused(_pauseType);
}
if (block.timestamp < pauseExpiryTimestamp) {
revert PauseNotExpired(pauseExpiryTimestamp);
}
_pauseTypeStatusesBitMap &= ~(1 << uint256(_pauseType));
emit UnPausedDueToExpiry(_pauseType);
}
/**
* @notice Check if a pause type is enabled.
* @param _pauseType The pause type value.
* @return pauseTypeIsPaused Returns true if the pause type if paused, false otherwise.
*/
function isPaused(PauseType _pauseType) public view returns (bool pauseTypeIsPaused) {
pauseTypeIsPaused = (_pauseTypeStatusesBitMap & (1 << uint256(_pauseType))) != 0;
}
/**
* @notice Update the pause type role mapping.
* @dev Throws if UNUSED pause type is used.
* @dev Throws if role not different.
* @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(SECURITY_COUNCIL_ROLE) {
bytes32 previousRole = _pauseTypeRoles[_pauseType];
if (previousRole == _newRole) {
revert RolesNotDifferent();
}
_pauseTypeRoles[_pauseType] = _newRole;
emit PauseTypeRoleUpdated(_pauseType, _newRole, previousRole);
}
/**
* @notice Update the unpause type role mapping.
* @dev Throws if UNUSED pause type is used.
* @dev Throws if role not different.
* @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(SECURITY_COUNCIL_ROLE) {
bytes32 previousRole = _unPauseTypeRoles[_pauseType];
if (previousRole == _newRole) {
revert RolesNotDifferent();
}
_unPauseTypeRoles[_pauseType] = _newRole;
emit UnPauseTypeRoleUpdated(_pauseType, _newRole, previousRole);
}
}