Sablier

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

Vulnerability in SablierV2LockupLinear::_calculateStreamedAmount Leading to Denial of Service (DoS) Due to Underflow For Function Who Inherits

Summary

The function SablierV2LockupLinear::_calculateStreamedAmount does not verify whether startTime is greater than blockTimestamp. This oversight means that if the current stream has not yet started, invoking _calculateStreamedAmount will result in an error and a revert. This could potentially lead to Denial of Service (DoS) vulnerabilities.

Vulnerability Details

The SablierV2LockupLinear::_calculateStreamedAmount function does not check whether startTime is greater than blockTimestamp. This situation causes an elapsedTime math operations to underflow specifically in the calculation ud(blockTimestamp - startTime). Although it verifies if cliffTime > blockTimestamp, the absence of a check for startTime can lead to an error during the conversion of streamedAmount from UD60x18 to uint256. This oversight can result in a Denial of Service (DoS) vulnerability under certain conditions.

This issue affects almost all functions that inherit _calculateStreamedAmount, including SablierV2Lockup::refundableAmountOf, SablierV2Lockup::_streamedAmountOf, and SablierV2Lockup::_cancel (as well as SablierV2Lockup::cancel). When these functions are invoked before the stream has started, they can encounter errors, potentially disrupting their normal operation and exposing the contract to DoS attacks.

function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) {
uint256 cliffTime = uint256(_cliffs[streamId]);
uint256 blockTimestamp = block.timestamp;
// If the cliff time is in the future, return zero.
if (cliffTime > blockTimestamp) {
return 0;
}
uint256 endTime = uint256(_streams[streamId].endTime);
if (blockTimestamp >= endTime) {
return _streams[streamId].amounts.deposited;
}
unchecked {
uint256 startTime = uint256(_streams[streamId].startTime);
// @audit Doesn't check startTime > blockTimestamp. Potentially error and underflow
UD60x18 elapsedTime = ud(blockTimestamp - startTime);
UD60x18 totalDuration = ud(endTime - startTime);
UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration);
UD60x18 depositedAmount = ud(_streams[streamId].amounts.deposited);
UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmount);
if (streamedAmount.gt(depositedAmount)) {
return _streams[streamId].amounts.withdrawn;
}
return uint128(streamedAmount.intoUint256());
}
}

Proof Of Code

Proof of Concept for [Potential DoS in calculateStreamedAmount function with certain conditions]

Overview:

calculateStreamedAmount doesn't check whether startTime is greater than blockTimestamp.

Actors:

Working Test Case:

Please paste code below in new file:

function calculateStreamedAmount() public pure returns (uint128) {
// small number are added for simplicity.
uint256 cliffTime = 60;
uint256 blockTimestamp = 119;
// If the cliff time is in the future, return zero.
if (cliffTime > blockTimestamp) {
return 0;
}
uint256 endTime = 150;
if (blockTimestamp >= endTime) {
return 0;
}
unchecked {
uint256 startTime = 120;
// @audit Doesn't check startTime > blockTimestamp. Potentially error and underflow
UD60x18 elapsedTime = ud(blockTimestamp - startTime);
UD60x18 totalDuration = ud(endTime - startTime);
UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration);
UD60x18 depositedAmount = ud(100e18);
UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmount);
if (streamedAmount.gt(depositedAmount)) {
return 0;
}
return uint128(streamedAmount.intoUint256());
}
}

Test file:

function test_CalculateStreamedAmount() public {
uint256 number = lockupLinear.calculateStreamedAmount();
}

And you will get this error:
[FAIL. Reason: PRBMath_MulDiv_Overflow(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], 1000000000000000000 [1e18], 30)] test_CalculateStreamedAmount() (gas: 8733)

Impact

  • If the sender inputs the wrong recipient, they will be unable to cancel the stream until it has started, leading to potential loss. For example, if the sender sets up a stream of 500,000 with a one-day duration, and the stream begins for only 10 seconds, approximately 57.87 will be sent to the unintended recipient.

  • Sender will unable to cancel the stream if the stream's status is pending. This does not reflect what is written in the docs where ISablierV2Lockup::cancel could be implemented while the status is warm (Pending and Streaming).

  • SablierV2Lockup::_streamedAmountOf will always return error if the stream's current status is pending.

  • SablierV2Lockup::refundableAmountOf will return error if isCancelable == true and isDepleted == false because _calculateStreamedAmount used in the math operation for refundableAmount variable.

Tools Used

Manual Review

Recommendations

function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) {
uint256 cliffTime = uint256(_cliffs[streamId]);
uint256 blockTimestamp = block.timestamp;
if (cliffTime > blockTimestamp) {
return 0;
}
+ uint256 startTime = uint256(_streams[streamId].startTime);
+ if(startTime > blockTimestamp) {
+ return 0;
+ }
uint256 endTime = uint256(_streams[streamId].endTime);
if (blockTimestamp >= endTime) {
return _streams[streamId].amounts.deposited;
}
unchecked {
- uint256 startTime = uint256(_streams[streamId].startTime);
// @audit Doesn't check startTime > blockTimestamp. Potentially error and underflow
UD60x18 elapsedTime = ud(blockTimestamp - startTime);
UD60x18 totalDuration = ud(endTime - startTime);
UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration);
UD60x18 depositedAmount = ud(_streams[streamId].amounts.deposited);
UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmount);
if (streamedAmount.gt(depositedAmount)) {
return _streams[streamId].amounts.withdrawn;
}
return uint128(streamedAmount.intoUint256());
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

In LL context `_calculateStreamedAmount` reverts if start time is in the future and clif = 0

Support

FAQs

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