Summary
In SablierFlow
the _restart
function lacks proper balance validation, allowing streams to be restarted with zero or insufficient balance. This can lead to streams appearing active despite having little or no funds to fulfill their obligations, which will create misleading states and accounting inconsistencies.
Vulnerability Details
This can be found in the _restart
function of the SablierFlow.sol
contract. The function only checks if the stream is paused but fails to validate if there's sufficient balance to restart the stream.
https://github.com/Cyfrin/2024-10-sablier/blob/963bf61b9d8ffe3eb06cbcf1c53f0ab88dbf0eb0/src/SablierFlow.sol#L714-L725
function _restart(uint256 streamId, UD21x18 ratePerSecond) internal {
if (_streams[streamId].ratePerSecond.unwrap() != 0) {
revert Errors.SablierFlow_StreamNotPaused(streamId);
}
_adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ratePerSecond });
emit ISablierFlow.RestartFlowStream(streamId, msg.sender, ratePerSecond);
}
Impact
This allows streams to be restarted without sufficient funds, leading to:
Streams appearing active but unable to transfer funds
Misleading status information
Potential accounting inconsistencies
False expectations for recipients
Proof of Concept test and results:
pragma solidity >=0.8.22;
import { Test } from "forge-std/src/Test.sol";
import { SablierFlow } from "../src/SablierFlow.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ud21x18 } from "@prb/math/src/UD21x18.sol";
import { IFlowNFTDescriptor } from "../src/interfaces/IFlowNFTDescriptor.sol";
import { Flow } from "../src/types/DataTypes.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MTK") {
_mint(msg.sender, 1_000_000e18);
}
function decimals() public pure override returns (uint8) {
return 18;
}
}
contract SablierFlowRestartTest is Test {
SablierFlow public sablierFlow;
IERC20 public mockToken;
address public sender;
address public recipient;
uint256 public streamId;
function setUp() public {
mockToken = new MockERC20();
sablierFlow = new SablierFlow(address(this), IFlowNFTDescriptor(address(0)));
sender = address(1);
recipient = address(2);
vm.startPrank(sender);
deal(address(mockToken), sender, 1000e18);
mockToken.approve(address(sablierFlow), type(uint256).max);
vm.stopPrank();
}
function testRestartWithZeroBalance() public {
vm.startPrank(sender);
streamId = sablierFlow.create(
sender,
recipient,
ud21x18(1e18),
mockToken,
true
);
sablierFlow.pause(streamId);
uint256 balanceBefore = mockToken.balanceOf(address(sablierFlow));
vm.expectRevert(abi.encodeWithSignature("SablierFlow_InsufficientBalance(uint256)", streamId));
sablierFlow.restart(streamId, ud21x18(1e18));
uint256 balanceAfter = mockToken.balanceOf(address(sablierFlow));
assertEq(balanceAfter, balanceBefore);
assertEq(balanceAfter, 0);
vm.stopPrank();
}
function testRestartWithPartialBalance() public {
vm.startPrank(sender);
streamId = sablierFlow.create(
sender,
recipient,
ud21x18(2e18),
mockToken,
true
);
sablierFlow.deposit(
streamId,
10e18,
sender,
recipient
);
vm.warp(block.timestamp + 5);
sablierFlow.pause(streamId);
vm.expectRevert(abi.encodeWithSignature("SablierFlow_InsufficientBalance(uint256)", streamId));
sablierFlow.restart(streamId, ud21x18(2e18));
assertTrue(sablierFlow.statusOf(streamId) == Flow.Status.PAUSED_INSOLVENT);
vm.stopPrank();
}
}
Run with: forge test --match-contract SablierFlowRestartTest -vvvv
Test Results:
[FAIL: next call did not revert as expected] testRestartWithZeroBalance() (gas: 205924)
[FAIL: next call did not revert as expected] testRestartWithPartialBalance() (gas: 265964)
Tools Used
Foundry Framework
Manual Code Review
Unit Testing
Recommendations
Implement proper balance validation in the _restart
function:
function _restart(uint256 streamId, UD21x18 ratePerSecond) internal {
if (_streams[streamId].ratePerSecond.unwrap() != 0) {
revert Errors.SablierFlow_StreamNotPaused(streamId);
}
if (_streams[streamId].balance == 0) {
revert Errors.SablierFlow_InsufficientBalance(streamId);
}
uint256 totalDebt = _totalDebtOf(streamId);
if (_streams[streamId].balance < totalDebt) {
revert Errors.SablierFlow_InsufficientBalance(streamId);
}
_adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ratePerSecond });
emit ISablierFlow.RestartFlowStream(streamId, msg.sender, ratePerSecond);
}