Users who staked 1 or more epoch before reward is added dont get the reward. This happens because they are still in newStaked, and the calculation for these is wrong.
In the _checkEpochRollover() function, the pendingRewardsPerToken is calculated only for the totalStaked at that point.
The intention as seen in the CODE COMMENTS is to disregard users who staked in that current epoch(as newStaked is not used in the division). But this is wrong since, newStaked may contain users who have deposited in the epoch before,. This happens if addreward() is the first transaction to go through in an epoch.
But this also doesnt rectify it, since the calculated reward function returns 0 in this scenario (this happens because rewardPerToken[ud.unredeemedEpoch] and rewardPerToken[currentEpoch-1] will be the same here).
pragma solidity =0.8.21;
import "../src/FjordStaking.sol";
import { FjordPoints } from "../src/FjordPoints.sol";
import { Test } from "forge-std/Test.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import { FjordPointsMock } from "./mocks/FjordPointsMock.sol";
import { ISablierV2LockupLinear } from "lib/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2Lockup } from "lib/v2-core/src/interfaces/ISablierV2Lockup.sol";
import { Broker, LockupLinear } from "lib/v2-core/src/types/DataTypes.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud60x18 } from "@prb/math/src/UD60x18.sol";
import "lib/v2-core/src/libraries/Errors.sol";
import "forge-std/console.sol";
contract FjordStakingBase is Test {
event Staked(address indexed user, uint16 indexed epoch, uint256 amount);
event VestedStaked(
address indexed user, uint16 indexed epoch, uint256 indexed streamID, uint256 amount
);
event RewardAdded(uint16 indexed epoch, address rewardAdmin, uint256 amount);
event RewardClaimed(address indexed user, uint256 amount);
event EarlyRewardClaimed(address indexed user, uint256 rewardAmount, uint256 penaltyAmount);
event Unstaked(address indexed user, uint16 indexed epoch, uint256 stakedAmount);
event VestedUnstaked(
address indexed user, uint16 indexed epoch, uint256 stakedAmount, uint256 streamID
);
event ClaimReceiptCreated(address indexed user, uint16 requestEpoch);
event UnstakedAll(
address indexed user,
uint256 totalStakedAmount,
uint256[] activeDepositsBefore,
uint256[] activeDepositsAfter
);
event RewardPerTokenChanged(uint16 epoch, uint256 rewardPerToken);
event SablierWithdrawn(address indexed user, uint256 streamID, address caller, uint256 amount);
event SablierCanceled(address indexed user, uint256 streamID, address caller, uint256 amount);
uint256 constant addRewardPerEpoch = 1 ether;
FjordStaking fjordStaking;
FjordPoints fjordpoints;
MockERC20 token;
address minter = makeAddr("minter");
address newMinter = makeAddr("new_minter");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address charlie = makeAddr("charlie");
address internal constant SABLIER_ADDRESS = address(0xB10daee1FCF62243aE27776D7a92D39dC8740f95);
address points;
bool isMock = true;
ISablierV2LockupLinear SABLIER = ISablierV2LockupLinear(SABLIER_ADDRESS);
address authorizedSender = address(this);
bool isFuzzOrInvariant = false;
function beforeSetup() internal virtual { }
function afterSetup() internal virtual { }
function setUp() public {
beforeSetup();
if (!isFuzzOrInvariant) {
vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 19_595_905 });
}
fjordpoints = new FjordPoints();
points = address(fjordpoints);
token = new MockERC20("Fjord", "FJO", 18);
fjordStaking =
new FjordStaking(address(token), minter, SABLIER_ADDRESS, authorizedSender, points);
deal(address(token), address(this), 10000 ether);
token.approve(address(fjordStaking), 10000 ether);
deal(address(token), minter, 10000 ether);
vm.prank(minter);
token.approve(address(fjordStaking), 10000 ether);
deal(address(token), alice, 10000 ether);
vm.prank(alice);
token.approve(address(fjordStaking), 10000 ether);
deal(address(token), bob, 10000 ether);
vm.prank(bob);
token.approve(address(fjordStaking), 10000 ether);
deal(address(token), charlie, 10000 ether);
vm.prank(charlie);
token.approve(address(fjordStaking), 10000 ether);
fjordpoints.setStakingContract(address(fjordStaking));
afterSetup();
}
function testcase() public {
uint256 EPOCH_DURATION = 1 weeks;
vm.prank(alice);
fjordStaking.stake(10 ether);
vm.warp(vm.getBlockTimestamp() + 1*EPOCH_DURATION);
vm.prank(bob);
fjordStaking.stake(10 ether);
vm.warp(vm.getBlockTimestamp() + 1*EPOCH_DURATION);
vm.prank(minter);
fjordStaking.addReward(100 ether);
vm.warp(vm.getBlockTimestamp() + 7*EPOCH_DURATION);
vm.prank(alice);
(uint256 alice_reward, uint256 alice_penalty) = fjordStaking.claimReward(false);
console.log("Alice's reward is :", alice_reward);
console.log("Alice's penalty is :", alice_penalty);
vm.prank(bob);
vm.expectRevert(FjordStaking.NothingToClaim.selector);
(uint256 bob_reward, uint256 bob_penalty) = fjordStaking.claimReward(false);
console.log("Bob's reward is :", bob_reward);
console.log("Bob's penalty is :", bob_penalty);
}
We can see that eventhoug bob staked the epoch before addReward was called, he doesnt get any rewards.