Sablier

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

Airdrop Creation Permits Fund Rescue After Grace Period and Expiration, Enabling Potential Rug Pull

Summary

The Merkle streamer has a SablierV2MerkleLockup::clawback function to protect the sender and rescue unclaimed tokens if the expiration time has not ended or before the grace period ends. However, the sender can claw back the funds already claimed without this restriction.

Vulnerability Details

When any protocol or team launches an airdrop campaign, they can call SablierV2MerkleLockup::clawback for the unclaimed tokens under the following conditions:

  • The caller must be the admin.

  • The campaign must either be expired or not have an expiration.

However, as explained in the README.md of this contest:

"For MerkleLockup , a grace period is defined as the initial period during which clawback can be used. It ends 7 days after the first airstream claim has been made. Thus, airstream creators are assumed to be trusted during the grace period."

This means the airstream creators are only trusted during the grace period, but they can rug pull all the recipients who have already claimed their airdrop.

This is possible because the parameter cancelable: CANCELABLE can be set to true, and when the recipient claims the airdrop, the sender has the ability to call SablierV2Lockup::cancel and reclaim the funds even after the grace period has ended, at any time in the future.

This case applies to campaigns where CANCELABLE=true is set and the campaign does not have an infinite expiration time.

PoC

Add this test in test/integration/merkle-lockup/ll/clawback/clawback.t.sol

function test_clawbackAfterGracePeriod() public
whenCallerAdmin
afterFirstClaim
postGracePeriod
givenCampaignExpired
{
assertEq(merkleLL.CANCELABLE(), true);
lockupLinear.cancel(1);
}

Modify in v2-periphery/test/utils/Defaults.sol :

- uint40 public constant TOTAL_DURATION = 10_000 seconds;
+ uint40 public constant TOTAL_DURATION = 20 days;
...
+ EXPIRATION = uint40(block.timestamp) + 1 weeks;
- EXPIRATION = uint40(block.timestamp) + 12 weeks;
...
+ bool public constant CANCELABLE = true;
- bool public constant CANCELABLE = false;

Run the test with the following command:

bun run test --mt test_clawbackAfterGracePeriod

Impact

The user who has minted their stream can be rug pulled even after the grace period has ended.

Tools Used

  • Manual code review

Recommendations

When using SablierV2MerkleLockupFactory to create a lockup with SablierV2MerkleLockupFactory::createMerkleLL or SablierV2MerkleLockupFactory::createMerkleLT, there should no longer be a need to pass the CANCELABLE parameter. Update the SablierV2MerkleLockup contract accordingly to remove this parameter.

constructor(MerkleLockup.ConstructorParams memory params) {
// Check: the campaign name is not greater than 32 bytes
if (bytes(params.name).length > 32) {
revert Errors.SablierV2MerkleLockup_CampaignNameTooLong({
nameLength: bytes(params.name).length,
maxLength: 32
});
}
admin = params.initialAdmin;
ASSET = params.asset;
- CANCELABLE = params.cancelable;
+ CANCELABLE = false;
EXPIRATION = params.expiration;
ipfsCID = params.ipfsCID;
MERKLE_ROOT = params.merkleRoot;
NAME = bytes32(abi.encodePacked(params.name));
TRANSFERABLE = params.transferable;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice
pina Submitter
over 1 year ago
inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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