Flow

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

Incorrect `depletionTime` Value When Adjusting `ratePerSecond` Within the Same Block.

Vulnerability Details

The SablierFlow::depletionTimeOf function calculates the time at which a stream will run out of funds. It does this by dividing the excess scaled balance by the ratePerSecond to determine the number of remaining seconds. If the totalDebt is greater than the streamBalance, then depletionTime should be zero.

However, an issue arises in a solvent stream (totalDebt < streamBalance). If you adjust ratePerSecond to a value greater than the current streamBalance and then call depletionTimeOf within the same block (using a batch operation), the depletionTime will be incorrect. This results in an unrealistic number of seconds, likely returning either the snapshotTime or snapshotTime + 1 instead. This happens because solvencyPeriod becomes zero, as streamBalance is divided by ratePerSecond when streamBalance < ratePerSecond:

function depletionTimeOf(uint256 streamId)
external
view
override
notNull(streamId)
notPaused(streamId)
returns (uint256 depletionTime)
{
// ...
// If the total debt exceeds balance, return zero.
if (snapshotDebtScaled + _ongoingDebtScaledOf(streamId) >= balanceScaled + oneMVTScaled) {
return 0;
}
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
unchecked {
uint256 solvencyAmount = balanceScaled - snapshotDebtScaled + oneMVTScaled;
uint256 solvencyPeriod = solvencyAmount / ratePerSecond;
// If the division is exact, return the depletion time.
if (solvencyAmount % ratePerSecond == 0) {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
}
// Otherwise, round up before returning since the division by rate per second has round down the result.
else {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod + 1;
}
}
}

At first glance, this issue might not seem significant, but when other protocols integrate with Sablier, it can become a major problem, as this type of data can be crucial in determining whether certain operations are allowed.

Impact

Incorrect depletionTime value

PoC

Scenario:

  • Start timestamp: October-01-2024

  • Deposit: 50_000e6

  • Wrap 30 days: accrue some debt

  • In the same block:

    • Adjust rate: 100_000e18 tokens per second.

    • Call depletionTimeOf: get incorrect value (should be 0)

Paste the following code in /tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol

Before executing the test, paste this import at the beginning of the contract:

import {console} from "forge-std/src/console.sol";
function test_IncorrectDepletionTime() external {
// Return to October 1 2024
vm.warp(OCT_1_2024);
// Deposit 50_000e6
flow.deposit(defaultStreamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient);
// Accrue some debt
vm.warp(WARP_ONE_MONTH);
// Inflate the rpm value, greater than the balance (100_000e18)
UD21x18 newRatePerSecond = ud21x18(100_000e18);
flow.adjustRatePerSecond({ streamId: defaultStreamId, newRatePerSecond: newRatePerSecond });
// Incorrect value of depletion time
uint40 actualDepletionTime = uint40(flow.depletionTimeOf(defaultStreamId));
uint256 expectedDepletionTime = 0;
assertNotEq(actualDepletionTime, expectedDepletionTime);
console.log("depletion of time: ", actualDepletionTime);
console.log("block timestamp: ", block.timestamp);
}

Recommendations

After calculating solvencyPeriod, add a check to return zero if solvencyPeriod is zero.

unchecked {
uint256 solvencyAmount = balanceScaled - snapshotDebtScaled + oneMVTScaled;
uint256 solvencyPeriod = solvencyAmount / ratePerSecond;
+ if (solvencyPeriod == 0) {
+ return 0;
+ }
// If the division is exact, return the depletion time.
if (solvencyAmount % ratePerSecond == 0) {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
}
// Otherwise, round up before returning since the division by rate per second has round down the result.
else {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod + 1;
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

mrkaplan Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
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.