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

Stakers can lose tokens due to unhandled Sablier stream withdrawals in FjordStaking contract

Summary

The FjordStaking contract fails to properly handle Sablier stream withdrawals, potentially leading to loss of staked tokens for users. When a stream sender withdraws tokens from a Sablier stream that has been staked in the FjordStaking contract, the withdrawn tokens are sent to the contract but not accounted for.

Vulnerability Details

The onStreamWithdrawn function in the FjordStaking contract is currently left unimplemented:

function onStreamWithdrawn(
uint256, /*streamId*/
address, /*caller*/
address, /*to*/
uint128 /*amount*/
) external override onlySablier {
// Left blank intentionally
}

This lack of implementation creates an issue in the following scenario:

  • Alice creates a Sablier stream with 100 Fjord tokens to Bob.

  • Bob stakes the stream NFT in the FjordStaking contract.

  • Alice calls withdraw on the Sablier contract, sending 100 Fjord tokens to the FjordStaking contract (the current owner of the NFT).

  • The withdrawable amount of the NFT is now 0, but the FjordStaking contract has not updated its internal accounting.

  • When Bob attempts to unstake the NFT using unstakeVested, he receives back the NFT, but it now has 0 withdrawable tokens.

  • The 100 Fjord tokens are now trapped in the FjordStaking contract, with no mechanism to retrieve them.

Impact

  • Direct Token Loss: Stakers can permanently lose access to their staked tokens if a stream withdrawal occurs before they unstake.

  • The contract's internal accounting becomes inaccurate, leading to discrepancies between actual and recorded token balances.

Tools Used

Manual

Recommendations

Implement the onStreamWithdrawn function in the FjordStaking contract to handle increase the DepositReceipt.staked and reduce DepositReceipt.vestedStaked by the amount. For example:

function onStreamWithdrawn(
uint256 streamId,
address caller,
address to,
uint128 amount
) external override onlySablier {
address streamOwner = _streamIDOwners[streamId];
require(streamOwner != address(0), "Stream owner not found");
DepositReceipt storage receipt = deposits[streamOwner][_streamIDs[streamOwner][streamId].epoch];
receipt.staked += amount;
receipt.vestedStaked -= amount;
// Update total staked amounts
totalStaked += amount;
totalVestedStaked -= amount;
emit StreamWithdrawn(streamId, streamOwner, caller, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

ast3ros Submitter
10 months ago
inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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