Flow

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

Stream can be voided in pause state also

Summary

The void function lacks a notPaused modifier, allowing streams to be permanently voided during temporary maintenance pauses. Despite protocol's assumption of trust between all entities (sender, recipient, and approved operators), this can lead to unintentional permanent termination of streams that were only meant to be temporarily paused.

Vulnerability Details

function void(uint256 streamId)
external
override
noDelegateCall
notNull(streamId)
notVoided(streamId) // Only checks if not already voided - @audit
updateMetadata(streamId)
{
_void(streamId);
}
// Missing crucial check in _void():
function _void(uint256 streamId) internal {
// No check if stream is paused
if (msg.sender != _streams[streamId].sender &&
!_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierFlow_Unauthorized(...);
}
_streams[streamId].isVoided = true;
_streams[streamId].ratePerSecond = ud21x18(0);
// Stream can never be restarted
}

Important Note: While the readme docs states:

It is assumed that a trust relationship is formed between the sender, recipient, and approved operators participating in a stream.

This issue can occur even with complete trust between parties, as it stems from normal operational activities rather than malicious intent.

Impact

HIGH. Because:

  1. Paused streams can be permanently voided by trusted parties acting in good faith

  2. Once voided, streams cannot be restarted

  3. Affects payroll and payment systems during routine maintenance

  4. Forces creation of new streams after maintenance

  5. Disrupts accounting and stream history

Likelihood

LOW. This can occur in in business operations when:

  1. System maintenance requiring temporary pauses

  2. Different departments responding to stream states

  3. Communication gaps in standard procedures

Proof of Concept

Let's do a pseudo code PoC as example

// Scenario: Trusted parties, routine maintenance
function testTrustedPartiesMaintenanceScenario() public {
// Setup: All parties trust each other
uint256 streamId = flow.create(sender, recipient, rate, token, true);
// Finance department pauses for maintenance
vm.prank(sender); // Finance dept
flow.pause(streamId);
assertEq(flow.statusOf(streamId), PAUSED_SOLVENT);
// HR department (trusted recipient) sees non-working stream
vm.prank(recipient); // HR dept
flow.void(streamId); // Acting in good faith
// Finance can't restart after maintenance
vm.expectRevert("Stream voided");
flow.restart(streamId, rate);
}

Recommendation

Add notPaused modifier to void:

function void(uint256 streamId)
external
override
noDelegateCall
notNull(streamId)
notVoided(streamId)
notPaused(streamId) // Add this modifier
updateMetadata(streamId)
{
_void(streamId);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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