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.
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
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
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
245
246 previousTimestamp = stream.startTime;
247 } else {
248
249
250 previousTimestamp = segments[index - 1].timestamp;
251 }
252
253
254 SD59x18 elapsedTime = (blockTimestamp - previousTimestamp).intoSD59x18();
255 SD59x18 segmentDuration = (currentSegmentTimestamp - previousTimestamp).intoSD59x18();
256
257
258 SD59x18 elapsedTimePercentage = elapsedTime.div(segmentDuration);
259
260
261 SD59x18 multiplier = elapsedTimePercentage.pow(currentSegmentExponent);
262 SD59x18 segmentStreamedAmount = multiplier.mul(currentSegmentAmount);
263
264
265
266
267
268 if (segmentStreamedAmount.gt(currentSegmentAmount)) {
269 return previousSegmentAmounts > stream.amounts.withdrawn
270 ? previousSegmentAmounts
271 : stream.amounts.withdrawn;
272 }
273
274
275
276 return previousSegmentAmounts + uint128(segmentStreamedAmount.intoUint256());
277 }
278 }
316 function _create(LockupDynamic.CreateWithTimestamps memory params) internal returns (uint256 streamId) {
317
318 Lockup.CreateAmounts memory createAmounts =
319 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
320
321
322 Helpers.checkCreateLockupDynamic(createAmounts.deposit, params.segments, MAX_SEGMENT_COUNT, params.startTime);
323
324
325 streamId = nextStreamId;
326
327
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
339 uint256 segmentCount = params.segments.length;
340 stream.endTime = params.segments[segmentCount - 1].timestamp;
341
342
343
344 for (uint256 i = 0; i < segmentCount; ++i) {
345 _segments[streamId].push(params.segments[i]);
346 }
347
348
349
350 nextStreamId = streamId + 1;
351 }
352
353
354 _mint({ to: params.recipient, tokenId: streamId });
355
356
357 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
358
359
360 if (createAmounts.brokerFee > 0) {
361 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
362 }
363
364
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
239 Lockup.CreateAmounts memory createAmounts =
240 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
241
242
243 Helpers.checkCreateLockupLinear(createAmounts.deposit, params.timestamps);
244
245
246 streamId = nextStreamId;
247
248
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
263 if (params.timestamps.cliff > 0) {
264 _cliffs[streamId] = params.timestamps.cliff;
265 }
266
267
268
269 unchecked {
270 nextStreamId = streamId + 1;
271 }
272
273
274 _mint({ to: params.recipient, tokenId: streamId });
275
276
277 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
278
279
280 if (createAmounts.brokerFee > 0) {
281 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
282 }
283
284
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
222 Lockup.CreateAmounts memory createAmounts =
223 Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE);
224
225
226 Helpers.checkCreateLockupTranched(createAmounts.deposit, params.tranches, MAX_TRANCHE_COUNT, params.startTime);
227
228
229 streamId = nextStreamId;
230
231
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
243 uint256 trancheCount = params.tranches.length;
244 stream.endTime = params.tranches[trancheCount - 1].timestamp;
245
246
247
248 for (uint256 i = 0; i < trancheCount; ++i) {
249 _tranches[streamId].push(params.tranches[i]);
250 }
251
252
253
254 nextStreamId = streamId + 1;
255 }
256
257
258 _mint({ to: params.recipient, tokenId: streamId });
259
260
261 params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit });
262
263
264 if (createAmounts.brokerFee > 0) {
265 params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
266 }
267
268
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
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
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
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
85 (vars.success, vars.returnData) =
86 address(vars.sablier).staticcall(abi.encodeCall(ISablierV2Lockup.isTransferable, (streamId)));
87
88
89 vars.isTransferable = vars.success ? abi.decode(vars.returnData, (bool)) : true;
90
91
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
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
344 if (_streams[streamId].isDepleted) {
345 revert Errors.SablierV2Lockup_StreamDepleted(streamId);
346 }
347
348
349 if (to == address(0)) {
350 revert Errors.SablierV2Lockup_WithdrawToZeroAddress(streamId);
351 }
352
353
354 if (amount == 0) {
355 revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId);
356 }
357
358
359 address recipient = _ownerOf(streamId);
360
361
362
363 if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId)) {
364 revert Errors.SablierV2Lockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to);
365 }
366
367
368 uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
369 if (amount > withdrawableAmount) {
370 revert Errors.SablierV2Lockup_Overdraw(streamId, amount, withdrawableAmount);
371 }
372
373
374 address sender = _streams[streamId].sender;
375
376
377 _withdraw(streamId, to, amount);
378
379
380
381
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
392
393
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
553 uint128 streamedAmount = _calculateStreamedAmount(streamId);
554
555
556 Lockup.Amounts memory amounts = _streams[streamId].amounts;
557
558
559 if (streamedAmount >= amounts.deposited) {
560 revert Errors.SablierV2Lockup_StreamSettled(streamId);
561 }
562
563
564 if (!_streams[streamId].isCancelable) {
565 revert Errors.SablierV2Lockup_StreamNotCancelable(streamId);
566 }
567
568
569 uint128 senderAmount;
570 unchecked {
571 senderAmount = amounts.deposited - streamedAmount;
572 }
573
574
575 uint128 recipientAmount = streamedAmount - amounts.withdrawn;
576
577
578 _streams[streamId].wasCanceled = true;
579
580
581 _streams[streamId].isCancelable = false;
582
583
584 if (recipientAmount == 0) {
585 _streams[streamId].isDepleted = true;
586 }
587
588
589 _streams[streamId].amounts.refunded = senderAmount;
590
591
592 address sender = _streams[streamId].sender;
593 address recipient = _ownerOf(streamId);
594
595
596 IERC20 asset = _streams[streamId].asset;
597
598
599 asset.safeTransfer({ to: sender, value: senderAmount });
600
601
602 emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount);
603
604
605 emit MetadataUpdate({ _tokenId: streamId });
606
607
608
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
236 if (startTime >= segments[0].timestamp) {
237 revert Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp(
238 startTime, segments[0].timestamp
239 );
240 }
241
242
243 uint128 segmentAmountsSum;
244 uint40 currentSegmentTimestamp;
245 uint40 previousSegmentTimestamp;
246
247
248
249
250
251 uint256 count = segments.length;
252 for (uint256 index = 0; index < count; ++index) {
253
254 segmentAmountsSum += segments[index].amount;
255
256
257 currentSegmentTimestamp = segments[index].timestamp;
258 if (currentSegmentTimestamp <= previousSegmentTimestamp) {
259 revert Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered(
260 index, previousSegmentTimestamp, currentSegmentTimestamp
261 );
262 }
263
264
265 previousSegmentTimestamp = currentSegmentTimestamp;
266 }
267
268
269
270
271 uint40 blockTimestamp = uint40(block.timestamp);
272 if (blockTimestamp >= currentSegmentTimestamp) {
273 revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentSegmentTimestamp);
274 }
275
276
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
299 if (startTime >= tranches[0].timestamp) {
300 revert Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp(
301 startTime, tranches[0].timestamp
302 );
303 }
304
305
306 uint128 trancheAmountsSum;
307 uint40 currentTrancheTimestamp;
308 uint40 previousTrancheTimestamp;
309
310
311
312
313
314 uint256 count = tranches.length;
315 for (uint256 index = 0; index < count; ++index) {
316
317 trancheAmountsSum += tranches[index].amount;
318
319
320 currentTrancheTimestamp = tranches[index].timestamp;
321 if (currentTrancheTimestamp <= previousTrancheTimestamp) {
322 revert Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered(
323 index, previousTrancheTimestamp, currentTrancheTimestamp
324 );
325 }
326
327
328 previousTrancheTimestamp = currentTrancheTimestamp;
329 }
330
331
332
333
334 uint40 blockTimestamp = uint40(block.timestamp);
335 if (blockTimestamp >= currentTrancheTimestamp) {
336 revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentTrancheTimestamp);
337 }
338
339
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
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
58 (vars.statusWidth, vars.statusCard) =
59 SVGElements.card({ cardType: SVGElements.CardType.STATUS, content: params.status });
60
61
62 (vars.amountWidth, vars.amountCard) =
63 SVGElements.card({ cardType: SVGElements.CardType.AMOUNT, content: params.amount });
64
65
66 (vars.durationWidth, vars.durationCard) =
67 SVGElements.card({ cardType: SVGElements.CardType.DURATION, content: params.duration });
68
69 unchecked {
70
71 vars.cardsWidth =
72 vars.amountWidth + vars.durationWidth + vars.progressWidth + vars.statusWidth + CARD_MARGIN * 3;
73
74
75
76
77
78
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
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
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0
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
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
279 cancel(streamIds[i]);
280 }
452 for (uint256 i = 0; i < streamIdsCount; ++i) {
453
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
254 segmentAmountsSum += segments[index].amount;
255
256
257 currentSegmentTimestamp = segments[index].timestamp;
258 if (currentSegmentTimestamp <= previousSegmentTimestamp) {
259 revert Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered(
260 index, previousSegmentTimestamp, currentSegmentTimestamp
261 );
262 }
263
264
265 previousSegmentTimestamp = currentSegmentTimestamp;
266 }
315 for (uint256 index = 0; index < count; ++index) {
316
317 trancheAmountsSum += tranches[index].amount;
318
319
320 currentTrancheTimestamp = tranches[index].timestamp;
321 if (currentTrancheTimestamp <= previousTrancheTimestamp) {
322 revert Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered(
323 index, previousTrancheTimestamp, currentTrancheTimestamp
324 );
325 }
326
327
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
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
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
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
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
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
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
61 totalDuration += tranchesWithPercentages[i].duration;
62 }
63 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLockupFactory.sol#L0:0
At a glance, it's quite difficult to understand how big this number is. Use underscores to make values more clear.
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2NFTDescriptor.sol#L0:0
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/abstracts/SablierV2MerkleLockup.sol#L0:0
Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens.