[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>
This commit is contained in:
kyzooghost
2025-03-01 00:46:53 +11:00
committed by GitHub
parent 14c64cf906
commit d05475241d
10 changed files with 452 additions and 129 deletions

View File

@@ -18,14 +18,6 @@ bytes32 SET_MESSAGE_SERVICE_ROLE
Role used for setting the message service address.
### SET_REMOTE_TOKENBRIDGE_ROLE
```solidity
bytes32 SET_REMOTE_TOKENBRIDGE_ROLE
```
Role used for setting the remote token bridge address.
### SET_RESERVED_TOKEN_ROLE
```solidity
@@ -204,25 +196,6 @@ _Contract will be used as proxy implementation._
| ---- | ---- | ----------- |
| _initializationData | struct ITokenBridge.InitializationData | The initial data used for initializing the TokenBridge contract. |
### reinitializePauseTypesAndPermissions
```solidity
function reinitializePauseTypesAndPermissions(address _defaultAdmin, struct IPermissionsManager.RoleAddress[] _roleAddresses, struct IPauseManager.PauseTypeRole[] _pauseTypeRoles, struct IPauseManager.PauseTypeRole[] _unpauseTypeRoles) external
```
Sets permissions for a list of addresses and their roles as well as initialises the PauseManager pauseType:role mappings.
_This function is a reinitializer and can only be called once per version. Should be called using an upgradeAndCall transaction to the ProxyAdmin._
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| _defaultAdmin | address | The default admin account's address. |
| _roleAddresses | struct IPermissionsManager.RoleAddress[] | The list of addresses and roles to assign permissions to. |
| _pauseTypeRoles | struct IPauseManager.PauseTypeRole[] | The list of pause types to associate with roles. |
| _unpauseTypeRoles | struct IPauseManager.PauseTypeRole[] | The list of unpause types to associate with roles. |
### bridgeToken
```solidity
@@ -345,21 +318,6 @@ _Change the status of tokens to DEPLOYED. New bridge transaction will not
| ---- | ---- | ----------- |
| _nativeTokens | address[] | Array of native tokens for which the DEPLOYED status must be set. |
### setRemoteTokenBridge
```solidity
function setRemoteTokenBridge(address _remoteTokenBridge) external
```
_Sets the address of the remote token bridge. Can only be called once.
SET_REMOTE_TOKENBRIDGE_ROLE is required to execute._
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| _remoteTokenBridge | address | The address of the remote token bridge to be set. |
### deployBridgedToken
```solidity

View File

@@ -9,6 +9,7 @@ struct InitializationData {
address tokenBeacon;
uint256 sourceChainId;
uint256 targetChainId;
address remoteSender;
address[] reservedTokens;
struct IPermissionsManager.RoleAddress[] roleAddresses;
struct IPauseManager.PauseTypeRole[] pauseTypeRoles;
@@ -455,20 +456,6 @@ _Linea can reserve tokens. In this case, the token cannot be bridged.
| ---- | ---- | ----------- |
| _token | address | The address of the token to be set as reserved. |
### setRemoteTokenBridge
```solidity
function setRemoteTokenBridge(address _remoteTokenBridge) external
```
_Sets the address of the remote token bridge. Can only be called once._
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| _remoteTokenBridge | address | The address of the remote token bridge to be set. |
### removeReserved
```solidity

View File

@@ -16,12 +16,45 @@ bytes32 UNPAUSE_ALL_ROLE
This is used to unpause all unpausable functions.
### pauseTypeStatuses
### SECURITY_COUNCIL_ROLE
```solidity
mapping(bytes32 => bool) pauseTypeStatuses
bytes32 SECURITY_COUNCIL_ROLE
```
Role assigned to the security council that enables indefinite pausing and bypassing the cooldown period.
_Is not a pause or unpause role; a specific pause/unpause role is still required for specific pause/unpause types._
### PAUSE_DURATION
```solidity
uint256 PAUSE_DURATION
```
Duration of pauses, after which pauses will expire (except by the SECURITY_COUNCIL_ROLE).
### COOLDOWN_DURATION
```solidity
uint256 COOLDOWN_DURATION
```
Duration of cooldown after a pause expires, during which no pauses (except by the SECURITY_COUNCIL_ROLE) can be enacted.
_This prevents indefinite pause chaining by a non-SECURITY_COUNCIL_ROLE._
### pauseExpiryTimestamp
```solidity
uint256 pauseExpiryTimestamp
```
Unix timestamp of pause expiry.
_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).
This prevents indefinite pause chaining by a non-SECURITY_COUNCIL_ROLE._
### onlyUsedPausedTypes
```solidity
@@ -120,7 +153,9 @@ function pauseByType(enum IPauseManager.PauseType _pauseType) external
Pauses functionality by specific type.
_Throws if UNUSED pause type is used.
Requires the role mapped in `_pauseTypeRoles` for the pauseType._
Requires the role mapped in `_pauseTypeRoles` for the pauseType.
Non-SECURITY_COUNCIL_ROLE can only pause after cooldown has passed.
SECURITY_COUNCIL_ROLE can pause without cooldown or expiry restrictions._
#### Parameters
@@ -137,7 +172,25 @@ function unPauseByType(enum IPauseManager.PauseType _pauseType) external
Unpauses functionality by specific type.
_Throws if UNUSED pause type is used.
Requires the role mapped in `_unPauseTypeRoles` for the pauseType._
Requires the role mapped in `_unPauseTypeRoles` for the pauseType.
SECURITY_COUNCIL_ROLE unpause will reset the cooldown, enabling non-SECURITY_COUNCIL_ROLE pausing._
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| _pauseType | enum IPauseManager.PauseType | The pause type value. |
### unPauseByExpiredType
```solidity
function unPauseByExpiredType(enum IPauseManager.PauseType _pauseType) external
```
Unpauses a specific pause type when the pause has expired.
_Can be called by anyone.
Throws if UNUSED pause type is used, or the pause expiry period has not passed._
#### Parameters
@@ -175,7 +228,7 @@ Update the pause type role mapping.
_Throws if UNUSED pause type is used.
Throws if role not different.
PAUSE_ALL_ROLE role is required to execute this function._
SECURITY_COUNCIL_ROLE role is required to execute this function._
#### Parameters
@@ -194,7 +247,7 @@ Update the unpause type role mapping.
_Throws if UNUSED pause type is used.
Throws if role not different.
UNPAUSE_ALL_ROLE role is required to execute this function._
SECURITY_COUNCIL_ROLE role is required to execute this function._
#### Parameters

View File

@@ -55,6 +55,20 @@ Emitted when a pause type is unpaused.
| messageSender | address | The address performing the unpause. |
| pauseType | enum IPauseManager.PauseType | The indexed pause type that was unpaused. |
### UnPausedDueToExpiry
```solidity
event UnPausedDueToExpiry(enum IPauseManager.PauseType pauseType)
```
Emitted when a pause type is unpaused due to pause expiry passing.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| pauseType | enum IPauseManager.PauseType | The pause type that was unpaused. |
### PauseTypeRoleSet
```solidity
@@ -125,6 +139,14 @@ error IsPaused(enum IPauseManager.PauseType pauseType)
_Thrown when a specific pause type is paused._
### PauseNotExpired
```solidity
error PauseNotExpired(uint256 expiryEnd)
```
_Thrown when unpauseDueToExpiry is attempted before a pause has expired._
### IsNotPaused
```solidity
@@ -133,6 +155,14 @@ error IsNotPaused(enum IPauseManager.PauseType pauseType)
_Thrown when a specific pause type is not paused and expected to be._
### PauseUnavailableDueToCooldown
```solidity
error PauseUnavailableDueToCooldown(uint256 cooldownEnd)
```
_Thrown when pausing is attempted during the cooldown period by a non-SECURITY_COUNCIL_ROLE._
### PauseTypeNotUsed
```solidity
@@ -158,7 +188,9 @@ function pauseByType(enum IPauseManager.PauseType _pauseType) external
Pauses functionality by specific type.
_Throws if UNUSED pause type is used.
Requires the role mapped in pauseTypeRoles for the pauseType._
Requires the role mapped in `_pauseTypeRoles` for the pauseType.
Non-SECURITY_COUNCIL_ROLE can only pause after cooldown has passed.
SECURITY_COUNCIL_ROLE can pause without cooldown or expiry restrictions._
#### Parameters
@@ -175,7 +207,25 @@ function unPauseByType(enum IPauseManager.PauseType _pauseType) external
Unpauses functionality by specific type.
_Throws if UNUSED pause type is used.
Requires the role mapped in unPauseTypeRoles for the pauseType._
Requires the role mapped in `_unPauseTypeRoles` for the pauseType.
SECURITY_COUNCIL_ROLE unpause will reset the cooldown, enabling non-SECURITY_COUNCIL_ROLE pausing._
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| _pauseType | enum IPauseManager.PauseType | The pause type value. |
### unPauseByExpiredType
```solidity
function unPauseByExpiredType(enum IPauseManager.PauseType _pauseType) external
```
Unpauses a specific pause type when the pause has expired.
_Can be called by anyone.
Throws if UNUSED pause type is used, or the pause expiry period has not passed._
#### Parameters
@@ -213,7 +263,7 @@ Update the pause type role mapping.
_Throws if UNUSED pause type is used.
Throws if role not different.
PAUSE_ALL_ROLE role is required to execute this function._
SECURITY_COUNCIL_ROLE role is required to execute this function._
#### Parameters
@@ -232,7 +282,7 @@ Update the unpause type role mapping.
_Throws if UNUSED pause type is used.
Throws if role not different.
UNPAUSE_ALL_ROLE role is required to execute this function._
SECURITY_COUNCIL_ROLE role is required to execute this function._
#### Parameters

View File

@@ -5,7 +5,7 @@ import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/ac
import { IPauseManager } from "./interfaces/IPauseManager.sol";
/**
* @title Contract to manage cross-chain function pausing.
* @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
*/
@@ -16,8 +16,19 @@ 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");
/// @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) public pauseTypeStatuses;
mapping(bytes32 pauseType => bool pauseStatus) private pauseTypeStatuses;
/// @dev The bitmap containing the pause statuses mapped by type.
uint256 private _pauseTypeStatusesBitMap;
@@ -28,10 +39,15 @@ 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.
/// @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[7] private __gap;
uint256[6] private __gap;
/**
* @dev Modifier to prevent usage of unused PauseType.
@@ -124,6 +140,8 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable {
* @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(
@@ -132,6 +150,17 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable {
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);
@@ -141,6 +170,7 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable {
* @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(
@@ -150,10 +180,33 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable {
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.
@@ -167,14 +220,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 +241,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();

View File

@@ -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 passing.
* @param pauseType The 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 by a non-SECURITY_COUNCIL_ROLE.
*/
error PauseUnavailableDueToCooldown(uint256 cooldownEnd);
/**
* @dev Thrown when the unused paused type is used.
*/
@@ -103,7 +119,9 @@ interface IPauseManager {
/**
* @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 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;
@@ -111,11 +129,20 @@ interface IPauseManager {
/**
* @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 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;
/**
* @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;
/**
* @notice Check if a pause type is enabled.
* @param _pauseType The pause type value.
@@ -127,7 +154,7 @@ interface IPauseManager {
* @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.
*/
@@ -137,7 +164,7 @@ interface IPauseManager {
* @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.
*/

View File

@@ -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

View File

@@ -5,3 +5,4 @@ export * from "./hashing";
export * from "./dataGeneration";
export * from "./dataLoader";
export * from "./expectations";
export * from "./time";

View File

@@ -0,0 +1,12 @@
import { time } from "@nomicfoundation/hardhat-network-helpers";
export const getLastBlockTimestamp = async (): Promise<bigint> => {
return BigInt(await time.latest());
};
export const setFutureTimestampForNextBlock = async (secondsInTheFuture: number | bigint = 1): Promise<bigint> => {
const lastBlockTimestamp: number = await time.latest();
const futureTimestamp: bigint = BigInt(lastBlockTimestamp) + BigInt(secondsInTheFuture);
await time.setNextBlockTimestamp(futureTimestamp);
return futureTimestamp;
};

View File

@@ -19,6 +19,7 @@ import {
PAUSE_BLOB_SUBMISSION_ROLE,
UNPAUSE_FINALIZATION_ROLE,
UNPAUSE_BLOB_SUBMISSION_ROLE,
SECURITY_COUNCIL_ROLE,
BLOB_SUBMISSION_PAUSE_TYPE,
CALLDATA_SUBMISSION_PAUSE_TYPE,
FINALIZATION_PAUSE_TYPE,
@@ -32,6 +33,8 @@ import {
expectEvent,
expectRevertWithCustomError,
expectRevertWithReason,
getLastBlockTimestamp,
setFutureTimestampForNextBlock,
} from "../common/helpers";
async function deployTestPauseManagerFixture(): Promise<TestPauseManager> {
@@ -46,13 +49,15 @@ describe("PauseManager", () => {
let defaultAdmin: SignerWithAddress;
let pauseManagerAccount: SignerWithAddress;
let nonManager: SignerWithAddress;
let securityCouncil: SignerWithAddress;
let pauseManager: TestPauseManager;
beforeEach(async () => {
[defaultAdmin, pauseManagerAccount, nonManager] = await ethers.getSigners();
[defaultAdmin, pauseManagerAccount, nonManager, securityCouncil] = await ethers.getSigners();
pauseManager = await loadFixture(deployTestPauseManagerFixture);
await Promise.all([
// Roles for pauseManagerAccount
pauseManager.grantRole(PAUSE_ALL_ROLE, pauseManagerAccount.address),
pauseManager.grantRole(UNPAUSE_ALL_ROLE, pauseManagerAccount.address),
pauseManager.grantRole(PAUSE_L1_L2_ROLE, pauseManagerAccount.address),
@@ -63,6 +68,14 @@ describe("PauseManager", () => {
pauseManager.grantRole(UNPAUSE_BLOB_SUBMISSION_ROLE, pauseManagerAccount.address),
pauseManager.grantRole(PAUSE_FINALIZATION_ROLE, pauseManagerAccount.address),
pauseManager.grantRole(UNPAUSE_FINALIZATION_ROLE, pauseManagerAccount.address),
// Roles for securityCouncil
pauseManager.grantRole(PAUSE_ALL_ROLE, securityCouncil.address),
pauseManager.grantRole(UNPAUSE_ALL_ROLE, securityCouncil.address),
pauseManager.grantRole(UNPAUSE_L1_L2_ROLE, securityCouncil.address),
pauseManager.grantRole(UNPAUSE_L2_L1_ROLE, securityCouncil.address),
pauseManager.grantRole(UNPAUSE_BLOB_SUBMISSION_ROLE, securityCouncil.address),
pauseManager.grantRole(UNPAUSE_FINALIZATION_ROLE, securityCouncil.address),
pauseManager.grantRole(SECURITY_COUNCIL_ROLE, securityCouncil.address),
]);
});
@@ -74,6 +87,10 @@ describe("PauseManager", () => {
return pauseManager.connect(account).unPauseByType(pauseType);
}
async function unPauseByExpiredType(pauseType: number, account: SignerWithAddress) {
return pauseManager.connect(account).unPauseByExpiredType(pauseType);
}
describe("Initialization checks", () => {
it("Deployer has default admin role", async () => {
expect(await pauseManager.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin.address)).to.be.true;
@@ -98,32 +115,34 @@ describe("PauseManager", () => {
});
it("should fail updatePauseTypeRole if correct role not used", async () => {
const updateCall = pauseManager.connect(nonManager).updatePauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectRevertWithReason(updateCall, buildAccessErrorMessage(nonManager, PAUSE_ALL_ROLE));
const updateCall = pauseManager
.connect(pauseManagerAccount)
.updatePauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectRevertWithReason(updateCall, buildAccessErrorMessage(pauseManagerAccount, SECURITY_COUNCIL_ROLE));
});
it("should fail updateUnpauseTypeRole if correct role not used", async () => {
const updateCall = pauseManager.connect(nonManager).updateUnpauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectRevertWithReason(updateCall, buildAccessErrorMessage(nonManager, UNPAUSE_ALL_ROLE));
const updateCall = pauseManager
.connect(pauseManagerAccount)
.updateUnpauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectRevertWithReason(updateCall, buildAccessErrorMessage(pauseManagerAccount, SECURITY_COUNCIL_ROLE));
});
it("should fail updateUnpauseTypeRole if roles are not different", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.updatePauseTypeRole(GENERAL_PAUSE_TYPE, PAUSE_ALL_ROLE);
const updateCall = pauseManager.connect(securityCouncil).updatePauseTypeRole(GENERAL_PAUSE_TYPE, PAUSE_ALL_ROLE);
await expectRevertWithCustomError(pauseManager, updateCall, "RolesNotDifferent");
});
it("should fail updateUnpauseTypeRole if roles are not different", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.connect(securityCouncil)
.updateUnpauseTypeRole(GENERAL_PAUSE_TYPE, UNPAUSE_ALL_ROLE);
await expectRevertWithCustomError(pauseManager, updateCall, "RolesNotDifferent");
});
it("should update pause type role with pausing working", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.connect(securityCouncil)
.updatePauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectEvent(pauseManager, updateCall, "PauseTypeRoleUpdated", [
GENERAL_PAUSE_TYPE,
@@ -137,7 +156,7 @@ describe("PauseManager", () => {
it("should fail to pause with old role", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.connect(securityCouncil)
.updatePauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectEvent(pauseManager, updateCall, "PauseTypeRoleUpdated", [
GENERAL_PAUSE_TYPE,
@@ -146,14 +165,14 @@ describe("PauseManager", () => {
]);
await expectRevertWithReason(
pauseManager.connect(pauseManagerAccount).pauseByType(GENERAL_PAUSE_TYPE),
buildAccessErrorMessage(pauseManagerAccount, DEFAULT_ADMIN_ROLE),
pauseManager.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE),
buildAccessErrorMessage(securityCouncil, DEFAULT_ADMIN_ROLE),
);
});
it("should update unpause type role with unpausing working", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.connect(securityCouncil)
.updateUnpauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectEvent(pauseManager, updateCall, "UnPauseTypeRoleUpdated", [
GENERAL_PAUSE_TYPE,
@@ -162,7 +181,7 @@ describe("PauseManager", () => {
]);
// pause with non-modified pausing account
await pauseManager.connect(pauseManagerAccount).pauseByType(GENERAL_PAUSE_TYPE);
await pauseManager.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.true;
await pauseManager.connect(defaultAdmin).unPauseByType(GENERAL_PAUSE_TYPE);
@@ -171,7 +190,7 @@ describe("PauseManager", () => {
it("should fail to unpause with old role", async () => {
const updateCall = pauseManager
.connect(pauseManagerAccount)
.connect(securityCouncil)
.updateUnpauseTypeRole(GENERAL_PAUSE_TYPE, DEFAULT_ADMIN_ROLE);
await expectEvent(pauseManager, updateCall, "UnPauseTypeRoleUpdated", [
GENERAL_PAUSE_TYPE,
@@ -180,17 +199,17 @@ describe("PauseManager", () => {
]);
// pause with non-modified pausing account
await pauseManager.connect(pauseManagerAccount).pauseByType(GENERAL_PAUSE_TYPE);
await pauseManager.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.true;
await expectRevertWithReason(
pauseManager.connect(pauseManagerAccount).unPauseByType(GENERAL_PAUSE_TYPE),
buildAccessErrorMessage(pauseManagerAccount, DEFAULT_ADMIN_ROLE),
pauseManager.connect(securityCouncil).unPauseByType(GENERAL_PAUSE_TYPE),
buildAccessErrorMessage(securityCouncil, DEFAULT_ADMIN_ROLE),
);
});
});
describe("General pausing", () => {
describe("Pausing and unpausing with GENERAL_PAUSE_TYPE", () => {
// can pause as PAUSE_ALL_ROLE
it("should pause the contract if PAUSE_ALL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
@@ -222,24 +241,6 @@ describe("PauseManager", () => {
});
});
describe("Pause and unpause event emitting", () => {
it("should pause the L1_L2_PAUSE_TYPE", async () => {
await expectEvent(pauseManager, pauseByType(L1_L2_PAUSE_TYPE), "Paused", [
pauseManagerAccount.address,
L1_L2_PAUSE_TYPE,
]);
});
it("should unpause the L1_L2_PAUSE_TYPE", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await expectEvent(pauseManager, unPauseByType(L1_L2_PAUSE_TYPE), "UnPaused", [
pauseManagerAccount.address,
L1_L2_PAUSE_TYPE,
]);
});
});
describe("Specific type pausing", () => {
describe("Unused pause type", () => {
it("should revert when pausing with the unused pause type", async () => {
@@ -257,9 +258,17 @@ describe("PauseManager", () => {
"PauseTypeNotUsed",
);
});
it("should revert when unPauseByExpiredType with the unused pause type", async () => {
await expectRevertWithCustomError(
pauseManager,
pauseManager.unPauseByExpiredType(UNUSED_PAUSE_TYPE),
"PauseTypeNotUsed",
);
});
});
describe("With permissions as PAUSE_ALL_ROLE", () => {
describe("With permissions granted by granular pause role", () => {
it("should pause the L1_L2_PAUSE_TYPE", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
expect(await pauseManager.isPaused(L1_L2_PAUSE_TYPE)).to.be.true;
@@ -320,7 +329,8 @@ describe("PauseManager", () => {
expect(await pauseManager.isPaused(FINALIZATION_PAUSE_TYPE)).to.be.false;
});
});
describe("Without permissions - non-PAUSE_ALL_ROLE", () => {
describe("Without permissions granted by granular pause role", () => {
it("cannot pause the L1_L2_PAUSE_TYPE as non-manager", async () => {
await expect(pauseByType(L1_L2_PAUSE_TYPE, nonManager)).to.be.revertedWith(
buildAccessErrorMessage(nonManager, PAUSE_L1_L2_ROLE),
@@ -391,27 +401,198 @@ describe("PauseManager", () => {
);
});
});
describe("Incorrect states for pausing and unpausing", () => {
describe("Incorrect pausing and unpausing", () => {
it("Should pause and fail to pause when paused", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await expect(pauseByType(L1_L2_PAUSE_TYPE)).to.be.revertedWithCustomError(pauseManager, "IsPaused");
});
it("Should allow other types to pause if one is paused", async () => {
it("Should not allow other types to pause if one is already paused", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await expect(pauseByType(L1_L2_PAUSE_TYPE)).to.be.revertedWithCustomError(pauseManager, "IsPaused");
await expectEvent(pauseManager, pauseByType(BLOB_SUBMISSION_PAUSE_TYPE), "Paused", [
pauseManagerAccount.address,
BLOB_SUBMISSION_PAUSE_TYPE,
]);
const expectedCooldown = (await pauseManager.pauseExpiryTimestamp()) + (await pauseManager.COOLDOWN_DURATION());
await expectRevertWithCustomError(
pauseManager,
pauseManager.connect(pauseManagerAccount).pauseByType(L2_L1_PAUSE_TYPE),
"PauseUnavailableDueToCooldown",
[expectedCooldown],
);
});
it("Should fail to unpause if not paused", async () => {
await expect(unPauseByType(L1_L2_PAUSE_TYPE)).to.be.revertedWithCustomError(pauseManager, "IsNotPaused");
});
it("Should fail to unPauseByExpiredType if not paused", async () => {
await expect(unPauseByExpiredType(L1_L2_PAUSE_TYPE, nonManager)).to.be.revertedWithCustomError(
pauseManager,
"IsNotPaused",
);
});
});
});
describe("Pause and unpause event emitting", () => {
it("should pause the L1_L2_PAUSE_TYPE", async () => {
await expectEvent(pauseManager, pauseByType(L1_L2_PAUSE_TYPE), "Paused", [
pauseManagerAccount.address,
L1_L2_PAUSE_TYPE,
]);
});
it("should unpause the L1_L2_PAUSE_TYPE", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await expectEvent(pauseManager, unPauseByType(L1_L2_PAUSE_TYPE), "UnPaused", [
pauseManagerAccount.address,
L1_L2_PAUSE_TYPE,
]);
});
it("should unPauseByExpiredType the L1_L2_PAUSE_TYPE", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await expectEvent(pauseManager, unPauseByExpiredType(L1_L2_PAUSE_TYPE, nonManager), "UnPausedDueToExpiry", [
L1_L2_PAUSE_TYPE,
]);
});
});
describe("Pausing/unpausing with expiry and cooldown:", () => {
it("Pause should set pauseExpiryTimestamp to a time in the future", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
const lastBlockTimestamp = await getLastBlockTimestamp();
expect(await pauseManager.pauseExpiryTimestamp()).to.equal(
lastBlockTimestamp + (await pauseManager.PAUSE_DURATION()),
);
});
it("unPauseByExpiredType should fail after pause, if pause has not expired", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
await expectRevertWithCustomError(
pauseManager,
pauseManager.connect(pauseManagerAccount).unPauseByExpiredType(GENERAL_PAUSE_TYPE),
"PauseNotExpired",
[await pauseManager.pauseExpiryTimestamp()],
);
});
it("unPauseByExpiredType should succeed after pause has expired", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await unPauseByExpiredType(GENERAL_PAUSE_TYPE, nonManager);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.false;
});
it("unPauseByExpiredType should not change the pauseExpiryTimestamp", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
const beforePauseExpiry = await pauseManager.pauseExpiryTimestamp();
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await unPauseByExpiredType(GENERAL_PAUSE_TYPE, nonManager);
const afterPauseExpiry = await pauseManager.pauseExpiryTimestamp();
expect(beforePauseExpiry).to.equal(afterPauseExpiry);
});
it("Should not be able to pause while cooldown is active", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await unPauseByExpiredType(GENERAL_PAUSE_TYPE, nonManager);
const expectedCooldown = (await pauseManager.pauseExpiryTimestamp()) + (await pauseManager.COOLDOWN_DURATION());
await expectRevertWithCustomError(
pauseManager,
pauseManager.connect(pauseManagerAccount).pauseByType(GENERAL_PAUSE_TYPE),
"PauseUnavailableDueToCooldown",
[expectedCooldown],
);
});
it("Should be able to pause after cooldown has passed", async () => {
await pauseByType(GENERAL_PAUSE_TYPE);
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await unPauseByExpiredType(GENERAL_PAUSE_TYPE, nonManager);
await setFutureTimestampForNextBlock(await pauseManager.COOLDOWN_DURATION());
await pauseByType(GENERAL_PAUSE_TYPE);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.true;
});
});
describe("Pausing/unpausing with SECURITY_COUNCIL_ROLE", () => {
it("should pause the contract with SECURITY_COUNCIL_ROLE, when another pause type is already active", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.true;
});
// Should not revert due to overflow from `pauseExpiryTimestamp + COOLDOWN_DURATION`, but due to custom error.
it("after pause with SECURITY_COUNCIL_ROLE, should not be able to pause with a non-SECURITY_COUNCIL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await expectRevertWithCustomError(
pauseManager,
pauseManager.connect(pauseManagerAccount).pauseByType(L1_L2_PAUSE_TYPE),
"PauseUnavailableDueToCooldown",
[ethers.MaxUint256],
);
});
it("should set pauseExpiryTimestamp to an unreachable timestamp if pause enacted by SECURITY_COUNCIL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
expect(await pauseManager.pauseExpiryTimestamp()).to.equal(
ethers.MaxUint256 - (await pauseManager.COOLDOWN_DURATION()),
);
});
it("Should be unable to unPauseByExpiredType after pause with SECURITY_COUNCIL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
const pauseTimestamp = await getLastBlockTimestamp();
const expectedPauseExpiry = pauseTimestamp + (await pauseManager.PAUSE_DURATION());
await setFutureTimestampForNextBlock((await pauseManager.PAUSE_DURATION()) + BigInt(1));
await expectRevertWithCustomError(
pauseManager,
pauseManager.connect(pauseManagerAccount).unPauseByExpiredType(GENERAL_PAUSE_TYPE),
"PauseNotExpired",
[await pauseManager.pauseExpiryTimestamp()],
);
// Assert that we have passed expected pause expiry
expect(await getLastBlockTimestamp()).to.be.above(expectedPauseExpiry);
});
it("should reset the pause cooldown when unpause contract with SECURITY_COUNCIL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
const unPauseBlockTimestamp = await setFutureTimestampForNextBlock();
await unPauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
expect(await pauseManager.pauseExpiryTimestamp()).to.equal(
unPauseBlockTimestamp - (await pauseManager.COOLDOWN_DURATION()),
);
});
it("should not reset the pause cooldown when unpause contract with non-SECURITY_COUNCIL_ROLE", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await unPauseByType(GENERAL_PAUSE_TYPE, pauseManagerAccount);
expect(await pauseManager.pauseExpiryTimestamp()).to.equal(
ethers.MaxUint256 - (await pauseManager.COOLDOWN_DURATION()),
);
});
it("after unpause contract with SECURITY_COUNCIL_ROLE, any pause should be possible", async () => {
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await unPauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await pauseByType(L1_L2_PAUSE_TYPE);
});
it("during pause cooldown, SECURITY_COUNCIL_ROLE should be able to pause", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await setFutureTimestampForNextBlock(await pauseManager.PAUSE_DURATION());
await unPauseByExpiredType(L1_L2_PAUSE_TYPE, nonManager);
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
});
it("Non-SECURITY_COUNCIL_ROLE pause -> SECURITY_COUNCIL_ROLE pause -> SECURITY_COUNCIL_ROLE unpause -> unPauseByExpiredType should work", async () => {
await pauseByType(L1_L2_PAUSE_TYPE);
await pauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await unPauseByType(GENERAL_PAUSE_TYPE, securityCouncil);
await unPauseByExpiredType(L1_L2_PAUSE_TYPE, nonManager);
expect(await pauseManager.isPaused(GENERAL_PAUSE_TYPE)).to.be.false;
expect(await pauseManager.isPaused(L1_L2_PAUSE_TYPE)).to.be.false;
});
});
});