Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: medium
Invalid

Streamed Amount Check Issue in _calculateStreamedAmountForOneSegment Function

Summary

The _calculateStreamedAmountForOneSegment function in the SablierV2LockupDynamic contract contains logic that checks if the calculated streamed amount exceeds the deposited amount. If it does, the function defaults to returning the withdrawn amount. While this prevents returning an incorrect streamed amount, it can mask underlying issues in the calculation or segment configuration, potentially leading to unnoticed errors.

function _calculateStreamedAmountForOneSegment(uint256 streamId) internal view returns (uint128) {
unchecked {
// Calculate how much time has passed since the stream started, and the stream's total duration.
SD59x18 elapsedTime = (uint40(block.timestamp) - _streams[streamId].startTime).intoSD59x18();
SD59x18 totalDuration = (_streams[streamId].endTime - _streams[streamId].startTime).intoSD59x18();
// Divide the elapsed time by the stream's total duration.
SD59x18 elapsedTimePercentage = elapsedTime.div(totalDuration);
// Cast the stream parameters to SD59x18.
SD59x18 exponent = _segments[streamId][0].exponent.intoSD59x18();
SD59x18 depositedAmount = _streams[streamId].amounts.deposited.intoSD59x18();
// Calculate the streamed amount using the special formula.
SD59x18 multiplier = elapsedTimePercentage.pow(exponent);
SD59x18 streamedAmount = multiplier.mul(depositedAmount);
// Although the streamed amount should never exceed the deposited amount, this condition is checked
// without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn
// amount is considered to be the streamed amount, and the stream is effectively frozen.
if (streamedAmount.gt(depositedAmount)) {
return _streams[streamId].amounts.withdrawn;
}
// Cast the streamed amount to uint128. This is safe due to the check above.
return uint128(streamedAmount.intoUint256());
}
}

Proof of Concept (PoC)

  1. A user creates a stream with a single segment and an exponent that could lead to a miscalculated streamed amount.

  2. The _calculateStreamedAmountForOneSegment function is called, and due to the exponentiation, the calculated amount exceeds the deposited amount.

  3. Instead of flagging an error, the function returns the amount already withdrawn, which may not accurately represent the actual streamed amount.

  4. The user or the contract relies on this returned value for further operations, such as withdrawals, potentially leading to the user withdrawing less than they are entitled to or creating confusion about the stream's status.

  5. Over time, users may notice discrepancies between expected and actual withdrawal amounts, undermining confidence in the platform's financial accuracy and reliability.

Impact

The function includes a check to prevent the returned streamed amount from exceeding the total deposited amount. If the calculation results in a higher value than the deposited amount, the function defaults to returning the amount already withdrawn. This could potentially mask underlying issues with the segment's configuration or the calculation logic, leading to incorrect streaming behavior.

Tools Used

Manual Review

Recommendations

Here is a recommended modified function:

function _calculateStreamedAmountForOneSegment(uint256 streamId) internal view returns (uint128) {
require(_streams[streamId].startTime != 0, "Stream does not exist");
unchecked {
// Calculate how much time has passed since the stream started, and the stream's total duration.
SD59x18 elapsedTime = (uint40(block.timestamp) - _streams[streamId].startTime).intoSD59x18();
SD59x18 totalDuration = (_streams[streamId].endTime - _streams[streamId].startTime).intoSD59x18();
// Divide the elapsed time by the stream's total duration.
SD59x18 elapsedTimePercentage = elapsedTime.div(totalDuration);
// Cast the stream parameters to SD59x18.
SD59x18 exponent = _segments[streamId][0].exponent.intoSD59x18();
SD59x18 depositedAmount = _streams[streamId].amounts.deposited.intoSD59x18();
// Calculate the streamed amount using the special formula.
SD59x18 multiplier = elapsedTimePercentage.pow(exponent);
SD59x18 streamedAmount = multiplier.mul(depositedAmount);
// Cap the streamed amount at the deposited amount.
if (streamedAmount.gt(depositedAmount)) {
streamedAmount = depositedAmount;
}
// Cast the streamed amount to uint128. This is safe due to the cap above.
return uint128(streamedAmount.intoUint256());
} }

This updated _calculateStreamedAmountForOneSegment function includes a modification to the logic that handles the case when the calculated streamed amount exceeds the deposited amount. Instead of returning the amount already withdrawn, it now caps the streamed amount at the deposited amount. This ensures that the function never reports more funds as streamed than have actually been deposited, which aligns with the expected behavior of the streaming calculation.

By capping the streamed amount, this updated function resolves the issue by preventing the return of an incorrect amount that could mislead users or result in erroneous financial transactions. It also eliminates the need for the event emission that was previously added to log when the streamed amount exceeded the deposited amount, as this condition will no longer occur with the new logic in place.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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