Summary
If the admin of the deployed SablierV2MerkleLT and SablierV2MerkleLL contracts is modified, the original admin may cancel airdrop streams.
Vulnerability Details
In SablierV2MerkleLT/SablierV2MerkleLL, when users claim, one stream will be created, and admin will be the created stream's sender. If SablierV2MerkleLockup/SablierV2MerkleLL transfers admin ownership. The existed created streams' sender is the original admin.
In readme, Sablier mentions one similar case in Known Issues
If the admin of the deployed SablierV2MerkleLT and SablierV2MerkleLL contracts is modified, the onLockupStreamWithdrawn() hook callback, if it is implemented, will continue to be made to the original admin for the airstreams that were already claimed at the time of the change.
Actually, if the admin is modified, the original owner can cancel existing streams if there streams are cancelable. The left funds will be returned back to the original admin.
function claim(
uint256 index,
address recipient,
uint128 amount,
bytes32[] calldata merkleProof
)
external
override
returns (uint256 streamId)
{
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))));
_checkClaim(index, leaf, merkleProof);
_claimedBitMap.set(index);
streamId = LOCKUP_LINEAR.createWithDurations(
LockupLinear.CreateWithDurations({
@==> sender: admin,
recipient: recipient,
totalAmount: amount,
asset: ASSET,
cancelable: CANCELABLE,
transferable: TRANSFERABLE,
durations: streamDurations,
broker: Broker({ account: address(0), fee: ud(0) })
})
);
emit Claim(index, recipient, amount, streamId);
}
function _cancel(uint256 streamId) internal {
uint128 streamedAmount = _calculateStreamedAmount(streamId);
Lockup.Amounts memory amounts = _streams[streamId].amounts;
uint128 senderAmount;
unchecked {
senderAmount = amounts.deposited - streamedAmount;
}
uint128 recipientAmount = streamedAmount - amounts.withdrawn;
_streams[streamId].wasCanceled = true;
_streams[streamId].isCancelable = false;
......
_streams[streamId].amounts.refunded = senderAmount;
address sender = _streams[streamId].sender;
address recipient = _ownerOf(streamId);
IERC20 asset = _streams[streamId].asset;
@==> transfer funds to original owner.
asset.safeTransfer({ to: sender, value: senderAmount });
......
}
Impact
Original admin can cancel existing streams which are claimed before the ownership changes.
Tools Used
Manual
Recommendations
Considering to limit the function of transfer ownership.