Flow

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

Fake Token Stream Creation Leading to to Protocol Manipulation and User Risks

Summary

The SablierFlow protocol allows the creation of token streams with any ERC20 token without validation beyond checking decimals ≤ 18. This enables malicious actors to create streams using fake tokens that mimic legitimate ones, potentially leading to social engineering attacks and financial losses.

Vulnerability Details

The vulnerability exists in the stream creation logic in _create function where token validation is minimal:

function _create(
address sender,
address recipient,
UD21x18 ratePerSecond,
IERC20 token,
bool transferable
) internal returns (uint256 streamId) {
// Only validates decimals
uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
if (tokenDecimals > 18) {
revert Errors.SablierFlow_InvalidTokenDecimals(address(token));
}
// Creates stream without further token validation
_streams[streamId] = Flow.Stream({
token: token,
// ... other fields
});
}

The contract trusts any token that implements the IERC20 interface and has valid decimals, making it possible to create streams with malicious or worthless tokens.

Impact

The unrestricted token creation in Sablier streams presents multiple severe risks:

  1. Unlimited NFT Minting:

    • Attackers can create unlimited streams using worthless tokens to mint NFTs for themselves by setting the recipient to an address they own.

    • No limit on streams per address or token validation enables:

      • NFT farming for potential airdrops

      • Manipulation of NFT marketplace statistics

      • Protocol storage bloat

  2. Financial Risks:

    • Users might trade valuable assets for stream NFTs backed by worthless tokens

    • Integration protocols could accept these streams as collateral

    • DEXs or lending protocols might list these fake tokens

    • Users could make financial decisions based on expected future income from fake streams

  3. Protocol Manipulation:

    • Ability to spam the protocol with fake token streams

    • Inflation of TVL metrics with worthless tokens

    • Degradation of protocol usability through spam

  4. Integration/Technical Risks:

    • Third-party protocols integrating with Sablier might not properly validate tokens

    • Price oracles could be affected if they track stream values

    • Indexers and analytics platforms could show incorrect data

    • NFT-based governance systems could be manipulated

  5. Social Engineering:

    • Creation of streams with tokens mimicking legitimate ones (e.g., fake "USDC")

    • False sense of future income leading to poor financial decisions

    • Potential for phishing through seemingly valuable streams

Proof Of Concept

Actors:

  • Attacker: Creates fake token and stream

  • Victim: Receives stream and believes it's legitimate USDC

  • Protocol: Sablier protocol that processes the stream

contract SablierFlowAttackTest is Test {
SablierFlow public sablier;
FakeUSDC public fakeUSDC;
address public attacker;
address public victim;
function setUp() public {
// Setup accounts
attacker = makeAddr("attacker");
victim = makeAddr("victim");
// Deploy contracts
MockNFTDescriptor descriptor = new MockNFTDescriptor();
sablier = new SablierFlow(address(this), descriptor);
// Deploy FakeUSDC and mint to attacker directly
vm.startPrank(attacker);
fakeUSDC = new FakeUSDC(); // This will mint 1M tokens to attacker directly
fakeUSDC.approve(address(sablier), type(uint256).max);
vm.stopPrank();
// Fund attacker with ETH (might need this for gas)
vm.deal(attacker, 100 ether);
}
function testFakeTokenStreamAttack() public {
// Log initial balance to verify
console2.log("Initial fake USDC balance of attacker:", fakeUSDC.balanceOf(attacker));
vm.startPrank(attacker);
// Calculate rate: 1000 USDC per day
uint256 baseRate = 11574;
uint256 ratePerSecond = baseRate * 10**18;
// Create stream
uint256 streamId = sablier.create(
attacker,
victim,
UD21x18.wrap(uint128(ratePerSecond)),
IERC20(address(fakeUSDC)),
true
);
// Deposit initial amount (1 day worth)
sablier.deposit(streamId, uint128(1000 * 10**6), attacker, victim);
vm.stopPrank();
// Verify stream creation
assertTrue(sablier.ownerOf(streamId) == victim, "Stream NFT not minted to victim");
// Fast forward 1 day
vm.warp(block.timestamp + 1 days);
// Check withdrawable amount
uint128 withdrawable = sablier.withdrawableAmountOf(streamId);
console2.log("Withdrawable amount after 1 day (in USDC wei):", withdrawable);
// Victim tries to withdraw
vm.startPrank(victim);
sablier.withdraw(streamId, victim, withdrawable);
vm.stopPrank();
// Show final balances
console2.log("Victim's fake USDC balance:", fakeUSDC.balanceOf(victim));
}
}

Test Result:

Ran 1 test for tests/SablierFlowAttackTest.t.sol:SablierFlowAttackTest
[PASS] testFakeTokenStreamAttack() (gas: 240483)
Logs:
Initial fake USDC balance of attacker: 1000000000000
Withdrawable amount after 1 day (in USDC wei): 1000000000
Victim's fake USDC balance: 1000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.18ms (1.35ms CPU time)
Ran 1 test suite in 6.30ms (2.18ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

  • Manual Review

  • Foundry

  • Remix IDE

Recommendations

  • Token Allowlist

contract SablierFlow {
mapping(address => bool) public allowedTokens;
bool public requireAllowlist;
function create(...) external {
if (requireAllowlist) {
require(allowedTokens[address(token)], "Token not allowed");
}
// ... existing logic
}
}
  • Enhanced Token Validation

    • Check contract code size

    • Integrate with token registry services

  • UI Warnings

    • Clear warnings about unverified tokens

    • Visual indicators for non-allowlisted tokens

    • Token verification status display

  • Documentation

    • Clear documentation about token risks

    • Guidelines for users to verify token authenticity

    • Best practices for stream recipients

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

solgoodman Submitter
8 months ago
solgoodman Submitter
8 months ago
solgoodman Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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