Flow

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

Lack of Validation on `ratePerSecond` in `adjustRatePerSecond` Function

Summary

The adjustRatePerSecond function in the SablierFlow contract allows the sender to modify the ratePerSecond of a stream without any upper limit validation. There is no check to ensure that the newRatePerSecond is within safe and reasonable bounds. As a result, setting an excessively high ratePerSecond can lead to arithmetic overflows in functions that rely on this value, particularly during debt calculations. This can cause transactions to revert and also render the stream unusable for both the sender and the recipient.

Vulnerability Details

https://github.com/Cyfrin/2024-10-sablier/blob/8a2eac7a916080f2022527408b004578b21c51d0/src/SablierFlow.sol#L189

The adjustRatePerSecond function lacks upper limit validation:

function adjustRatePerSecond(
uint256 streamId,
UD21x18 newRatePerSecond
)
external
override
noDelegateCall
notNull(streamId)
notPaused(streamId)
onlySender(streamId)
updateMetadata(streamId)
{
UD21x18 oldRatePerSecond = _streams[streamId].ratePerSecond;
// Effects and Interactions: adjust the rate per second.
_adjustRatePerSecond(streamId, newRatePerSecond);
// Log the adjustment.
emit ISablierFlow.AdjustFlowStream({
streamId: streamId,
totalDebt: _totalDebtOf(streamId),
oldRatePerSecond: oldRatePerSecond,
newRatePerSecond: newRatePerSecond
});
}

The _adjustRatePerSecond function directly assigns the new rate without validation:

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);
}
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
// Update the snapshot debt only if the stream has ongoing debt.
if (ongoingDebtScaled > 0) {
// Effect: update the snapshot debt.
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
// Effect: update the snapshot time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
// Effect: set the new rate per second.
_streams[streamId].ratePerSecond = newRatePerSecond;
}

The _ongoingDebtScaledOf function uses ratePerSecond in arithmetic operations:

function _ongoingDebtScaledOf(uint256 streamId) internal view returns (uint256) {
uint256 blockTimestamp = block.timestamp;
uint256 snapshotTime = _streams[streamId].snapshotTime;
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
// Check: if the rate per second is zero or the `block.timestamp` is less than the `snapshotTime`.
if (ratePerSecond == 0 || blockTimestamp <= snapshotTime) {
return 0;
}
uint256 elapsedTime;
// Safe to use unchecked because subtraction cannot underflow.
unchecked {
// Calculate time elapsed since the last snapshot.
elapsedTime = blockTimestamp - snapshotTime;
}
// Calculate the ongoing debt scaled accrued by multiplying the elapsed time by the rate per second.
return elapsedTime * ratePerSecond;
}

Exploit Prerequisites:

  • The sender has control over a stream and can call adjustRatePerSecond. The sender intends to set an excessively high ratePerSecond.

  • The sender calls adjustRatePerSecond with a newRatePerSecond value that is extremely large.

sablierFlow.adjustRatePerSecond(streamId, UD21x18.wrap(veryLargeValue));
  • Functions that calculate debt, such as _ongoingDebtScaledOf, attempt to compute elapsedTime * ratePerSecond.

  • If ratePerSecond is excessively high, this multiplication can exceed uint256 limits, causing an overflow.

Impact

Both sender and recipient are affected, as they cannot interact with the stream without encountering errors. The issue affects individual streams where the sender sets an excessive rate. Other streams and the overall protocol remain unaffected.

Tools Used

Manual Review

Recommendations

(1) Use SafeMath or Solidity's checked arithmetic to handle potential overflows gracefully. Catch overflows and provide meaningful error messages instead of allowing reverts without context.

Or,

(2) Introduce a cap on the ratePerSecond to prevent values that can cause overflows. The maximum value should be calculated based on the token's decimals and the maximum expected elapsed time.

uint256 maxRatePerSecond = type(uint256).max / MAX_EXPECTED_ELAPSED_TIME;
if (newRatePerSecond.unwrap() > maxRatePerSecond) {
revert Errors.SablierFlow_RatePerSecondTooHigh(newRatePerSecond, maxRatePerSecond);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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