Flow

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

Incorrect Partial Debt Write-off

Summary

Incorrect Partial Debt Write-off

Vulnerability Details

function _void(uint256 streamId) internal {
// Check: `msg.sender` is either the stream's sender, recipient or an approved third party.
if (msg.sender != _streams[streamId].sender && !_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierFlow_Unauthorized({ streamId: streamId, caller: msg.sender });
}
uint256 debtToWriteOff = _uncoveredDebtOf(streamId);
// If the stream is solvent, update the total debt normally.
if (debtToWriteOff == 0) {
uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
if (ongoingDebtScaled > 0) {
// Effect: Update the snapshot debt by adding the ongoing debt.
_streams[streamId].snapshotDebtScaled += ongoingDebtScaled;
}
}
// If the stream is insolvent, write off the uncovered debt.
else {
// Effect: update the total debt by setting snapshot debt to the stream balance.
_streams[streamId].snapshotDebtScaled =
Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals });
}
// Effect: update the snapshot time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
// Effect: set the rate per second to zero.
_streams[streamId].ratePerSecond = ud21x18(0);
// Effect: set the stream as voided.
_streams[streamId].isVoided = true;
// Log the void.
emit ISablierFlow.VoidFlowStream({
streamId: streamId,
sender: _streams[streamId].sender,
recipient: _ownerOf(streamId),
caller: msg.sender,
newTotalDebt: _totalDebtOf(streamId),
writtenOffDebt: debtToWriteOff
});
}

// Current approach only has two states:

if (debtToWriteOff == 0) {

// Handle fully solvent case

} else {

// Write off ALL uncovered debt
}

This creates issues in scenarios like:

Stream Balance: 1000 tokens

Total Debt: 1500 tokens

Uncovered Debt: 500 tokens

Recoverable Amount: 200 tokens pending

Current behavior: Writes off all 500 tokens

Correct behavior: Should write off 300 tokens, keep 200 recoverable

// Effect: update the total debt by setting snapshot debt to the stream balance.
_streams[streamId].snapshotDebtScaled =
Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals });
}

This fails to account for:

  • Pending deposits

  • Partial payments

  • Collateral liquidation

  • Fee rebates

// Current implementation forces complete write-off

_streams[streamId].isVoided = true;

_streams[streamId].ratePerSecond = ud21x18(0);

Impact

system accounting for debt write-off would be incorrect leading to wrong states

Tools Used

Manual Review

Recommendations

Support partial write-offs. Track recoverable amounts. Allow flexible recovery periods

Updates

Lead Judging Commences

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

Support

FAQs

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