Click to show 8 findings
File: v2-core/src/SablierV2LockupDynamic.sol
80 function getSegments(uint256 streamId)
81 external
82 view
83 override
84 notNull(streamId)
85 returns (LockupDynamic.Segment[] memory segments)
86 {
87 segments = _segments[streamId];
88 }
91 function getStream(uint256 streamId)
92 external
93 view
94 override
95 notNull(streamId)
96 returns (LockupDynamic.StreamLD memory stream)
97 {
98
99 Lockup.Stream memory lockupStream = _streams[streamId];
100
101
102 if (_statusOf(streamId) == Lockup.Status.SETTLED) {
103 lockupStream.isCancelable = false;
104 }
105
106 stream = LockupDynamic.StreamLD({
107 amounts: lockupStream.amounts,
108 asset: lockupStream.asset,
109 endTime: lockupStream.endTime,
110 isCancelable: lockupStream.isCancelable,
111 isDepleted: lockupStream.isDepleted,
112 isStream: lockupStream.isStream,
113 isTransferable: lockupStream.isTransferable,
114 recipient: _ownerOf(streamId),
115 segments: _segments[streamId],
116 sender: lockupStream.sender,
117 startTime: lockupStream.startTime,
118 wasCanceled: lockupStream.wasCanceled
119 });
120 }
123 function getTimestamps(uint256 streamId)
124 external
125 view
126 override
127 notNull(streamId)
128 returns (LockupDynamic.Timestamps memory timestamps)
129 {
130 timestamps = LockupDynamic.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime });
131 }
138 function createWithDurations(LockupDynamic.CreateWithDurations calldata params)
139 external
140 override
141 noDelegateCall
142 returns (uint256 streamId)
143 {
144
145 LockupDynamic.Segment[] memory segments = Helpers.calculateSegmentTimestamps(params.segments);
146
147
148 streamId = _create(
149 LockupDynamic.CreateWithTimestamps({
150 sender: params.sender,
151 recipient: params.recipient,
152 totalAmount: params.totalAmount,
153 asset: params.asset,
154 cancelable: params.cancelable,
155 transferable: params.transferable,
156 startTime: uint40(block.timestamp),
157 segments: segments,
158 broker: params.broker
159 })
160 );
161 }
164 function createWithTimestamps(LockupDynamic.CreateWithTimestamps calldata params)
165 external
166 override
167 noDelegateCall
168 returns (uint256 streamId)
169 {
170
171 streamId = _create(params);
172 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupDynamic.sol#L0:0
File: v2-core/src/SablierV2LockupLinear.sol
71 function getCliffTime(uint256 streamId) external view override notNull(streamId) returns (uint40 cliffTime) {
72 cliffTime = _cliffs[streamId];
73 }
76 function getStream(uint256 streamId)
77 external
78 view
79 override
80 notNull(streamId)
81 returns (LockupLinear.StreamLL memory stream)
82 {
83
84 Lockup.Stream memory lockupStream = _streams[streamId];
85
86
87 if (_statusOf(streamId) == Lockup.Status.SETTLED) {
88 lockupStream.isCancelable = false;
89 }
90
91 stream = LockupLinear.StreamLL({
92 amounts: lockupStream.amounts,
93 asset: lockupStream.asset,
94 cliffTime: _cliffs[streamId],
95 endTime: lockupStream.endTime,
96 isCancelable: lockupStream.isCancelable,
97 isTransferable: lockupStream.isTransferable,
98 isDepleted: lockupStream.isDepleted,
99 isStream: lockupStream.isStream,
100 recipient: _ownerOf(streamId),
101 sender: lockupStream.sender,
102 startTime: lockupStream.startTime,
103 wasCanceled: lockupStream.wasCanceled
104 });
105 }
108 function getTimestamps(uint256 streamId)
109 external
110 view
111 override
112 notNull(streamId)
113 returns (LockupLinear.Timestamps memory timestamps)
114 {
115 timestamps = LockupLinear.Timestamps({
116 start: _streams[streamId].startTime,
117 cliff: _cliffs[streamId],
118 end: _streams[streamId].endTime
119 });
120 }
127 function createWithDurations(LockupLinear.CreateWithDurations calldata params)
128 external
129 override
130 noDelegateCall
131 returns (uint256 streamId)
132 {
133
134 LockupLinear.Timestamps memory timestamps;
135 timestamps.start = uint40(block.timestamp);
136
137
138
139
140 unchecked {
141 if (params.durations.cliff > 0) {
142 timestamps.cliff = timestamps.start + params.durations.cliff;
143 }
144 timestamps.end = timestamps.start + params.durations.total;
145 }
146
147
148 streamId = _create(
149 LockupLinear.CreateWithTimestamps({
150 sender: params.sender,
151 recipient: params.recipient,
152 totalAmount: params.totalAmount,
153 asset: params.asset,
154 cancelable: params.cancelable,
155 transferable: params.transferable,
156 timestamps: timestamps,
157 broker: params.broker
158 })
159 );
160 }
163 function createWithTimestamps(LockupLinear.CreateWithTimestamps calldata params)
164 external
165 override
166 noDelegateCall
167 returns (uint256 streamId)
168 {
169
170 streamId = _create(params);
171 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/SablierV2LockupLinear.sol#L0:0
File: v2-core/src/SablierV2LockupTranched.sol
75 function getStream(uint256 streamId)
76 external
77 view
78 override
79 notNull(streamId)
80 returns (LockupTranched.StreamLT memory stream)
81 {
82
83 Lockup.Stream memory lockupStream = _streams[streamId];
84
85
86 if (_statusOf(streamId) == Lockup.Status.SETTLED) {
87 lockupStream.isCancelable = false;
88 }
89
90 stream = LockupTranched.StreamLT({
91 amounts: lockupStream.amounts,
92 asset: lockupStream.asset,
93 endTime: lockupStream.endTime,
94 isCancelable: lockupStream.isCancelable,
95 isDepleted: lockupStream.isDepleted,
96 isStream: lockupStream.isStream,
97 isTransferable: lockupStream.isTransferable,
98 recipient: _ownerOf(streamId),
99 sender: lockupStream.sender,
100 startTime: lockupStream.startTime,
101 tranches: _tranches[streamId],
102 wasCanceled: lockupStream.wasCanceled
103 });
104 }
107 function getTimestamps(uint256 streamId)
108 external
109 view
110 override
111 notNull(streamId)
112 returns (LockupTranched.Timestamps memory timestamps)
113 {
114 timestamps = LockupTranched.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime });
115 }
118 function getTranches(uint256 streamId)
119 external
120 view
121 override
122 notNull(streamId)
123 returns (LockupTranched.Tranche[] memory tranches)
124 {
125 tranches = _tranches[streamId];
126 }
133 function createWithDurations(LockupTranched.CreateWithDurations calldata params)
134 external
135 override
136 noDelegateCall
137 returns (uint256 streamId)
138 {
139
140 LockupTranched.Tranche[] memory tranches = Helpers.calculateTrancheTimestamps(params.tranches);
141
142
143 streamId = _create(
144 LockupTranched.CreateWithTimestamps({
145 sender: params.sender,
146 recipient: params.recipient,
147 totalAmount: params.totalAmount,
148 asset: params.asset,
149 cancelable: params.cancelable,
150 transferable: params.transferable,
151 startTime: uint40(block.timestamp),
152 tranches: tranches,
153 broker: params.broker
154 })
155 );
156 }
159 function createWithTimestamps(LockupTranched.CreateWithTimestamps calldata params)
160 external
161 override
162 noDelegateCall
163 returns (uint256 streamId)
164 {
165
166 streamId = _create(params);
167 }
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-periphery/src/SablierV2BatchLockup.sol
25 function createWithDurationsLD(
26 ISablierV2LockupDynamic lockupDynamic,
27 IERC20 asset,
28 BatchLockup.CreateWithDurationsLD[] calldata batch
29 )
30 external
31 override
32 returns (uint256[] memory streamIds)
33 {
34
35 uint256 batchSize = batch.length;
36 if (batchSize == 0) {
37 revert Errors.SablierV2BatchLockup_BatchSizeZero();
38 }
39
40
41
42 uint256 i;
43 uint256 transferAmount;
44 for (i = 0; i < batchSize; ++i) {
45 unchecked {
46 transferAmount += batch[i].totalAmount;
47 }
48 }
49
50
51 _handleTransfer(address(lockupDynamic), asset, transferAmount);
52
53
54 streamIds = new uint256[](batchSize);
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 }
70 }
73 function createWithTimestampsLD(
74 ISablierV2LockupDynamic lockupDynamic,
75 IERC20 asset,
76 BatchLockup.CreateWithTimestampsLD[] calldata batch
77 )
78 external
79 override
80 returns (uint256[] memory streamIds)
81 {
82
83 uint256 batchSize = batch.length;
84 if (batchSize == 0) {
85 revert Errors.SablierV2BatchLockup_BatchSizeZero();
86 }
87
88
89
90 uint256 i;
91 uint256 transferAmount;
92 for (i = 0; i < batchSize; ++i) {
93 unchecked {
94 transferAmount += batch[i].totalAmount;
95 }
96 }
97
98
99 _handleTransfer(address(lockupDynamic), asset, transferAmount);
100
101
102 streamIds = new uint256[](batchSize);
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 }
119 }
126 function createWithDurationsLL(
127 ISablierV2LockupLinear lockupLinear,
128 IERC20 asset,
129 BatchLockup.CreateWithDurationsLL[] calldata batch
130 )
131 external
132 override
133 returns (uint256[] memory streamIds)
134 {
135
136 uint256 batchSize = batch.length;
137 if (batchSize == 0) {
138 revert Errors.SablierV2BatchLockup_BatchSizeZero();
139 }
140
141
142
143 uint256 i;
144 uint256 transferAmount;
145 for (i = 0; i < batchSize; ++i) {
146 unchecked {
147 transferAmount += batch[i].totalAmount;
148 }
149 }
150
151
152 _handleTransfer(address(lockupLinear), asset, transferAmount);
153
154
155 streamIds = new uint256[](batchSize);
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 }
171 }
174 function createWithTimestampsLL(
175 ISablierV2LockupLinear lockupLinear,
176 IERC20 asset,
177 BatchLockup.CreateWithTimestampsLL[] calldata batch
178 )
179 external
180 override
181 returns (uint256[] memory streamIds)
182 {
183
184 uint256 batchSize = batch.length;
185 if (batchSize == 0) {
186 revert Errors.SablierV2BatchLockup_BatchSizeZero();
187 }
188
189
190
191 uint256 i;
192 uint256 transferAmount;
193 for (i = 0; i < batchSize; ++i) {
194 unchecked {
195 transferAmount += batch[i].totalAmount;
196 }
197 }
198
199
200 _handleTransfer(address(lockupLinear), asset, transferAmount);
201
202
203 streamIds = new uint256[](batchSize);
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 }
219 }
226 function createWithDurationsLT(
227 ISablierV2LockupTranched lockupTranched,
228 IERC20 asset,
229 BatchLockup.CreateWithDurationsLT[] calldata batch
230 )
231 external
232 override
233 returns (uint256[] memory streamIds)
234 {
235
236 uint256 batchSize = batch.length;
237 if (batchSize == 0) {
238 revert Errors.SablierV2BatchLockup_BatchSizeZero();
239 }
240
241
242
243 uint256 i;
244 uint256 transferAmount;
245 for (i = 0; i < batchSize; ++i) {
246 unchecked {
247 transferAmount += batch[i].totalAmount;
248 }
249 }
250
251
252 _handleTransfer(address(lockupTranched), asset, transferAmount);
253
254
255 streamIds = new uint256[](batchSize);
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 }
271 }
274 function createWithTimestampsLT(
275 ISablierV2LockupTranched lockupTranched,
276 IERC20 asset,
277 BatchLockup.CreateWithTimestampsLT[] calldata batch
278 )
279 external
280 override
281 returns (uint256[] memory streamIds)
282 {
283
284 uint256 batchSize = batch.length;
285 if (batchSize == 0) {
286 revert Errors.SablierV2BatchLockup_BatchSizeZero();
287 }
288
289
290
291 uint256 i;
292 uint256 transferAmount;
293 for (i = 0; i < batchSize; ++i) {
294 unchecked {
295 transferAmount += batch[i].totalAmount;
296 }
297 }
298
299
300 _handleTransfer(address(lockupTranched), asset, transferAmount);
301
302
303 streamIds = new uint256[](batchSize);
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 }
320 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2BatchLockup.sol#L0:0
File: v2-periphery/src/SablierV2MerkleLL.sol
59 function claim(
60 uint256 index,
61 address recipient,
62 uint128 amount,
63 bytes32[] calldata merkleProof
64 )
65 external
66 override
67 returns (uint256 streamId)
68 {
69
70
71 bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))));
72
73
74 _checkClaim(index, leaf, merkleProof);
75
76
77 _claimedBitMap.set(index);
78
79
80 streamId = LOCKUP_LINEAR.createWithDurations(
81 LockupLinear.CreateWithDurations({
82 sender: admin,
83 recipient: recipient,
84 totalAmount: amount,
85 asset: ASSET,
86 cancelable: CANCELABLE,
87 transferable: TRANSFERABLE,
88 durations: streamDurations,
89 broker: Broker({ account: address(0), fee: ud(0) })
90 })
91 );
92
93
94 emit Claim(index, recipient, amount, streamId);
95 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLL.sol#L0:0
File: v2-periphery/src/SablierV2MerkleLT.sol
65 function getTranchesWithPercentages() external view override returns (MerkleLT.TrancheWithPercentage[] memory) {
66 return _tranchesWithPercentages;
67 }
74 function claim(
75 uint256 index,
76 address recipient,
77 uint128 amount,
78 bytes32[] calldata merkleProof
79 )
80 external
81 override
82 returns (uint256 streamId)
83 {
84
85
86 bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))));
87
88
89 _checkClaim(index, leaf, merkleProof);
90
91
92 LockupTranched.TrancheWithDuration[] memory tranches = _calculateTranches(amount);
93
94
95 _claimedBitMap.set(index);
96
97
98 streamId = LOCKUP_TRANCHED.createWithDurations(
99 LockupTranched.CreateWithDurations({
100 sender: admin,
101 recipient: recipient,
102 totalAmount: amount,
103 asset: ASSET,
104 cancelable: CANCELABLE,
105 transferable: TRANSFERABLE,
106 tranches: tranches,
107 broker: Broker({ account: address(0), fee: ZERO })
108 })
109 );
110
111
112 emit Claim(index, recipient, amount, streamId);
113 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLT.sol#L0:0
File: v2-periphery/src/SablierV2MerkleLockupFactory.sol
25 function createMerkleLL(
26 MerkleLockup.ConstructorParams memory baseParams,
27 ISablierV2LockupLinear lockupLinear,
28 LockupLinear.Durations memory streamDurations,
29 uint256 aggregateAmount,
30 uint256 recipientCount
31 )
32 external
33 returns (ISablierV2MerkleLL merkleLL)
34 {
35
36 merkleLL = new SablierV2MerkleLL(baseParams, lockupLinear, streamDurations);
37
38
39 emit CreateMerkleLL(merkleLL, baseParams, lockupLinear, streamDurations, aggregateAmount, recipientCount);
40 }
43 function createMerkleLT(
44 MerkleLockup.ConstructorParams memory baseParams,
45 ISablierV2LockupTranched lockupTranched,
46 MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages,
47 uint256 aggregateAmount,
48 uint256 recipientCount
49 )
50 external
51 returns (ISablierV2MerkleLT merkleLT)
52 {
53
54 uint64 totalPercentage;
55 uint256 totalDuration;
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 }
64
65
66 if (totalPercentage != uUNIT) {
67 revert Errors.SablierV2MerkleLockupFactory_TotalPercentageNotOneHundred(totalPercentage);
68 }
69
70
71 merkleLT = new SablierV2MerkleLT(baseParams, lockupTranched, tranchesWithPercentages);
72
73
74 emit CreateMerkleLT(
75 merkleLT,
76 baseParams,
77 lockupTranched,
78 tranchesWithPercentages,
79 totalDuration,
80 aggregateAmount,
81 recipientCount
82 );
83 }
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-periphery/src/SablierV2MerkleLockupFactory.sol#L0:0
Consider using formal verification to mathematically prove that your code does what is intended, and does not have any edge cases with unexpected behavior. The solidity compiler itself has this functionality built in based off of SMTChecker.
Using scientific notation for large multiples of ten improves code readability. Instead of writing large decimal literals, consider using scientific notation.
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/SVGElements.sol#L0:0
The instances below point to one of two functions with the same name. Consider naming each function differently, in order to make code navigation and analysis easier.
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/SVGElements.sol#L0:0
Timelocks prevent users from being surprised by changes.
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0
The longer a string of operations is, the harder it is to understand it. Consider splitting the full calculation into more steps, with more descriptive temporary variable names, and add extensive comments.
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/NFTSVG.sol#L0:0
Consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns.
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/abstracts/SablierV2Lockup.sol#L0:0
https://github.com/Cyfrin/2024-05-Sablier/tree/main/v2-core/src/libraries/Helpers.sol#L0:0