Flow

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

Lack of Access Control on withdraw Function

Summary

The withdraw function lacks access control, allowing any external account to call it and withdraw funds from any stream by providing a valid streamId, to address, and amount. Without an onlySender or equivalent ownership check, malicious actors can withdraw funds from any stream, leading to unauthorized fund transfers and potential financial loss

Vulnerability Details

Impact

Unauthorized Fund Withdrawals: An attacker could exploit this to drain funds from streams they do not own.

  • Financial Loss: Users can suffer significant financial losses as funds are withdrawn by unauthorized parties

Vulnerable code

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);
}

Proof of Concept (PoC)

A practical scenario demonstrating the exploit:

  1. Assume Alice owns a stream with streamId = 1 and has deposited 1000 tokens.

  2. An attacker, Mallory, executes the following script:

// Mallory's attack script in JavaScript (using ethers.js)
const streamId = 1; // ID of Alice's stream
const to = Mallory.address; // Mallory's address to receive funds
const amount = 1000; // Amount to withdraw (assuming this is the balance in the stream)
// Execute the withdraw function
await contract.withdraw(streamId, to, amount);

Result: Mallory successfully withdraws the entire balance from Alice’s stream without being the owner.

Tools Used

Manual Review

Recommendations

To fix this issue, implement access control to restrict the withdraw function so that only the stream owner can withdraw funds:

  1. Add onlySender Modifier: Include a modifier that checks if msg.sender is the owner of streamId and apply it to the withdraw function:

modifier onlySender(uint256 streamId) {
require(msg.sender == streamOwner[streamId], "Unauthorized: Not the stream owner");
_;
}
function withdraw(
uint256 streamId,
address to,
uint128 amount
)
external
override
noDelegateCall
notNull(streamId)
updateMetadata(streamId)
onlySender(streamId) // Protects function with access control
returns (uint128 withdrawnAmount, uint128 protocolFeeAmount)
{
(withdrawnAmount, protocolFeeAmount) = _withdraw(streamId, to, amount);
}
  1. Use Role-Based Access Control (if multiple user roles need access): Consider OpenZeppelin’s AccessControl for fine-grained permissions.

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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