Flow

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

RatePerSecond Adjustment Lacks Balance Validation Leading to Instant Stream Depletion

Summary

The _adjustRatePerSecond function in Sablier allows stream senders to modify token streaming rates. When increasing rates, it should ensure the stream's remaining balance can sustain the new rate for a reasonable duration. However, the current implementation lacks this validation, potentially allowing rates that could drain streams instantly rather than maintaining gradual token distribution.

Here's the current vulnerable implementation:

function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
if (newRatePerSecond.unwrap() == _streams[streamId].ratePerSecond.unwrap()) {
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
if (ongoingDebtScaled > 0) {
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
_streams[streamId].ratePerSecond = newRatePerSecond;
}

The issue is that this implementation only checks if the new rate is different from the current rate. It doesn't validate whether the stream's balance can sustain the new rate for a reasonable duration. This allows for setting rates that could deplete the entire stream balance almost instantly.

Here's an example of how this can be exploited:

// Initial setup with reasonable rate
uint256 streamId = sablier.create(sender, recipient, ud21x18(1e18), token, false);
sablier.deposit(streamId, 100e18); // 100 tokens deposited
// Can be changed to drain immediately
sablier.adjustRatePerSecond(streamId, ud21x18(100e18)); // 100 tokens per second

In this scenario, a stream meant to last 100 seconds at 1 token/second can be modified to drain in just 1 second.

Impact

  • Streams can be configured to drain instantly rather than gradually

  • Users can bypass intended streaming duration by setting unsustainable rates

  • Recipients may not receive tokens over expected timeframes

  • Protocol's core streaming mechanism becomes unreliable

  • Risk of premature stream depletion

Fix

Here's how the function should be implemented with proper balance validation:

function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
if (newRatePerSecond.unwrap() == _streams[streamId].ratePerSecond.unwrap()) {
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}
uint128 remainingBalance = _streams[streamId].balance - _coveredDebtOf(streamId);
if (newRatePerSecond.unwrap() > 0) {
uint256 streamDuration = remainingBalance / newRatePerSecond.unwrap();
if (streamDuration < MINIMUM_STREAM_DURATION) {
revert Errors.SablierFlow_InsufficientBalanceForRate(
streamId,
remainingBalance,
newRatePerSecond
);
}
}
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
if (ongoingDebtScaled > 0) {
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
_streams[streamId].snapshotTime = uint40(block.timestamp);
_streams[streamId].ratePerSecond = newRatePerSecond;
}

This improved version adds crucial balance validation by:

  1. Calculating the remaining available balance

  2. Determining how long the stream can last at the new rate

  3. Ensuring the stream will last at least a minimum duration

  4. Reverting if the new rate would deplete the balance too quickly

This protection maintains the protocol's core purpose of gradual token streaming and prevents instant balance depletion through rate manipulation.

The fix ensures that any rate adjustment must allow the stream to maintain operation for a reasonable minimum duration, preserving the intended token distribution mechanism.

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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