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

A malicious individual can Withdraw streamed claimable vestedStaked `FjordToken` token to the `FjordStaking` contract, Leading to a staker's loss of staked Tokens.

Summary

In the Sablier contract, anybody can withdraw tokens to the recipient address regardless he is the recipient or not, and calling this on vested tokens will lead to a fatal loss of tokens by the VestedStaker who staked with this stream

Vulnerability Details

In the Sablier contract, anybody can withdraw funds for the recipient of a stream.

Code from SablierV2LockUp::withdraw Line 385

// Check: if `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address
// must be the recipient.
if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierV2Lockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to);
}

According to the above check, anyone can pass in a to address as the recipient and withdraw to the recipient address the streamed and claimeable amount.

Since the recipient is the owner of the stream nft, for this case the recipient will be the FjordStaking contract address. This call will send the vestedStaker's funds from the Sablier lockup contract to FjordStaking contract address.

These sent funds will be distributed as rewards to all stakers due to the logic in FjordStaking::_checkEpochRollover() function which subtracts already recorded values and distributes the remaining as rewards.

code from FjordStaking::_checkEpochRollover() Line 691

if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
// no distribute the rewards to the users coming in the current epoch
//@audit those who unstake continue earning rewords,prove me wrong
uint256 pendingRewards = (currentBalance +
totalVestedStaked +
newVestedStaked) -
totalStaked -
newStaked - //"@audit don't forget new staked"
totalRewards;
uint256 pendingRewardsPerToken = (pendingRewards *
PRECISION_18) / totalStaked;
totalRewards += pendingRewards;
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] =
rewardPerToken[lastEpochRewarded] +
pendingRewardsPerToken;
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
} else {
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded];
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
}

One may say that the below code in Sablier::withdraw function will call the FjordStaking::onStreamWithdrawn function but this function will do nothing since it is intentionally left blank by the developers.

Code from sablierV2LockUp::withdraw function Line 404:

if (msg.sender != recipient && _allowedToHook[recipient]) {
bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupWithdraw({
streamId: streamId,
caller: msg.sender,
to: to,
amount: amount
});

The _allowToHook[recipient] cannot be true since this contract does not implement the supportInterface function which is required for allowToHook to be set to true. Even if its true the FjordStaking::onStreamWithdraw function is blank and will do nothing to handle the sent tokens.

The blank FjordStaking::onStreamWithdrawn function Line 792:

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

The root cause of this issue is the fact that the above FjordStaking::onStreamWithdrawn function does nothing to handle such a withdraw to it in such a scenario to allocate the staker's funds.

Impact

During unstaking, the vestedStaker will not be able to recover his staked funds hence a loss of tokens.

Tools Used

Manual Review

Recommendation

Consider implemnting the FjordStaking::onStreamWithdrawn function to handle these tokens and add them to the user's staked ammount and prevent it from being distributed as rewards

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.