Summary
The create
function lacks a validation check for the recipient address, allowing the possibility of minting NFTs to the zero address. This oversight can lead to streams being unowned and funds becoming irretrievable. Implementing a check to prevent zero address assignments will enhance the security and reliability of the contract.
Vulnerability Details
In the current implementation of the create
function, there is no validation to ensure that the recipient address is not the zero address
function _create(
address sender,
address recipient,
UD21x18 ratePerSecond,
IERC20 token,
bool transferable
)
internal
returns (uint256 streamId)
{
if (sender == address(0)) {
revert Errors.SablierFlow_SenderZeroAddress();
}
uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
if (tokenDecimals > 18) {
revert Errors.SablierFlow_InvalidTokenDecimals(address(token));
}
streamId = nextStreamId;
_streams[streamId] = Flow.Stream({
balance: 0,
isStream: true,
isTransferable: transferable,
isVoided: false,
ratePerSecond: ratePerSecond,
sender: sender,
snapshotDebtScaled: 0,
snapshotTime: uint40(block.timestamp),
token: token,
tokenDecimals: tokenDecimals
});
unchecked {
nextStreamId = streamId + 1;
}
_mint({ to: recipient, tokenId: streamId });
emit ISablierFlow.CreateFlowStream({
streamId: streamId,
sender: sender,
recipient: recipient,
ratePerSecond: ratePerSecond,
token: token,
transferable: transferable
});
}
As a result, if a stream is created with the recipient set to the zero address, the NFT representing the stream is minted to this invalid address. This leads to the stream being unowned, as the zero address cannot manage or withdraw from the stream. This will exposes the contract to potential loss of funds and operational inefficiencies, as the stream cannot be accessed or controlled by any user.
Impact
This will result into
Loss of Ownership: The NFT is minted to the zero address, making it unowned and unmanageable by any user.
Loss of Funds: Funds deposited into the stream could become irretrievable, leading to a potential financial loss for the sender.
Operational Disruption: The inability to manage or withdraw from the stream could disrupt the intended flow of funds and operations.
Tools Used
Manual Review
Recommendations
Introduce a check in the create
function to ensure the recipient
address is not the zero address.
function _create(
address sender,
address recipient,
UD21x18 ratePerSecond,
IERC20 token,
bool transferable
)
internal
returns (uint256 streamId)
{
// Check: the sender is not the zero address.
if (sender == address(0)) {
revert Errors.SablierFlow_SenderZeroAddress();
}
+ // Check: the recipient is not the zero address.
+ if (recipient == address(0)) {
+ revert Errors.SablierFlow_recipientZeroAddress();
+ }
uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
// Check: the token decimals are not greater than 18.
if (tokenDecimals > 18) {
revert Errors.SablierFlow_InvalidTokenDecimals(address(token));
}
// Load the stream ID.
streamId = nextStreamId;
// Effect: create the stream.
_streams[streamId] = Flow.Stream({
balance: 0,
isStream: true,
isTransferable: transferable,
isVoided: false,
ratePerSecond: ratePerSecond,
sender: sender,
snapshotDebtScaled: 0,
snapshotTime: uint40(block.timestamp),
token: token,
tokenDecimals: tokenDecimals
});
// Using unchecked arithmetic because this calculation can never realistically overflow.
unchecked {
// Effect: bump the next stream ID.
nextStreamId = streamId + 1;
}
// Effect: mint the NFT to the recipient.
_mint({ to: recipient, tokenId: streamId });
// Log the newly created stream.
emit ISablierFlow.CreateFlowStream({
streamId: streamId,
sender: sender,
recipient: recipient,
ratePerSecond: ratePerSecond,
token: token,
transferable: transferable
});
}