Vulnerability Details
When users stake and unstake with FjordStaking::stake()
and FjordStaking::unstake()
, it calls FjordPoints::onStaked
and FjordPoints::onUnstaked
respectively:
FjordPoints.sol#L192-L227
* @notice Records the amount of tokens staked by a user.
* @param user The address of the user staking tokens.
* @param amount The amount of tokens being staked.
*/
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);
}
* @notice Records the amount of tokens unstaked by a user.
* @param user The address of the user unstaking tokens.
* @param amount The amount of tokens being unstaked.
*/
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 onStaked()
and onUnstaked()
call distributePoints()
and updatePendingPoints(msg.sender)
to update users' points:
FjordPoints.sol#L142-L153
* @dev Modifier to update pending points for a user.
* @param user The address of the user to update points for.
*/
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;
_;
}
If users unstake in the same epoch in which they staked, they can unstake immediately and bypass the lockCycle
:
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();
}
Thus, if users call FjordStaking::stake()
and FjordStaking::unstake()
in the same transaction, they will earn points
without actually having staked.
Impact
Users can unfairly earn points
without having actually staked.
Recommendations
Ensure that points
don't accumulate if users unstake in the same epoch in which they staked.