Skip to content

Commit 69dbb33

Browse files
committed
feat: add pause expiry in PauseController
1 parent 42de954 commit 69dbb33

File tree

2 files changed

+129
-3
lines changed

2 files changed

+129
-3
lines changed

src/misc/PauseController.sol

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ contract PauseController is OwnableUpgradeable {
2323
/// @param component The component that is unpaused.
2424
event Unpause(address indexed component);
2525

26+
/// @notice Emitted when a component's pause time is extended.
27+
/// @param component The component that is paused.
28+
/// @param timestamp The new pause expiry timestamp.
29+
event SetPauseExpiry(address indexed component, uint256 timestamp);
30+
2631
/// @notice Emitted when the pause cooldown period of a component is reset.
2732
/// @param component The component that has its pause cooldown period reset.
2833
event ResetPauseCooldownPeriod(address indexed component);
@@ -51,13 +56,19 @@ contract PauseController is OwnableUpgradeable {
5156
/// @dev Thrown when the execution of `ScrollOwner` contract fails.
5257
error ErrorExecuteUnpauseFailed();
5358

59+
/// @dev Thrown when the provided pause expiry timestamp is invalid.
60+
error ErrorInvalidPauseExpiry();
61+
5462
/*************
5563
* Constants *
5664
*************/
5765

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

69+
/// @notice The default pause expiry duration, after which anyone can unpause the component.
70+
uint256 public constant DEFAULT_PAUSE_EXPIRY = 7 days;
71+
6172
/***********************
6273
* Immutable Variables *
6374
***********************/
@@ -75,6 +86,9 @@ contract PauseController is OwnableUpgradeable {
7586
/// @notice The last unpause time of each component.
7687
mapping(address => uint256) private lastUnpauseTime;
7788

89+
/// @notice The last unpause time of each component.
90+
mapping(address => uint256) private pauseExpiry;
91+
7892
/***************
7993
* Constructor *
8094
***************/
@@ -128,12 +142,21 @@ contract PauseController is OwnableUpgradeable {
128142
revert ErrorExecutePauseFailed();
129143
}
130144

145+
uint256 timestamp = block.timestamp + DEFAULT_PAUSE_EXPIRY;
146+
pauseExpiry[address(component)] = timestamp;
147+
131148
emit Pause(address(component));
149+
emit SetPauseExpiry(address(component), timestamp);
132150
}
133151

134152
/// @notice Unpause a component.
135153
/// @param component The component to unpause.
136-
function unpause(IPausable component) external onlyOwner {
154+
function unpause(IPausable component) external {
155+
// Skip owner check after the pause expiry time
156+
if (pauseExpiry[address(component)] == 0 || pauseExpiry[address(component)] > block.timestamp) {
157+
_checkOwner();
158+
}
159+
137160
if (!component.paused()) {
138161
revert ErrorComponentNotPaused();
139162
}
@@ -145,12 +168,13 @@ contract PauseController is OwnableUpgradeable {
145168
PAUSE_CONTROLLER_ROLE
146169
);
147170

148-
lastUnpauseTime[address(component)] = block.timestamp;
149-
150171
if (component.paused()) {
151172
revert ErrorExecuteUnpauseFailed();
152173
}
153174

175+
lastUnpauseTime[address(component)] = block.timestamp;
176+
pauseExpiry[address(component)] = 0;
177+
154178
emit Unpause(address(component));
155179
}
156180

@@ -168,6 +192,36 @@ contract PauseController is OwnableUpgradeable {
168192
_updatePauseCooldownPeriod(newPauseCooldownPeriod);
169193
}
170194

195+
/// @notice Extend the pause expiry time of a component.
196+
/// @param component The component to pause.
197+
/// @param newTimestamp The new pause expiry timestamp.
198+
function extendPause(IPausable component, uint256 newTimestamp) external onlyOwner {
199+
if (newTimestamp <= block.timestamp || newTimestamp <= pauseExpiry[address(component)]) {
200+
revert ErrorInvalidPauseExpiry();
201+
}
202+
203+
// Re-pause if needed, in case there is a race between signing the
204+
// extendPause transaction and the permissionless unpause.
205+
if (!component.paused()) {
206+
ScrollOwner(payable(SCROLL_OWNER)).execute(
207+
address(component),
208+
0,
209+
abi.encodeWithSelector(IPausable.setPause.selector, true),
210+
PAUSE_CONTROLLER_ROLE
211+
);
212+
213+
emit Pause(address(component));
214+
}
215+
216+
if (!component.paused()) {
217+
revert ErrorComponentNotPaused();
218+
}
219+
220+
pauseExpiry[address(component)] = newTimestamp;
221+
222+
emit SetPauseExpiry(address(component), newTimestamp);
223+
}
224+
171225
/**********************
172226
* Internal Functions *
173227
**********************/

src/test/misc/PauseController.t.sol

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,76 @@ contract PauseControllerTest is Test {
190190

191191
vm.stopPrank();
192192
}
193+
194+
function test_Unpause_Permissionless() public {
195+
vm.startPrank(owner);
196+
pauseController.pause(mockPausable);
197+
assertTrue(mockPausable.paused());
198+
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();
199+
vm.stopPrank();
200+
201+
address notOwner = makeAddr("notOwner");
202+
vm.startPrank(notOwner);
203+
204+
vm.warp(pauseExpiry - 1);
205+
vm.expectRevert("Ownable: caller is not the owner");
206+
pauseController.unpause(mockPausable);
207+
assertTrue(mockPausable.paused());
208+
209+
vm.warp(pauseExpiry);
210+
pauseController.unpause(mockPausable);
211+
assertFalse(mockPausable.paused());
212+
213+
vm.stopPrank();
214+
}
215+
216+
function test_Pause_Extend() public {
217+
vm.startPrank(owner);
218+
219+
pauseController.pause(mockPausable);
220+
assertTrue(mockPausable.paused());
221+
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();
222+
223+
vm.expectRevert(PauseController.ErrorInvalidPauseExpiry.selector);
224+
pauseController.extendPause(mockPausable, pauseExpiry);
225+
226+
pauseController.extendPause(mockPausable, pauseExpiry + 1 days);
227+
228+
vm.stopPrank();
229+
230+
address notOwner = makeAddr("notOwner");
231+
vm.startPrank(notOwner);
232+
233+
vm.warp(pauseExpiry);
234+
vm.expectRevert("Ownable: caller is not the owner");
235+
pauseController.unpause(mockPausable);
236+
assertTrue(mockPausable.paused());
237+
238+
vm.warp(pauseExpiry + 1 days);
239+
pauseController.unpause(mockPausable);
240+
assertFalse(mockPausable.paused());
241+
242+
vm.stopPrank();
243+
}
244+
245+
function test_Pause_Unpause_Extend() public {
246+
vm.startPrank(owner);
247+
pauseController.pause(mockPausable);
248+
uint256 pauseExpiry = block.timestamp + pauseController.DEFAULT_PAUSE_EXPIRY();
249+
assertTrue(mockPausable.paused());
250+
vm.stopPrank();
251+
252+
vm.warp(pauseExpiry);
253+
254+
address notOwner = makeAddr("notOwner");
255+
vm.startPrank(notOwner);
256+
pauseController.unpause(mockPausable);
257+
assertFalse(mockPausable.paused());
258+
vm.stopPrank();
259+
260+
vm.startPrank(owner);
261+
pauseController.extendPause(mockPausable, pauseExpiry + 1 days);
262+
assertTrue(mockPausable.paused()); // auto re-pause contract
263+
vm.stopPrank();
264+
}
193265
}

0 commit comments

Comments
 (0)