Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: high
Invalid

`createMerkleLT` doesn't check `LOCKUP_TRANCHED::MAX_TRANCHE_COUNT`, which may lead to creator fund being locked

Summary

SablierV2MerkleLT is used to create system specific airstreams, which close to an airdrop, but using sablier lockup streams to unlock the tokens in time. Currently it is possible to for users to create linear and tranche streams.
When a user create such airstream, he interacts with SablierV2MerkleLockupFactory and he has to provide array of tranches with corresponding duration and % of the total amount, which is being unlocked for that "tranche". Also user provides necessary creational params for such SablierV2MerkleLockup to populate the following state vars of the airstream:

IERC20 public immutable override ASSET;
/// @inheritdoc ISablierV2MerkleLockup
bool public immutable override CANCELABLE;
/// @inheritdoc ISablierV2MerkleLockup
uint40 public immutable override EXPIRATION;
/// @inheritdoc ISablierV2MerkleLockup
bytes32 public immutable override MERKLE_ROOT;
/// @dev The name of the campaign stored as bytes32.
bytes32 internal immutable NAME;
/// @inheritdoc ISablierV2MerkleLockup
bool public immutable override TRANSFERABLE;

Vulnerability Details

We can see that creator may specify whether the streams, which are claimed could be more adjusted to be more or less "decentralized" by specifying address of admin, expiration, etc. As we are in web3 world, we believe that more decentralization = better. So we may assume a scenario of an airstream with no admin (address(0)) and no expiration.
Creator should then send the correponding erc20 tokens to SablierV2MerkleLT, which are planned to be distributed in the airstrem.
But on creation of such stream, there are no enough validation checks, which may lead to funder loosing his tokens and claimers being unable to claim.
When lockupTranched is created, there are a couple of checks made. One such is whether provided tranches are less than a limit.
But when airstream is created there is no check whether provided array of tranches is less than the limit of the corresponding lockupTranched. Also such is missing in the factory function. This may lead to creator initiating such contract with more thances than the limit and funding it. A revert would be the result when user try to claim his shares. If creator has set no admin, all funds are lost.

EXAMPLE
Imagine the following scenario:

  1. Bob is a alternative employer and wants to incentives newcomers by creating a SablierV2MerkleLT for them.

  2. For him it is easier to deploy SablierV2MerkleLT and then each employee have to claim his stream, whenever they start/want.

  3. Because Bob wants to be fair and transparrent, he deploy it with expiration = 0 (No expiration time) and admin = address(0) so he cannot withdraw the assets.

  4. He fund the contract with 52 (weeks) * salaryPerWeekPerPerson * personCount = lets say $100 000 USDC.

  5. He has called SablierV2MerkleLockupFactory::createMerkleLT with tranchesWithPercentages array which is with 52 elements inside (weeks).

  6. The problem arrises when first user calls claim and try to create a stream with tranches array with length of 52

  7. The tx will always revert inside LOCKUP_TRANCHED.createWithDurations() => _create => Helpers.checkCreateLockupTranched(createAmounts.deposit, params.tranches, MAX_TRANCHE_COUNT, params.startTime);, because MAX_TRANCHE_COUNT = 30 root revert here

  8. Those $100K are lost, because there is no way for Bob to change tranchesWithPercentages array for SablierV2MerkleLT. Neither for somehow change MAX_TRANCHE_COUNT for the corresponding stream. Also Bob cannot withdraw the funds, because he set no admin and clawback is uncallable

Impact

DoS -> Loss of funds

Tools Used

Manual Review

Recommendations

When SablierV2MerkleLL is created, always check if provided tranchesWithPercentages array matches corresponding MAX_TRANCHE_COUNT limitation:

constructor(
MerkleLockup.ConstructorParams memory baseParams,
ISablierV2LockupTranched lockupTranched,
MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages
)
SablierV2MerkleLockup(baseParams)
{
LOCKUP_TRANCHED = lockupTranched;
uint256 count = tranchesWithPercentages.length;
if (count > LOCKUP_TRANCHED.MAX_TRANCHE_COUNT()){
revert Errors.SablierV2LockupTranched_TrancheCountTooHigh(trancheCount);
}
for (uint256 i = 0; i < count; ++i) {
_tranchesWithPercentages.push(tranchesWithPercentages[i]);
}
// Max approve the Sablier contract to spend funds from the MerkleLockup contract.
ASSET.forceApprove(address(LOCKUP_TRANCHED), type(uint256).max);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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