Flow

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

Inadequate Validation of Stream Parameters During Creation Can Cause a DoS

Summary

The _create function in the SablierFlow contract is responsible for the creation of new streams by initializing a Flow.Stream struct with user-provided parameters, including ratePerSecond. However, there is a lack of comprehensive validation for the ratePerSecond parameter to keep it within a reasonable range.

Vulnerability Details

The ratePerSecond value directly influences the debt accumulation rate, therefore an inadequate constraints may result in streams that are non-functional or destabilizing. Specifically, without setting bounds, it is possible to create streams with either an extremely low rate (e.g., zero) or an unusually high rate, which could lead to excessive gas consumption during updates causing Denial of Service for the stream owner.

function _create(
address sender,
address recipient,
UD21x18 ratePerSecond,
IERC20 token,
bool transferable
)
internal
returns (uint256 streamId)
{
// Check: the sender is not the zero address.
if (sender == address(0)) {
revert Errors.SablierFlow_SenderZeroAddress();
}
uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
// Check: the token decimals are not greater than 18.
if (tokenDecimals > 18) {
revert Errors.SablierFlow_InvalidTokenDecimals(address(token));
}
// Load the stream ID.
streamId = nextStreamId;
// Effect: create the stream.
_streams[streamId] = Flow.Stream({
balance: 0,
isStream: true,
isTransferable: transferable,
isVoided: false,
ratePerSecond: ratePerSecond,
sender: sender,
snapshotDebtScaled: 0,
snapshotTime: uint40(block.timestamp),
token: token,
tokenDecimals: tokenDecimals
});
// Using unchecked arithmetic because this calculation can never realistically overflow.
unchecked {
// Effect: bump the next stream ID.
nextStreamId = streamId + 1;
}
// Effect: mint the NFT to the recipient.
_mint({ to: recipient, tokenId: streamId });
// Log the newly created stream.
emit ISablierFlow.CreateFlowStream({
streamId: streamId,
sender: sender,
recipient: recipient,
ratePerSecond: ratePerSecond,
token: token,
transferable: transferable
});
}

Impact

Without setting up a bound to ratePerSecond value, it can expose it to two of its lowest and highest bound indices.

  • When ratePerSecond is set to zero, this means creating a stream with a zero rate, meaning no tokens will ever be streamed, rendering the stream non-functional.

  • Setting up ratePerSecond is set to extremely high value, can lead to arithmetic overflow during the calculation, causing the transactions to revert and thus causing a DoS conditions for the stream owner.

Exploit Prerequisites:

  • A user can call the _create function to create a new stream.

  • The user provides a ratePerSecond value of excessively large number.

  • Using an extremely high ratePerSecond can cause arithmetic operations such as multiplication in _ongoingDebtScaledOf to overflow.

/// @dev Calculates the ongoing debt, as a 18-decimals fixed point number, accrued since last snapshot. Return 0 if
/// the stream is paused or `block.timestamp` is less than or equal to snapshot time.
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; /// @audit - `uint256` * `uint256` can overflow for high values
}

For a zero rate, the impact is minimal since creating a stream with a zero rate does not affect other users or the protocol. The stream owner may be confused by the non-functional stream.

But for a a higher rate, transactions involving the affected stream will revert due to arithmetic overflows. However, the issue is isolated to the stream with the excessive rate and does not impact other streams or users. Therefore it results to a denial-of-service for the stream owner, who cannot interact with their own stream.

Tools Used

Manual Review

Recommendations

It is essential to validate ratePerSecond to bound it to a reasonable maximum limit in order to prevent overflows:

uint256 MAX_RATE_PER_SECOND = type(uint256).max / MAX_ELAPSED_TIME; // Define MAX_ELAPSED_TIME based on context
if (ratePerSecond.unwrap() > MAX_RATE_PER_SECOND) {
revert Errors.SablierFlow_RatePerSecondTooHigh(streamId, ratePerSecond);
}
Updates

Lead Judging Commences

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