Flow

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

Unexpected Behavior in `adjustRatePerSecond`: Setting Rate Per Second to Zero Instead of Updating It

Summary

The SablierFlow::adjustRatePerSecond function, according to the protocol documentation and NatSpec comments, is
designed to update or set a new Rate Per Second (RPS) for a stream. For pausing a stream, the protocol provides a
separate function, SablierFlow::pause. However, it is currently possible for a sender to effectively pause the stream
by setting the RPS to zero via adjustRatePerSecond, which is an unintended behavior.

/// @inheritdoc ISablierFlow
function adjustRatePerSecond(
uint256 streamId,
UD21x18 newRatePerSecond
)
external
override
noDelegateCall
notNull(streamId)
notPaused(streamId)
onlySender(streamId)
updateMetadata(streamId)
{
@> // missing zero RPS check
UD21x18 oldRatePerSecond = _streams[streamId].ratePerSecond;
// Effects and Interactions: adjust the rate per second.
_adjustRatePerSecond(streamId, newRatePerSecond);
// Log the adjustment.
emit ISablierFlow.AdjustFlowStream({
streamId: streamId,
totalDebt: _totalDebtOf(streamId),
oldRatePerSecond: oldRatePerSecond,
newRatePerSecond: newRatePerSecond
});
}

Vulnerability Details

  1. Insert the following test in AdjustRatePerSecond.t.sol alongside AdjustRatePerSecond_Integration_Concrete_Test.

contract SablierTesting is Test {
SablierFlow sabFlow;
address theAdmin = makeAddr("THE ADMIN");
address theSender = makeAddr("THE SENDER");
address theRecipient = makeAddr("THE RECIPIENT");
UD21x18 rpsNewDef = UD21x18.wrap(1e18);
IERC20 newToken;
function setUp() public {
ERC20Mock newTk = new ERC20Mock("My Token", "MTKN", 18);
newToken = IERC20(address(newTk));
FlowNFTDescriptor flowNftDes = new FlowNFTDescriptor();
sabFlow = new SablierFlow(theAdmin, flowNftDes);
deal(address(newToken), theSender, 1000e18);
}
function testSenderCanPauseStreamThroughAdjustRatePerSecond() public {
uint128 depositAmount = 10e18; // any amount
vm.startPrank(theSender);
newToken.approve(address(sabFlow), depositAmount);
uint256 streamId = sabFlow.createAndDeposit(theSender, theRecipient, rpsNewDef, newToken, true, depositAmount);
vm.stopPrank();
vm.startPrank(theSender);
sabFlow.adjustRatePerSecond(streamId, UD21x18.wrap(0));
vm.stopPrank();
uint256 newRps = sabFlow.getRatePerSecond(streamId).unwrap();
console.log("new rate per second: ", newRps);
assertEq(newRps, 0);
}
}
  1. Run the following command in your terminal:

forge test --mt testSenderCanPauseStreamThroughAdjustRatePerSecond -vv
  1. A similar test is also exists in the adjustRatePerSecond.t.sol:

forge test --mt test_WhenRatePerSecondZero

Both tests pass, confirming the unexpected behavior.

Impact

This behavior could lead to user confusion. For example:

  1. Alice creates a stream, knowing that she can adjust the RPS and that there’s a separate function to pause it.

  2. She sets the RPS to zero using SablierFlow::adjustRatePerSecond, unaware that this action will effectively pause
    the stream.

  3. When Alice subsequently tries to use SablierFlow::pause, she receives an error (SablierFlow_StreamPaused), as the
    stream is already paused.

Such unexpected behavior may reduce user confidence in the protocol.

Tools Used

Manual Review

Recommendations

Add a check in SablierFlow::adjustRatePerSecond to prevent setting RPS to zero.

Add to SablierFlow.sol:

/// @inheritdoc ISablierFlow
function adjustRatePerSecond(
uint256 streamId,
UD21x18 newRatePerSecond
)
external
override
noDelegateCall
notNull(streamId)
notPaused(streamId)
onlySender(streamId)
updateMetadata(streamId)
{
+ if (newRatePerSecond.unwrap() == 0) {
+ revert Errors.SablierFlow_RatePerSecondIsZero(newRatePerSecond);
+ }
UD21x18 oldRatePerSecond = _streams[streamId].ratePerSecond;
// Effects and Interactions: adjust the rate per second.
_adjustRatePerSecond(streamId, newRatePerSecond);
// Log the adjustment.
emit ISablierFlow.AdjustFlowStream({
streamId: streamId,
totalDebt: _totalDebtOf(streamId),
oldRatePerSecond: oldRatePerSecond,
newRatePerSecond: newRatePerSecond
});
}

Add to Errors.sol:

...
...
/// @notice Thrown when trying to set protocol fee more than the allowed.
error SablierFlowBase_ProtocolFeeTooHigh(UD60x18 newProtocolFee, UD60x18 maxFee);
/// @notice Thrown when trying to recover for a token with zero surplus.
error SablierFlowBase_SurplusZero(address token);
+ /// @notice Thrown when trying to set the RPS to zero.
+ error SablierFlow_RatePerSecondIsZero(UD21x18 ratePerSecond);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

theirrationalone Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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