-
Notifications
You must be signed in to change notification settings - Fork 35
feat: add pause controller #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
Thegaram marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (component.paused()) { | ||
| revert ErrorComponentAlreadyPaused(); | ||
| } | ||
|
|
||
| if (lastPauseTime[address(component)] + pauseCooldownPeriod >= block.timestamp) { | ||
Thegaram marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Thegaram marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| revert ErrorCooldownPeriodNotPassed(); | ||
| } | ||
|
|
||
| lastPauseTime[address(component)] = block.timestamp; | ||
|
|
||
| ScrollOwner(payable(SCROLL_OWNER)).execute( | ||
| address(component), | ||
| 0, | ||
| abi.encodeWithSelector(IPausable.setPause.selector, true), | ||
| PAUSE_CONTROLLER_ROLE | ||
Thegaram marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.