From 199442629604ad43254b6cbb2fa50bf21f2fa409 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:19:31 +0200 Subject: [PATCH] feat: granular and global pausable contract --- foundry/src/FoxStaking.sol | 61 ++++++++++++++++++++++++++++++++--- foundry/src/IFoxStaking.sol | 25 +++++++++++++- foundry/test/FoxStaking.t.sol | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 5 deletions(-) diff --git a/foundry/src/FoxStaking.sol b/foundry/src/FoxStaking.sol index 98e2ba9..931b734 100644 --- a/foundry/src/FoxStaking.sol +++ b/foundry/src/FoxStaking.sol @@ -3,16 +3,22 @@ pragma solidity ^0.8.25; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {IFoxStaking, StakingInfo} from "./IFoxStaking.sol"; import {console} from "forge-std/Script.sol"; contract FoxStaking is IFoxStaking, - Ownable(msg.sender) // Deployer is the owner + Ownable(msg.sender), // Deployer is the owner + Pausable { IERC20 public foxToken; mapping(address => StakingInfo) public stakingInfo; + bool public stakingPaused = false; + bool public withdrawalsPaused = false; + bool public unstakingPaused = false; + uint256 public cooldownPeriod = 28 days; event UpdateCooldownPeriod(uint256 newCooldownPeriod); @@ -26,12 +32,59 @@ contract FoxStaking is foxToken = IERC20(foxTokenAddress); } + function pauseStaking() external onlyOwner { + stakingPaused = true; + } + + function unpauseStaking() external onlyOwner { + stakingPaused = false; + } + + function pauseWithdrawals() external onlyOwner { + withdrawalsPaused = true; + } + + function unpauseWithdrawals() external onlyOwner { + withdrawalsPaused = false; + } + + function pauseUnstaking() external onlyOwner { + unstakingPaused = true; + } + + function unpauseUnstaking() external onlyOwner { + unstakingPaused = false; + } + + function pause() public onlyOwner { + _pause(); + } + + function unpause() public onlyOwner { + _unpause(); + } + + modifier whenStakingUnpaused() { + require(!stakingPaused, "Staking is paused"); + _; + } + + modifier whenUnstakingUnpaused() { + require(!unstakingPaused, "Unstaking is paused"); + _; + } + + modifier whenWithdrawalsUnpaused() { + require(!withdrawalsPaused, "Withdrawals are paused"); + _; + } + function setCooldownPeriod(uint256 _cooldownPeriod) external onlyOwner { cooldownPeriod = _cooldownPeriod; emit UpdateCooldownPeriod(_cooldownPeriod); } - function stake(uint256 amount, string memory runeAddress) external { + function stake(uint256 amount, string memory runeAddress) external whenNotPaused whenStakingUnpaused { require(bytes(runeAddress).length > 0, "Rune address cannot be empty"); require(amount > 0, "FOX amount to stake must be greater than 0"); // Transfer fundus from msg.sender to contract assuming allowance has been set - here goes nothing @@ -46,7 +99,7 @@ contract FoxStaking is emit Stake(msg.sender, amount, runeAddress); } - function unstake(uint256 amount) external { + function unstake(uint256 amount) external whenNotPaused whenUnstakingUnpaused { require(amount > 0, "Cannot unstake 0"); StakingInfo storage info = stakingInfo[msg.sender]; @@ -66,7 +119,7 @@ contract FoxStaking is emit Unstake(msg.sender, amount); } - function withdraw() external { + function withdraw() external whenNotPaused whenWithdrawalsUnpaused { StakingInfo storage info = stakingInfo[msg.sender]; require(info.unstakingBalance > 0, "Cannot withdraw 0"); diff --git a/foundry/src/IFoxStaking.sol b/foundry/src/IFoxStaking.sol index 699417f..8a4b09c 100644 --- a/foundry/src/IFoxStaking.sol +++ b/foundry/src/IFoxStaking.sol @@ -9,9 +9,32 @@ struct StakingInfo { } -/// @title WIP high-level interface for FOX token staking contract /// @notice This interface outlines the functions for staking FOX tokens, managing RUNE addresses for rewards, and claiming 'em. interface IFoxStaking { + /// @notice Pauses deposits + function pauseStaking() external; + + /// @notice Unpauses deposits + function unpauseStaking() external; + + /// @notice Pauses withdrawals + function pauseWithdrawals() external; + + /// @notice Unpauses withdrawals + function unpauseWithdrawals() external; + + /// @notice Pauses unstaking + function pauseUnstaking() external; + + /// @notice Unpauses unstaking + function unpauseUnstaking() external; + + // @notice Sets contract-level paused state + function pause() external; + + /// @notice Sets contract-level unpaused state + function unpause() external; + /// @notice Allows a user to stake a specified amount of FOX tokens and assign a RUNE address for rewards - which can be changed later on. /// This has to be initiated by the user itself i.e msg.sender only, cannot be called by an address for another /// @param amount The amount of FOX tokens to be staked. diff --git a/foundry/test/FoxStaking.t.sol b/foundry/test/FoxStaking.t.sol index 5ea04c0..b66cd36 100644 --- a/foundry/test/FoxStaking.t.sol +++ b/foundry/test/FoxStaking.t.sol @@ -6,6 +6,7 @@ import "../src/FoxStaking.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; contract MockFOXToken is ERC20 { constructor() ERC20("Mock FOX Token", "FOX") { @@ -58,6 +59,26 @@ contract FOXStakingTestStaking is Test { foxStaking = new FoxStaking(address(foxToken)); } + function testCannotStakeWhenStakingPaused() public { + foxStaking.pauseStaking(); + + address user = address(0xBABE); + vm.startPrank(user); + vm.expectRevert("Staking is paused"); + foxStaking.stake(1e18, "runeAddress"); + vm.stopPrank(); + } + + function testCannotStakeWhenContractPaused() public { + foxStaking.pause(); + + address user = address(0xBABE); + vm.startPrank(user); + vm.expectRevert(abi.encodeWithSelector(Pausable.EnforcedPause.selector)); + foxStaking.stake(1e18, "runeAddress"); + vm.stopPrank(); + } + function testStaking() public { address[] memory users = new address[](3); users[0] = address(0xBABE); @@ -114,6 +135,27 @@ contract FOXStakingTestUnstake is Test { vm.stopPrank(); } + function testCannotUnstakeWhenUnstakingPaused() public { + foxStaking.pauseUnstaking(); + + vm.startPrank(user); + vm.expectRevert("Unstaking is paused"); + foxStaking.unstake(amount); + vm.stopPrank(); + } + + function testCannotUnstakeWhenContractPaused() public { + foxStaking.pause(); + + vm.startPrank(user); + vm.expectRevert(abi.encodeWithSelector( + Pausable.EnforcedPause.selector + ) + ); + foxStaking.unstake(amount); + vm.stopPrank(); + } + function testunstake_cannotRequestZero() public { vm.startPrank(user); @@ -281,6 +323,24 @@ contract FOXStakingTestWithdraw is Test { vm.stopPrank(); } + function testCannotWithdrawWhenWithdrawalsPaused() public { + foxStaking.pauseWithdrawals(); + + vm.startPrank(user); + vm.expectRevert("Withdrawals are paused"); // Make sure this matches the actual revert message used in your contract + foxStaking.withdraw(); + vm.stopPrank(); + } + + function testCannotWithdrawWhenContractPaused() public { + foxStaking.pause(); + + vm.startPrank(user); + vm.expectRevert(abi.encodeWithSelector(Pausable.EnforcedPause.selector)); + foxStaking.withdraw(); + vm.stopPrank(); + } + function testWithdraw_cannotWithdrawBeforeCooldown() public { vm.startPrank(user);