This repository has been archived by the owner on Jul 21, 2024. It is now read-only.
Aamirusmani1552 - Sablier stream update in CouncilMember.sol
can cause loss of funds if the streamed balance is not withdrawn.
#99
Labels
Has Duplicates
A valid issue with 1+ other issues describing the same vulnerability
Medium
A valid Medium severity issue
Reward
A payout will be made for this issue
Sponsor Confirmed
The sponsor acknowledged this issue is valid
Will Fix
The sponsor confirmed this issue will be fixed
Aamirusmani1552
medium
Sablier stream update in
CouncilMember.sol
can cause loss of funds if the streamed balance is not withdrawn.Summary
The vulnerability in the
CouncilMember
contract pertains to the failure to withdraw streamed tokens during a contract stream update, potentially resulting in fund loss for both the contract and the entire council.Vulnerability Detail
Sablier Streams facilitate token streaming on a per-second basis, involving a sender who initiates the stream and a receiver who receives the streamed tokens. The receiver can withdraw tokens up to the elapsed seconds from the stream's start. The responsibility to claim streamed tokens lies with the receiver, as stated in the documentation and Sablier stream contracts (read the cancel stream docs here) . Once tokens are streamed, the sender cannot withdraw them.
Also sender has the authority to cancel the the stream and claim back the un-streamed amount. But streamed Balance upto the elapsed time can still be claimed by the receiver or person who is approved by the receiver only.
Check the
SablierV2Lockup::cancel()
here 👇:https://github.com/sablier-labs/v2-core/blob/b0016437ef3cc8606e1100965dd911d7e658b40b/src/abstracts/SablierV2Lockup.sol#L153-L168
Docs for the same could be find here 👇:
https://docs.sablier.com/contracts/v2/guides/stream-management/cancel
As we check from the resources given above, if a stream is canceled only the un-streamed balance will be available for the sender to withdraw. Rest if for the receiver.
The issue arises in the
CouncilMember
contract's functions (CouncilMember::updateStream(...)
,CouncilMember::updateID(...)
, andCouncilMember::updateTarget(...)
) as they do not check whether the entire streamed amount has been withdrawn from the Sablier stream before updating the stream states in the contract. Consequently, if there is an active streamed balance in the Sablier stream, theCouncilMember
contract will not be able to withdraw it. And now the balance is lying idle in the Sablier stream contract.However, the previously streamed balance can be reclaimed by adding the old stream back to the
CouncilMember
contract, provided the sender is aware that the streamed balance has not been withdrawn. Nonetheless, complications may arise if modifications are made to theCouncilMember
contract following the stream update. For instance, the removal of a Council Member could lead to the omitted member not receiving their balance, while the addition of a new member may result in every old member receiving fewer tokens and new members gaining tokens share. This can happen because all update stream functions are handled by the roleGOVERNANCE_COUNCIL_ROLE
in theCouncilMember
contract. And if it is a multi-sig or governance then it would required a vote to happend in order to perform the new updated. And sponsor confirmed that the multi-sig can be added for this role. Here is the conversation:Question Asked by me:
Answer from Sponsor:
So if this is the case then new update will be done after some time and a lot of things might happen in that time.
Also the sender's awareness play important role in this. Two scenarios may unfold because of this:
CouncilMember
contract.In both of the scenarios if the sender is unaware then it will be complete loss of tokens.
Impact
Council members face potential token losses due to the inability to withdraw streamed balances.
Code Snippet
https://github.com/sherlock-audit/2024-01-telcoin/blob/main/telcoin-audit/contracts/sablier/core/CouncilMember.sol#L229C3-L257C1
Tool used
Recommendation
To mitigate this potential issue, the following actions are advised:
1. Implement Sablier Hooks:
Sablier offers essential hooks to address scenarios where the receiver is a contract. These hooks enable the receiver contract to update its state accurately. While these hooks are optional, Sablier strongly recommends their implementation. Of particular relevance in this context is the onStreamCanceled hook, triggered by the Sablier stream contract when the sender cancels the stream. By incorporating this hook in the CouncilMember contract, the receiver can invoke the _retrieve() function upon stream cancellation, ensuring the withdrawal of the entire streamed balance.
File: CouncilMember.sol
2. Check Stream Depletion in Update Function:
In the stream update function, verify whether the stream is depleted or not. If not, withdraw the streamed tokens before updating the balances. It is crucial to check if the stream is depleted because if the
_retrieve()
function is directly called and the stream has been depleted (all tokens withdrawn by the receiver), invokingstream.withdrawMax()
will revert. This could lead to a revert in the_retrieve()
function and potentially cause a denial-of-service (DoS) situation in the stream update function.File: CouncilMember.sol
Note: Make necessary adjustments in the interfaces used.
The text was updated successfully, but these errors were encountered: