Flow

Sablier
FoundryDeFi
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Stream Assets Vulnerable to Draining Through Admin Surplus Recovery Exploit

Summary

Tokens can be drained if the admin recovers the surplus amount when a token has two addresses. Some tokens are deployed behind a proxy, giving them at least two entry points. This can cause users to lose all their funds, resulting in significant financial losses.

Vulnerability Details

Some tokens on the blockchain are deployed behind a proxy, providing at least two entry points (the proxy and the implementation) for their functionality. For example, Synthetix’s ProxyERC20 allows interaction with tokens such as sUSD, sBTC, etc. If tokens like these are used for streaming, an attacker could potentially exploit them to disrupt the protocol.

Scenario:

  1. Stream Creation by Alice:
    Alice creates a stream using sUSD and deposits 100e18 tokens. Now:

    • _streams[Alice streamId].balance = 100e18

    • aggregateBalance[sUSD] = 100e18

    • sUSD.balanceOf(SablierFlow) = 100e18.

    function create(
    address sender,
    address recipient,
    UD21x18 ratePerSecond,
    IERC20 token,
    bool transferable
    ) external override noDelegateCall returns (uint256 streamId) {
    streamId = _create(sender, recipient, ratePerSecond, token, transferable);
    }
  2. Admin Surplus Recovery:
    The admin wants to recover any surplus amount of sUSD that may be stuck in the contract. They call recover() to withdraw this surplus:

    function recover(IERC20 token, address to) external override onlyAdmin {
    uint256 surplus = token.balanceOf(address(this)) - aggregateBalance[token];
    if (surplus == 0) {
    revert Errors.SablierFlowBase_SurplusZero(address(token));
    }
    token.safeTransfer(to, surplus);
    emit Recover(msg.sender, token, to, surplus);
    }
  3. Malicious Stream Creation by Bob:
    Bob, a malicious attacker, observes the recovery function and creates a stream using the ProxyERC20 address for the sUSD token, depositing 100e18 tokens. Now:

    • _streams[Bob’s streamId].balance = 100e18

    • aggregateBalance[sUSD Proxy] = 100e18

    • sUSD.balanceOf(SablierFlow) = 200e18 (since the proxy contract also deposits sUSD tokens into the contract).

  4. Admin Recovery Function Execution:
    When the admin calls recover(), the surplus amount is calculated as follows:

    uint256 surplus = sUSD.balanceOf(address(this)) - aggregateBalance[sUSD];

    Here:

    • sUSD.balanceOf(address(this)) = 200e18 (since the proxy also transfers sUSD)

    • aggregateBalance[sUSD] = 100e18

    This results in a calculated surplus of 200e18 - 100e18 = 100e18, so 100e18 sUSD is removed from the contract. Now, sUSD.balanceOf(SablierFlow) = 100e18.

  5. Exploit by Bob:
    Bob then back-runs this transaction to remove his tokens. He can do this in two ways:

    • Withdrawal: If the rate per second (rps) is high enough, he can withdraw his tokens.

    • Refund: If protocol fees apply to the proxy address, Bob can set rps = 0 and assign himself as the sender, then call refund() to retrieve all his tokens.

    After Bob’s refund transaction, the state becomes:

    • _streams[Bob’s streamId].balance = 0

    • aggregateBalance[sUSD Proxy] = 0

    • sUSD.balanceOf(SablierFlow) = 0

    function refund(uint256 streamId, uint128 amount) external override noDelegateCall notNull(streamId) onlySender(streamId) updateMetadata(streamId) {
    _refund(streamId, amount);
    }
  6. Denial of Service (DoS) Attack:
    After this, the state reflects:

    • _streams[Alice streamId].balance = 100e18

    • aggregateBalance[sUSD] = 100e18

    • sUSD.balanceOf(SablierFlow) = 0

    Now, if Alice attempts to withdraw the streamed amount, sUSD.safeTransfer() will revert because there are no sUSD tokens left in the contract to transfer. This causes Alice to lose all her funds and leads to a denial-of-service (DoS) attack on the protocol.

    function withdraw(uint256 streamId, address to, uint128 amount) external override noDelegateCall notNull(streamId) updateMetadata(streamId) returns (uint128 withdrawnAmount, uint128 protocolFeeAmount) {
    (withdrawnAmount, protocolFeeAmount) = _withdraw(streamId, to, amount);
    }

Impact

The impact of this vulnerability is substantial:

  1. Loss of User Funds: Users who stream or deposit tokens in the contract risk losing their funds if the admin unknowingly withdraws what appears to be a "surplus" due to proxy interactions.

  2. Denial of Service (DoS): Once funds are removed by the admin, legitimate users may be unable to withdraw or interact with their streams, effectively blocking access to their assets.

  3. Protocol Exploitation: Malicious actors can exploit the discrepancy between proxy and implementation balances to repeatedly trigger "surplus" conditions, draining funds and disrupting normal protocol functions.

Tools Used

Manual Review

Recommendations

Restrict tokens that can be used in streams to a whitelist of approved token addresses (main implementation addresses only), preventing proxy contracts from being accepted in the first place.

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Tokens with two addresses

Appeal created

0xg0p1 Submitter
8 months ago
serialcoder Auditor
8 months ago
inallhonesty Lead Judge
8 months ago
inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Tokens with two addresses

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.