Skip to content

Commit 598d630

Browse files
authored
feat: add pause controller (#117)
1 parent 6c715b1 commit 598d630

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

src/misc/IPausable.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
interface IPausable {
5+
/// @notice Returns true if the contract is paused, and false otherwise.
6+
function paused() external view returns (bool);
7+
8+
/// @notice Pause or unpause this contract.
9+
/// @param _status Pause this contract if it is true, otherwise unpause this contract.
10+
function setPause(bool _status) external;
11+
}

src/misc/PauseController.sol

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5+
6+
import {IPausable} from "./IPausable.sol";
7+
8+
import {ScrollOwner} from "./ScrollOwner.sol";
9+
10+
/// @title PauseController
11+
/// @notice This contract is used to pause and unpause components in Scroll.
12+
/// @dev The owner of this contract should be `ScrollOwner` contract to allow fine-grained control over the pause and unpause of components.
13+
contract PauseController is OwnableUpgradeable {
14+
/**********
15+
* Events *
16+
**********/
17+
18+
/// @notice Emitted when a component is paused.
19+
/// @param component The component that is paused.
20+
event Pause(address indexed component);
21+
22+
/// @notice Emitted when a component is unpaused.
23+
/// @param component The component that is unpaused.
24+
event Unpause(address indexed component);
25+
26+
/// @notice Emitted when the pause cooldown period is updated.
27+
/// @param oldPauseCooldownPeriod The old pause cooldown period.
28+
/// @param newPauseCooldownPeriod The new pause cooldown period.
29+
event UpdatePauseCooldownPeriod(uint256 oldPauseCooldownPeriod, uint256 newPauseCooldownPeriod);
30+
31+
/**********
32+
* Errors *
33+
**********/
34+
35+
/// @dev Thrown when the cooldown period is not passed.
36+
error ErrorCooldownPeriodNotPassed();
37+
38+
/// @dev Thrown when the component is already paused.
39+
error ErrorComponentAlreadyPaused();
40+
41+
/// @dev Thrown when the component is not paused.
42+
error ErrorComponentNotPaused();
43+
44+
/// @dev Thrown when the execution of `ScrollOwner` contract fails.
45+
error ErrorExecutePauseFailed();
46+
47+
/// @dev Thrown when the execution of `ScrollOwner` contract fails.
48+
error ErrorExecuteUnpauseFailed();
49+
50+
/*************
51+
* Constants *
52+
*************/
53+
54+
/// @notice The role for pause controller in `ScrollOwner` contract.
55+
bytes32 public constant PAUSE_CONTROLLER_ROLE = keccak256("PAUSE_CONTROLLER_ROLE");
56+
57+
/***********************
58+
* Immutable Variables *
59+
***********************/
60+
61+
/// @notice The address of the ScrollOwner contract.
62+
address public immutable SCROLL_OWNER;
63+
64+
/*********************
65+
* Storage Variables *
66+
*********************/
67+
68+
/// @notice The pause cooldown period. That is the minimum time between two consecutive pauses.
69+
uint256 public pauseCooldownPeriod;
70+
71+
/// @notice The last unpause time of each component.
72+
mapping(address => uint256) private lastUnpauseTime;
73+
74+
/***************
75+
* Constructor *
76+
***************/
77+
78+
constructor(address _scrollOwner) {
79+
SCROLL_OWNER = _scrollOwner;
80+
81+
_disableInitializers();
82+
}
83+
84+
function initialize(uint256 _pauseCooldownPeriod) external initializer {
85+
__Ownable_init();
86+
87+
_updatePauseCooldownPeriod(_pauseCooldownPeriod);
88+
}
89+
90+
/*************************
91+
* Public View Functions *
92+
*************************/
93+
94+
/// @notice Get the last unpause timestamp of a component.
95+
/// @param component The component to get the last unpause timestamp.
96+
/// @return The last unpause timestamp of the component.
97+
function getLastUnpauseTime(IPausable component) external view returns (uint256) {
98+
return lastUnpauseTime[address(component)];
99+
}
100+
101+
/************************
102+
* Restricted Functions *
103+
************************/
104+
105+
/// @notice Pause a component.
106+
/// @param component The component to pause.
107+
function pause(IPausable component) external onlyOwner {
108+
if (component.paused()) {
109+
revert ErrorComponentAlreadyPaused();
110+
}
111+
112+
if (lastUnpauseTime[address(component)] + pauseCooldownPeriod >= block.timestamp) {
113+
revert ErrorCooldownPeriodNotPassed();
114+
}
115+
116+
ScrollOwner(payable(SCROLL_OWNER)).execute(
117+
address(component),
118+
0,
119+
abi.encodeWithSelector(IPausable.setPause.selector, true),
120+
PAUSE_CONTROLLER_ROLE
121+
);
122+
123+
if (!component.paused()) {
124+
revert ErrorExecutePauseFailed();
125+
}
126+
127+
emit Pause(address(component));
128+
}
129+
130+
/// @notice Unpause a component.
131+
/// @param component The component to unpause.
132+
function unpause(IPausable component) external onlyOwner {
133+
if (!component.paused()) {
134+
revert ErrorComponentNotPaused();
135+
}
136+
137+
ScrollOwner(payable(SCROLL_OWNER)).execute(
138+
address(component),
139+
0,
140+
abi.encodeWithSelector(IPausable.setPause.selector, false),
141+
PAUSE_CONTROLLER_ROLE
142+
);
143+
144+
lastUnpauseTime[address(component)] = block.timestamp;
145+
146+
if (component.paused()) {
147+
revert ErrorExecuteUnpauseFailed();
148+
}
149+
150+
emit Unpause(address(component));
151+
}
152+
153+
/// @notice Set the pause cooldown period.
154+
/// @param newPauseCooldownPeriod The new pause cooldown period.
155+
function updatePauseCooldownPeriod(uint256 newPauseCooldownPeriod) external onlyOwner {
156+
_updatePauseCooldownPeriod(newPauseCooldownPeriod);
157+
}
158+
159+
/**********************
160+
* Internal Functions *
161+
**********************/
162+
163+
/// @dev Internal function to set the pause cooldown period.
164+
/// @param newPauseCooldownPeriod The new pause cooldown period.
165+
function _updatePauseCooldownPeriod(uint256 newPauseCooldownPeriod) internal {
166+
uint256 oldPauseCooldownPeriod = pauseCooldownPeriod;
167+
pauseCooldownPeriod = newPauseCooldownPeriod;
168+
169+
emit UpdatePauseCooldownPeriod(oldPauseCooldownPeriod, newPauseCooldownPeriod);
170+
}
171+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
6+
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
7+
import {ITransparentUpgradeableProxy, TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
8+
9+
import {PauseController} from "../../misc/PauseController.sol";
10+
import {IPausable} from "../../misc/IPausable.sol";
11+
import {ScrollOwner} from "../../misc/ScrollOwner.sol";
12+
13+
contract MockPausable is IPausable {
14+
bool private _paused;
15+
16+
function setPause(bool _status) external {
17+
_paused = _status;
18+
}
19+
20+
function paused() external view returns (bool) {
21+
return _paused;
22+
}
23+
}
24+
25+
contract PauseControllerTest is Test {
26+
event Pause(address indexed component);
27+
event Unpause(address indexed component);
28+
event UpdatePauseCooldownPeriod(uint256 oldPauseCooldownPeriod, uint256 newPauseCooldownPeriod);
29+
30+
uint256 public constant PAUSE_COOLDOWN_PERIOD = 1 days;
31+
32+
ProxyAdmin public admin;
33+
PauseController public pauseController;
34+
MockPausable public mockPausable;
35+
ScrollOwner public scrollOwner;
36+
address public owner;
37+
38+
function setUp() public {
39+
owner = makeAddr("owner");
40+
vm.startPrank(owner);
41+
42+
admin = new ProxyAdmin();
43+
scrollOwner = new ScrollOwner();
44+
PauseController impl = new PauseController(address(scrollOwner));
45+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
46+
address(impl),
47+
address(admin),
48+
abi.encodeCall(PauseController.initialize, (PAUSE_COOLDOWN_PERIOD))
49+
);
50+
pauseController = PauseController(address(proxy));
51+
mockPausable = new MockPausable();
52+
53+
bytes4[] memory selectors = new bytes4[](1);
54+
selectors[0] = IPausable.setPause.selector;
55+
scrollOwner.updateAccess(address(mockPausable), selectors, pauseController.PAUSE_CONTROLLER_ROLE(), true);
56+
scrollOwner.grantRole(pauseController.PAUSE_CONTROLLER_ROLE(), address(pauseController));
57+
58+
vm.stopPrank();
59+
60+
vm.warp(1e9);
61+
}
62+
63+
function test_Pause() public {
64+
vm.startPrank(owner);
65+
66+
vm.expectEmit(true, false, false, true);
67+
emit Pause(address(mockPausable));
68+
pauseController.pause(mockPausable);
69+
assertTrue(mockPausable.paused());
70+
71+
vm.stopPrank();
72+
}
73+
74+
function test_Pause_AlreadyPaused() public {
75+
vm.startPrank(owner);
76+
77+
pauseController.pause(mockPausable);
78+
79+
vm.expectRevert(PauseController.ErrorComponentAlreadyPaused.selector);
80+
pauseController.pause(mockPausable);
81+
82+
vm.stopPrank();
83+
}
84+
85+
function test_Pause_CooldownPeriodNotPassed() public {
86+
vm.startPrank(owner);
87+
88+
pauseController.pause(mockPausable);
89+
pauseController.unpause(mockPausable);
90+
uint256 lastUnpauseTime = pauseController.getLastUnpauseTime(mockPausable);
91+
assertEq(lastUnpauseTime, block.timestamp);
92+
93+
vm.warp(lastUnpauseTime + PAUSE_COOLDOWN_PERIOD - 1);
94+
vm.expectRevert(PauseController.ErrorCooldownPeriodNotPassed.selector);
95+
pauseController.pause(mockPausable);
96+
assertFalse(mockPausable.paused());
97+
98+
vm.warp(lastUnpauseTime + PAUSE_COOLDOWN_PERIOD);
99+
vm.expectRevert(PauseController.ErrorCooldownPeriodNotPassed.selector);
100+
pauseController.pause(mockPausable);
101+
assertFalse(mockPausable.paused());
102+
103+
vm.warp(lastUnpauseTime + PAUSE_COOLDOWN_PERIOD + 1);
104+
pauseController.pause(mockPausable);
105+
assertTrue(mockPausable.paused());
106+
107+
vm.stopPrank();
108+
}
109+
110+
function test_Unpause() public {
111+
vm.startPrank(owner);
112+
113+
pauseController.pause(mockPausable);
114+
115+
assertEq(pauseController.getLastUnpauseTime(mockPausable), 0);
116+
vm.expectEmit(true, false, false, true);
117+
emit Unpause(address(mockPausable));
118+
pauseController.unpause(mockPausable);
119+
assertEq(pauseController.getLastUnpauseTime(mockPausable), block.timestamp);
120+
121+
assertFalse(mockPausable.paused());
122+
123+
vm.stopPrank();
124+
}
125+
126+
function test_Unpause_NotPaused() public {
127+
vm.startPrank(owner);
128+
129+
vm.expectRevert(PauseController.ErrorComponentNotPaused.selector);
130+
pauseController.unpause(mockPausable);
131+
132+
vm.stopPrank();
133+
}
134+
135+
function test_UpdatePauseCooldownPeriod() public {
136+
vm.startPrank(owner);
137+
138+
uint256 newCooldownPeriod = 2 days;
139+
140+
vm.expectEmit(false, false, false, true);
141+
emit UpdatePauseCooldownPeriod(PAUSE_COOLDOWN_PERIOD, newCooldownPeriod);
142+
pauseController.updatePauseCooldownPeriod(newCooldownPeriod);
143+
144+
assertEq(pauseController.pauseCooldownPeriod(), newCooldownPeriod);
145+
146+
vm.stopPrank();
147+
}
148+
149+
function test_UpdatePauseCooldownPeriod_NotOwner() public {
150+
address notOwner = makeAddr("notOwner");
151+
vm.startPrank(notOwner);
152+
153+
vm.expectRevert("Ownable: caller is not the owner");
154+
pauseController.updatePauseCooldownPeriod(2 days);
155+
156+
vm.stopPrank();
157+
}
158+
159+
function test_Pause_NotOwner() public {
160+
address notOwner = makeAddr("notOwner");
161+
vm.startPrank(notOwner);
162+
163+
vm.expectRevert("Ownable: caller is not the owner");
164+
pauseController.pause(mockPausable);
165+
166+
vm.stopPrank();
167+
}
168+
169+
function test_Unpause_NotOwner() public {
170+
address notOwner = makeAddr("notOwner");
171+
vm.startPrank(notOwner);
172+
173+
vm.expectRevert("Ownable: caller is not the owner");
174+
pauseController.unpause(mockPausable);
175+
176+
vm.stopPrank();
177+
}
178+
}

0 commit comments

Comments
 (0)