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

Users can bypass requirements and stake their own Sablier NFT

Summary

Fjord allows users to stake Sablier NFT as long as they stream Fjord Tokens using the stakeVested() function.

This function performs checks regarding the nature of the NFT before proceeding to the staking.

One of them makes sure the NFT was emitted by trusted parties which are said to be "authorised multisig addresses of ours" by the Fjord.

Vulnerability Details

Sablier streams can be cancelled but only by the address returned by getSender().

The stakeVested() function verifies the creator of the stream using Sablier getSender() function.

https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordStaking.sol#L403-L405

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();
}

The sponsor stated "We will only allow linear streams which are created by a set of authorised multisig addresses of ours."

However, the Sablier NFT sender can be set to an arbitrary address at creation which allows anyone to create their own stream and stake them in Fjord, effectively bypassing the intention of Fjord to only allow staking streams emitted by one of the multisig address.

The sablier createWithDurations() function responsible for the stream creation takes user supplied params meaning it can be set to any address (including one of the authorized multisig address):

https://github.com/sablier-labs/v2-core/blob/main/src/SablierV2LockupLinear.sol#L154-L164

function createWithDurations(
LockupLinear.CreateWithDurations calldata params
)
external
override
noDelegateCall
returns (uint256 streamId)
{
// Set the current block timestamp as the stream's start time.
LockupLinear.Timestamps memory timestamps;
timestamps.start = uint40(block.timestamp);
// Calculate the cliff time and the end time. It is safe to use unchecked arithmetic because {_create} will
// nonetheless check that the end time is greater than the cliff time, and also that the cliff time, if set,
// is greater than or equal to the start time.
unchecked {
if (params.durations.cliff > 0) {
timestamps.cliff = timestamps.start + params.durations.cliff;
}
timestamps.end = timestamps.start + params.durations.total;
}
// Checks, Effects and Interactions: create the stream.
streamId = _create(
LockupLinear.CreateWithTimestamps({
@ sender: params.sender,
recipient: params.recipient,
totalAmount: params.totalAmount,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
timestamps: timestamps,
broker: params.broker
})
);
}

Impact

Users are able to bypass the Fjord intention to only allow restricted Sablier streams to be staked in the contract.

Tools Used

Manual review

Recommendations

The nature of the emitter of a Sablier stream can't be verified easily.

However, an additional check can be implemented to only allow cancellable streams to be staked.

This way, if the described situation occurs, Fjord still has the ability to cancel the stream and enforce their restrictions.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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