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

`FjordPoints` Point Distribution Is Vulnerable To Sandwich Attacks

Summary

FjordPoints distributes points in a stepwise fashion, making it vulnerable to sandwich attacks by poisoned liquidity.

Vulnerability Details

When calling distributePoints , FjordPoints immediately assigns the pointsPerToken in a stepwise fashion:

function distributePoints() public {
if (block.timestamp < lastDistribution + EPOCH_DURATION) {
return;
}
if (totalStaked == 0) {
return;
}
uint256 weeksPending = (block.timestamp - lastDistribution) / EPOCH_DURATION;
/// @audit pointsPerToken is increased uniformly for all stakers:
pointsPerToken =
pointsPerToken.add(weeksPending * (pointsPerEpoch.mul(PRECISION_18).div(totalStaked)));
totalPoints = totalPoints.add(pointsPerEpoch * weeksPending);
lastDistribution = lastDistribution + (weeksPending * 1 weeks);
emit PointsDistributed(pointsPerEpoch, pointsPerToken);
}

This means an MEV searcher may establish a token stake before an impending call to distributePoints, then immediately unstake to redeem unfarily accrued rewards. This happens because when updating a stakers pending rewards, new reward rates are assigned instantaneously in a stepwise fashion:

modifier updatePendingPoints(address user) {
UserInfo storage userInfo = users[user];
/// @audit The stepwise jump in `pointsPerToken` caused by
/// @audit caused by invoking `distributePoints` may
/// @audit immediately be redeemed - an MEV searcher
/// @audit merely stakes using the `lastPointsPerToken` and
/// @audit post-sandwich is atomically rewarded as if they
/// @audit had been staked for the entire duration:
uint256 owed = userInfo.stakedAmount.mul(pointsPerToken.sub(userInfo.lastPointsPerToken))
.div(PRECISION_18);
userInfo.pendingPoints = userInfo.pendingPoints.add(owed);
userInfo.lastPointsPerToken = pointsPerToken;
_;
}

Consequently, there is diminished incentive to stake for the entire epoch.

Furthermore, there are no second-order protections against this style of poisoned liquidity, since FjordStaking specifically accomodates for accounts to stake and unstake within the same epoch:

// _epoch is same as current epoch then user can unstake immediately /// @audit atomically_staking_and_unstaking_is_supported
if (currentEpoch != _epoch) {
// _epoch less than current epoch then user can unstake after at complete lockCycle
if (currentEpoch - _epoch <= lockCycle) revert UnstakeEarly();
}

https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordStaking.sol#L462C9-L466C10

This design decision specifically accommodates for poisoned liquidity.

Impact

Unfair reward distribution resulting in losses for existing stakers.

Tools Used

Manual Review

Recommendations

  1. Have the pointsPerToken grow monotonically over the reward duration to reward stakers for the precise amount of time they have been staked.

  2. Do not give stakers the privilege to unstake early without suffering a penalty.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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