diff --git a/foundry/src/StakingV1.sol b/foundry/src/StakingV1.sol index 42e0dc2..56b4965 100644 --- a/foundry/src/StakingV1.sol +++ b/foundry/src/StakingV1.sol @@ -29,7 +29,7 @@ contract StakingV1 is uint256 public totalStaked; uint256 public totalCoolingDown; - uint256 public constant REWARD_RATE = 1_000_000_000; + uint256 public constant REWARD_RATE = 1e27; uint256 public constant WAD = 1e18; uint256 public lastUpdateTimestamp; uint256 public rewardPerTokenStored; @@ -51,6 +51,9 @@ contract StakingV1 is string indexed oldRuneAddress, string indexed newRuneAddress ); + event StakingPausedChanged(bool isPaused); + event WithdrawalsPausedChanged(bool isPaused); + event UnstakingPausedChanged(bool isPaused); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -61,6 +64,7 @@ contract StakingV1 is __Ownable_init(msg.sender); __UUPSUpgradeable_init(); __Pausable_init(); + __ReentrancyGuard_init(); stakingToken = IERC20(stakingTokenAddress); cooldownPeriod = 28 days; lastUpdateTimestamp = block.timestamp; @@ -75,33 +79,42 @@ contract StakingV1 is } /// @notice Pauses deposits - function pauseStaking() external onlyOwner { + function pauseStaking() external onlyOwner whenStakingNotPaused { stakingPaused = true; + emit StakingPausedChanged(true); } /// @notice Unpauses deposits function unpauseStaking() external onlyOwner { + require(stakingPaused, "Staking is not paused"); stakingPaused = false; + emit StakingPausedChanged(false); } /// @notice Pauses withdrawals - function pauseWithdrawals() external onlyOwner { + function pauseWithdrawals() external onlyOwner whenWithdrawalsNotPaused { withdrawalsPaused = true; + emit WithdrawalsPausedChanged(true); } /// @notice Unpauses withdrawals function unpauseWithdrawals() external onlyOwner { + require(withdrawalsPaused, "Withdrawals are not paused"); withdrawalsPaused = false; + emit WithdrawalsPausedChanged(false); } /// @notice Pauses unstaking - function pauseUnstaking() external onlyOwner { + function pauseUnstaking() external onlyOwner whenUnstakingNotPaused { unstakingPaused = true; + emit UnstakingPausedChanged(true); } /// @notice Unpauses unstaking function unpauseUnstaking() external onlyOwner { + require(unstakingPaused, "Unstaking is not paused"); unstakingPaused = false; + emit UnstakingPausedChanged(false); } /// @notice Sets contract-level paused state @@ -171,14 +184,14 @@ contract StakingV1 is "Rune address must be 43 characters" ); require(amount > 0, "amount to stake must be greater than 0"); - updateReward(msg.sender); - stakingToken.safeTransferFrom(msg.sender, address(this), amount); + _updateReward(msg.sender); StakingInfo storage info = stakingInfo[msg.sender]; info.stakingBalance += amount; info.runeAddress = runeAddress; totalStaked += amount; + stakingToken.safeTransferFrom(msg.sender, address(this), amount); emit Stake(msg.sender, amount, runeAddress); } @@ -196,7 +209,7 @@ contract StakingV1 is amount <= info.stakingBalance, "Unstake amount exceeds staked balance" ); - updateReward(msg.sender); + _updateReward(msg.sender); // Set staking / unstaking amounts info.stakingBalance -= amount; @@ -330,7 +343,7 @@ contract StakingV1 is /// @notice Updates all variables when changes to staking amounts are made. /// @param account The address of the account to update. - function updateReward(address account) internal { + function _updateReward(address account) internal { rewardPerTokenStored = rewardPerToken(); lastUpdateTimestamp = block.timestamp; StakingInfo storage info = stakingInfo[account]; diff --git a/foundry/test/StakingTestAcocunting.t.sol b/foundry/test/StakingTestAcocunting.t.sol index a537bfe..4b362eb 100644 --- a/foundry/test/StakingTestAcocunting.t.sol +++ b/foundry/test/StakingTestAcocunting.t.sol @@ -224,7 +224,7 @@ contract FOXStakingTestStaking is Test { // we should confirm that they still recieve expected rewards when. // create mega whale - uint256 megaWhaleAmount = 900_000_000 * 1e18; // 900 million FOX tokens with 18 decimals + uint256 megaWhaleAmount = 9_000_000_000 * 1e18; // 9 billion FOX tokens with 18 decimals foxToken.makeItRain(userOne, megaWhaleAmount); vm.prank(userOne); @@ -246,7 +246,7 @@ contract FOXStakingTestStaking is Test { function testRewardAmountsForOverflow() public { // create mega whale - uint256 megaWhaleAmount = 900_000_000 * 1e18; // 900 million FOX tokens with 18 decimals + uint256 megaWhaleAmount = 9_000_000_000 * 1e18; // 9 billion FOX tokens with 18 decimals foxToken.makeItRain(userOne, megaWhaleAmount); vm.prank(userOne); @@ -254,8 +254,8 @@ contract FOXStakingTestStaking is Test { vm.prank(userOne); foxStaking.stake(megaWhaleAmount, runeAddressOne); - // advance time by 15 years - vm.warp(block.timestamp + 365 * 15 days); + // advance time by 200 years + vm.warp(block.timestamp + 365 days * 200); // ensure this does not overflow foxStaking.earned(userOne); diff --git a/foundry/test/StakingTestStaking.t.sol b/foundry/test/StakingTestStaking.t.sol index f8f0dfa..fea68ab 100644 --- a/foundry/test/StakingTestStaking.t.sol +++ b/foundry/test/StakingTestStaking.t.sol @@ -24,6 +24,9 @@ contract FOXStakingTestStaking is Test { } function testCannotStakeWhenStakingPaused() public { + // confirm we emit the correct event + vm.expectEmit(); + emit StakingV1.StakingPausedChanged(true); foxStaking.pauseStaking(); address user = address(0xBABE); @@ -60,6 +63,8 @@ contract FOXStakingTestStaking is Test { foxStaking.stake(amount, runeAddress); vm.stopPrank(); + vm.expectEmit(); + emit StakingV1.StakingPausedChanged(false); foxStaking.unpauseStaking(); vm.startPrank(user); @@ -243,4 +248,21 @@ contract FOXStakingTestStaking is Test { assertEq(runeAddress, runeAddresses[i]); } } + + function testStaking_cannotPauseAlreadyPaused() public { + vm.expectEmit(); + emit StakingV1.StakingPausedChanged(true); + foxStaking.pauseStaking(); + + vm.expectRevert("Staking is paused"); + foxStaking.pauseStaking(); + + // unpause and try to unpause again + vm.expectEmit(); + emit StakingV1.StakingPausedChanged(false); + foxStaking.unpauseStaking(); + + vm.expectRevert("Staking is not paused"); + foxStaking.unpauseStaking(); + } } diff --git a/foundry/test/StakingTestUnstake.t.sol b/foundry/test/StakingTestUnstake.t.sol index d7f1c84..7aeca82 100644 --- a/foundry/test/StakingTestUnstake.t.sol +++ b/foundry/test/StakingTestUnstake.t.sol @@ -46,6 +46,8 @@ contract FOXStakingTestUnstake is Test { } function testCannotUnstakeWhenUnstakingPaused() public { + vm.expectEmit(); + emit StakingV1.UnstakingPausedChanged(true); foxStaking.pauseUnstaking(); vm.startPrank(user); @@ -76,6 +78,8 @@ contract FOXStakingTestUnstake is Test { foxStaking.unstake(amount); vm.stopPrank(); + vm.expectEmit(); + emit StakingV1.UnstakingPausedChanged(false); foxStaking.unpauseUnstaking(); vm.startPrank(user); @@ -873,4 +877,21 @@ contract FOXStakingTestUnstake is Test { balAfter = foxToken.balanceOf(user2); vm.assertEq(balAfter - balBefore, 51); } + + function testUnstake_cannotPauseAlreadyPaused() public { + vm.expectEmit(); + emit StakingV1.UnstakingPausedChanged(true); + foxStaking.pauseUnstaking(); + + vm.expectRevert("Unstaking is paused"); + foxStaking.pauseUnstaking(); + + // unpause and try to unpause again + vm.expectEmit(); + emit StakingV1.UnstakingPausedChanged(false); + foxStaking.unpauseUnstaking(); + + vm.expectRevert("Unstaking is not paused"); + foxStaking.unpauseUnstaking(); + } } diff --git a/foundry/test/StakingTestWithdraw.t.sol b/foundry/test/StakingTestWithdraw.t.sol index 6b81e30..4f824a2 100644 --- a/foundry/test/StakingTestWithdraw.t.sol +++ b/foundry/test/StakingTestWithdraw.t.sol @@ -40,6 +40,8 @@ contract FOXStakingTestWithdraw is Test { } function testCannotWithdrawWhenWithdrawalsPaused() public { + vm.expectEmit(); + emit StakingV1.WithdrawalsPausedChanged(true); foxStaking.pauseWithdrawals(); vm.startPrank(user); @@ -86,6 +88,8 @@ contract FOXStakingTestWithdraw is Test { foxStaking.withdraw(); vm.stopPrank(); + vm.expectEmit(); + emit StakingV1.WithdrawalsPausedChanged(false); foxStaking.unpauseWithdrawals(); vm.startPrank(user); @@ -170,4 +174,21 @@ contract FOXStakingTestWithdraw is Test { vm.stopPrank(); } + + function testWithdraw_cannotPauseAlreadyPaused() public { + vm.expectEmit(); + emit StakingV1.WithdrawalsPausedChanged(true); + foxStaking.pauseWithdrawals(); + + vm.expectRevert("Withdrawals are paused"); + foxStaking.pauseWithdrawals(); + + // unpause and try to unpause again + vm.expectEmit(); + emit StakingV1.WithdrawalsPausedChanged(false); + foxStaking.unpauseWithdrawals(); + + vm.expectRevert("Withdrawals are not paused"); + foxStaking.unpauseWithdrawals(); + } } diff --git a/foundry/test/utils/MockFOXToken.sol b/foundry/test/utils/MockFOXToken.sol index d11e8a4..b24d8d1 100644 --- a/foundry/test/utils/MockFOXToken.sol +++ b/foundry/test/utils/MockFOXToken.sol @@ -6,7 +6,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockFOXToken is ERC20 { constructor() ERC20("Mock FOX Token", "FOX") { - _mint(address(this), 1e27); // 1 billion FOX with 18 decimals + _mint(address(this), 10e27); // 10 billion FOX with 18 decimals - more than mainnet, for testing. } function makeItRain(address to, uint256 amount) public {