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

Unsupported `streams` will continue to accrue `rewards`

Summary

The stakeVested() function in the FjordStaking contract allows users to stake tokens from Sablier streams, provided the stream sender is authorized. However, if the contract owner changes the authorization status of a sender after a stream has been staked, the stream continues to accrue rewards. This could lead to inconsistencies in reward distribution and policy enforcement.

Vulnerability Details

The function checks if the stream sender is authorized at the time of staking.

if (!authorizedSablierSenders[sablier.getSender(_streamID)]) {
revert StreamNotSupported();
}

This check is intended to ensure that only streams from senders who are explicitly authorized by the contract owner can be staked.

  • Changing Authorization Status:

The contract owner has the ability to change the authorization status of a sender using removeAuthorizedSablierSender()function.

function removeAuthorizedSablierSender(address _address) external onlyOwner {
if (authorizedSablierSenders[_address]) authorizedSablierSenders[_address] = false;
}

If the owner removes a sender from the authorized list after a stream has been staked, it means that the stream in question is no longer supported. However, the stream will continue to accrue rewards despite the sender no longer being authorized.

Impact

Streams from senders who are no longer authorized will continue to benefit from staking rewards, potentially violating the intended policy of only rewarding authorized streams.

Tools Used

Manual Review

Recommendations

Unstake the vesting for the streamOwner in removeAuthorizedSablierSender().

+ // @audit Import the EnumerableSet library
+ import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+ // @audit Use the EnumerableSet for UintSet
+ using EnumerableSet for EnumerableSet.UintSet;
+ // @audit Mapping to track streams by sender
+ mapping(address => EnumerableSet.UintSet) private senderStreams;
+ // @audit Function to add a stream to the sender's list
+ function addStreamToSender(address sender, uint256 streamID) internal {
+ senderStreams[sender].add(streamID);
+ }
+ // @audit Function to remove a stream from the sender's list
+ function removeStreamFromSender(address sender, uint256 streamID) internal {
+ senderStreams[sender].remove(streamID);
+ }
+ // @audit Function to retrieve all streamIDs associated with a sender
+ function getStreamsBySender(address sender) internal view returns (uint256[] memory) {
+ return senderStreams[sender].values();
+ }
+ // @audit Modify the removeAuthorizedSablierSender function to handle unstaking
- function removeAuthorizedSablierSender(address _address) external onlyOwner {
+ function removeAuthorizedSablierSender(address _address) external onlyOwner checkEpochRollover {
if (authorizedSablierSenders[_address]) {
authorizedSablierSenders[_address] = false;
+ // @audit Retrieve all streamIDs associated with the sender
+ uint256[] memory streams = getStreamsBySender(_address);
+ // @audit Iterate over each streamID to handle unstaking
+ for (uint256 i = 0; i < streams.length; i++) {
+ uint256 streamID = streams[i];
+ address streamOwner = _streamIDOwners[streamID];
+ if (streamOwner != address(0)) {
+ // @audit Accumulate unclaimed rewards for the user
+ _redeem(streamOwner);
+ // @audit Unstake the stream for the streamOwner
+ _unstakeVested(streamOwner, streamID, _streamIDs[streamOwner][streamID].amount);
+ // @audit Optionally, remove the stream from the sender's list
+ removeStreamFromSender(_address, streamID);
+ }
+ }
}
}
Updates

Lead Judging Commences

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.