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

Pending streams can be staked

Vulnerability Details

The stakeVested function in the Fjord Staking contract enables users to stake their Sablier vesting NFTs, which contain FJORD tokens, and earn rewards for those staked NFTs. However, the function only verifies whether the stream is "cold" (i.e., settled, canceled, or depleted) and does not check if the stream is active(streaming) or pending:

function stakeVested(uint256 _streamID) external checkEpochRollover redeemPendingRewards {
//CHECK
if (!sablier.isStream(_streamID)) revert NotAStream();
if (sablier.isCold(_streamID)) revert NotAWarmStream();
// only allow authorized stream sender to stake cancelable stream
if (!authorizedSablierSenders[sablier.getSender(_streamID)]) {
revert StreamNotSupported();
}
if (address(sablier.getAsset(_streamID)) != address(fjordToken)) revert InvalidAsset();
// @audit No check if the stream is pending or not
// --SNIP
}

As seen in the code above, pending streams can be staked and accrue rewards, which is problematic because the funds in pending streams remain locked until the start date. So, users can stake their Sablier vesting NFTs during the pending period and earn rewards and unstake their on the start date

Proof Of Concept

The following example demonstrates how Alice can earn rewards for staking a pending stream for two weeks. To replicate, copy and paste the following test case into test/unit/stakeVested.t.sol:

function test_pendingStreamsCanBeStaked() public {
// Create the stream
uint amount = 10 ether;
deal(address(token), address(this), amount);
token.approve(address(SABLIER), amount);
LockupLinear.CreateWithRange memory params;
params.sender = address(this);
params.recipient = alice;
params.totalAmount = uint128(amount);
params.asset = IERC20(address(token));
params.cancelable = false;
params.range = LockupLinear.Range({
// >>>>>>>>>> NOTICE that it starts after 14 days
start: uint40(vm.getBlockTimestamp() + 14 days),
cliff: uint40(vm.getBlockTimestamp() + 15 days),
end: uint40(vm.getBlockTimestamp() + 45 days)
});
params.broker = Broker(address(0), ud60x18(0));
uint streamID = SABLIER.createWithRange(params);
// Alice stakes the stream
vm.startPrank(alice);
SABLIER.approve(address(fjordStaking), streamID);
vm.expectEmit();
emit VestedStaked(alice, 1, streamID, amount);
fjordStaking.stakeVested(streamID);
// epochs are passed, till now, the stream is STILL PENDING
_addRewardAndEpochRollover(1 ether, 2);
vm.startPrank(alice);
vm.expectEmit();
emit ClaimReceiptCreated(alice, 3);
fjordStaking.claimReward(false);
(, uint256 unclaimedRewards, ,) = fjordStaking.userData(address(alice));
// Alice earned rewards even the stream was in pending state
assertEq(2 ether, unclaimedRewards);
}

Impact

Pending streams can be staked and accrue rewards

Tools Used

Manual Review

Recommendations

Consider checking whether the stream is started or not, use isWarm to detect that

Updates

Lead Judging Commences

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

Appeal created

0xbrivan2 Submitter
about 1 year ago
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.