Flow

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

Precision Loss in Stream Depletion Time Calculation Causes Inaccurate Early Stream Termination

Summary

The depletionTimeOf function calculates stream insolvency timing through integer division of scaled token amounts by rate per second. While it attempts to handle non-exact divisions by adding one second for any remainder, this simplistic rounding mechanism falls short when dealing with significant remainders. The function's core calculation solvencyPeriod = solvencyAmount / ratePerSecond represents a critical precision point where valuable streaming duration data can be lost through truncation, affecting the accuracy of stream timing predictions.

The precision loss issue in depletionTimeOf centers on the division operation that calculates solvencyPeriod:

https://github.com/Cyfrin/2024-10-sablier/blob/main/src/SablierFlow.sol#L57

function depletionTimeOf(uint256 streamId) ... {
// ... scaling calculations ...
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
unchecked {
uint256 solvencyAmount = balanceScaled - snapshotDebtScaled + oneMVTScaled;
uint256 solvencyPeriod = solvencyAmount / ratePerSecond; // Integer division truncates
// Attempt to handle truncation with simple +1
if (solvencyAmount % ratePerSecond == 0) {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
} else {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod + 1;
}
}
}

While the code attempts to handle non-exact division by adding 1 to the period when there's a remainder, this crude rounding can be problematic when dealing with large rates or amounts. Consider:

// Example scenario
solvencyAmount = 1000000 (1M tokens scaled)
ratePerSecond = 333333 (0.333333 tokens/sec)
solvencyPeriod = 1000000 / 333333 = 3 // truncated
remainder = 1 (almost an entire period lost)

The +1 compensation becomes inadequate when the remainder represents a significant portion of the rate, leading to materially incorrect depletion time calculations. This imprecision can cause streams to deplete earlier than the actual available funds would allow.

Impact

The precision loss in the depletion time calculation creates a systemic underestimation of stream durations, particularly pronounced with large amounts or non-standard rates. When integer division truncates significant remainders, adding a single second fails to adequately compensate for the lost precision. This means streams consistently report earlier depletion times than their actual token reserves would support, leading to premature settlement triggers and incorrect solvency assessments in integrated DeFi protocols. The cumulative effect ripples through any financial planning or risk assessment systems depending on accurate stream forecasting, potentially triggering unnecessary liquidations or missed streaming opportunities due to artificially shortened duration calculations.

Fix

function depletionTimeOf(uint256 streamId)
external
view
returns (uint256 depletionTime)
{
uint128 balance = _streams[streamId].balance;
if (balance == 0) return 0;
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
if (ratePerSecond == 0) return 0; // Handle zero rate case
uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 balanceScaled = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });
uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;
uint256 oneMVTScaled = Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
uint256 totalDebtScaled = snapshotDebtScaled + _ongoingDebtScaledOf(streamId);
if (totalDebtScaled >= balanceScaled + oneMVTScaled) return 0;
unchecked {
uint256 solvencyAmount = balanceScaled - snapshotDebtScaled + oneMVTScaled;
// More precise duration calculation
uint256 wholePeriods = solvencyAmount / ratePerSecond;
uint256 remainder = solvencyAmount % ratePerSecond;
// If remainder is more than half the rate, round up
uint256 additionalSeconds = (remainder > (ratePerSecond / 2)) ? 1 : 0;
return _streams[streamId].snapshotTime + wholePeriods + additionalSeconds;
}
}

The key improvement is implementing proper "round-to-nearest" logic instead of always rounding up for any remainder. When the remainder exceeds half the rate per second, we round up; otherwise, we round down. This provides more accurate depletion time calculations, particularly important for:

  1. High-value streams where precision matters

  2. Non-standard rates where remainders can be significant

  3. Integration contracts requiring precise timing

For example, if solvencyAmount = 1000000 and ratePerSecond = 333333, the old code would potentially underestimate by nearly a full period, while the new code makes a more informed rounding decision based on the actual size of the remainder relative to the rate.

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.