Flow

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

Incorrect Withdrawal Handling During Stream Pause

Summary

Incorrect Withdrawal Handling During Stream Pause

Vulnerability Details

function _withdraw(
uint256 streamId,
address to,
uint128 amount
)
internal
returns (uint128 withdrawnAmount, uint128 protocolFeeAmount)
{
// Check: the withdraw amount is not zero.
if (amount == 0) {
revert Errors.SablierFlow_WithdrawAmountZero(streamId);
}
// Check: the withdrawal address is not zero.
if (to == address(0)) {
revert Errors.SablierFlow_WithdrawToZeroAddress(streamId);
}
// Check: `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address
// must be the recipient.
if (to != _ownerOf(streamId) && !_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierFlow_WithdrawalAddressNotRecipient({ streamId: streamId, caller: msg.sender, to: to });
}
uint8 tokenDecimals = _streams[streamId].tokenDecimals;
// Calculate the total debt.
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);
}

// Stream rate is used to determine if paused

uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();

// But withdrawal logic doesn't check this

// Example: Paused with Pending Debt

Stream {

balance: 1000 tokens

snapshotDebtScaled: 800 tokens (scaled)

ratePerSecond: 0 (paused)
}

// Current behavior: Allows withdrawal based on total debt

// Correct behavior: Should only allow withdrawal of snapshotDebt

// Another Example : Paused with Zero Debt

Stream {

balance: 1000 tokens

snapshotDebtScaled: 0

ratePerSecond: 0 (paused)
}

// Current behavior: Might allow full balance withdrawal

// Correct behavior: Should check pause conditions

Impact

Miscalculation of withdrawable amounts. Potential for excess withdrawals and Incorrect debt accounting.

Tools Used

Manual Review

Recommendations

Implement Proper Pause State Handling. Calculate withdrawable amount differently for paused streams. Process withdrawal with pause awareness. For paused streams, only consider snapshot debt

Updates

Lead Judging Commences

inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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