diff --git a/staking/src/RWAStaking.sol b/staking/src/RWAStaking.sol index 55ba3e3..aed1c6f 100644 --- a/staking/src/RWAStaking.sol +++ b/staking/src/RWAStaking.sol @@ -47,6 +47,8 @@ contract RWAStaking is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar mapping(IERC20 stablecoin => bool allowed) allowedStablecoins; /// @dev Timestamp of when pre-staking ends, when the admin withdraws all stablecoins uint256 endTime; + /// @dev True if the RWAStaking contract is paused for deposits, false otherwise + bool paused; } // keccak256(abi.encode(uint256(keccak256("plume.storage.RWAStaking")) - 1)) & ~bytes32(uint256(0xff)) @@ -92,8 +94,23 @@ contract RWAStaking is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar */ event Staked(address indexed user, IERC20 indexed stablecoin, uint256 amount); + /// @notice Emitted when the RWAStaking contract is paused for deposits + event Paused(); + + /// @notice Emitted when the RWAStaking contract is unpaused for deposits + event Unpaused(); + // Errors + /// @notice Indicates a failure because the contract is paused for deposits + error DepositPaused(); + + /// @notice Indicates a failure because the contract is already paused for deposits + error AlreadyPaused(); + + /// @notice Indicates a failure because the contract is not paused for deposits + error NotPaused(); + /// @notice Indicates a failure because the pre-staking period has ended error StakingEnded(); @@ -193,6 +210,34 @@ contract RWAStaking is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar $.endTime = block.timestamp; } + /** + * @notice Pause the RWAStaking contract for deposits + * @dev Only the admin can pause the RWAStaking contract for deposits + */ + function pause() external onlyRole(ADMIN_ROLE) { + RWAStakingStorage storage $ = _getRWAStakingStorage(); + if ($.paused) { + revert AlreadyPaused(); + } + $.paused = true; + emit Paused(); + } + + // Errors + + /** + * @notice Unpause the RWAStaking contract for deposits + * @dev Only the admin can unpause the RWAStaking contract for deposits + */ + function unpause() external onlyRole(ADMIN_ROLE) { + RWAStakingStorage storage $ = _getRWAStakingStorage(); + if (!$.paused) { + revert NotPaused(); + } + $.paused = false; + emit Unpaused(); + } + // User Functions /** @@ -205,6 +250,9 @@ contract RWAStaking is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar if ($.endTime != 0) { revert StakingEnded(); } + if ($.paused) { + revert DepositPaused(); + } if (!$.allowedStablecoins[stablecoin]) { revert NotAllowedStablecoin(stablecoin); } @@ -303,4 +351,9 @@ contract RWAStaking is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar return _getRWAStakingStorage().endTime; } + /// @notice Returns true if the RWAStaking contract is pauseWhether the RWAStaking contract is paused for deposits + function isPaused() external view returns (bool) { + return _getRWAStakingStorage().paused; + } + } diff --git a/staking/src/ReserveStaking.sol b/staking/src/ReserveStaking.sol index adbd612..8ca43ec 100644 --- a/staking/src/ReserveStaking.sol +++ b/staking/src/ReserveStaking.sol @@ -57,6 +57,8 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy mapping(address user => UserState userState) userStates; /// @dev Timestamp of when pre-staking ends, when the admin withdraws all SBTC and STONE uint256 endTime; + /// @dev True if the ReserveStaking contract is paused for deposits, false otherwise + bool paused; } // keccak256(abi.encode(uint256(keccak256("plume.storage.ReserveStaking")) - 1)) & ~bytes32(uint256(0xff)) @@ -102,16 +104,31 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy */ event Staked(address indexed user, uint256 sbtcAmount, uint256 stoneAmount); + /// @notice Emitted when the ReserveStaking contract is paused for deposits + event Paused(); + + /// @notice Emitted when the ReserveStaking contract is unpaused for deposits + event Unpaused(); + // Errors + /// @notice Indicates a failure because the contract is paused for deposits + error DepositPaused(); + + /// @notice Indicates a failure because the contract is already paused for deposits + error AlreadyPaused(); + + /// @notice Indicates a failure because the contract is not paused for deposits + error NotPaused(); + /// @notice Indicates a failure because the pre-staking period has ended error StakingEnded(); /** * @notice Indicates a failure because the user does not have enough SBTC or STONE staked * @param user Address of the user who does not have enough SBTC or STONE staked - * @param sbtcAmount Amount of SBTC that the user does not have enough of - * @param stoneAmount Amount of STONE that the user does not have enough of + * @param sbtcAmount Amount of SBTC that the user wants to withdraw + * @param stoneAmount Amount of STONE that the user wants to withdraw * @param sbtcAmountStaked Amount of SBTC that the user has staked * @param stoneAmountStaked Amount of STONE that the user has staked */ @@ -144,8 +161,9 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy _grantRole(ADMIN_ROLE, owner); _grantRole(UPGRADER_ROLE, owner); - _getReserveStakingStorage().sbtc = sbtc; - _getReserveStakingStorage().stone = stone; + ReserveStakingStorage storage $ = _getReserveStakingStorage(); + $.sbtc = sbtc; + $.stone = stone; } // Override Functions @@ -180,6 +198,32 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy emit AdminWithdrawn(msg.sender, sbtcAmount, stoneAmount); } + /** + * @notice Pause the ReserveStaking contract for deposits + * @dev Only the admin can pause the ReserveStaking contract for deposits + */ + function pause() external onlyRole(ADMIN_ROLE) { + ReserveStakingStorage storage $ = _getReserveStakingStorage(); + if ($.paused) { + revert AlreadyPaused(); + } + $.paused = true; + emit Paused(); + } + + /** + * @notice Unpause the ReserveStaking contract for deposits + * @dev Only the admin can unpause the ReserveStaking contract for deposits + */ + function unpause() external onlyRole(ADMIN_ROLE) { + ReserveStakingStorage storage $ = _getReserveStakingStorage(); + if (!$.paused) { + revert NotPaused(); + } + $.paused = false; + emit Unpaused(); + } + // User Functions /** @@ -192,6 +236,9 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy if ($.endTime != 0) { revert StakingEnded(); } + if ($.paused) { + revert DepositPaused(); + } uint256 timestamp = block.timestamp; UserState storage userState = $.userStates[msg.sender]; @@ -271,7 +318,8 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy stone.safeTransfer(msg.sender, stoneAmount); uint256 newBalance = stone.balanceOf(address(this)); actualStoneAmount = previousBalance - newBalance; - userState.stoneAmountSeconds -= userState.stoneAmountSeconds * actualStoneAmount / userState.stoneAmountStaked; + userState.stoneAmountSeconds -= + userState.stoneAmountSeconds * actualStoneAmount / userState.stoneAmountStaked; userState.stoneAmountStaked -= actualStoneAmount; userState.stoneLastUpdate = timestamp; $.stoneTotalAmountStaked -= actualStoneAmount; @@ -330,4 +378,9 @@ contract ReserveStaking is AccessControlUpgradeable, UUPSUpgradeable, Reentrancy return _getReserveStakingStorage().endTime; } + /// @notice Returns true if the ReserveStaking contract is paused for deposits, otherwise false + function isPaused() external view returns (bool) { + return _getReserveStakingStorage().paused; + } + } diff --git a/staking/test/RWAStaking.t.sol b/staking/test/RWAStaking.t.sol index b926082..15c322a 100644 --- a/staking/test/RWAStaking.t.sol +++ b/staking/test/RWAStaking.t.sol @@ -115,6 +115,8 @@ contract RWAStakingTest is Test { rwaStaking.stake(100 ether, usdc); vm.expectRevert(abi.encodeWithSelector(RWAStaking.StakingEnded.selector)); rwaStaking.adminWithdraw(); + vm.expectRevert(abi.encodeWithSelector(RWAStaking.StakingEnded.selector)); + rwaStaking.withdraw(100 ether, usdc); vm.stopPrank(); } @@ -216,6 +218,80 @@ contract RWAStakingTest is Test { assertEq(rwaStaking.getEndTime(), startTime + timeskipAmount); } + function test_pauseFail() public { + vm.startPrank(user1); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, user1, rwaStaking.ADMIN_ROLE() + ) + ); + rwaStaking.pause(); + + vm.stopPrank(); + + vm.startPrank(owner); + + vm.expectEmit(false, false, false, true, address(rwaStaking)); + emit RWAStaking.Paused(); + rwaStaking.pause(); + + vm.expectRevert(abi.encodeWithSelector(RWAStaking.AlreadyPaused.selector)); + rwaStaking.pause(); + + vm.stopPrank(); + } + + function test_pause() public { + vm.startPrank(owner); + + assertEq(rwaStaking.isPaused(), false); + + vm.expectEmit(false, false, false, true, address(rwaStaking)); + emit RWAStaking.Paused(); + rwaStaking.pause(); + assertEq(rwaStaking.isPaused(), true); + + vm.expectRevert(abi.encodeWithSelector(RWAStaking.DepositPaused.selector)); + rwaStaking.stake(100 ether, usdc); + + vm.stopPrank(); + } + + function test_unpauseFail() public { + vm.startPrank(user1); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, user1, rwaStaking.ADMIN_ROLE() + ) + ); + rwaStaking.unpause(); + + vm.stopPrank(); + + vm.startPrank(owner); + + vm.expectRevert(abi.encodeWithSelector(RWAStaking.NotPaused.selector)); + rwaStaking.unpause(); + + vm.stopPrank(); + } + + function test_unpause() public { + vm.startPrank(owner); + + rwaStaking.pause(); + assertEq(rwaStaking.isPaused(), true); + + vm.expectEmit(false, false, false, true, address(rwaStaking)); + emit RWAStaking.Unpaused(); + rwaStaking.unpause(); + assertEq(rwaStaking.isPaused(), false); + + vm.stopPrank(); + } + function test_withdrawFail() public { uint256 stakeAmount = 100 ether; diff --git a/staking/test/ReserveStaking.t.sol b/staking/test/ReserveStaking.t.sol index 89898d7..ca3f9de 100644 --- a/staking/test/ReserveStaking.t.sol +++ b/staking/test/ReserveStaking.t.sol @@ -220,6 +220,80 @@ contract ReserveStakingTest is Test { assertEq(staking.getEndTime(), startTime + timeskipAmount); } + function test_pauseFail() public { + vm.startPrank(user1); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, user1, staking.ADMIN_ROLE() + ) + ); + staking.pause(); + + vm.stopPrank(); + + vm.startPrank(owner); + + vm.expectEmit(false, false, false, true, address(staking)); + emit ReserveStaking.Paused(); + staking.pause(); + + vm.expectRevert(abi.encodeWithSelector(ReserveStaking.AlreadyPaused.selector)); + staking.pause(); + + vm.stopPrank(); + } + + function test_pause() public { + vm.startPrank(owner); + + assertEq(staking.isPaused(), false); + + vm.expectEmit(false, false, false, true, address(staking)); + emit ReserveStaking.Paused(); + staking.pause(); + assertEq(staking.isPaused(), true); + + vm.expectRevert(abi.encodeWithSelector(ReserveStaking.DepositPaused.selector)); + staking.stake(100 ether, 0); + + vm.stopPrank(); + } + + function test_unpauseFail() public { + vm.startPrank(user1); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, user1, staking.ADMIN_ROLE() + ) + ); + staking.unpause(); + + vm.stopPrank(); + + vm.startPrank(owner); + + vm.expectRevert(abi.encodeWithSelector(ReserveStaking.NotPaused.selector)); + staking.unpause(); + + vm.stopPrank(); + } + + function test_unpause() public { + vm.startPrank(owner); + + staking.pause(); + assertEq(staking.isPaused(), true); + + vm.expectEmit(false, false, false, true, address(staking)); + emit ReserveStaking.Unpaused(); + staking.unpause(); + assertEq(staking.isPaused(), false); + + vm.stopPrank(); + } + function test_withdrawFail() public { uint256 sbtcAmount = 100 ether; uint256 stoneAmount = 50 ether;