Open sherlock-admin2 opened 10 months ago
1 comment(s) were left on this issue during the judging contest.
takarez commented:
valid because { This is valid a a dupp of 086; the watson claims its hight but will still make it meduim due to the impact mentioned in issue 086; but making it the best report as the POC is well written and implemented}
@amshirif Is there anyway the admin can unblock DoS in withdrawals?
@nevillehuang No, a new contract with these fixes would need to be deployed to prevent DoS because those two values had to be the same prior to the fix.
The protocol team fixed this issue in PR/commit https://github.com/telcoin/telcoin-audit/pull/43.
The Lead Senior Watson signed-off on the fix.
0xadrii
high
Wrong parameter when retrieving causes a complete DoS of the protocol
Summary
A wrong parameter in the
_retrieve()
prevents the protocol from properly interacting with Sablier, causing a Denial of Service in all functions calling_retrieve()
.Vulnerability Detail
The
CouncilMember
contract is designed to interact with a Sablier stream. As time passes, the Sablier stream will unlock more TELCOIN tokens which will be available to be retrieved fromCouncilMember
.The
_retrieve()
internal function will be used in order to fetch the rewards from the stream and distribute them among the Council Member NFT holders (snippet reduced for simplicity):The most important part in
_retrieve()
regarding the vulnerability that we’ll dive into is the_stream.execute()
interaction and the params it receives. In order to understand such interaction, we first need understand the importance of the_stream
and the_target
variables.Sablier allows developers to integrate Sablier via Periphery contracts, which prevents devs from dealing with the complexity of directly integrating Sablier’s Core contracts. Telcoin developers have decided to use these periphery contracts. Concretely, the following contracts have been used:
_target
variable, this contract acts as the target for a PRBProxy contract. It contains all the complex interactions with the underlying stream. Concretely, Telcoin uses the[withdrawMax()](https://github.com/sablier-labs/v2-periphery/blob/ba3926d2c3e059a230211077087b73afe46acf64/src/abstracts/SablierV2ProxyTarget.sol#L141C5-L143C6)
function in the proxy target to withdraw all the available funds from the stream (as seen in the previous code snippet)._stream
variable, this contract acts as a forwarding (non-upgradable) proxy, acting as a smart wallet that enables multiple contract calls within a single transaction.Knowing this, we can now move on to explaining Telcoin’s approach to withdrawing the available tokens from the stream. As seen in the code snippet above, the
_retrieve()
function will perform two steps to actually perform a withdraw from the stream:It will first call the
_stream
'sexecute()
function (remember_stream
is a PRBProxy). This function receives atarget
and somedata
as parameter, and performs a delegatecall aiming at thetarget
:In the
_retrieve()
function, the target where the call will be forwarded to is the_target
parameter, which is a ProxyTarget contract. Concretely, the delegatecall function that will be triggered in the ProxyTarget will bewithdrawMax()
:As we can see, the
withdrawMax()
function has as parameters thelockup
stream contract to withdraw from, thestreamId
and the addressto
which will receive the available funds from the stream. The vulnerability lies in the parameters passed when calling thewithdrawMax()
function in_retrieve()
. As we can see, the first encoded parameter in theencodeWithSelector()
call after the selector is the_target
:This means that the proxy target’s
withdrawMax()
function will be triggered with the_target
contract as thelockup
parameter, which is incorrect. This will make all calls eventually executewithdrawMax()
on the PRBProxy contract, always reverting.The parameter needed to perform the
withdrawMax()
call correctly is the actual Sablier lockup contract, which is currently not stored in theCouncilMember
contract.The following diagram also summarizes the current wrong interactions for clarity:
Impact
High. ALL withdrawals from the Sablier stream will revert, effectively causing a DoS in the _retrieve() function. Because the _retrieve() function is called in all the main protocol functions, this vulnerability essentially prevents the protocol from ever functioning correctly.
Proof of Concept
Because the current Telcoin repo does not include actual tests with the real Sablier contracts (instead, a
TestStream
contract is used, which has led to not unveiling this vulnerability), [I’ve created a repository](https://github.com/0xadrii/telcoin-proof-of-concept) where the poc can be executed (the repository will be public after the audit finishes (on 15 jan. 2024 at 16:00 CET)). ThetestPoc()
function shows how any interaction (in this case, a call to themint()
function) will fail because the proper Sablier contracts are used (PRBProxy and proxy target):Code Snippet
https://github.com/sherlock-audit/2024-01-telcoin/blob/main/telcoin-audit/contracts/sablier/core/CouncilMember.sol#L275
Tool used
Manual Review, foundry
Recommendation
In order to fix the vulnerability, the proper address needs to be passed when calling
withdrawMax()
.