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

Incorrect Sablier integration in the `onStreamWithdrawn()` callback function can lead to user fund loss.

Summary

In Sablier version 1.1.2, used by the Fjord protocol, the stream sender can force withdrawal of streams they created. When this occurs, the onStreamWithdrawn() function callback is triggered. However, the onStreamWithdrawn() function in the FjordStaking contract does not contain code to handle this scenario, which can lead to user funds being lost.

Vulnerability Details

Similar issue was identified in the Cyfrin audit (and was incorrectly "fixed" by removing the code within the onStreamWithdrawn() function).

Current implementation allows users to stake streams only from authorizedSablierSenders, but the code seems to assume that stream cancellation is the only operation that an authorized stream sender might perform.

Unfortunately, Sablier's code allows for a force withdrawal by the stream's sender:

File: SablierV2Lockup.sol
259: // Checks: if `msg.sender` is the stream's sender, the withdrawal address must be the recipient.
260: if (isCallerStreamSender && to != recipient) {
261: revert Errors.SablierV2Lockup_InvalidSenderWithdrawal(streamId, msg.sender, to);
262: }

If a partially trusted authorizedSablierSender initiates a withdrawal for all stream IDs deposited into the FjordStaking contract, all transferred FJO tokens might be treated as rewards, even though these tokens actually belong to individual users.

This mismanagement occurs because the onStreamWithdrawn() function does not properly account for such scenarios.

Proper Sablier integration in the onStreamWithdrawn() function is necessary to prevent this issue.

Impact

  • Loss of user funds.

Code Snippet

https://github.com/sablier-labs/v2-core/blob/v1.1.2/src/abstracts/SablierV2Lockup.sol#L260-L262
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordStaking.sol#L792-L799

Tools Used

Manual review.

Recommendations

The force withdrawal from stream can't be prevented. It is essential to properly handle onStreamWithdrawn() callback.

In the event that the onStreamWithdrawn() callback is triggered, retrieve the stream owner and the epoch of the vesting stake of the streamID. Adjust the accounting in the appropriate user DepositReceipt and move the corresponding amount from vestedStaked to staked. Additionally, decrease the global totalVestedStaked by the amount parameter from the onStreamWithdrawn() function.

This approach ensures that the vested portion of the stake is correctly transferred to the staked portion, corresponding to the amount of tokens withdrawn through a force withdrawal.

Updates

Lead Judging Commences

inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Incorrect Sablier integration in the `onStreamWithdrawn()` callback function

Appeal created

eeyore Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Incorrect Sablier integration in the `onStreamWithdrawn()` callback function

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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