Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/misc/IPausable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IPausable {
/// @notice Returns true if the contract is paused, and false otherwise.
function paused() external view returns (bool);

/// @notice Pause or unpause this contract.
/// @param _status Pause this contract if it is true, otherwise unpause this contract.
function setPause(bool _status) external;
}
165 changes: 165 additions & 0 deletions src/misc/PauseController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

import {IPausable} from "./IPausable.sol";

import {ScrollOwner} from "./ScrollOwner.sol";

/// @title PauseController
/// @notice This contract is used to pause and unpause components in Scroll.
/// @dev The owner of this contract should be `ScrollOwner` contract to allow fine-grained control over the pause and unpause of components.
contract PauseController is Ownable {
/**********
* Events *
**********/

/// @notice Emitted when a component is paused.
/// @param component The component that is paused.
event Pause(address indexed component);

/// @notice Emitted when a component is unpaused.
/// @param component The component that is unpaused.
event Unpause(address indexed component);

/// @notice Emitted when the pause cooldown period is updated.
/// @param oldPauseCooldownPeriod The old pause cooldown period.
/// @param newPauseCooldownPeriod The new pause cooldown period.
event UpdatePauseCooldownPeriod(uint256 oldPauseCooldownPeriod, uint256 newPauseCooldownPeriod);

/**********
* Errors *
**********/

/// @dev Thrown when the cooldown period is not passed.
error ErrorCooldownPeriodNotPassed();

/// @dev Thrown when the component is already paused.
error ErrorComponentAlreadyPaused();

/// @dev Thrown when the component is not paused.
error ErrorComponentNotPaused();

/// @dev Thrown when the execution of `ScrollOwner` contract fails.
error ErrorExecutePauseFailed();

/// @dev Thrown when the execution of `ScrollOwner` contract fails.
error ErrorExecuteUnpauseFailed();

/*************
* Constants *
*************/

/// @notice The role for pause controller in `ScrollOwner` contract.
bytes32 public constant PAUSE_CONTROLLER_ROLE = keccak256("PAUSE_CONTROLLER_ROLE");

/***********************
* Immutable Variables *
***********************/

/// @notice The address of the ScrollOwner contract.
address public immutable SCROLL_OWNER;

/*********************
* Storage Variables *
*********************/

/// @notice The pause cooldown period. That is the minimum time between two consecutive pauses.
uint256 public pauseCooldownPeriod;

/// @notice The last pause time of each component.
mapping(address => uint256) private lastPauseTime;

/***************
* Constructor *
***************/

constructor(address _scrollOwner, uint256 _pauseCooldownPeriod) {
SCROLL_OWNER = _scrollOwner;

_updatePauseCooldownPeriod(_pauseCooldownPeriod);
}

/*************************
* Public View Functions *
*************************/

/// @notice Get the last pause time of a component.
/// @param component The component to get the last pause time.
/// @return The last pause time of the component.
function getLastPauseTime(IPausable component) external view returns (uint256) {
return lastPauseTime[address(component)];
}

/************************
* Restricted Functions *
************************/

/// @notice Pause a component.
/// @param component The component to pause.
function pause(IPausable component) external onlyOwner {
if (component.paused()) {
revert ErrorComponentAlreadyPaused();
}

if (lastPauseTime[address(component)] + pauseCooldownPeriod >= block.timestamp) {
revert ErrorCooldownPeriodNotPassed();
}

lastPauseTime[address(component)] = block.timestamp;

ScrollOwner(payable(SCROLL_OWNER)).execute(
address(component),
0,
abi.encodeWithSelector(IPausable.setPause.selector, true),
PAUSE_CONTROLLER_ROLE
);

if (!component.paused()) {
revert ErrorExecutePauseFailed();
}

emit Pause(address(component));
}

/// @notice Unpause a component.
/// @param component The component to unpause.
function unpause(IPausable component) external onlyOwner {
if (!component.paused()) {
revert ErrorComponentNotPaused();
}

ScrollOwner(payable(SCROLL_OWNER)).execute(
address(component),
0,
abi.encodeWithSelector(IPausable.setPause.selector, false),
PAUSE_CONTROLLER_ROLE
);

if (component.paused()) {
revert ErrorExecuteUnpauseFailed();
}

emit Unpause(address(component));
}

/// @notice Set the pause cooldown period.
/// @param newPauseCooldownPeriod The new pause cooldown period.
function updatePauseCooldownPeriod(uint256 newPauseCooldownPeriod) external onlyOwner {
_updatePauseCooldownPeriod(newPauseCooldownPeriod);
}

/**********************
* Internal Functions *
**********************/

/// @dev Internal function to set the pause cooldown period.
/// @param newPauseCooldownPeriod The new pause cooldown period.
function _updatePauseCooldownPeriod(uint256 newPauseCooldownPeriod) internal {
uint256 oldPauseCooldownPeriod = pauseCooldownPeriod;
pauseCooldownPeriod = newPauseCooldownPeriod;

emit UpdatePauseCooldownPeriod(oldPauseCooldownPeriod, newPauseCooldownPeriod);
}
}
168 changes: 168 additions & 0 deletions src/test/misc/PauseController.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";

import {PauseController} from "../../misc/PauseController.sol";
import {IPausable} from "../../misc/IPausable.sol";
import {ScrollOwner} from "../../misc/ScrollOwner.sol";

contract MockPausable is IPausable {
bool private _paused;

function setPause(bool _status) external {
_paused = _status;
}

function paused() external view returns (bool) {
return _paused;
}
}

contract PauseControllerTest is Test {
PauseController public pauseController;
MockPausable public mockPausable;
ScrollOwner public scrollOwner;
address public owner;
uint256 public constant PAUSE_COOLDOWN_PERIOD = 1 days;

event Pause(address indexed component);
event Unpause(address indexed component);
event UpdatePauseCooldownPeriod(uint256 oldPauseCooldownPeriod, uint256 newPauseCooldownPeriod);

function setUp() public {
owner = makeAddr("owner");
vm.startPrank(owner);

scrollOwner = new ScrollOwner();
pauseController = new PauseController(address(scrollOwner), PAUSE_COOLDOWN_PERIOD);
mockPausable = new MockPausable();

bytes4[] memory selectors = new bytes4[](1);
selectors[0] = IPausable.setPause.selector;
scrollOwner.updateAccess(address(mockPausable), selectors, pauseController.PAUSE_CONTROLLER_ROLE(), true);
scrollOwner.grantRole(pauseController.PAUSE_CONTROLLER_ROLE(), address(pauseController));

vm.stopPrank();

vm.warp(1e9);
}

function test_Pause() public {
vm.startPrank(owner);

vm.expectEmit(true, false, false, true);
emit Pause(address(mockPausable));
pauseController.pause(mockPausable);

assertTrue(mockPausable.paused());
assertEq(pauseController.getLastPauseTime(mockPausable), block.timestamp);

vm.stopPrank();
}

function test_Pause_AlreadyPaused() public {
vm.startPrank(owner);

pauseController.pause(mockPausable);

vm.expectRevert(PauseController.ErrorComponentAlreadyPaused.selector);
pauseController.pause(mockPausable);

vm.stopPrank();
}

function test_Pause_CooldownPeriodNotPassed() public {
vm.startPrank(owner);

pauseController.pause(mockPausable);
uint256 lastPauseTime = pauseController.getLastPauseTime(mockPausable);
assertEq(lastPauseTime, block.timestamp);

mockPausable.setPause(false);

vm.warp(lastPauseTime + PAUSE_COOLDOWN_PERIOD - 1);
vm.expectRevert(PauseController.ErrorCooldownPeriodNotPassed.selector);
pauseController.pause(mockPausable);
assertFalse(mockPausable.paused());

vm.warp(lastPauseTime + PAUSE_COOLDOWN_PERIOD);
vm.expectRevert(PauseController.ErrorCooldownPeriodNotPassed.selector);
pauseController.pause(mockPausable);
assertFalse(mockPausable.paused());

vm.warp(lastPauseTime + PAUSE_COOLDOWN_PERIOD + 1);
pauseController.pause(mockPausable);
assertEq(pauseController.getLastPauseTime(mockPausable), lastPauseTime + PAUSE_COOLDOWN_PERIOD + 1);
assertTrue(mockPausable.paused());

vm.stopPrank();
}

function test_Unpause() public {
vm.startPrank(owner);

pauseController.pause(mockPausable);

vm.expectEmit(true, false, false, true);
emit Unpause(address(mockPausable));
pauseController.unpause(mockPausable);

assertFalse(mockPausable.paused());

vm.stopPrank();
}

function test_Unpause_NotPaused() public {
vm.startPrank(owner);

vm.expectRevert(PauseController.ErrorComponentNotPaused.selector);
pauseController.unpause(mockPausable);

vm.stopPrank();
}

function test_UpdatePauseCooldownPeriod() public {
vm.startPrank(owner);

uint256 newCooldownPeriod = 2 days;

vm.expectEmit(false, false, false, true);
emit UpdatePauseCooldownPeriod(PAUSE_COOLDOWN_PERIOD, newCooldownPeriod);
pauseController.updatePauseCooldownPeriod(newCooldownPeriod);

assertEq(pauseController.pauseCooldownPeriod(), newCooldownPeriod);

vm.stopPrank();
}

function test_UpdatePauseCooldownPeriod_NotOwner() public {
address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);

vm.expectRevert("Ownable: caller is not the owner");
pauseController.updatePauseCooldownPeriod(2 days);

vm.stopPrank();
}

function test_Pause_NotOwner() public {
address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);

vm.expectRevert("Ownable: caller is not the owner");
pauseController.pause(mockPausable);

vm.stopPrank();
}

function test_Unpause_NotOwner() public {
address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);

vm.expectRevert("Ownable: caller is not the owner");
pauseController.unpause(mockPausable);

vm.stopPrank();
}
}
Loading