Github link
Summary
Users can earn points without actual staking by executing stake() and unstake() within the same transaction.
Vulnerability Details
When a user takes staking operation (staking/unstaking), they call FjordPoints::onStaked and FjordPoints::onUnstaked functions:
FjordPoints.sol#L192-L227
function onStaked(address user, uint256 amount)
external
onlyStaking
checkDistribution
updatePendingPoints(user)
{
UserInfo storage userInfo = users[user];
userInfo.stakedAmount = userInfo.stakedAmount.add(amount);
totalStaked = totalStaked.add(amount);
emit Staked(user, amount);
}
...
function onUnstaked(address user, uint256 amount)
external
onlyStaking
checkDistribution
updatePendingPoints(user)
{
UserInfo storage userInfo = users[user];
if (amount > userInfo.stakedAmount) {
revert UnstakingAmountExceedsStakedAmount();
}
userInfo.stakedAmount = userInfo.stakedAmount.sub(amount);
totalStaked = totalStaked.sub(amount);
emit Unstaked(user, amount);
}
Both functions call updatePendingPoints(msg.sender) to update points for the user:
modifier updatePendingPoints(address user) {
UserInfo storage userInfo = users[user];
uint256 owed = userInfo.stakedAmount.mul(pointsPerToken.sub(userInfo.lastPointsPerToken))
.div(PRECISION_18);
userInfo.pendingPoints = userInfo.pendingPoints.add(owed);
userInfo.lastPointsPerToken = pointsPerToken;
_;
}
Due to the current implementation of unstake function, it will bypass if user unstakes from currentEpoch. This results in earning unfair points for the user without having actually staked.
function unstake(uint16 _epoch, uint256 _amount)
external
checkEpochRollover
redeemPendingRewards
returns (uint256 total)
{
if (_amount == 0) revert InvalidAmount();
DepositReceipt storage dr = deposits[msg.sender][_epoch];
if (dr.epoch == 0) revert DepositNotFound();
if (dr.staked < _amount) revert UnstakeMoreThanDeposit();
->
-> if (currentEpoch != _epoch) {
if (currentEpoch - _epoch <= lockCycle) revert UnstakeEarly();
}
Impact
Users can earn unfair FjordPoints by instant staking/unstaking action, which disrupts points distribution for honest users.
Tools Used
Manual Review
Recommendations
Make sure that points do not accumulate if users unstake within the same epoch they staked.