Flow

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

Stream balance front-running vulnerability in NFT transfer

Summary

Stream NFT holders are vulnerable to front-running attacks where stream senders can withdraw all funds just before an NFT purchase transaction, leaving buyers with empty streams.

Vulnerability details

The _withdraw function allows stream senders to withdraw any available balance without restrictions, even when the stream NFT is listed for sale. There's no mechanism to lock funds or prevent withdrawals during NFT transfers.

Key code:

function _withdraw(uint256 streamId, address to, uint128 amount) internal {
// Only checks recipient ownership
if (to != _ownerOf(streamId) && !_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierFlow_WithdrawalAddressNotRecipient();
}
// No checks for NFT transfer status
_streams[streamId].balance -= amount;
token.safeTransfer({ to: to, value: amount });
}

Streams can be created with a transferable option, allowing the sender to list them on NFT marketplaces. However, this setup introduces a risk: before a buyer completes the purchase, the stream’s owner could front-run the transaction by withdrawing all deposited funds, leaving the buyer with an empty stream\

Impact

HIGH. Buyers can lose significant funds by purchasing seemingly funded stream NFTs that get drained before transfer completes.

Likelihood

HIGH. The attack requires minimal setup and standard MEV tools can easily monitor and front-run NFT purchase transactions.

Proof of concept

Let's do a pseudo code PoC

function exploit() external {
// Alice creates stream with 2000 USDC
uint256 streamId = flow.create(alice, alice, ratePerSec, USDC, true);
flow.deposit(streamId, 2000e6);
// Alice lists NFT on marketplace
marketplace.list(streamId, price);
// Bob attempts to buy it seeing that it has 2000 USDC
// Alice front-runs with:
flow.withdrawMax(streamId, alice);
// Bob's purchase completes but stream is empty
marketplace.buy(streamId) // Bob receives worthless NFT
}

Recommendation

  1. Add a "locked" state when NFT is listed:

struct Stream {
bool isListed;
uint256 lockedBalance;
}
function _withdraw() internal {
require(!stream.isListed, "Funds locked during listing");
// ... rest of withdraw logic
}
  1. Implement atomic NFT+funds transfers where marketplace handles both NFT and stream balance transfer in single transaction.

Updates

Lead Judging Commences

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

Appeal created

0xtheblackpanther Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
0xtheblackpanther Submitter
8 months ago
0xstalin Auditor
8 months ago
0xtheblackpanther Submitter
8 months ago
0xstalin Auditor
8 months ago
0xtheblackpanther Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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