Flow

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

Division by Zero in depletionTimeOf Function Causes Protocol-Wide DoS When Streams Enter Zero-Rate State

Summary

The depletionTimeOf function in SablierFlow contains a vulnerability where a division by zero can occur despite having a notPaused modifier. This creates a potential DoS condition and contract instability.

The issue stems from a mismatch between stream state validation and mathematical operations.

First, the function comes with a notPaused modifier:

function depletionTimeOf(uint256 streamId)
external
view
override
notNull(streamId)
notPaused(streamId) // This modifier is meant to prevent zero-rate operations
returns (uint256 depletionTime)

The stream can enter a zero-rate state through legitimate operations. For example, via adjustRatePerSecond:

function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
// Check: the new rate per second is different from the current rate per second.
if (newRatePerSecond.unwrap() == _streams[streamId].ratePerSecond.unwrap()) {
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}
// Rate can be set to zero here
_streams[streamId].ratePerSecond = newRatePerSecond;
}

Later in depletionTimeOf, we perform division using the rate:

uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap(); // Could be 0
unchecked {
uint256 solvencyAmount = balanceScaled - snapshotDebtScaled + oneMVTScaled;
uint256 solvencyPeriod = solvencyAmount / ratePerSecond; // Division by zero!
if (solvencyAmount % ratePerSecond == 0) {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
} else {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod + 1;
}
}

The issue can occur through this sequence:

// 1. Create stream with non-zero rate
flow.create(sender, recipient, ud21x18(1e18), token, true);
// 2. Adjust rate to zero (this succeeds)
flow.adjustRatePerSecond(streamId, ud21x18(0));
// 3. Try to check depletion time (this reverts)
flow.depletionTimeOf(streamId); // Reverts on division by zero

What makes this particularly dangerous is that the stream can enter this state through normal operations. The notPaused modifier is checking the wrong condition - a stream can have a zero rate without technically being "paused". This creates a mathematical impossibility: trying to calculate how long until a stream depletes when it's not moving at all.

Impact

The division by zero vulnerability in depletionTimeOf() creates a cascading effect throughout the protocol's core streaming mechanics. When a stream enters a zero-rate state through legitimate operations like adjustRatePerSecond(), the depletion time calculation becomes impossible, causing the function to revert. This reversion breaks a fundamental invariant of the streaming protocol - the ability to determine when a stream will become insolvent.

The failure of this calculation has far-reaching implications. Smart contracts integrating with Sablier that rely on depletionTimeOf() to make streaming decisions will fail to execute their logic. For instance, automated portfolio management systems that rebalance based on stream depletion forecasts will be unable to make informed decisions. Compound-like protocols that use stream solvency as part of their risk calculations would have inaccurate or failing risk models.

More critically, since depletionTimeOf() is a key component in determining stream health, its failure compromises the protocol's ability to maintain accurate solvency data. This could lead to scenarios where streams appear healthy in some protocol functions but revert in others, creating inconsistent contract state. Automated liquidation or refinancing systems would be particularly vulnerable, as they would be unable to properly assess when to take action.

The timing aspect compounds the severity - the issue manifests not at stream creation or rate adjustment, but at the point of querying depletion time. This means a stream could appear perfectly healthy until someone attempts to check its depletion status, at which point the operation fails. This delayed failure pattern is especially dangerous as it could allow the creation of streams that later become impossible to properly manage or monitor.

In production environments, this could trigger a domino effect where initial function failures lead to stuck transactions, failed automated processes, and ultimately frozen funds as safety checks and management functions become inoperable. The economic impact extends beyond direct protocol interactions to any financial planning or risk assessment systems that depend on accurate stream depletion forecasts.

Fix

The proper fix requires explicit rate handling:

function depletionTimeOf(uint256 streamId)
external
view
returns (uint256 depletionTime)
{
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
// Early return for zero rate instead of reverting
if (ratePerSecond == 0) {
return 0; // Stream with zero rate never depletes
}
// Rest of the calculation...
uint256 solvencyPeriod = solvencyAmount / ratePerSecond; // Now safe from division by zero
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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