Summary
For the FjordStaking contract, the rewards come from three sources: RewardAdmin addReward and the penalty amount if some stakers claim early or someone sends FJO tokens to FjordStaking.
Based on the calculation for the pendingRewards, No direct relationships between pendingRewardsand the input amount for the function addReward.
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked) - totalStaked - newStaked - totalRewards;
Supply one view function showing the pendingRewardscan help owner and user get more accurate pendingRewards Info
Vulnerability Details
Currently, the pendingRewards calculation is as below; there are no direct relationships between the pendingRewards and the input amount when calling addReward.
So, the pending rewards for the admin seem to be a random number for admin. It will be more beneficial for admin if one view function is added to supply the pendingRewards. Not only supply help when calling addReward, but also give the owner or the user the current situations for pendingRewards
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked) - totalStaked - newStaked - totalRewards;
function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked)
- totalStaked - newStaked - totalRewards;
uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;
totalRewards += pendingRewards;
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded] + pendingRewardsPerToken;
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
} else {
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded];
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
}
totalStaked += newStaked;
totalVestedStaked += newVestedStaked;
newStaked = 0;
newVestedStaked = 0;
lastEpochRewarded = currentEpoch - 1;
}
}
Impact
Admin can't quickly get the accurate pending reward information if someone pays the penalty amount or others mistakenly send FJO tokens to FjordStaking.
Tools Used
Manual
Recommendations
Create public view function pendingRewards.
Apply calcPendingRewards in _checkEpochRollover.
function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 pendingRewards = calcPendingRewards();
uint256 pendingRewardsPerToken = (pendingRewards *
PRECISION_18) / totalStaked;
totalRewards += pendingRewards;
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] =
rewardPerToken[lastEpochRewarded] +
pendingRewardsPerToken;
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
} else {
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded];
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
}
totalStaked += newStaked;
totalVestedStaked += newVestedStaked;
newStaked = 0;
newVestedStaked = 0;
lastEpochRewarded = currentEpoch - 1;
}
}
function calcPendingRewards() public view returns (uint256) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
uint256 pendingRewards = (currentBalance +
totalVestedStaked +
newVestedStaked) -
totalStaked -
newStaked -
totalRewards;
return pendingRewards;
}