DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: high
Invalid

Users can earn `points` even if they don't stake by calling `stake()` and `unstake()` in the same tx

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();
-> // _epoch is same as current epoch then user can unstake immediately
-> if (currentEpoch != _epoch) {
// _epoch less than current epoch then user can unstake after at complete lockCycle
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.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.