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 {
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);
_streams[streamId].ratePerSecond = newRatePerSecond;
}
Problems this creates:
Extremely High Rates:
sablier.adjustRatePerSecond(streamId, ud21x18(1000e18));
Near-Zero Rates:
sablier.adjustRatePerSecond(streamId, ud21x18(1));
Balance Depletion:
uint128 balance = _streams[streamId].balance;
Impacts
-
Stream Disruption: Setting extremely high rates can drain streams instantly, disrupting the intended gradual token distribution mechanism
-
Economic Waste: Very low rates create dust amounts, leading to:
-
User Experience:
Recipients may receive tokens too quickly or too slowly
Stream durations become unpredictable
Balance depletion before intended end time
-
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
uint256 constant MIN_RATE = 1e6;
uint256 constant MAX_RATE = 1e24;
uint256 constant MIN_STREAM_DURATION = 1 hours;
function _adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) internal {
if (newRatePerSecond.unwrap() == _streams[streamId].ratePerSecond.unwrap()) {
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}
if (newRatePerSecond.unwrap() > 0 && newRatePerSecond.unwrap() < MIN_RATE) {
revert Errors.SablierFlow_RateTooLow(streamId, newRatePerSecond);
}
if (newRatePerSecond.unwrap() > MAX_RATE) {
revert Errors.SablierFlow_RateTooHigh(streamId, newRatePerSecond);
}
if (newRatePerSecond.unwrap() > 0) {
uint128 balance = _streams[streamId].balance;
uint128 coveredDebt = _coveredDebtOf(streamId);
uint128 availableBalance = balance - coveredDebt;
uint256 streamDuration = availableBalance / newRatePerSecond.unwrap();
if (streamDuration < MIN_STREAM_DURATION) {
revert Errors.SablierFlow_InsufficientBalance(
streamId,
availableBalance,
newRatePerSecond
);
}
}
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
if (ongoingDebtScaled > 0) {
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
_streams[streamId].snapshotTime = uint40(block.timestamp);
_streams[streamId].ratePerSecond = newRatePerSecond;
}