Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
60 changes: 57 additions & 3 deletions src/misc/PauseController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ contract PauseController is OwnableUpgradeable {
/// @param component The component that is unpaused.
event Unpause(address indexed component);

/// @notice Emitted when a component's pause time is extended.
/// @param component The component that is paused.
/// @param timestamp The new pause expiry timestamp.
event SetPauseExpiry(address indexed component, uint256 timestamp);

/// @notice Emitted when the pause cooldown period of a component is reset.
/// @param component The component that has its pause cooldown period reset.
event ResetPauseCooldownPeriod(address indexed component);
Expand Down Expand Up @@ -51,13 +56,19 @@ contract PauseController is OwnableUpgradeable {
/// @dev Thrown when the execution of `ScrollOwner` contract fails.
error ErrorExecuteUnpauseFailed();

/// @dev Thrown when the provided pause expiry timestamp is invalid.
error ErrorInvalidPauseExpiry();

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

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

/// @notice The default pause expiry duration, after which anyone can unpause the component.
uint256 public constant DEFAULT_PAUSE_EXPIRY = 7 days;

/***********************
* Immutable Variables *
***********************/
Expand All @@ -75,6 +86,9 @@ contract PauseController is OwnableUpgradeable {
/// @notice The last unpause time of each component.
mapping(address => uint256) private lastUnpauseTime;

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

/***************
* Constructor *
***************/
Expand Down Expand Up @@ -128,12 +142,21 @@ contract PauseController is OwnableUpgradeable {
revert ErrorExecutePauseFailed();
}

uint256 timestamp = block.timestamp + DEFAULT_PAUSE_EXPIRY;
pauseExpiry[address(component)] = timestamp;

emit Pause(address(component));
emit SetPauseExpiry(address(component), timestamp);
}

/// @notice Unpause a component.
/// @param component The component to unpause.
function unpause(IPausable component) external onlyOwner {
function unpause(IPausable component) external {
// Skip owner check after the pause expiry time
if (pauseExpiry[address(component)] == 0 || pauseExpiry[address(component)] > block.timestamp) {
_checkOwner();
}

if (!component.paused()) {
revert ErrorComponentNotPaused();
}
Expand All @@ -145,12 +168,13 @@ contract PauseController is OwnableUpgradeable {
PAUSE_CONTROLLER_ROLE
);

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

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

lastUnpauseTime[address(component)] = block.timestamp;
pauseExpiry[address(component)] = 0;

emit Unpause(address(component));
}

Expand All @@ -168,6 +192,36 @@ contract PauseController is OwnableUpgradeable {
_updatePauseCooldownPeriod(newPauseCooldownPeriod);
}

/// @notice Extend the pause expiry time of a component.
/// @param component The component to pause.
/// @param newTimestamp The new pause expiry timestamp.
function extendPause(IPausable component, uint256 newTimestamp) external onlyOwner {
if (newTimestamp <= block.timestamp || newTimestamp <= pauseExpiry[address(component)]) {
revert ErrorInvalidPauseExpiry();
}

// Re-pause if needed, in case there is a race between signing the
// extendPause transaction and the permissionless unpause.
if (!component.paused()) {
ScrollOwner(payable(SCROLL_OWNER)).execute(
address(component),
0,
abi.encodeWithSelector(IPausable.setPause.selector, true),
PAUSE_CONTROLLER_ROLE
);

emit Pause(address(component));
}

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

pauseExpiry[address(component)] = newTimestamp;

emit SetPauseExpiry(address(component), newTimestamp);
}

/**********************
* Internal Functions *
**********************/
Expand Down
72 changes: 72 additions & 0 deletions src/test/misc/PauseController.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,76 @@ contract PauseControllerTest is Test {

vm.stopPrank();
}

function test_Unpause_Permissionless() public {
vm.startPrank(owner);
pauseController.pause(mockPausable);
assertTrue(mockPausable.paused());
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();
vm.stopPrank();

address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);

vm.warp(pauseExpiry - 1);
vm.expectRevert("Ownable: caller is not the owner");
pauseController.unpause(mockPausable);
assertTrue(mockPausable.paused());

vm.warp(pauseExpiry);
pauseController.unpause(mockPausable);
assertFalse(mockPausable.paused());

vm.stopPrank();
}

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

pauseController.pause(mockPausable);
assertTrue(mockPausable.paused());
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();

vm.expectRevert(PauseController.ErrorInvalidPauseExpiry.selector);
pauseController.extendPause(mockPausable, pauseExpiry);

pauseController.extendPause(mockPausable, pauseExpiry + 1 days);

vm.stopPrank();

address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);

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

vm.warp(pauseExpiry + 1 days);
pauseController.unpause(mockPausable);
assertFalse(mockPausable.paused());

vm.stopPrank();
}

function test_Pause_Unpause_Extend() public {
vm.startPrank(owner);
pauseController.pause(mockPausable);
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();
assertTrue(mockPausable.paused());
vm.stopPrank();

vm.warp(pauseExpiry);

address notOwner = makeAddr("notOwner");
vm.startPrank(notOwner);
pauseController.unpause(mockPausable);
assertFalse(mockPausable.paused());
vm.stopPrank();

vm.startPrank(owner);
pauseController.extendPause(mockPausable, pauseExpiry + 1 days);
assertTrue(mockPausable.paused()); // auto re-pause contract
vm.stopPrank();
}
}
Loading