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

users may stake only after addRewards() and then cancel their stake in the next epoch

Summary

Rewards are added arbitrarly by the Fjord team, and these rewards are distributed in proportion to the tokens users stake.
The protocol also allows users to stake sablier revenue streams, but these revenue streams may be cancelled.
Thus this may encourage users to only stake when an epoch has rewards, claim in the next epoch and cancel their revenue stream in the next epoch, allowing them to bypass the 6 weeks of minimum stake duration.

Vulnerability Details

The protocol currently has a flaw that allows users to bypass the intended staking rules. Normally, when a user wants to unstake their tokens, they're required to wait for a minimum of 6 epochs (a set time period) before they can withdraw. However, there's a loophole:

  1. If a user has staked tokens that have fully vested (meaning they've completed their lock-up period),

  2. They can exploit this to unstake and withdraw their tokens immediately, without waiting.

While this loophole doesn't pose significant financial risks to the protocol, it does create two main problems:

  1. Reduced staking stability: The waiting period is designed to maintain a stable pool of staked tokens. By bypassing this, users can suddenly remove large amounts of staked tokens, potentially destabilizing the system.

  2. Fairness issues: This loophole gives an unfair advantage to users with vested tokens. They can react more quickly to market changes or opportunities, while regular stakers are still bound by the waiting period.

Consider the above scenario described by the POC below:

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "../FjordStakingBase.t.sol";
import "forge-std/console.sol";
//Stake vested 1 day
//claim early
//cancel
//
contract UnstakeCancelClaim is FjordStakingBase {
function test_stakeClaimCancelScenarioUltime() public {
// _addRewardAndEpochRollover(15 ether, 4);
uint256 startEpoch = fjordStaking.currentEpoch();
console.log("start epoch is", startEpoch);
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration() - 1);
vm.prank(minter);
fjordStaking.addReward(15 ether);
// // stake
uint256 streamID = createStreamAndStake();
// /////
_addRewardAndEpochRollover(15 ether, 2);
// //claim
uint256 claimEpoch = fjordStaking.currentEpoch();
console.log("claim epoch is", claimEpoch);
vm.prank(alice);
(uint256 rewardAmount, uint256 penaltyAmount) = fjordStaking.claimReward(true);
// console.log("reward and penalties are:", rewardAmount, penaltyAmount);
// //cancel
SABLIER.cancel(streamID);
}

Tools Used

Manual review

Recommendations

Again, allowing cancellable revenue streams staking completely defeats the purpose of having a minimum 6 epoch waiting period.

It would be more wise to only allow un-cancellable staking of Sablier revenue streams.

function stakeVested(uint256 _streamID) external checkEpochRollover redeemPendingRewards {
//CHECK
if (!sablier.isStream(_streamID)) revert NotAStream();
if (sablier.isCold(_streamID)) revert NotAWarmStream();
+ if (sablier.isCancelable(_streamID)) revert streamIsCancellable();
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

Support

FAQs

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