Summary
The depletionOf
function contains a conditional check intended to return a value of zero when the total debt exceeds the stream’s balance. However, the current implementation returns zero when the total debt is greater than or equal to the balance plus one unit (balance + 1)
. This deviation from the intended logic could cause unexpected outcomes in depletion calculations, potentially returning zero prematurely when the debt equals the adjusted balance threshold.
Vulnerability Details
function depletionTimeOf(uint256 streamId)
external
view
override
notNull(streamId)
notPaused(streamId)
returns (uint256 depletionTime)
{
uint128 balance = _streams[streamId].balance;
if (balance == 0) {
return 0;
}
uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 balanceScaled = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });
uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;
uint256 oneMVTScaled = Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
>> 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 (solvencyAmount % ratePerSecond == 0) {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
}
else {
depletionTime = _streams[streamId].snapshotTime + solvencyPeriod + 1;
}
}
}
The condition checks if the total debt is greater than or equal to balance + 1
unit (oneMVTScaled
). The issue is that the function should return zero only when the total debt strictly exceeds balance + 1
, as intended. With the current logic, the function may incorrectly return zero when the debt merely equals the threshold, resulting in an unintended zero output.
Impact
When the debt matches the threshold exactly, the function incorrectly returns zero, leading to an inaccurate depletion value. This may cause downstream functions or integrations to operate on faulty depletion values.
Tools Used
Manual Review
Recommendations
Modify the condition to check only if the debt strictly exceeds balance + 1, rather than checking for equality as well:
function depletionTimeOf(uint256 streamId)
external
view
override
notNull(streamId)
notPaused(streamId)
returns (uint256 depletionTime)
{
uint128 balance = _streams[streamId].balance;
// If the stream balance is zero, return zero.
if (balance == 0) {
return 0;
}
uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 balanceScaled = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });
uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;
// MVT represents Minimum Value Transferable, the smallest amount of token that can be transferred, which is
// always 1 in token's decimal.
uint256 oneMVTScaled = Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
// If the total debt exceeds balance, return zero.
- if (snapshotDebtScaled + _ongoingDebtScaledOf(streamId) >= balanceScaled + oneMVTScaled) {
- return 0;
- }
+ if (snapshotDebtScaled + _ongoingDebtScaledOf(streamId) >= balanceScaled + oneMVTScaled) {
+ return 0;
+ }
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
// Depletion time is defined as the UNIX timestamp at which the total debt exceeds stream balance by 1 unit of
// token (mvt). So we calculate it by solving: total debt at depletion time = stream balance + 1. This ensures
// that we find the lowest timestamp at which the total debt exceeds the stream balance.
// Safe to use unchecked because the calculations cannot overflow or underflow.
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;
}
}
}