Flow

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

Boundary Condition Error (Off-By-One Error) in SablierFlow::_coveredDebtOf can lead to incorrect results in edge cases where balance is exactly equal to totalDebt.

Summary

The SablierFlow::_coveredDebtOf function contains an off-by-one error in the conditional statement that checks if the stream balance is less than or equal to the total debt. Currently, the code only checks if balance < totalDebt, failing to account for cases where balance is exactly equal to totalDebt. This can result in incorrect calculations for covered debt in edge cases.

Vulnerability Details

In the SablierFlow::_coveredDebtOf function there is an off-by-one error in the condition that checks if the stream balance is less than or equal to the total debt. According to the comment description on the conditional statement If the stream balance is less than or equal to the total debt, return the stream balance. but the current implementation uses balance < totalDebt, which does not account for cases where balance is exactly equal to totalDebt.

// If the stream balance is less than or equal to the total debt, return the stream balance.
// @audit the comparison operator less than or equal to is
// wrongly used
if (balance < totalDebt) {
return balance;
}

Proof of Concept: In the modified testFuzz_PreDepletion function below, after computing the new expected covered debt, it checks if the equalbalance is exactly equal to totalDebt. If fail, it asserts that the boundary condition error affects correct result.

Proof of Code

Replace the testFuzz_PreDepletion function in coveredDebtOf.t.sol with the code below

function testFuzz_PreDepletion(
uint256 streamId,
uint40 warpTimestamp,
uint8 decimals
)
external
givenNotNull
givenNotPaused
{
(streamId, decimals,) = useFuzzedStreamOrCreate(streamId, decimals);
// Bound the time jump so that it is less than the depletion timestamp.
warpTimestamp = boundUint40(warpTimestamp, getBlockTimestamp(), uint40(flow.depletionTimeOf(streamId)) - 1);
// Simulate the passage of time.
vm.warp({ newTimestamp: warpTimestamp });
uint128 ratePerSecond = flow.getRatePerSecond(streamId).unwrap();
// Assert that the covered debt equals the ongoing debt.
uint256 actualCoveredDebt = flow.coveredDebtOf(streamId);
uint256 expectedCoveredDebt = getDescaledAmount(ratePerSecond * (warpTimestamp - OCT_1_2024), decimals);
assertEq(actualCoveredDebt, expectedCoveredDebt);
// Check that the balance equals the total debt, should return balance.
uint256 totalDebt = flow.totalDebtOf(streamId);
uint128 balance = flow.getBalance(streamId);
flow.deposit(streamId, (uint128(totalDebt) - balance), address(this), users.recipient);
uint128 equalBalance = flow.getBalance(streamId);
uint256 newActualCoveredDebt = flow.coveredDebtOf(streamId);
if (equalBalance == totalDebt) {
assertEq(newActualCoveredDebt, equalBalance);
}
}

OR add the code block below to the testFuzz_PreDepletion function in coveredDebtOf.t.sol

// Check that the balance equals the total debt, should return balance.
uint256 totalDebt = flow.totalDebtOf(streamId);
uint128 balance = flow.getBalance(streamId);
flow.deposit(streamId, (uint128(totalDebt) - balance), address(this), users.recipient);
uint128 equalBalance = flow.getBalance(streamId);
uint256 newActualCoveredDebt = flow.coveredDebtOf(streamId);
if (equalBalance == totalDebt) {
assertEq(newActualCoveredDebt, equalBalance);
}

Impact

This boundary condition error (Off-by-one error) may lead to incorrect calculations regarding covered debt, potentially results in:

  • Inaccurate reporting of available funds.

  • Users being unable to withdraw their exact due amounts when balance equals totalDebt.

Tools Used

  • Manual code review

  • Static analysis: Slither, aderyn, cloc

Recommendations

To mitigate the off-by-one error in the SablierFlow::_coveredDebtOf function, modify the condition to correctly check if balance is less than or equal to totalDebt. This will ensure accurate calculations in scenarios where balance is exactly equal to totalDebt, preventing potential withdrawal issues.

Code
function _coveredDebtOf(uint256 streamId) internal view returns (uint128) {
uint128 balance = _streams[streamId].balance;
// If the balance is zero, return zero.
if (balance == 0) {
return 0;
}
uint256 totalDebt = _totalDebtOf(streamId);
// If the stream balance is less than or equal to the total debt, return the stream balance.
// @audit the comparison operator less than or equal to is
// wrongly used
- if (balance < totalDebt) {
- return balance;
- }
+ if (balance < totalDebt) {
+ return balance;
+ }
// At this point, the total debt fits within `uint128`, as it is less than or equal to the balance.
return totalDebt.toUint128();
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[INVALID]`_coveredDebtOf` discrepancy between condition and comment `balance < totalDebt`

Appeal created

auditbyte Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[INVALID]`_coveredDebtOf` discrepancy between condition and comment `balance < totalDebt`

Support

FAQs

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