The protocol has a bug protection mechanism which, in case of ever being exeuted, would allow stream senders to cancel streamed funds. Streamed funds are supposed to be streamed and not cancellable anymore.
In the case of a bug appearing, which I consider a valid scenario due to the very same codebae having this precautious measure for it, then streamed funds could be sent to the sender via the cancel() function.
This problem affects the LockupLinear contract and the LockupDynamic contract.
In both of these contracts there is a code similar to the following one:
Then inside the _cancel(), _calculateStreamedAmount() is called and the funds sent to sender are computed like so: senderAmount = amounts.deposited - streamedAmount;. streamedAmount is the return value of _calculateStreamedAmount().
But if this bug happens, the return value will be withdrawn amount, which does not necessarily correspond to the streamed amount. As the recipient could have withdrawn 0, but the funds are still streaming. Thus senderAmount would be deposited - 0 and all deposited funds, which part of them are streamed and must not be cancellable, would be sent to the sender.
The reason why to return withdrawn amount is to protect the funds from being drained by a bug. This is because then the withdrawableAmountOf() (see here) would always return 0, effectively frozening them until endTime has passed. This proves that, yet frozen, the funds are still streaming. It is just that the recipient won't be able to claim them until endTime is reached in the case of LockupLinear for example.
In case of bug protection being activated, streamed funds can be stolen by the sender.
There are 3 parts on the codebase where this bug could be exploited:
LokcupLinear.sol here.
LockupDynamic.sol in 1 segment streams, here.
LockupDynamic.sol in >1 segment streams, here. But notice, in this case the amount of streamed funds that can be stolen is limited to the amount of funds streamed on the segment affected by the bug. If time passes by and the next segment is activated and not affected by the .gt() condition, then the streamed funds of the affected segment won't be able to be cancelled anymore. This is due to this part of the code not always returning the withdrawn value.
The execution flow is pretty simple so I will link the key aspects of the execution:
A bug appears and then the if (streamedAmount.gt(depositedAmount)) will enter returning withdrawn amount.
sender calls cancel() which calls _cancel(), which after some checks that do not check for frozen funds, calls _calculateStreamedAmount() here.
You can see that until senderAmount is computed here, there is only checks with 'if' statements thus the value returned by _calculateStreamedAmount() has not been altered. And, none of this checks will revert becase streamedAmount returned will be withdrawn amount. This is the only check concerning amounts (this one) and it will not revert because as said earlier, recipient can have withdrawn 0 so far.
And finally the senderAmount value is not altered nor checked against anything until it is eventually transfered here.
Conclusion, in case of bug sender can steal streamed funds from recipient.
Manual analysis.
Frozen funds should not be cancellable. Add an extra return value to _calculateStreamedAmount(), a boolean flag indicating frozen funds.
If entered in the if statement which means frozen, return true and inside the _cancel(), if returned true revert as streamed funds would be cancelled and that should not happen.
https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity
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.