The issue is somewhat similar to the issue reported in Cantina's audit report section 3.3.4* (copied below). It describes a problem that depletionTimeOf() returns 0 when it should still return the timestamp of when the totalDebt will exceed the balance by one token. But in this case I want to highlight the return difference of when balance is and is NOT 0.
Inconsistent returns from depletionTimeOf(), depending if balance is 0 or not, even though the time and rate doesn't change. This is because the function first checks if there is no balance and returns 0. But if stream has balance > 0, then it will only return 0 if the debt is above balance + 1 MVT (Minimum Value Transferable).
This creates a scenario where if the value is withdrawn at the time when stream balance
= totalDebt
the depletionTimeOf()
will shift from returning a timestamp slightly into the future to returning 0. This means that the depletion isn't correctly configured of what it considers the "depletion time" if it is the timestamp of when the total debt EXCEEDS the balance or when total debt is EQUAL to the balance.
An example of the problem is that the depletionTimeOf()
returns:
rps > 0; balance = 1; totalDebt = 1 -> output is a timestamp
rps > 0; balance = 0; totalDebt = 0 -> output is 0
Even though in both cases the values are equal, one will provide a timestamp and will consider the stream not yet depleted, while in the other case it is considered depleted.
For a POC example using the project test case please look at "Tools Used" section.
Not completely accurate depletionTimeOf() value. Returns 0 if balance is 0, but returns timestamp if balance = totalDebt (but their return values should match).
Manual review + foundry tests
The test does this:
Matches the totalDebt to stream balance using timestamp warp (prints them out for proof)
Prints out the timestamp of now and the expected depletionTime which differ by 1 second
Performs a withdrawal to recipient with the amount matching the balance. Post withdraw balance = 0.
Prints out the timetstamps again, we see that "now" is still the same, but the depletion time is now 0. Even though the block timestamp hasn't changed and any rate parameters haven't changed either.
The script will print out:
This discrepancy is very minor edge case, that the function can return two different outputs at the same time without changing the rates. And that the depletion time doesn't have to be reached for it to be "depleted". Which is a slight conflict in the depletionTimeOf logic.
Probably the simplest solution is to simply remove the if(balance == 0)
case and always follow the same logic path of totalDebt has to EXCEED the balance. Otherwise alter the further down logic checks to switch to checking when the totalDebt EQUALS the balance.
The @notice comment in ISablierFlow.sol inaccurately describes the depletionTimeOf()
It should probably say that ...If the total debt is MORE than...
Cantina 3.3.4 report (for reference):
3.3.4 depletionTimeOf() returns 0 when still solvent at edge of depletion time
Severity: Low Risk
Context: SablierFlow.sol#L75
Description: The Natspec of the depletionTimeOf() states that it returns 0 when there is uncovered debt.
The solvencyPeriod is also calculated based on when the debt exceeds the balance by 1.
Therfore depletionTimeOf() should not return 0 when the totalDebt == balance, but rather the timestamp at which 1 more token will be streamed.
Recommendation: Change the depleteiontimeOf()
as follows:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.