diff --git a/foundry/src/FoxStakingV1.sol b/foundry/src/FoxStakingV1.sol index 5af05fd..bc55207 100644 --- a/foundry/src/FoxStakingV1.sol +++ b/foundry/src/FoxStakingV1.sol @@ -7,6 +7,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {StakingInfo} from "./StakingInfo.sol"; import {UnstakingRequest} from "./UnstakingRequest.sol"; @@ -14,7 +15,8 @@ contract FoxStakingV1 is Initializable, PausableUpgradeable, UUPSUpgradeable, - OwnableUpgradeable + OwnableUpgradeable, + ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; IERC20 public foxToken; @@ -24,6 +26,14 @@ contract FoxStakingV1 is bool public unstakingPaused; uint256 public cooldownPeriod; + uint256 public totalStaked; + uint256 public totalCoolingDown; + + uint256 public constant REWARD_RATE = 1_000_000_000; + uint256 public constant WAD = 1e18; + uint256 public lastUpdateTimestamp; + uint256 public rewardPerTokenStored; + event UpdateCooldownPeriod(uint256 newCooldownPeriod); event Stake( address indexed account, @@ -52,10 +62,8 @@ contract FoxStakingV1 is __UUPSUpgradeable_init(); __Pausable_init(); foxToken = IERC20(foxTokenAddress); - stakingPaused = false; - withdrawalsPaused = false; - unstakingPaused = false; cooldownPeriod = 28 days; + lastUpdateTimestamp = block.timestamp; } function _authorizeUpgrade( @@ -121,11 +129,35 @@ contract FoxStakingV1 is _; } + /// @notice Sets the cooldown period for unstaking requests. + /// @param newCooldownPeriod The new cooldown period to be set. function setCooldownPeriod(uint256 newCooldownPeriod) external onlyOwner { cooldownPeriod = newCooldownPeriod; emit UpdateCooldownPeriod(newCooldownPeriod); } + /// @notice Returns the current amount of reward allocated per staked token. + function rewardPerToken() public view returns (uint256) { + if (totalStaked == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored + + (((block.timestamp - lastUpdateTimestamp) * REWARD_RATE * WAD) / + totalStaked); + } + + /// @notice Returns the total reward earnings associated with a given address for its entire lifetime of staking. + /// @param account The address we're getting the earned rewards for. + function earned(address account) public view returns (uint256) { + StakingInfo memory info = stakingInfo[account]; + return + (info.stakingBalance * + (rewardPerToken() - info.rewardPerTokenStored)) / + WAD + + info.earnedRewards; + } + /// @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. @@ -133,17 +165,19 @@ contract FoxStakingV1 is function stake( uint256 amount, string memory runeAddress - ) external whenNotPaused whenStakingNotPaused { + ) external whenNotPaused whenStakingNotPaused nonReentrant { require( bytes(runeAddress).length == 43, "Rune address must be 43 characters" ); require(amount > 0, "FOX amount to stake must be greater than 0"); + updateReward(msg.sender); foxToken.safeTransferFrom(msg.sender, address(this), amount); StakingInfo storage info = stakingInfo[msg.sender]; info.stakingBalance += amount; info.runeAddress = runeAddress; + totalStaked += amount; emit Stake(msg.sender, amount, runeAddress); } @@ -153,7 +187,7 @@ contract FoxStakingV1 is /// @param amount The amount of FOX tokens to be unstaked. function unstake( uint256 amount - ) external whenNotPaused whenUnstakingNotPaused { + ) external whenNotPaused whenUnstakingNotPaused nonReentrant { require(amount > 0, "Cannot unstake 0"); StakingInfo storage info = stakingInfo[msg.sender]; @@ -162,10 +196,13 @@ contract FoxStakingV1 is amount <= info.stakingBalance, "Unstake amount exceeds staked balance" ); + updateReward(msg.sender); // Set staking / unstaking amounts info.stakingBalance -= amount; info.unstakingBalance += amount; + totalStaked -= amount; + totalCoolingDown += amount; UnstakingRequest memory unstakingRequest = UnstakingRequest({ unstakingBalance: amount, @@ -181,7 +218,7 @@ contract FoxStakingV1 is /// @param index The index of the claim to withdraw function withdraw( uint256 index - ) public whenNotPaused whenWithdrawalsNotPaused { + ) public whenNotPaused whenWithdrawalsNotPaused nonReentrant { StakingInfo storage info = stakingInfo[msg.sender]; require( info.unstakingRequests.length > 0, @@ -211,6 +248,7 @@ contract FoxStakingV1 is delete info.unstakingRequests; } info.unstakingBalance -= unstakingRequest.unstakingBalance; + totalCoolingDown -= unstakingRequest.unstakingBalance; foxToken.safeTransfer(msg.sender, unstakingRequest.unstakingBalance); emit Withdraw(msg.sender, unstakingRequest.unstakingBalance); } @@ -286,4 +324,14 @@ contract FoxStakingV1 is ) external view returns (uint256) { return stakingInfo[account].unstakingRequests.length; } + + /// @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 { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTimestamp = block.timestamp; + StakingInfo storage info = stakingInfo[account]; + info.earnedRewards = earned(account); + info.rewardPerTokenStored = rewardPerTokenStored; + } } diff --git a/foundry/src/StakingInfo.sol b/foundry/src/StakingInfo.sol index 962db4f..6184a4c 100644 --- a/foundry/src/StakingInfo.sol +++ b/foundry/src/StakingInfo.sol @@ -3,9 +3,18 @@ pragma solidity ^0.8.25; import {UnstakingRequest} from "./UnstakingRequest.sol"; +/// @notice Struct to store staking information for a given user. +/// @param stakingBalance The total amount of tokens staked by the user. +/// @param unstakingBalance The total amount of tokens pending unstaking. +/// @param earnedRewards The rewards earned by the user since the last epoch. +/// @param rewardPerTokenStored The user-level reward per token stored. +/// @param runeAddress The users configured RUNE address. +/// @param unstakingRequests The list of pending unstaking requests for the user. struct StakingInfo { uint256 stakingBalance; uint256 unstakingBalance; + uint256 earnedRewards; + uint256 rewardPerTokenStored; string runeAddress; UnstakingRequest[] unstakingRequests; } diff --git a/foundry/test/FOXStakingTestAcocunting.t.sol b/foundry/test/FOXStakingTestAcocunting.t.sol new file mode 100644 index 0000000..bcd97f4 --- /dev/null +++ b/foundry/test/FOXStakingTestAcocunting.t.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; +import {FoxStakingV1} from "../src/FoxStakingV1.sol"; +import {StakingInfo} from "../src/StakingInfo.sol"; +import {MockFOXToken} from "./utils/MockFOXToken.sol"; +import {FoxStakingTestDeployer} from "./utils/FoxStakingTestDeployer.sol"; + +contract FOXStakingTestStaking is Test { + FoxStakingTestDeployer public deployer; + FoxStakingV1 public foxStaking; + MockFOXToken public foxToken; + address userOne = address(0xBEEF); + address userTwo = address(0xDEAD); + address userThree = address(0xDEADBEEF); + uint256 amount = 500 * 1e18; // 500 FOX tokens with 18 decimals + + string constant runeAddressOne = + "thor17gw75axcnr8747pkanye45pnrwk7p9c3cqncs1"; + string constant runeAddressTwo = + "thor17gw75axcnr8747pkanye45pnrwk7p9c3cqncs2"; + string constant runeAddressThree = + "thor17gw75axcnr8747pkanye45pnrwk7p9c3cqncs3"; + + function setUp() public { + foxToken = new MockFOXToken(); + deployer = new FoxStakingTestDeployer(); + address proxyAddress = deployer.deployV1( + address(this), + address(foxToken) + ); + foxStaking = FoxStakingV1(proxyAddress); + + // Free FOX tokens for users + foxToken.makeItRain(userOne, amount); + foxToken.makeItRain(userTwo, amount); + foxToken.makeItRain(userThree, amount); + + // approve staking contract to spend FOX tokens for all users + vm.prank(userOne); + foxToken.approve(address(foxStaking), amount); + vm.prank(userTwo); + foxToken.approve(address(foxStaking), amount); + vm.prank(userThree); + foxToken.approve(address(foxStaking), amount); + vm.stopPrank(); + } + + function testRewardAmountsWithNoUnstakes() public { + ( + uint256 stakingBalance, + , + uint256 earnedRewards, + uint256 rewardPerTokenPaid, + + ) = foxStaking.stakingInfo(userOne); + vm.assertEq(earnedRewards, 0, "User should have no earnedRewards"); + vm.assertEq( + rewardPerTokenPaid, + 0, + "User should have no rewardPerTokenPaid" + ); + vm.assertEq(stakingBalance, 0, "User should have no stakingBalance"); + vm.assertEq( + foxStaking.totalStaked(), + 0, + "Total staked should be 0 before staking" + ); + + // stake small amount to make sure rewards are calculated correctly and with enough precision + // for small stakers. + uint256 smallStakingAmount = 1 * 1e18; // 1 FOX token with 18 decimals + vm.prank(userOne); + foxStaking.stake(smallStakingAmount, runeAddressOne); + vm.prank(userTwo); + foxStaking.stake(smallStakingAmount, runeAddressTwo); + vm.prank(userThree); + foxStaking.stake(smallStakingAmount, runeAddressThree); + uint256 blockTimeOfStaked = block.timestamp; + + // right now all of their earned token amounts should be 0 + vm.assertEq( + foxStaking.earned(userOne), + 0, + "UserOne should have no earned rewards" + ); + vm.assertEq( + foxStaking.earned(userTwo), + 0, + "UserTwo should have no earned rewards" + ); + vm.assertEq( + foxStaking.earned(userThree), + 0, + "UserThree should have no earned rewards" + ); + + // total staked should be 3 tokens + vm.assertEq( + foxStaking.totalStaked(), + smallStakingAmount * 3, + "Total staked should be 3 tokens" + ); + + // advance time by 1 day + vm.warp(block.timestamp + 1 days); + + uint256 userOneEarned = foxStaking.earned(userOne); + uint256 userTwoEarned = foxStaking.earned(userTwo); + uint256 userThreeEarned = foxStaking.earned(userThree); + + // everyone should have equal rewards + userOneEarned = foxStaking.earned(userOne); + userTwoEarned = foxStaking.earned(userTwo); + userThreeEarned = foxStaking.earned(userThree); + + vm.assertEq( + userOneEarned, + userTwoEarned, + "UserOne and UserTwo should have equal rewards" + ); + vm.assertEq( + userTwoEarned, + userThreeEarned, + "UserTwo and UserThree should have equal rewards" + ); + + // advance time by 1 day + vm.warp(block.timestamp + 1 days); + + // ensure the values have changed since the last time we checked + vm.assertNotEq( + foxStaking.earned(userOne), + userOneEarned, + "UserOne should have earned more rewards" + ); + vm.assertNotEq( + foxStaking.earned(userTwo), + userTwoEarned, + "UserTwo should have earned more rewards" + ); + vm.assertNotEq( + foxStaking.earned(userThree), + userThreeEarned, + "UserThree should have earned more rewards" + ); + + // all users have now been staked for 2 days. Lets remove one of the users and see if the rewards are calculated correctly + userOneEarned = foxStaking.earned(userOne); + vm.prank(userOne); + foxStaking.unstake(smallStakingAmount); + // unstaking should not change the amount user1 has earned + vm.assertEq( + foxStaking.earned(userOne), + userOneEarned, + "UserOne should have the same earned rewards after unstaking" + ); + + vm.assertEq( + foxStaking.totalStaked(), + smallStakingAmount * 2, + "Total staked should be 2 tokens" + ); + + // advance time by 2 days + vm.warp(block.timestamp + 2 days); + + // userOne should not have earned anymore + vm.assertEq( + foxStaking.earned(userOne), + userOneEarned, + "UserOne should have the same earned rewards after unstaking" + ); + + // userTwo and userThree should have earned rewards for 4 days + vm.assertNotEq( + foxStaking.earned(userTwo), + userTwoEarned, + "UserTwo should have earned more rewards" + ); + vm.assertNotEq( + foxStaking.earned(userThree), + userThreeEarned, + "UserThree should have earned more rewards" + ); + + uint256 blockTimeStampAtEnd = block.timestamp; + vm.assertEq( + blockTimeOfStaked + 4 days, + blockTimeStampAtEnd, + "time of staked should be the same as time of unstaked" + ); + + // userOne should have earned rewards for 2 days, userTwo and userThree should have earned rewards for 4 days + // rewards are constant... so userOne accumulated 1/3 per day staked of the daily amount, and then the other two received 1/2 the rewards + // over the subsequent 2 days. + // userOne Total = 1/3 + 1/3 = 2/3 total + // userTwo Total = 1/3 + 1/3 + 1/2 + 1/2 = 5/3 total + // userThree Total = 1/3 + 1/3 + 1/2 + 1/2 = 5/3 total + + userOneEarned = foxStaking.earned(userOne); + userTwoEarned = foxStaking.earned(userTwo); + userThreeEarned = foxStaking.earned(userThree); + + vm.assertEq( + userTwoEarned, + userThreeEarned, + "UserTwo should have the same rewards as UserThree" + ); + + vm.assertEq( + (userOneEarned * 5) / 2, + userTwoEarned, + "UserOne should have 2/3 of the rewards of UserTwo" + ); + } + + function testRewardAmountsForPrecision() public { + // the worse scenario for precision is when we have + // a large amount of fox tokens staked, a small staker, and very little time passed + // 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 + foxToken.makeItRain(userOne, megaWhaleAmount); + + vm.prank(userOne); + foxToken.approve(address(foxStaking), megaWhaleAmount); + vm.prank(userOne); + foxStaking.stake(megaWhaleAmount, runeAddressOne); + + uint256 smallStakingAmount = 1 * 1e18; // 1 FOX token with 18 decimals + vm.prank(userTwo); + foxStaking.stake(smallStakingAmount, runeAddressTwo); + + // advance time by 1 sec + vm.warp(block.timestamp + 1); + uint256 userTwoEarned = foxStaking.earned(userTwo); + + // ensure that the small staker rewards aren't rounded down to 0 for a second of staking. + vm.assertNotEq(userTwoEarned, 0, "UserTwo should have earned rewards"); + } + + function testRewardAmountsForOverflow() public { + // create mega whale + uint256 megaWhaleAmount = 900_000_000 * 1e18; // 900 million FOX tokens with 18 decimals + foxToken.makeItRain(userOne, megaWhaleAmount); + + vm.prank(userOne); + foxToken.approve(address(foxStaking), megaWhaleAmount); + vm.prank(userOne); + foxStaking.stake(megaWhaleAmount, runeAddressOne); + + // advance time by 15 years + vm.warp(block.timestamp + 365 * 15 days); + + // ensure this does not overflow + foxStaking.earned(userOne); + } + + function testRewardAmountsWithUnstakes() public { + // store userOne's balance before staking + uint256 userOneBalance = foxToken.balanceOf(userOne); + + // stake 100 FOX tokens for each user + uint256 stakingAmount = 100 * 1e18; // 100 FOX token with 18 decimals + vm.prank(userOne); + foxStaking.stake(stakingAmount, runeAddressOne); + vm.prank(userTwo); + foxStaking.stake(stakingAmount, runeAddressTwo); + vm.prank(userThree); + foxStaking.stake(stakingAmount, runeAddressThree); + + vm.warp(block.timestamp + 10 days); + + // unstake userOne + vm.prank(userOne); + foxStaking.unstake(stakingAmount); + + // userOne should have earned rewards for 10 days, save that amount to check against later + uint256 userOneEarned = foxStaking.earned(userOne); + + // fast forward so userOne can withdraw + vm.warp(block.timestamp + 30 days); + vm.prank(userOne); + foxStaking.withdraw(); + + // confirm userOne has the same balance as the start + vm.assertEq( + foxToken.balanceOf(userOne), + userOneBalance, + "UserOne should have the same balance as the start" + ); + + // userOne should have no staking balance + (uint256 stakingBalance, , uint256 earnedRewards, , ) = foxStaking + .stakingInfo(userOne); + vm.assertEq(stakingBalance, 0, "UserOne should have no stakingBalance"); + vm.assertEq( + userOneEarned, + earnedRewards, + "UserOne should have the same earnedRewards from when the unstaked" + ); + + // confirm that userTwo and userThree have earned the same and correct amount of rewards + uint256 userTwoEarned = foxStaking.earned(userTwo); + uint256 userThreeEarned = foxStaking.earned(userThree); + vm.assertEq( + userTwoEarned, + userThreeEarned, + "UserTwo and UserThree should have the same rewards" + ); + + // userOne received 1/3 of the rewards for 10 days, userTwo and userThree received 2/3 of the rewards for 10 days, plus 1/2 the rewards for 30 days + // so userOne = 10/3rds + // userTwo = 10/3rds + 30/2nds = 55/3rds + // userThree = 10/3rds + 30/2nds = 55/3rds + uint256 expectedUserTwoEarned = (userOneEarned * 55) / 10; + vm.assertEq( + userTwoEarned, + expectedUserTwoEarned, + "UserTwo should have the correct amount of rewards" + ); + + // now if we have userOne restake, they should received the same rewards as userTwo and userThree from now on. + // dev note: this is also similiar to how we can do the off chain accounting essentially a snapshot at start of epoch, and then at the end of the epoch + // time warp 10 days and store all balances, like a new epoch + vm.warp(block.timestamp + 10 days); + userOneEarned = foxStaking.earned(userOne); + userTwoEarned = foxStaking.earned(userTwo); + userThreeEarned = foxStaking.earned(userThree); + + // have userOne stake the same amount again + vm.prank(userOne); + foxStaking.stake(stakingAmount, runeAddressOne); + + // time warp 10 days. All users should have received the same amount of rewards since the last time we checked + vm.warp(block.timestamp + 10 days); + uint256 userOneDelta = foxStaking.earned(userOne) - userOneEarned; + uint256 userTwoDelta = foxStaking.earned(userTwo) - userTwoEarned; + uint256 userThreeDelta = foxStaking.earned(userThree) - userThreeEarned; + + vm.assertEq( + userOneDelta, + userTwoDelta, + "UserOne and UserTwo should have the same rewards" + ); + vm.assertEq( + userTwoDelta, + userThreeDelta, + "UserTwo and UserThree should have the same rewards" + ); + } + + function testRewardAmountsWithMultipleUnstakes() public { + // stake 100 FOX tokens for each user + uint256 stakingAmount = 100 * 1e18; // 100 FOX token with 18 decimals + vm.prank(userOne); + foxStaking.stake(stakingAmount, runeAddressOne); + vm.prank(userTwo); + foxStaking.stake(stakingAmount, runeAddressTwo); + vm.prank(userThree); + foxStaking.stake(stakingAmount, runeAddressThree); + + vm.warp(block.timestamp + 10 days); + + // unstake half of userOne's stake + vm.prank(userOne); + foxStaking.unstake(stakingAmount / 2); + + vm.warp(block.timestamp + 10 days); + vm.prank(userOne); + foxStaking.unstake(stakingAmount / 2); + + // fast forward so userOne can withdraw one of the unstakes + vm.warp(block.timestamp + 18 days); + vm.prank(userOne); + foxStaking.withdraw(); + + // userOne should have no staking balance + (uint256 stakingBalance, , , , ) = foxStaking.stakingInfo(userOne); + vm.assertEq(stakingBalance, 0, "UserOne should have no stakingBalance"); + + // confirm that userTwo and userThree have earned the same and correct amount of rewards + uint256 userOneEarned = foxStaking.earned(userOne); + uint256 userTwoEarned = foxStaking.earned(userTwo); + uint256 userThreeEarned = foxStaking.earned(userThree); + vm.assertEq( + userTwoEarned, + userThreeEarned, + "UserTwo and UserThree should have the same rewards" + ); + + // userOne received 1/3 of the rewards for 10 days (10/3) and then 1/5 (50/5) for another 10 days + // 10/3 + 50/5 = 16/3 + + // userTwo received 1/3 of the rewards for 10 days (10/3) and then 2/5 (200/5) for another 10 days, and finally 1/2 (18/2) for 18 days + // 10/3 + 20/5 + 18/2 = 49/3 + uint256 expectedUserTwoEarned = (userOneEarned * 49) / 16; + vm.assertEq( + userTwoEarned, + expectedUserTwoEarned, + "UserTwo should have the correct amount of rewards" + ); + } +} diff --git a/foundry/test/FOXStakingTestRuneAddress.t.sol b/foundry/test/FOXStakingTestRuneAddress.t.sol index 422abc6..91390cc 100644 --- a/foundry/test/FOXStakingTestRuneAddress.t.sol +++ b/foundry/test/FOXStakingTestRuneAddress.t.sol @@ -30,7 +30,7 @@ contract FOXStakingTestRuneAddress is Test { foxStaking.setRuneAddress(newRuneAddress); - (, , string memory runeAddress) = foxStaking.stakingInfo(user); + (, , , , string memory runeAddress) = foxStaking.stakingInfo(user); assertEq( runeAddress, newRuneAddress, diff --git a/foundry/test/FOXStakingTestStaking.t.sol b/foundry/test/FOXStakingTestStaking.t.sol index 102b91e..2fb385e 100644 --- a/foundry/test/FOXStakingTestStaking.t.sol +++ b/foundry/test/FOXStakingTestStaking.t.sol @@ -45,6 +45,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 0); @@ -68,6 +70,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -100,6 +104,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 0); @@ -124,6 +130,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -139,7 +147,7 @@ contract FOXStakingTestStaking is Test { vm.startPrank(user); // Check user staking balances - (uint256 stakingBalance, uint256 unstakingBalance, ) = foxStaking + (uint256 stakingBalance, uint256 unstakingBalance, , , ) = foxStaking .stakingInfo(user); vm.assertEq(stakingBalance + unstakingBalance, 0); vm.assertEq(stakingBalance, 0); @@ -153,6 +161,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 0); @@ -168,7 +178,7 @@ contract FOXStakingTestStaking is Test { vm.startPrank(user); // Check user staking balances - (uint256 stakingBalance, uint256 unstakingBalance, ) = foxStaking + (uint256 stakingBalance, uint256 unstakingBalance, , , ) = foxStaking .stakingInfo(user); vm.assertEq(stakingBalance + unstakingBalance, 0); vm.assertEq(stakingBalance, 0); @@ -182,6 +192,8 @@ contract FOXStakingTestStaking is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 0); @@ -225,7 +237,9 @@ contract FOXStakingTestStaking is Test { assertEq(total, amounts[i]); // Verify each user's rune address - (, , string memory runeAddress) = foxStaking.stakingInfo(users[i]); + (, , , , string memory runeAddress) = foxStaking.stakingInfo( + users[i] + ); assertEq(runeAddress, runeAddresses[i]); } } diff --git a/foundry/test/FOXStakingTestUnstake.t.sol b/foundry/test/FOXStakingTestUnstake.t.sol index c8d6507..8842835 100644 --- a/foundry/test/FOXStakingTestUnstake.t.sol +++ b/foundry/test/FOXStakingTestUnstake.t.sol @@ -60,6 +60,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -83,6 +85,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -107,7 +111,7 @@ contract FOXStakingTestUnstake is Test { vm.startPrank(user); // Check user staking balances - (uint256 stakingBalance, uint256 unstakingBalance, ) = foxStaking + (uint256 stakingBalance, uint256 unstakingBalance, , , ) = foxStaking .stakingInfo(user); vm.assertEq(stakingBalance + unstakingBalance, 1000); vm.assertEq(stakingBalance, 1000); @@ -121,6 +125,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); @@ -138,6 +144,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -152,6 +160,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -168,6 +178,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -181,6 +193,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -197,6 +211,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -210,6 +226,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 1000); @@ -227,6 +245,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -244,6 +264,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_one, uint256 unstakingBalance_one, + , + , ) = foxStaking.stakingInfo(user); uint256 cooldownExpiry_one = foxStaking @@ -266,6 +288,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_two, uint256 unstakingBalance_two, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_two + unstakingBalance_two, 700); @@ -279,6 +303,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_three, uint256 unstakingBalance_three, + , + , ) = foxStaking.stakingInfo(user); uint256 cooldownExpiry_three = foxStaking @@ -301,6 +327,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -318,6 +346,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_one, uint256 unstakingBalance_one, + , + , ) = foxStaking.stakingInfo(user); uint256 cooldownExpiry_one = foxStaking @@ -349,6 +379,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_two, uint256 unstakingBalance_two, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(unstakingBalance_two, 301 + 302); @@ -369,6 +401,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_three, uint256 unstakingBalance_three, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(unstakingBalance_three, 301 + 302 + 303); @@ -396,6 +430,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_four, uint256 unstakingBalance_four, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_four, 1000 - 301 - 302 - 303); @@ -428,6 +464,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_five, uint256 unstakingBalance_five, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_five, 1000 - 301 - 302 - 303); @@ -453,6 +491,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_six, uint256 unstakingBalance_six, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_six, 1000 - 301 - 302 - 303); @@ -471,6 +511,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -488,6 +530,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_one, uint256 unstakingBalance_one, + , + , ) = foxStaking.stakingInfo(user); uint256 cooldownExpiry_one = foxStaking @@ -518,6 +562,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_two, uint256 unstakingBalance_two, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(unstakingBalance_two, 301 + 302); @@ -584,6 +630,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -601,6 +649,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_one, uint256 unstakingBalance_one, + , + , ) = foxStaking.stakingInfo(user); uint256 cooldownExpiry_one = foxStaking @@ -631,6 +681,8 @@ contract FOXStakingTestUnstake is Test { ( uint256 stakingBalance_two, uint256 unstakingBalance_two, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(unstakingBalance_two, 301 + 302); @@ -670,7 +722,7 @@ contract FOXStakingTestUnstake is Test { // Time warp another 45 days - should allow to remove the next one, but not the last vm.warp(block.timestamp + 45 days); - + // calling again should withdraw the 302 balBefore = foxToken.balanceOf(user); // Withdraw the 302 FOX diff --git a/foundry/test/FOXStakingTestWithdraw.t.sol b/foundry/test/FOXStakingTestWithdraw.t.sol index 15cd7f0..254ddf8 100644 --- a/foundry/test/FOXStakingTestWithdraw.t.sol +++ b/foundry/test/FOXStakingTestWithdraw.t.sol @@ -65,6 +65,8 @@ contract FOXStakingTestWithdraw is Test { ( uint256 stakingBalance_before, uint256 unstakingBalance_before, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_before + unstakingBalance_before, 1000); @@ -93,6 +95,8 @@ contract FOXStakingTestWithdraw is Test { ( uint256 stakingBalance_after, uint256 unstakingBalance_after, + , + , ) = foxStaking.stakingInfo(user); vm.assertEq(stakingBalance_after + unstakingBalance_after, 0); diff --git a/foundry/test/utils/MockFOXToken.sol b/foundry/test/utils/MockFOXToken.sol index 4a6a9e7..d11e8a4 100644 --- a/foundry/test/utils/MockFOXToken.sol +++ b/foundry/test/utils/MockFOXToken.sol @@ -6,8 +6,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockFOXToken is ERC20 { constructor() ERC20("Mock FOX Token", "FOX") { - // 1M FOX for testing, only in local chain can't use this as voting power soz - _mint(address(this), 1e24); + _mint(address(this), 1e27); // 1 billion FOX with 18 decimals } function makeItRain(address to, uint256 amount) public { diff --git a/foundry/test/utils/MockFoxStakingV2.sol b/foundry/test/utils/MockFoxStakingV2.sol index a13bf81..e3fc2e9 100644 --- a/foundry/test/utils/MockFoxStakingV2.sol +++ b/foundry/test/utils/MockFoxStakingV2.sol @@ -8,6 +8,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {StakingInfo} from "../../src/StakingInfo.sol"; @@ -16,7 +17,8 @@ contract MockFoxStakingV2 is Initializable, PausableUpgradeable, UUPSUpgradeable, - OwnableUpgradeable + OwnableUpgradeable, + ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; IERC20 public foxToken; @@ -26,6 +28,14 @@ contract MockFoxStakingV2 is bool public unstakingPaused; uint256 public cooldownPeriod; + uint256 public totalStaked; + uint256 public totalCoolingDown; + + uint256 public constant REWARD_RATE = 1_000_000_000; + uint256 public constant WAD = 1e18; + uint256 public lastUpdateTimestamp; + uint256 public rewardPerTokenStored; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers();