Sablier

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

Non Critical issues (21-30) of 73

NC021 - Large or complicated code bases should implement invariant tests:

Large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts, should implement invariant fuzzing tests. Invariant fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and invariant fuzzers, with properly and extensively-written invariants, can close this testing gap significantly.

NC022 - Long functions should be refactored into multiple, smaller, functions:

Click to show 7 findings
File: v2-core/src/SablierV2LockupDynamic.sol
221 function _calculateStreamedAmountForMultipleSegments(uint256 streamId) internal view returns (uint128) {
222 unchecked {
223 uint40 blockTimestamp = uint40(block.timestamp);
224 Lockup.Stream memory stream = _streams[streamId];
225 LockupDynamic.Segment[] memory segments = _segments[streamId];
226
227 // Sum the amounts in all segments that precede the block timestamp.
228 uint128 previousSegmentAmounts;
229 uint40 currentSegmentTimestamp = segments[0].timestamp;
230 uint256 index = 0;
231 while (currentSegmentTimestamp < blockTimestamp) {
232 previousSegmentAmounts += segments[index].amount;
233 index += 1;
234 currentSegmentTimestamp = segments[index].timestamp;
235 }
236
237 // After exiting the loop, the current segment is at `index`.
238 SD59x18 currentSegmentAmount = segments[index].amount.intoSD59x18();
239 SD59x18 currentSegmentExponent = segments[index].exponent.intoSD59x18();
240 currentSegmentTimestamp = segments[index].timestamp;
241
242 uint40 previousTimestamp;
243 if (index == 0) {
244 // When the current segment's index is equal to 0, the current segment is the first, so use the start
245 // time as the previous timestamp.
246 previousTimestamp = stream.startTime;
247 } else {
248 // Otherwise, when the current segment's index is greater than zero, it means that the segment is not
249 // the first. In this case, use the previous segment's timestamp.
250 previousTimestamp = segments[index - 1].timestamp;
251 }
252
253 // Calculate how much time has passed since the segment started, and the total duration of the segment.
254 SD59x18 elapsedTime = (blockTimestamp - previousTimestamp).intoSD59x18();
255 SD59x18 segmentDuration = (currentSegmentTimestamp - previousTimestamp).intoSD59x18();
256
257 // Divide the elapsed time by the total duration of the segment.
258 SD59x18 elapsedTimePercentage = elapsedTime.div(segmentDuration);
259
260 // Calculate the streamed amount using the special formula.
261 SD59x18 multiplier = elapsedTimePercentage.pow(currentSegmentExponent);
262 SD59x18 segmentStreamedAmount = multiplier.mul(currentSegmentAmount);
263
264 // Although the segment streamed amount should never exceed the total segment amount, this condition is
265 // checked without asserting to avoid locking funds in case of a bug. If this situation occurs, the
266 // amount streamed in the segment is considered zero (except for past withdrawals), and the segment is
267 // effectively voided.
268 if (segmentStreamedAmount.gt(currentSegmentAmount)) {
269 return previousSegmentAmounts > stream.amounts.withdrawn
270 ? previousSegmentAmounts
271 : stream.amounts.withdrawn;
272 }
273
274 // Calculate the total streamed amount by adding the previous segment amounts and the amount streamed in
275 // the current segment. Casting to uint128 is safe due to the if statement above.
276 return previousSegmentAmounts + uint128(segmentStreamedAmount.intoUint256());
277 }
278 }
316 function _create(LockupDynamic.CreateWithTimestamps memory params) internal returns (uint256 streamId) {
317 // Check: verify the broker fee and calculate the amounts.
318 Lockup.CreateAmounts memory createAmounts =
319 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
320
321 // Check: validate the user-provided parameters.
322 Helpers.checkCreateLockupDynamic(createAmounts.deposit, params.segments, MAX_SEGMENT_COUNT, params.startTime);
323
324 // Load the stream ID in a variable.
325 streamId = nextStreamId;
326
327 // Effect: create the stream.
328 Lockup.Stream storage stream = _streams[streamId];
329 stream.amounts.deposited = createAmounts.deposit;
330 stream.asset = params.asset;
331 stream.isCancelable = params.cancelable;
332 stream.isStream = true;
333 stream.isTransferable = params.transferable;
334 stream.sender = params.sender;
335 stream.startTime = params.startTime;
336
337 unchecked {
338 // The segment count cannot be zero at this point.
339 uint256 segmentCount = params.segments.length;
340 stream.endTime = params.segments[segmentCount - 1].timestamp;
341
342 // Effect: store the segments. Since Solidity lacks a syntax for copying arrays of structs directly from
343 // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783.
344 for (uint256 i = 0; i < segmentCount; ++i) {
345 _segments[streamId].push(params.segments[i]);
346 }
347
348 // Effect: bump the next stream ID.
349 // Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
350 nextStreamId = streamId + 1;
351 }
352
353 // Effect: mint the NFT to the recipient.
354 _mint({ to: params.recipient, tokenId: streamId });
355
356 // Interaction: transfer the deposit amount.
357 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
358
359 // Interaction: pay the broker fee, if not zero.
360 if (createAmounts.brokerFee > 0) {
361 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
362 }
363
364 // Log the newly created stream.
365 emit ISablierV2LockupDynamic.CreateLockupDynamicStream({
366 streamId: streamId,
367 funder: msg.sender,
368 sender: params.sender,
369 recipient: params.recipient,
370 amounts: createAmounts,
371 asset: params.asset,
372 cancelable: params.cancelable,
373 transferable: params.transferable,
374 segments: params.segments,
375 timestamps: LockupDynamic.Timestamps({ start: stream.startTime, end: stream.endTime }),
376 broker: params.broker.account
377 });
378 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupDynamic.sol#L0:0

File: v2-core/src/SablierV2LockupLinear.sol
237 function _create(LockupLinear.CreateWithTimestamps memory params) internal returns (uint256 streamId) {
238 // Check: verify the broker fee and calculate the amounts.
239 Lockup.CreateAmounts memory createAmounts =
240 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
241
242 // Check: validate the user-provided parameters.
243 Helpers.checkCreateLockupLinear(createAmounts.deposit, params.timestamps);
244
245 // Load the stream ID.
246 streamId = nextStreamId;
247
248 // Effect: create the stream.
249 _streams[streamId] = Lockup.Stream({
250 amounts: Lockup.Amounts({ deposited: createAmounts.deposit, refunded: 0, withdrawn: 0 }),
251 asset: params.asset,
252 endTime: params.timestamps.end,
253 isCancelable: params.cancelable,
254 isDepleted: false,
255 isStream: true,
256 isTransferable: params.transferable,
257 sender: params.sender,
258 startTime: params.timestamps.start,
259 wasCanceled: false
260 });
261
262 // Effect: set the cliff time if it is greater than zero.
263 if (params.timestamps.cliff > 0) {
264 _cliffs[streamId] = params.timestamps.cliff;
265 }
266
267 // Effect: bump the next stream ID.
268 // Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
269 unchecked {
270 nextStreamId = streamId + 1;
271 }
272
273 // Effect: mint the NFT to the recipient.
274 _mint({ to: params.recipient, tokenId: streamId });
275
276 // Interaction: transfer the deposit amount.
277 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
278
279 // Interaction: pay the broker fee, if not zero.
280 if (createAmounts.brokerFee > 0) {
281 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
282 }
283
284 // Log the newly created stream.
285 emit ISablierV2LockupLinear.CreateLockupLinearStream({
286 streamId: streamId,
287 funder: msg.sender,
288 sender: params.sender,
289 recipient: params.recipient,
290 amounts: createAmounts,
291 asset: params.asset,
292 cancelable: params.cancelable,
293 transferable: params.transferable,
294 timestamps: params.timestamps,
295 broker: params.broker.account
296 });
297 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupLinear.sol#L0:0

File: v2-core/src/SablierV2LockupTranched.sol
220 function _create(LockupTranched.CreateWithTimestamps memory params) internal returns (uint256 streamId) {
221 // Check: verify the broker fee and calculate the amounts.
222 Lockup.CreateAmounts memory createAmounts =
223 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
224
225 // Check: validate the user-provided parameters.
226 Helpers.checkCreateLockupTranched(createAmounts.deposit, params.tranches, MAX_TRANCHE_COUNT, params.startTime);
227
228 // Load the stream ID in a variable.
229 streamId = nextStreamId;
230
231 // Effect: create the stream.
232 Lockup.Stream storage stream = _streams[streamId];
233 stream.amounts.deposited = createAmounts.deposit;
234 stream.asset = params.asset;
235 stream.isCancelable = params.cancelable;
236 stream.isStream = true;
237 stream.isTransferable = params.transferable;
238 stream.sender = params.sender;
239 stream.startTime = params.startTime;
240
241 unchecked {
242 // The tranche count cannot be zero at this point.
243 uint256 trancheCount = params.tranches.length;
244 stream.endTime = params.tranches[trancheCount - 1].timestamp;
245
246 // Effect: store the tranches. Since Solidity lacks a syntax for copying arrays of structs directly from
247 // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783.
248 for (uint256 i = 0; i < trancheCount; ++i) {
249 _tranches[streamId].push(params.tranches[i]);
250 }
251
252 // Effect: bump the next stream ID.
253 // Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
254 nextStreamId = streamId + 1;
255 }
256
257 // Effect: mint the NFT to the recipient.
258 _mint({ to: params.recipient, tokenId: streamId });
259
260 // Interaction: transfer the deposit amount.
261 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
262
263 // Interaction: pay the broker fee, if not zero.
264 if (createAmounts.brokerFee > 0) {
265 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
266 }
267
268 // Log the newly created stream.
269 emit ISablierV2LockupTranched.CreateLockupTranchedStream({
270 streamId: streamId,
271 funder: msg.sender,
272 sender: params.sender,
273 recipient: params.recipient,
274 amounts: createAmounts,
275 asset: params.asset,
276 cancelable: params.cancelable,
277 transferable: params.transferable,
278 tranches: params.tranches,
279 timestamps: LockupTranched.Timestamps({ start: stream.startTime, end: stream.endTime }),
280 broker: params.broker.account
281 });
282 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupTranched.sol#L0:0

File: v2-core/src/SablierV2NFTDescriptor.sol
47 function tokenURI(IERC721Metadata sablier, uint256 streamId) external view override returns (string memory uri) {
48 TokenURIVars memory vars;
49
50 // Load the contracts.
51 vars.sablier = ISablierV2Lockup(address(sablier));
52 vars.sablierModel = mapSymbol(sablier);
53 vars.sablierStringified = address(sablier).toHexString();
54 vars.asset = address(vars.sablier.getAsset(streamId));
55 vars.assetSymbol = safeAssetSymbol(vars.asset);
56 vars.depositedAmount = vars.sablier.getDepositedAmount(streamId);
57
58 // Load the stream's data.
59 vars.status = stringifyStatus(vars.sablier.statusOf(streamId));
60 vars.streamedPercentage = calculateStreamedPercentage({
61 streamedAmount: vars.sablier.streamedAmountOf(streamId),
62 depositedAmount: vars.depositedAmount
63 });
64
65 // Generate the SVG.
66 vars.svg = NFTSVG.generateSVG(
67 NFTSVG.SVGParams({
68 accentColor: generateAccentColor(address(sablier), streamId),
69 amount: abbreviateAmount({ amount: vars.depositedAmount, decimals: safeAssetDecimals(vars.asset) }),
70 assetAddress: vars.asset.toHexString(),
71 assetSymbol: vars.assetSymbol,
72 duration: calculateDurationInDays({
73 startTime: vars.sablier.getStartTime(streamId),
74 endTime: vars.sablier.getEndTime(streamId)
75 }),
76 sablierAddress: vars.sablierStringified,
77 progress: stringifyPercentage(vars.streamedPercentage),
78 progressNumerical: vars.streamedPercentage,
79 status: vars.status,
80 sablierModel: vars.sablierModel
81 })
82 );
83
84 // Performs a low-level call to handle older deployments that miss the `isTransferable` function.
85 (vars.success, vars.returnData) =
86 address(vars.sablier).staticcall(abi.encodeCall(ISablierV2Lockup.isTransferable, (streamId)));
87
88 // When the call has failed, the stream NFT is assumed to be transferable.
89 vars.isTransferable = vars.success ? abi.decode(vars.returnData, (bool)) : true;
90
91 // Generate the JSON metadata.
92 vars.json = string.concat(
93 '{"attributes":',
94 generateAttributes({
95 assetSymbol: vars.assetSymbol,
96 sender: vars.sablier.getSender(streamId).toHexString(),
97 status: vars.status
98 }),
99 ',"description":"',
100 generateDescription({
101 sablierModel: vars.sablierModel,
102 assetSymbol: vars.assetSymbol,
103 sablierStringified: vars.sablierStringified,
104 assetAddress: vars.asset.toHexString(),
105 streamId: streamId.toString(),
106 isTransferable: vars.isTransferable
107 }),
108 '","external_url":"https://sablier.com","name":"',
109 generateName({ sablierModel: vars.sablierModel, streamId: streamId.toString() }),
110 '","image":"data:image/svg+xml;base64,',
111 Base64.encode(bytes(vars.svg)),
112 '"}'
113 );
114
115 // Encode the JSON metadata in Base64.
116 uri = string.concat("data:application/json;base64,", Base64.encode(bytes(vars.json)));
117 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0

File: v2-core/src/abstracts/SablierV2Lockup.sol
332 function withdraw(
333 uint256 streamId,
334 address to,
335 uint128 amount
336 )
337 public
338 override
339 noDelegateCall
340 notNull(streamId)
341 updateMetadata(streamId)
342 {
343 // Check: the stream is not depleted.
344 if (_streams[streamId].isDepleted) {
345 revert Errors.SablierV2Lockup_StreamDepleted(streamId);
346 }
347
348 // Check: the withdrawal address is not zero.
349 if (to == address(0)) {
350 revert Errors.SablierV2Lockup_WithdrawToZeroAddress(streamId);
351 }
352
353 // Check: the withdraw amount is not zero.
354 if (amount == 0) {
355 revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId);
356 }
357
358 // Retrieve the recipient from storage.
359 address recipient = _ownerOf(streamId);
360
361 // Check: if `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address
362 // must be the recipient.
363 if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId)) {
364 revert Errors.SablierV2Lockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to);
365 }
366
367 // Check: the withdraw amount is not greater than the withdrawable amount.
368 uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
369 if (amount > withdrawableAmount) {
370 revert Errors.SablierV2Lockup_Overdraw(streamId, amount, withdrawableAmount);
371 }
372
373 // Retrieve the sender from storage.
374 address sender = _streams[streamId].sender;
375
376 // Effects and Interactions: make the withdrawal.
377 _withdraw(streamId, to, amount);
378
379 // Interaction: if `msg.sender` is not the recipient and the recipient is a contract, try to invoke the
380 // withdraw hook on it without reverting if the hook is not implemented, and also without bubbling up
381 // any potential revert.
382 if (msg.sender != recipient && recipient.code.length > 0) {
383 try ISablierV2Recipient(recipient).onLockupStreamWithdrawn({
384 streamId: streamId,
385 caller: msg.sender,
386 to: to,
387 amount: amount
388 }) { } catch { }
389 }
390
391 // Interaction: if `msg.sender` is not the sender, the sender is a contract and is different from the
392 // recipient, try to invoke the withdraw hook on it without reverting if the hook is not implemented, and also
393 // without bubbling up any potential revert.
394 if (msg.sender != sender && sender.code.length > 0 && sender != recipient) {
395 try ISablierV2Sender(sender).onLockupStreamWithdrawn({
396 streamId: streamId,
397 caller: msg.sender,
398 to: to,
399 amount: amount
400 }) { } catch { }
401 }
402 }
551 function _cancel(uint256 streamId) internal {
552 // Calculate the streamed amount.
553 uint128 streamedAmount = _calculateStreamedAmount(streamId);
554
555 // Retrieve the amounts from storage.
556 Lockup.Amounts memory amounts = _streams[streamId].amounts;
557
558 // Check: the stream is not settled.
559 if (streamedAmount >= amounts.deposited) {
560 revert Errors.SablierV2Lockup_StreamSettled(streamId);
561 }
562
563 // Check: the stream is cancelable.
564 if (!_streams[streamId].isCancelable) {
565 revert Errors.SablierV2Lockup_StreamNotCancelable(streamId);
566 }
567
568 // Calculate the sender's amount.
569 uint128 senderAmount;
570 unchecked {
571 senderAmount = amounts.deposited - streamedAmount;
572 }
573
574 // Calculate the recipient's amount.
575 uint128 recipientAmount = streamedAmount - amounts.withdrawn;
576
577 // Effect: mark the stream as canceled.
578 _streams[streamId].wasCanceled = true;
579
580 // Effect: make the stream not cancelable anymore, because a stream can only be canceled once.
581 _streams[streamId].isCancelable = false;
582
583 // Effect: if there are no assets left for the recipient to withdraw, mark the stream as depleted.
584 if (recipientAmount == 0) {
585 _streams[streamId].isDepleted = true;
586 }
587
588 // Effect: set the refunded amount.
589 _streams[streamId].amounts.refunded = senderAmount;
590
591 // Retrieve the sender and the recipient from storage.
592 address sender = _streams[streamId].sender;
593 address recipient = _ownerOf(streamId);
594
595 // Retrieve the ERC-20 asset from storage.
596 IERC20 asset = _streams[streamId].asset;
597
598 // Interaction: refund the sender.
599 asset.safeTransfer({ to: sender, value: senderAmount });
600
601 // Log the cancellation.
602 emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount);
603
604 // Emits an ERC-4906 event to trigger an update of the NFT metadata.
605 emit MetadataUpdate({ _tokenId: streamId });
606
607 // Interaction: if the recipient is a contract, try to invoke the cancel hook on the recipient without
608 // reverting if the hook is not implemented, and without bubbling up any potential revert.
609 if (recipient.code.length > 0) {
610 try ISablierV2Recipient(recipient).onLockupStreamCanceled({
611 streamId: streamId,
612 sender: sender,
613 senderAmount: senderAmount,
614 recipientAmount: recipientAmount
615 }) { } catch { }
616 }
617 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0

File: v2-core/src/libraries/Helpers.sol
227 function _checkSegments(
228 LockupDynamic.Segment[] memory segments,
229 uint128 depositAmount,
230 uint40 startTime
231 )
232 private
233 view
234 {
235 // Check: the start time is strictly less than the first segment timestamp.
236 if (startTime >= segments[0].timestamp) {
237 revert Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp(
238 startTime, segments[0].timestamp
239 );
240 }
241
242 // Pre-declare the variables needed in the for loop.
243 uint128 segmentAmountsSum;
244 uint40 currentSegmentTimestamp;
245 uint40 previousSegmentTimestamp;
246
247 // Iterate over the segments to:
248 //
249 // 1. Calculate the sum of all segment amounts.
250 // 2. Check that the timestamps are ordered.
251 uint256 count = segments.length;
252 for (uint256 index = 0; index < count; ++index) {
253 // Add the current segment amount to the sum.
254 segmentAmountsSum += segments[index].amount;
255
256 // Check: the current timestamp is strictly greater than the previous timestamp.
257 currentSegmentTimestamp = segments[index].timestamp;
258 if (currentSegmentTimestamp <= previousSegmentTimestamp) {
259 revert Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered(
260 index, previousSegmentTimestamp, currentSegmentTimestamp
261 );
262 }
263
264 // Make the current timestamp the previous timestamp of the next loop iteration.
265 previousSegmentTimestamp = currentSegmentTimestamp;
266 }
267
268 // Check: the last timestamp is in the future.
269 // When the loop exits, the current segment's timestamp is the last segment's timestamp, i.e. the stream's end
270 // time. The variable is not renamed for gas efficiency purposes.
271 uint40 blockTimestamp = uint40(block.timestamp);
272 if (blockTimestamp >= currentSegmentTimestamp) {
273 revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentSegmentTimestamp);
274 }
275
276 // Check: the deposit amount is equal to the segment amounts sum.
277 if (depositAmount != segmentAmountsSum) {
278 revert Errors.SablierV2LockupDynamic_DepositAmountNotEqualToSegmentAmountsSum(
279 depositAmount, segmentAmountsSum
280 );
281 }
282 }
290 function _checkTranches(
291 LockupTranched.Tranche[] memory tranches,
292 uint128 depositAmount,
293 uint40 startTime
294 )
295 private
296 view
297 {
298 // Check: the start time is strictly less than the first tranche timestamp.
299 if (startTime >= tranches[0].timestamp) {
300 revert Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp(
301 startTime, tranches[0].timestamp
302 );
303 }
304
305 // Pre-declare the variables needed in the for loop.
306 uint128 trancheAmountsSum;
307 uint40 currentTrancheTimestamp;
308 uint40 previousTrancheTimestamp;
309
310 // Iterate over the tranches to:
311 //
312 // 1. Calculate the sum of all tranche amounts.
313 // 2. Check that the timestamps are ordered.
314 uint256 count = tranches.length;
315 for (uint256 index = 0; index < count; ++index) {
316 // Add the current tranche amount to the sum.
317 trancheAmountsSum += tranches[index].amount;
318
319 // Check: the current timestamp is strictly greater than the previous timestamp.
320 currentTrancheTimestamp = tranches[index].timestamp;
321 if (currentTrancheTimestamp <= previousTrancheTimestamp) {
322 revert Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered(
323 index, previousTrancheTimestamp, currentTrancheTimestamp
324 );
325 }
326
327 // Make the current timestamp the previous timestamp of the next loop iteration.
328 previousTrancheTimestamp = currentTrancheTimestamp;
329 }
330
331 // Check: the last timestamp is in the future.
332 // When the loop exits, the current tranche's timestamp is the last tranche's timestamp, i.e. the stream's end
333 // time. The variable is not renamed for gas efficiency purposes.
334 uint40 blockTimestamp = uint40(block.timestamp);
335 if (blockTimestamp >= currentTrancheTimestamp) {
336 revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentTrancheTimestamp);
337 }
338
339 // Check: the deposit amount is equal to the tranche amounts sum.
340 if (depositAmount != trancheAmountsSum) {
341 revert Errors.SablierV2LockupTranched_DepositAmountNotEqualToTrancheAmountsSum(
342 depositAmount, trancheAmountsSum
343 );
344 }
345 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/Helpers.sol#L0:0

File: v2-core/src/libraries/NFTSVG.sol
44 function generateSVG(SVGParams memory params) internal pure returns (string memory) {
45 SVGVars memory vars;
46
47 // Generate the progress card.
48 (vars.progressWidth, vars.progressCard) = SVGElements.card({
49 cardType: SVGElements.CardType.PROGRESS,
50 content: params.progress,
51 circle: SVGElements.progressCircle({
52 progressNumerical: params.progressNumerical,
53 accentColor: params.accentColor
54 })
55 });
56
57 // Generate the status card.
58 (vars.statusWidth, vars.statusCard) =
59 SVGElements.card({ cardType: SVGElements.CardType.STATUS, content: params.status });
60
61 // Generate the deposit amount card.
62 (vars.amountWidth, vars.amountCard) =
63 SVGElements.card({ cardType: SVGElements.CardType.AMOUNT, content: params.amount });
64
65 // Generate the duration card.
66 (vars.durationWidth, vars.durationCard) =
67 SVGElements.card({ cardType: SVGElements.CardType.DURATION, content: params.duration });
68
69 unchecked {
70 // Calculate the width of the row containing the cards and the margins between them.
71 vars.cardsWidth =
72 vars.amountWidth + vars.durationWidth + vars.progressWidth + vars.statusWidth + CARD_MARGIN * 3;
73
74 // Calculate the positions on the X axis based on the following layout:
75 //
76 // ___________________________ SVG Width (1000px) ___________________________
77 // | | | | | | | | | |
78 // | <-> | Progress | 16px | Status | 16px | Amount | 16px | Duration | <-> |
79 vars.progressXPosition = (1000 - vars.cardsWidth) / 2;
80 vars.statusXPosition = vars.progressXPosition + vars.progressWidth + CARD_MARGIN;
81 vars.amountXPosition = vars.statusXPosition + vars.statusWidth + CARD_MARGIN;
82 vars.durationXPosition = vars.amountXPosition + vars.amountWidth + CARD_MARGIN;
83 }
84
85 // Concatenate all cards.
86 vars.cards = string.concat(vars.progressCard, vars.statusCard, vars.amountCard, vars.durationCard);
87
88 return string.concat(
89 '<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">',
90 SVGElements.BACKGROUND,
91 generateDefs(params.accentColor, params.status, vars.cards),
92 generateFloatingText(params.sablierAddress, params.sablierModel, params.assetAddress, params.assetSymbol),
93 generateHrefs(vars.progressXPosition, vars.statusXPosition, vars.amountXPosition, vars.durationXPosition),
94 "</svg>"
95 );
96 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0

NC023 - Strings should use double quotes rather than single quotes:

See the Solidity Style Guide

File: v2-core/src/SablierV2NFTDescriptor.sol
252 '"},{"trait_type":"Sender","value":"',
250 '[{"trait_type":"Asset","value":"',
254 '"},{"trait_type":"Status","value":"',
110 '","image":"data:image/svg+xml;base64,',
108 '","external_url":"https://sablier.com","name":"',
112 '"}'
99 ',"description":"',
93 '{"attributes":',
256 '"}]'

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0

File: v2-core/src/libraries/NFTSVG.sol
169 '" y="790"/>',
158 '<use href="#Glow" x="1000" y="1000" fill-opacity=".9"/>',
161 '<use href="#Progress" x="',
170 '<use href="#Duration" x="',
159 '<use href="#Logo" x="170" y="170" transform="scale(.6)"/>'
160 '<use href="#Hourglass" x="150" y="90" transform="rotate(10)" transform-origin="500 500"/>',
167 '<use href="#Amount" x="',
163 '" y="790"/>',
164 '<use href="#Status" x="',
89 '<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">',
157 '<use href="#Glow" fill-opacity=".9"/>',
166 '" y="790"/>',
131 '<text text-rendering="optimizeSpeed">',
172 '" y="790"/>'

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0

File: v2-core/src/libraries/SVGElements.sol
162 '<stop offset="10%" stop-color="',
116 '" height="100" fill-opacity=".03" rx="15" ry="15" stroke="#fff" stroke-opacity=".1" stroke-width="4"/>',
39 '<path d="m481.46,481.54v81.01c-2.35.77-4.82,1.51-7.39,2.23-30.3,8.54-74.65,13.92-124.06,13.92-53.6,0-101.24-6.33-131.47-16.16v-81l46.3-46.31h170.33l46.29,46.31Z" fill="url(#SandBottom)"/><path d="m435.17,435.23c0,1.17-.46,2.32-1.33,3.44-7.11,9.08-41.93,15.98-83.81,15.98s-76.7-6.9-83.82-15.98c-.87-1.12-1.33-2.27-1.33-3.44v-.04l8.34-8.35.01-.01c13.72-6.51,42.95-11.02,76.8-11.02s62.97,4.49,76.72,11l8.42,8.42Z" fill="url(#SandTop)"/>';
167 '"/>',
176 '"/>',
144 '" stop-opacity=".6"/>',
33 '<path d="m566,161.201v-53.924c0-19.382-22.513-37.563-63.398-51.198-40.756-13.592-94.946-21.079-152.587-21.079s-111.838,7.487-152.602,21.079c-40.893,13.636-63.413,31.816-63.413,51.198v53.924c0,17.181,17.704,33.427,50.223,46.394v284.809c-32.519,12.96-50.223,29.206-50.223,46.394v53.924c0,19.382,22.52,37.563,63.413,51.198,40.763,13.592,94.954,21.079,152.602,21.079s111.831-7.487,152.587-21.079c40.886-13.636,63.398-31.816,63.398-51.198v-53.924c0-17.196-17.704-33.435-50.223-46.401V207.603c32.519-12.967,50.223-29.206,50.223-46.401Zm-347.462,57.793l130.959,131.027-130.959,131.013V218.994Zm262.924.022v262.018l-130.937-131.006,130.937-131.013Z" fill="#161822"></path>';
218 '" stroke-linecap="round" stroke-width="5" transform="rotate(-90)" transform-origin="166 50"/>',
130 '<textPath startOffset="',
48 '<filter id="Noise"><feFlood x="0" y="0" width="100%" height="100%" flood-color="hsl(230,21%,11%)" flood-opacity="1" result="floodFill"/><feTurbulence baseFrequency=".4" numOctaves="3" result="Noise" type="fractalNoise"/><feBlend in="Noise" in2="floodFill" mode="soft-light"/></filter>';
117 '<text x="20" y="34" font-family="\'Courier New\',Arial,monospace" font-size="22px">',
30 '<path d="M 50,360 a 300,300 0 1,1 600,0 a 300,300 0 1,1 -600,0" fill="#fff" fill-opacity=".02" stroke="url(#HourglassStroke)" stroke-width="4"/>';
213 '" stroke-width="10"/>',
214 '<circle cx="166" cy="50" pathLength="10000" r="22" stroke="',
145 '<stop offset="100%" stop-color="',
168 '<animate attributeName="x1" dur="6s" repeatCount="indefinite" values="30%;60%;120%;60%;30%;"/>',
177 '<stop offset="80%" stop-color="',
154 '"/>',
164 '"/>',
173 '<linearGradient id="HourglassStroke" gradientTransform="rotate(90)" gradientUnits="userSpaceOnUse">',
141 '<radialGradient id="RadialGlow">',
113 '" fill="#fff">',
22 '<path id="FloatingText" fill="none" d="M125 45h750s80 0 80 80v750s0 80 -80 80h-750s-80 0 -80 -80v-750s0 -80 80 -80"/>';
114 '<rect width="',
17 '<rect width="100%" height="100%" filter="url(#Noise)"/><rect x="70" y="70" width="860" height="860" fill="#fff" fill-opacity=".03" rx="45" ry="45" stroke="#fff" stroke-opacity=".1" stroke-width="4"/>';
210 '<g fill="none">',
27 '<path id="Logo" fill="#fff" fill-opacity=".1" d="m133.559,124.034c-.013,2.412-1.059,4.848-2.923,6.402-2.558,1.819-5.168,3.439-7.888,4.996-14.44,8.262-31.047,12.565-47.674,12.569-8.858.036-17.838-1.272-26.328-3.663-9.806-2.766-19.087-7.113-27.562-12.778-13.842-8.025,9.468-28.606,16.153-35.265h0c2.035-1.838,4.252-3.546,6.463-5.224h0c6.429-5.655,16.218-2.835,20.358,4.17,4.143,5.057,8.816,9.649,13.92,13.734h.037c5.736,6.461,15.357-2.253,9.38-8.48,0,0-3.515-3.515-3.515-3.515-11.49-11.478-52.656-52.664-64.837-64.837l.049-.037c-1.725-1.606-2.719-3.847-2.751-6.204h0c-.046-2.375,1.062-4.582,2.726-6.229h0l.185-.148h0c.099-.062,.222-.148,.37-.259h0c2.06-1.362,3.951-2.621,6.044-3.842C57.763-3.473,97.76-2.341,128.637,18.332c16.671,9.946-26.344,54.813-38.651,40.199-6.299-6.096-18.063-17.743-19.668-18.811-6.016-4.047-13.061,4.776-7.752,9.751l68.254,68.371c1.724,1.601,2.714,3.84,2.738,6.192Z"/>';
155 '<stop offset="100%" stop-color="',
132 '" href="#FloatingText" fill="#fff" font-family="\'Courier New\',Arial,monospace" fill-opacity=".8" font-size="26px">',
45 '<polygon points="350 350.026 415.03 284.978 285 284.978 350 350.026" fill="url(#SandBottom)"/><path d="m416.341,281.975c0,.914-.354,1.809-1.035,2.68-5.542,7.076-32.661,12.45-65.28,12.45-32.624,0-59.738-5.374-65.28-12.45-.681-.872-1.035-1.767-1.035-2.68,0-.914.354-1.808,1.035-2.676,5.542-7.076,32.656-12.45,65.28-12.45,32.619,0,59.738,5.374,65.28,12.45.681.867,1.035,1.762,1.035,2.676Z" fill="url(#SandTop)"/>';
36 '<g fill="none" stroke="url(#HourglassStroke)" stroke-linecap="round" stroke-miterlimit="10" stroke-width="4"><path d="m565.641,107.28c0,9.537-5.56,18.629-15.676,26.973h-.023c-9.204,7.596-22.194,14.562-38.197,20.592-39.504,14.936-97.325,24.355-161.733,24.355-90.48,0-167.948-18.582-199.953-44.948h-.023c-10.115-8.344-15.676-17.437-15.676-26.973,0-39.735,96.554-71.921,215.652-71.921s215.629,32.185,215.629,71.921Z"/><path d="m134.36,161.203c0,39.735,96.554,71.921,215.652,71.921s215.629-32.186,215.629-71.921"/><line x1="134.36" y1="161.203" x2="134.36" y2="107.28"/><line x1="565.64" y1="161.203" x2="565.64" y2="107.28"/><line x1="184.584" y1="206.823" x2="184.585" y2="537.579"/><line x1="218.181" y1="218.118" x2="218.181" y2="562.537"/><line x1="481.818" y1="218.142" x2="481.819" y2="562.428"/><line x1="515.415" y1="207.352" x2="515.416" y2="537.579"/><path d="m184.58,537.58c0,5.45,4.27,10.65,12.03,15.42h.02c5.51,3.39,12.79,6.55,21.55,9.42,30.21,9.9,78.02,16.28,131.83,16.28,49.41,0,93.76-5.38,124.06-13.92,2.7-.76,5.29-1.54,7.75-2.35,8.77-2.87,16.05-6.04,21.56-9.43h0c7.76-4.77,12.04-9.97,12.04-15.42"/><path d="m184.582,492.656c-31.354,12.485-50.223,28.58-50.223,46.142,0,9.536,5.564,18.627,15.677,26.969h.022c8.503,7.005,20.213,13.463,34.524,19.159,9.999,3.991,21.269,7.609,33.597,10.788,36.45,9.407,82.181,15.002,131.835,15.002s95.363-5.595,131.807-15.002c10.847-2.79,20.867-5.926,29.924-9.349,1.244-.467,2.473-.942,3.673-1.424,14.326-5.696,26.035-12.161,34.524-19.173h.022c10.114-8.342,15.677-17.433,15.677-26.969,0-17.562-18.869-33.665-50.223-46.15"/><path d="m134.36,592.72c0,39.735,96.554,71.921,215.652,71.921s215.629-32.186,215.629-71.921"/><line x1="134.36" y1="592.72" x2="134.36" y2="538.797"/><line x1="565.64" y1="592.72" x2="565.64" y2="538.797"/><polyline points="481.822 481.901 481.798 481.877 481.775 481.854 350.015 350.026 218.185 218.129"/><polyline points="218.185 481.901 218.231 481.854 350.015 350.026 481.822 218.152"/></g>';
174 '<stop offset="50%" stop-color="',
151 '<linearGradient id="SandTop" x1="0%" y1="0%">',
147 '" stop-opacity="0"/>',
211 '<circle cx="166" cy="50" r="22" stroke="',
188 '<g id="Hourglass">',
165 '<stop offset="100%" stop-color="',
133 '<animate additive="sum" attributeName="startOffset" begin="0s" dur="50s" from="0%" repeatCount="indefinite" to="100%"/>',
142 '<stop offset="0%" stop-color="',
111 '<g id="',
152 '<stop offset="0%" stop-color="',
216 '" stroke-dasharray="10000" stroke-dashoffset="',
24 string internal constant GLOW = '<circle id="Glow" r="500" fill="url(#RadialGlow)"/>';
161 '<linearGradient id="SandBottom" x1="100%" y1="100%">',
179 '"/>',
157 '"/>',
120 '<text x="20" y="72" font-family="\'Courier New\',Arial,monospace" font-size="26px">',
42 '<path d="m481.46,504.101v58.449c-2.35.77-4.82,1.51-7.39,2.23-30.3,8.54-74.65,13.92-124.06,13.92-53.6,0-101.24-6.33-131.47-16.16v-58.439h262.92Z" fill="url(#SandBottom)"/><ellipse cx="350" cy="504.101" rx="131.462" ry="28.108" fill="url(#SandTop)"/>';

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/SVGElements.sol#L0:0

NC024 - Consider using block.number instead of block.timestamp:

block.timestamp is vulnerable to miner manipulation and creates a potential front-running vulnerability. Consider using block.number instead.

Click to show 6 findings
File: v2-core/src/SablierV2LockupDynamic.sol
156 startTime: uint40(block.timestamp),
193 uint40 blockTimestamp = uint40(block.timestamp);
223 uint40 blockTimestamp = uint40(block.timestamp);
285 SD59x18 elapsedTime = (uint40(block.timestamp) - _streams[streamId].startTime).intoSD59x18();

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupDynamic.sol#L0:0

File: v2-core/src/SablierV2LockupLinear.sol
135 timestamps.start = uint40(block.timestamp);
192 uint256 blockTimestamp = block.timestamp;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupLinear.sol#L0:0

File: v2-core/src/SablierV2LockupTranched.sol
151 startTime: uint40(block.timestamp),
184 uint40 blockTimestamp = uint40(block.timestamp);

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupTranched.sol#L0:0

File: v2-core/src/abstracts/SablierV2Lockup.sol
488 if (block.timestamp < _streams[streamId].startTime) {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0

File: v2-core/src/libraries/Helpers.sol
26 uint40 startTime = uint40(block.timestamp);
59 uint40 startTime = uint40(block.timestamp);
176 uint40 blockTimestamp = uint40(block.timestamp);
271 uint40 blockTimestamp = uint40(block.timestamp);
334 uint40 blockTimestamp = uint40(block.timestamp);

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/Helpers.sol#L0:0

File: v2-periphery/src/abstracts/SablierV2MerkleLockup.sol
94 return EXPIRATION > 0 && EXPIRATION <= block.timestamp;
111 blockTimestamp: block.timestamp,
133 blockTimestamp: block.timestamp,
150 _firstClaimTime = uint40(block.timestamp);
157 return _firstClaimTime > 0 && block.timestamp > _firstClaimTime + 7 days;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/abstracts/SablierV2MerkleLockup.sol#L0:0

NC025 - Consider bounding input array length:

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

Click to show 5 findings
File: v2-core/src/abstracts/SablierV2Lockup.sol
277 for (uint256 i = 0; i < count; ++i) {
278 // Effects and Interactions: cancel the stream.
279 cancel(streamIds[i]);
280 }
452 for (uint256 i = 0; i < streamIdsCount; ++i) {
453 // Checks, Effects and Interactions: check the parameters and make the withdrawal.
454 withdraw({ streamId: streamIds[i], to: _ownerOf(streamIds[i]), amount: amounts[i] });
455 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0

File: v2-core/src/libraries/Helpers.sol
39 for (uint256 i = 1; i < segmentCount; ++i) {
40 segmentsWithTimestamps[i] = LockupDynamic.Segment({
41 amount: segments[i].amount,
42 exponent: segments[i].exponent,
43 timestamp: segmentsWithTimestamps[i - 1].timestamp + segments[i].duration
44 });
45 }
69 for (uint256 i = 1; i < trancheCount; ++i) {
70 tranchesWithTimestamps[i] = LockupTranched.Tranche({
71 amount: tranches[i].amount,
72 timestamp: tranchesWithTimestamps[i - 1].timestamp + tranches[i].duration
73 });
74 }
252 for (uint256 index = 0; index < count; ++index) {
253 // Add the current segment amount to the sum.
254 segmentAmountsSum += segments[index].amount;
255
256 // Check: the current timestamp is strictly greater than the previous timestamp.
257 currentSegmentTimestamp = segments[index].timestamp;
258 if (currentSegmentTimestamp <= previousSegmentTimestamp) {
259 revert Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered(
260 index, previousSegmentTimestamp, currentSegmentTimestamp
261 );
262 }
263
264 // Make the current timestamp the previous timestamp of the next loop iteration.
265 previousSegmentTimestamp = currentSegmentTimestamp;
266 }
315 for (uint256 index = 0; index < count; ++index) {
316 // Add the current tranche amount to the sum.
317 trancheAmountsSum += tranches[index].amount;
318
319 // Check: the current timestamp is strictly greater than the previous timestamp.
320 currentTrancheTimestamp = tranches[index].timestamp;
321 if (currentTrancheTimestamp <= previousTrancheTimestamp) {
322 revert Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered(
323 index, previousTrancheTimestamp, currentTrancheTimestamp
324 );
325 }
326
327 // Make the current timestamp the previous timestamp of the next loop iteration.
328 previousTrancheTimestamp = currentTrancheTimestamp;
329 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/Helpers.sol#L0:0

File: v2-periphery/src/SablierV2BatchLockup.sol
55 for (i = 0; i < batchSize; ++i) {
56 // Create the stream.
57 streamIds[i] = lockupDynamic.createWithDurations(
58 LockupDynamic.CreateWithDurations({
59 sender: batch[i].sender,
60 recipient: batch[i].recipient,
61 totalAmount: batch[i].totalAmount,
62 asset: asset,
63 cancelable: batch[i].cancelable,
64 transferable: batch[i].transferable,
65 segments: batch[i].segments,
66 broker: batch[i].broker
67 })
68 );
69 }
103 for (i = 0; i < batchSize; ++i) {
104 // Create the stream.
105 streamIds[i] = lockupDynamic.createWithTimestamps(
106 LockupDynamic.CreateWithTimestamps({
107 sender: batch[i].sender,
108 recipient: batch[i].recipient,
109 totalAmount: batch[i].totalAmount,
110 asset: asset,
111 cancelable: batch[i].cancelable,
112 transferable: batch[i].transferable,
113 startTime: batch[i].startTime,
114 segments: batch[i].segments,
115 broker: batch[i].broker
116 })
117 );
118 }
156 for (i = 0; i < batchSize; ++i) {
157 // Create the stream.
158 streamIds[i] = lockupLinear.createWithDurations(
159 LockupLinear.CreateWithDurations({
160 sender: batch[i].sender,
161 recipient: batch[i].recipient,
162 totalAmount: batch[i].totalAmount,
163 asset: asset,
164 cancelable: batch[i].cancelable,
165 transferable: batch[i].transferable,
166 durations: batch[i].durations,
167 broker: batch[i].broker
168 })
169 );
170 }
204 for (i = 0; i < batchSize; ++i) {
205 // Create the stream.
206 streamIds[i] = lockupLinear.createWithTimestamps(
207 LockupLinear.CreateWithTimestamps({
208 sender: batch[i].sender,
209 recipient: batch[i].recipient,
210 totalAmount: batch[i].totalAmount,
211 asset: asset,
212 cancelable: batch[i].cancelable,
213 transferable: batch[i].transferable,
214 timestamps: batch[i].timestamps,
215 broker: batch[i].broker
216 })
217 );
218 }
256 for (i = 0; i < batchSize; ++i) {
257 // Create the stream.
258 streamIds[i] = lockupTranched.createWithDurations(
259 LockupTranched.CreateWithDurations({
260 sender: batch[i].sender,
261 recipient: batch[i].recipient,
262 totalAmount: batch[i].totalAmount,
263 asset: asset,
264 cancelable: batch[i].cancelable,
265 transferable: batch[i].transferable,
266 tranches: batch[i].tranches,
267 broker: batch[i].broker
268 })
269 );
270 }
304 for (i = 0; i < batchSize; ++i) {
305 // Create the stream.
306 streamIds[i] = lockupTranched.createWithTimestamps(
307 LockupTranched.CreateWithTimestamps({
308 sender: batch[i].sender,
309 recipient: batch[i].recipient,
310 totalAmount: batch[i].totalAmount,
311 asset: asset,
312 cancelable: batch[i].cancelable,
313 transferable: batch[i].transferable,
314 startTime: batch[i].startTime,
315 tranches: batch[i].tranches,
316 broker: batch[i].broker
317 })
318 );
319 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2BatchLockup.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLT.sol
52 for (uint256 i = 0; i < count; ++i) {
53 _tranchesWithPercentages.push(tranchesWithPercentages[i]);
54 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLT.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLockupFactory.sol
56 for (uint256 i = 0; i < tranchesWithPercentages.length; ++i) {
57 uint64 percentage = tranchesWithPercentages[i].unlockPercentage.unwrap();
58 totalPercentage = totalPercentage + percentage;
59 unchecked {
60 // Safe to use `unchecked` because its only used in the event.
61 totalDuration += tranchesWithPercentages[i].duration;
62 }
63 }

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLockupFactory.sol#L0:0

NC026 - Large numeric literals should use underscores for readability:

At a glance, it's quite difficult to understand how big this number is. Use underscores to make values more clear.

File: v2-core/src/SablierV2NFTDescriptor.sol
158 truncatedAmount /= 1000;
180 } else if (durationInDays > 9999) {
156 while (truncatedAmount >= 1000) {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0

File: v2-core/src/libraries/NFTSVG.sol
79 vars.progressXPosition = (1000 - vars.cardsWidth) / 2;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0

NC027 - Cast to bytes or bytes32 for clearer semantic meaning:

Using a cast on a single argument, rather than abi.encodePacked() makes the intended operation more clear, leading to less reviewer confusion.

File: v2-periphery/src/abstracts/SablierV2MerkleLockup.sol
99 return string(abi.encodePacked(NAME));

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/abstracts/SablierV2MerkleLockup.sol#L0:0

NC028 - Variables should be named in mixedCase style:

As the Solidity Style Guide suggests: arguments, local variables and mutable state variables should be named in mixedCase style.

Click to show 9 findings
File: v2-core/src/SablierV2LockupDynamic.sol
65 ISablierV2NFTDescriptor initialNFTDescriptor,

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupDynamic.sol#L0:0

File: v2-core/src/SablierV2LockupLinear.sol
58 ISablierV2NFTDescriptor initialNFTDescriptor

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupLinear.sol#L0:0

File: v2-core/src/SablierV2LockupTranched.sol
60 ISablierV2NFTDescriptor initialNFTDescriptor,

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupTranched.sol#L0:0

File: v2-core/src/abstracts/SablierV2Lockup.sol
54 constructor(address initialAdmin, ISablierV2NFTDescriptor initialNFTDescriptor) {
315 function setNFTDescriptor(ISablierV2NFTDescriptor newNFTDescriptor) external override onlyAdmin {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0

File: v2-core/src/libraries/NFTSVG.sol
30 uint256 amountXPosition;
149 uint256 amountXPosition,
38 uint256 progressXPosition;
35 uint256 durationXPosition;
148 uint256 statusXPosition,
150 uint256 durationXPosition
147 uint256 progressXPosition,
41 uint256 statusXPosition;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLL.sol
43 LockupLinear.Durations memory streamDurations_

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLL.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLT.sol
130 UD60x18 claimAmountUD = ud60x18(claimAmount);

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLT.sol#L0:0

File: v2-periphery/src/abstracts/SablierV2MerkleLockup.sol
46 string public ipfsCID;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/abstracts/SablierV2MerkleLockup.sol#L0:0

File: v2-periphery/src/types/DataTypes.sol
106 string ipfsCID;

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/types/DataTypes.sol#L0:0

NC029 - Function names should use lowerCamelCase:

According to the Solidity style guide function names should be in mixedCase (lowerCamelCase).

Click to show 5 findings
File: v2-core/src/SablierV2NFTDescriptor.sol
47 function tokenURI(IERC721Metadata sablier, uint256 streamId) external view override returns (string memory uri) {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0

File: v2-core/src/abstracts/SablierV2Lockup.sol
209 function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) {
315 function setNFTDescriptor(ISablierV2NFTDescriptor newNFTDescriptor) external override onlyAdmin {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0

File: v2-core/src/libraries/NFTSVG.sol
44 function generateSVG(SVGParams memory params) internal pure returns (string memory) {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0

File: v2-periphery/src/SablierV2BatchLockup.sol
25 function createWithDurationsLD(
73 function createWithTimestampsLD(
126 function createWithDurationsLL(
174 function createWithTimestampsLL(
226 function createWithDurationsLT(
274 function createWithTimestampsLT(

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2BatchLockup.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLockupFactory.sol
25 function createMerkleLL(
43 function createMerkleLT(

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLockupFactory.sol#L0:0

NC030 - Consider adding a deny-list:

Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens.

Click to show 8 findings
File: v2-core/src/SablierV2LockupDynamic.sol
37 contract SablierV2LockupDynamic is

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupDynamic.sol#L0:0

File: v2-core/src/SablierV2LockupLinear.sol
36 contract SablierV2LockupLinear is

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupLinear.sol#L0:0

File: v2-core/src/SablierV2LockupTranched.sol
34 contract SablierV2LockupTranched is

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupTranched.sol#L0:0

File: v2-core/src/SablierV2NFTDescriptor.sol
20 contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0

File: v2-periphery/src/SablierV2BatchLockup.sol
17 contract SablierV2BatchLockup is ISablierV2BatchLockup {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2BatchLockup.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLL.sol
17 contract SablierV2MerkleLL is

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLL.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLT.sol
17 contract SablierV2MerkleLT is

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLT.sol#L0:0

File: v2-periphery/src/SablierV2MerkleLockupFactory.sol
19 contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory {

https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLockupFactory.sol#L0:0

Updates

Lead Judging Commences

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

Info/Gas/Invalid as per Docs

https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity

Support

FAQs

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