Flow

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

Unconstrained Stream Rate Adjustment in Sablier Protocol Allows Extreme Rates and Premature Balance Depletion

Summary

The _adjustRatePerSecond function is critical to Sablier's streaming protocol, enabling senders to modify how quickly tokens flow to recipients. The function handles the rate updates while maintaining debt calculations and stream integrity. Its implementation directly affects how reliably and safely the protocol can deliver its core token streaming functionality.

The issue is that _adjustRatePerSecond doesn't validate rate bounds, which could lead to problematic scenarios:

function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
// Only checks if rate is different, no bounds checking
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].snapshotTime = uint40(block.timestamp);
// Sets new rate without any validation
_streams[streamId].ratePerSecond = newRatePerSecond;
}

Problems this creates:

  1. Extremely High Rates:

// Can set extremely high rates that could drain stream quickly
// Example: If balance is 1000 tokens
sablier.adjustRatePerSecond(streamId, ud21x18(1000e18)); // 1000 tokens per second
  1. Near-Zero Rates:

// Can set rates so low they're effectively useless
// Example: Dust amounts per second
sablier.adjustRatePerSecond(streamId, ud21x18(1)); // 1 wei per second
  1. Balance Depletion:

uint128 balance = _streams[streamId].balance;
// No check if new rate would deplete balance too quickly
// Example: 100 token balance with 1000 token/second rate
// Would deplete in < 1 second

Impacts

  1. Stream Disruption: Setting extremely high rates can drain streams instantly, disrupting the intended gradual token distribution mechanism

  2. Economic Waste: Very low rates create dust amounts, leading to:

    • Unusable locked tokens

    • Failed transactions due to minimum transfer limits

    • Wasted gas on negligible transfers

  3. User Experience:

    • Recipients may receive tokens too quickly or too slowly

    • Stream durations become unpredictable

    • Balance depletion before intended end time

  4. Protocol Stability:

    • Extreme rates could stress protocol calculations

    • Potential for contract interactions to fail

    • Risk of numeric overflow with very high rates

While it doesn't lead to direct fund loss, it can significantly disrupt the protocol's core streaming functionality and user experience.

Recommended fix

// Add these constants
uint256 constant MIN_RATE = 1e6; // Minimum meaningful rate (e.g., 0.000001 tokens/sec)
uint256 constant MAX_RATE = 1e24; // Maximum safe rate
uint256 constant MIN_STREAM_DURATION = 1 hours; // Minimum time stream should last
function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
// Check current rate
if (newRatePerSecond.unwrap() == _streams[streamId].ratePerSecond.unwrap()) {
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}
// Validate minimum rate (if not zero)
if (newRatePerSecond.unwrap() > 0 && newRatePerSecond.unwrap() < MIN_RATE) {
revert Errors.SablierFlow_RateTooLow(streamId, newRatePerSecond);
}
// Validate maximum rate
if (newRatePerSecond.unwrap() > MAX_RATE) {
revert Errors.SablierFlow_RateTooHigh(streamId, newRatePerSecond);
}
// Check if rate is sustainable given current balance
if (newRatePerSecond.unwrap() > 0) {
uint128 balance = _streams[streamId].balance;
uint128 coveredDebt = _coveredDebtOf(streamId);
uint128 availableBalance = balance - coveredDebt;
// Calculate how long stream can maintain this rate
uint256 streamDuration = availableBalance / newRatePerSecond.unwrap();
if (streamDuration < MIN_STREAM_DURATION) {
revert Errors.SablierFlow_InsufficientBalance(
streamId,
availableBalance,
newRatePerSecond
);
}
}
// Rest of the original function...
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
if (ongoingDebtScaled > 0) {
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
_streams[streamId].snapshotTime = uint40(block.timestamp);
_streams[streamId].ratePerSecond = newRatePerSecond;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 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.