Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: medium
Valid

Using create opcode is suspicious to re-org attack as it will be deployed on all EVM compatible networks and would result in lost of funds

Summary

The deployment of SablierV2MerkleLL and SablierV2MerkleLT uses create op-code , This is valunerable to re-orgs Because there could be funds in these contract as the user need to transfer the assets to SablierV2MerkleLL and SablierV2MerkleLT before creating streams.

Vulnerability Details

The Protocol provide the factory contract which is responsible to deploy the SablierV2MerkleLL and SablierV2MerkleLT contracts for end users to create Airstream Campaign. After deployments of contracts the users nedd to transfer the assets first to these contract after that the user can create new stream via calling claim function. let break this down according to contract flow. here I will focus on SablierV2MerkleLL.

  1. User call the createMerkleLL function to create new MerkleLockup.

function createMerkleLL(
MerkleLockup.ConstructorParams memory baseParams,
ISablierV2LockupLinear lockupLinear,
LockupLinear.Durations memory streamDurations,
uint256 aggregateAmount,
uint256 recipientCount
)
external
returns (ISablierV2MerkleLL merkleLL)
{
// Deploy the MerkleLockup contract with CREATE.
@> merkleLL = new SablierV2MerkleLL(baseParams, lockupLinear, streamDurations);
// Log the creation of the MerkleLockup contract, including some metadata that is not stored on-chain.
emit CreateMerkleLL(merkleLL, baseParams, lockupLinear, streamDurations, aggregateAmount, recipientCount);
}
  1. MerkleLockup will give max approvals to its core stream type which in this case is Linear.

constructor(
MerkleLockup.ConstructorParams memory baseParams,
ISablierV2LockupLinear lockupLinear,
LockupLinear.Durations memory streamDurations_
)
SablierV2MerkleLockup(baseParams)
{
LOCKUP_LINEAR = lockupLinear;
streamDurations = streamDurations_;
// Max approve the Sablier contract to spend funds from the MerkleLockup contract.
ASSET.forceApprove(address(LOCKUP_LINEAR), type(uint256).max);
}
  1. User transfer the assets to this newly created MerkleLockup contract. this thing is done out side the contract.

  2. User will call claim function to create new stream.

function claim(
uint256 index,
address recipient,
uint128 amount,
bytes32[] calldata merkleProof
)
external
override
returns (uint256 streamId)
{
...
// Interaction: create the stream via {SablierV2LockupLinear}.
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) })
})
);
// Log the claim.
emit Claim(index, recipient, amount, streamId);
}
  1. The claim function will call LOCKUP_LINEAR.createWithDurations function. which will call the _create function , _create function apart form other things also transfer the assets from SablierV2MerkleLL contract to LOCKUP_LINEAR contract.

function _create(LockupLinear.CreateWithTimestamps memory params) internal returns (uint256 streamId) {
...
// Load the stream ID.
streamId = nextStreamId;
// Effect: create the stream.
_streams[streamId] = Lockup.Stream({
amounts: Lockup.Amounts({ deposited: createAmounts.deposit, refunded: 0, withdrawn: 0 }),
asset: params.asset,
endTime: params.timestamps.end,
isCancelable: params.cancelable,
isDepleted: false,
isStream: true,
isTransferable: params.transferable,
sender: params.sender,
startTime: params.timestamps.start,
wasCanceled: false
});
// Effect: set the cliff time if it is greater than zero.
if (params.timestamps.cliff > 0) {
_cliffs[streamId] = params.timestamps.cliff;
}
// Effect: bump the next stream ID.
// Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
unchecked {
nextStreamId = streamId + 1;
}
// Effect: mint the NFT to the recipient.
_mint({ to: params.recipient, tokenId: streamId });
// Interaction: transfer the deposit amount.
params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
...
}

The following case could occur:

  1. User create New SablierV2MerkleLL and transfer 100 tokens to it at block 10.

  2. user create stream for 50 token at block 11.

  3. re-org occur now block 10 gets drop.

  4. User will lose his token hold by SablierV2MerkleLL.

Impact

As the Smart contract are suppose to compatible with all EVM based chain. Re-org is known issue on Ethereum and other chains. If this happens the user funds locks in contract will be lost.

Tools Used

Manual Review

Recommendations

Use CREATE2 op-code with the salt compose of user defined number and msg.sender.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

CREATE is vulnerable to ChainReorgs

Support

FAQs

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