Flow

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

Unbounded Debt Accumulation in Sablier's Streaming Calculations Risks Protocol Integration Failures and Gas Inefficiency

Summary

The _ongoingDebtScaledOf function calculates streaming debt as a simple multiplication of elapsed time and rate per second, without any upper bounds or periodic settlement requirements. This calculation is fundamental to the protocol's accounting system, used in withdrawals, stream status checks, and interoperability with other protocols. The absence of limits allows debt to grow indefinitely as streams run longer or use higher rates.

The issue stems from how debt accumulates without upper limits:

function _ongoingDebtScaledOf(uint256 streamId) internal view returns (uint256) {
uint256 blockTimestamp = block.timestamp;
uint256 snapshotTime = _streams[streamId].snapshotTime;
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
if (ratePerSecond == 0 || blockTimestamp <= snapshotTime) {
return 0;
}
unchecked {
elapsedTime = blockTimestamp - snapshotTime;
}
// Debt grows unbounded: elapsedTime * ratePerSecond
// No maximum time limit or debt ceiling
return elapsedTime * ratePerSecond;
}
function _totalDebtOf(uint256 streamId) internal view returns (uint256) {
uint256 totalDebtScaled = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
// Can grow very large over time
return Helpers.descaleAmount({ amount: totalDebtScaled, decimals: _streams[streamId].tokenDecimals });
}

The debt calculation simply multiplies elapsed time by rate, allowing for:

  1. Unlimited time accumulation

  2. No maximum debt ceiling

  3. No periodic settlement requirement

  4. Growing computational costs with larger numbers

For example, a stream running for years with a high rate could accumulate massive debt values, potentially causing issues with gas costs, numerical handling, and protocol integrations.

Impact

The unbounded growth in debt calculations creates increasing computational overhead as numbers grow larger, leading to higher gas costs over time. More critically, extremely large debt values can cause integration failures with protocols that have stricter numerical bounds or different decimal handling. When these large values interact with other DeFi protocols' mathematical operations or storage limitations, it could result in transaction failures or incorrect state transitions, effectively breaking interoperability for long-running or high-rate streams.

Fix

contract SablierFlow {
// Add constants for bounds
uint256 public constant MAX_STREAM_DURATION = 365 days * 5; // 5 years
uint256 public constant MAX_TOTAL_DEBT = 1e40; // Reasonable ceiling
function _ongoingDebtScaledOf(uint256 streamId) internal view returns (uint256) {
uint256 blockTimestamp = block.timestamp;
uint256 snapshotTime = _streams[streamId].snapshotTime;
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
if (ratePerSecond == 0 || blockTimestamp <= snapshotTime) {
return 0;
}
uint256 elapsedTime = blockTimestamp - snapshotTime;
// Enforce maximum duration
require(elapsedTime <= MAX_STREAM_DURATION, "Stream duration exceeded");
uint256 ongoingDebt = elapsedTime * ratePerSecond;
// Check total debt ceiling
require(ongoingDebt <= MAX_TOTAL_DEBT, "Debt ceiling exceeded");
return ongoingDebt;
}
// Add periodic settlement requirement
function settleStream(uint256 streamId) external {
_updateSnapshot(streamId);
emit StreamSettled(streamId, block.timestamp);
}
}

This fix adds reasonable bounds for stream duration and total debt, plus a settlement mechanism to periodically reset accumulated debt. This prevents unbounded growth while maintaining protocol functionality.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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