Flow

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

SnapshotDebtScaled can never become 0

Summary

In the withdraw function, due to a precision loss the snapshotDebtScaledcant be made 0. Basically the entire stream cant be withdrawn by the recipient.

Vulnerability Details

Assume the following scenario:
1. totalDebtScaled = 111111111111111111 (18 1's)
2. totalDebt = 111111 (assume token with 6 decimals)
3. If the balance is 30e6, and the amount = 111111 (6 1's), amountScaled = 111111e12
4. Thus the snapshotDebtScaled = 111111111111111111 - 111111e12 = 111111111111 (12 1's).

As can be seen this leftover amount can never be made 0 (provided such a scenario occurs in the first place: which can occur given the rps)(basically this scenarios likelihood relies on the rps provided). Since the debt can never be made 0, the recipient can never completely withdraw his entire amount (Always a small amount will remain, provided a token with <18 decimals is used).

uint256 totalDebtScaled = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
uint256 totalDebt = Helpers.descaleAmount(totalDebtScaled, tokenDecimals);
// Calculate the withdrawable amount.
uint128 balance = _streams[streamId].balance;
uint128 withdrawableAmount;
if (balance < totalDebt) {
// If the stream balance is less than the total debt, the withdrawable amount is the balance.
withdrawableAmount = balance;
} else {
// Otherwise, the withdrawable amount is the total debt.
withdrawableAmount = totalDebt.toUint128();
}
// Check: the withdraw amount is not greater than the withdrawable amount.
if (amount > withdrawableAmount) {
revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount);
}
// Calculate the amount scaled.
uint256 amountScaled = Helpers.scaleAmount(amount, tokenDecimals);
// Safe to use unchecked, `amount` cannot be greater than the balance or total debt at this point.
unchecked {
// If the amount is less than the snapshot debt, reduce it from the snapshot debt and leave the snapshot
// time unchanged.
if (amountScaled <= _streams[streamId].snapshotDebtScaled) {
_streams[streamId].snapshotDebtScaled -= amountScaled;
}
// Else reduce the amount from the ongoing debt by setting snapshot time to `block.timestamp` and set the
// snapshot debt to the remaining total debt.
else {
_streams[streamId].snapshotDebtScaled = totalDebtScaled - amountScaled;
// Effect: update the stream time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
}
// Effect: update the stream balance.
_streams[streamId].balance -= amount;
}

(https://github.com/Cyfrin/2024-10-sablier/blob/8a2eac7a916080f2022527408b004578b21c51d0/src/SablierFlow.sol#L799-L840)

Impact

snapshotDebtScaled can never reach 0.

Tools Used

Manual review

Recommendations

Compare using the scaled quantities instead of the descaled ones here.

if (balance < totalDebt) {
// If the stream balance is less than the total debt, the withdrawable amount is the balance.
withdrawableAmount = balance;
} else {
// Otherwise, the withdrawable amount is the total debt.
withdrawableAmount = totalDebt.toUint128();
}
// Check: the withdraw amount is not greater than the withdrawable amount.
if (amount > withdrawableAmount) {
revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 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.