web3crit-scanner 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +685 -0
  2. package/bin/web3crit +10 -0
  3. package/package.json +59 -0
  4. package/src/analyzers/control-flow.js +256 -0
  5. package/src/analyzers/data-flow.js +720 -0
  6. package/src/analyzers/exploit-chain.js +751 -0
  7. package/src/analyzers/immunefi-classifier.js +515 -0
  8. package/src/analyzers/poc-validator.js +396 -0
  9. package/src/analyzers/solodit-enricher.js +1122 -0
  10. package/src/cli.js +546 -0
  11. package/src/detectors/access-control-enhanced.js +458 -0
  12. package/src/detectors/base-detector.js +213 -0
  13. package/src/detectors/callback-reentrancy.js +362 -0
  14. package/src/detectors/cross-contract-reentrancy.js +697 -0
  15. package/src/detectors/delegatecall.js +167 -0
  16. package/src/detectors/deprecated-functions.js +62 -0
  17. package/src/detectors/flash-loan.js +408 -0
  18. package/src/detectors/frontrunning.js +553 -0
  19. package/src/detectors/gas-griefing.js +701 -0
  20. package/src/detectors/governance-attacks.js +366 -0
  21. package/src/detectors/integer-overflow.js +487 -0
  22. package/src/detectors/oracle-manipulation.js +524 -0
  23. package/src/detectors/permit-exploits.js +368 -0
  24. package/src/detectors/precision-loss.js +408 -0
  25. package/src/detectors/price-manipulation-advanced.js +548 -0
  26. package/src/detectors/proxy-vulnerabilities.js +651 -0
  27. package/src/detectors/readonly-reentrancy.js +473 -0
  28. package/src/detectors/rebasing-token-vault.js +416 -0
  29. package/src/detectors/reentrancy-enhanced.js +359 -0
  30. package/src/detectors/selfdestruct.js +259 -0
  31. package/src/detectors/share-manipulation.js +412 -0
  32. package/src/detectors/signature-replay.js +409 -0
  33. package/src/detectors/storage-collision.js +446 -0
  34. package/src/detectors/timestamp-dependence.js +494 -0
  35. package/src/detectors/toctou.js +427 -0
  36. package/src/detectors/token-standard-compliance.js +465 -0
  37. package/src/detectors/unchecked-call.js +214 -0
  38. package/src/detectors/vault-inflation.js +421 -0
  39. package/src/index.js +42 -0
  40. package/src/package-lock.json +2874 -0
  41. package/src/package.json +39 -0
  42. package/src/scanner-enhanced.js +816 -0
@@ -0,0 +1,701 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Gas Griefing and DoS Detector (Enhanced)
5
+ * Detects patterns vulnerable to denial of service via gas manipulation
6
+ * with improved loop bound analysis to reduce false positives.
7
+ */
8
+ class GasGriefingDetector extends BaseDetector {
9
+ constructor() {
10
+ super(
11
+ 'Gas Griefing / DoS',
12
+ 'Detects patterns vulnerable to gas-based denial of service',
13
+ 'HIGH'
14
+ );
15
+ this.currentContract = null;
16
+ this.currentFunction = null;
17
+ this.currentFunctionNode = null;
18
+ this.isInLoop = false;
19
+ this.loopDepth = 0;
20
+ this.currentLoopIsUnbounded = false;
21
+
22
+ // Safe iteration limit - loops with bounds under this are considered safe
23
+ this.SAFE_ITERATION_LIMIT = 100;
24
+ // Warning threshold - bounded but could still be expensive
25
+ this.WARNING_ITERATION_LIMIT = 1000;
26
+ }
27
+
28
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
29
+ this.findings = [];
30
+ this.ast = ast;
31
+ this.sourceCode = sourceCode;
32
+ this.fileName = fileName;
33
+ this.sourceLines = sourceCode.split('\n');
34
+
35
+ this.traverse(ast);
36
+
37
+ return this.findings;
38
+ }
39
+
40
+ visitContractDefinition(node) {
41
+ this.currentContract = node.name;
42
+ }
43
+
44
+ visitFunctionDefinition(node) {
45
+ this.currentFunction = node.name || 'constructor';
46
+ this.currentFunctionNode = node;
47
+ this.isInLoop = false;
48
+ this.loopDepth = 0;
49
+ }
50
+
51
+ visitForStatement(node) {
52
+ this.loopDepth++;
53
+ this.isInLoop = true;
54
+
55
+ // Analyze the loop with context awareness
56
+ this.analyzeForLoop(node);
57
+
58
+ // After processing children
59
+ this.loopDepth--;
60
+ if (this.loopDepth === 0) {
61
+ this.isInLoop = false;
62
+ }
63
+ }
64
+
65
+ visitWhileStatement(node) {
66
+ this.loopDepth++;
67
+ this.isInLoop = true;
68
+
69
+ // Analyze while loop - check if it has proper bounds
70
+ this.analyzeWhileLoop(node);
71
+
72
+ this.loopDepth--;
73
+ if (this.loopDepth === 0) {
74
+ this.isInLoop = false;
75
+ }
76
+ }
77
+
78
+ visitFunctionCall(node) {
79
+ if (!node.expression) return;
80
+
81
+ const code = this.getCodeSnippet(node.loc);
82
+
83
+ // Check for external calls in loops (only if unbounded)
84
+ if (this.isInLoop && this.currentLoopIsUnbounded) {
85
+ if (code.includes('.call') || code.includes('.transfer') || code.includes('.send')) {
86
+ this.reportExternalCallInLoop(node);
87
+ }
88
+ }
89
+
90
+ // Check for push to dynamic array - only if it's user-controlled
91
+ if (node.expression.type === 'MemberAccess' && node.expression.memberName === 'push') {
92
+ this.checkArrayPush(node);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Analyze for loop with comprehensive bound checking
98
+ */
99
+ analyzeForLoop(node) {
100
+ const code = this.getCodeSnippet(node.loc);
101
+ const funcCode = this.currentFunctionNode ? this.getCodeSnippet(this.currentFunctionNode.loc) : code;
102
+
103
+ // Extract loop bounds analysis
104
+ const boundAnalysis = this.analyzeLoopBounds(node, code, funcCode);
105
+
106
+ // Track if this loop is unbounded for nested analysis
107
+ this.currentLoopIsUnbounded = !boundAnalysis.hasBound;
108
+
109
+ if (boundAnalysis.hasBound) {
110
+ // Loop has explicit bounds
111
+ if (boundAnalysis.boundValue <= this.SAFE_ITERATION_LIMIT) {
112
+ // Safe - small fixed bound, no report needed
113
+ return;
114
+ } else if (boundAnalysis.boundValue <= this.WARNING_ITERATION_LIMIT) {
115
+ // Warning - moderate bound
116
+ if (this.hasGasHeavyOperations(code)) {
117
+ this.reportBoundedButExpensive(node, boundAnalysis);
118
+ }
119
+ return;
120
+ }
121
+ // Large bound - continue to check if it's problematic
122
+ }
123
+
124
+ // Check for dynamic array iteration
125
+ const dynamicArrayAnalysis = this.analyzeDynamicArrayIteration(code, funcCode);
126
+
127
+ if (dynamicArrayAnalysis.iteratesOverDynamicArray) {
128
+ // Check if there's pagination or safe guards
129
+ if (dynamicArrayAnalysis.hasSafeGuards) {
130
+ // Has pagination, batching, or limits - safe
131
+ return;
132
+ }
133
+
134
+ // Check if it's truly unbounded and user-controllable
135
+ if (dynamicArrayAnalysis.isUserControllable) {
136
+ this.reportUnboundedIteration(node, dynamicArrayAnalysis);
137
+ }
138
+ }
139
+
140
+ // Check for gas-heavy operations in unbounded loops
141
+ if (!boundAnalysis.hasBound && this.hasGasHeavyOperations(code)) {
142
+ this.reportGasHeavyLoop(node);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Analyze while loop for proper termination bounds
148
+ */
149
+ analyzeWhileLoop(node) {
150
+ const code = this.getCodeSnippet(node.loc);
151
+ const funcCode = this.currentFunctionNode ? this.getCodeSnippet(this.currentFunctionNode.loc) : code;
152
+
153
+ // Check for counter-based while loops (actually bounded)
154
+ const hasCounterBound = this.hasCounterBasedBound(code);
155
+
156
+ if (hasCounterBound) {
157
+ // While loop with explicit counter check - analyze the bound
158
+ const boundAnalysis = this.analyzeWhileBounds(code, funcCode);
159
+
160
+ if (boundAnalysis.hasBound && boundAnalysis.boundValue <= this.WARNING_ITERATION_LIMIT) {
161
+ // Bounded while loop - safe
162
+ this.currentLoopIsUnbounded = false;
163
+ return;
164
+ }
165
+ }
166
+
167
+ // Check for state-based termination (e.g., queue processing)
168
+ const hasStateTermination = this.hasStateBasedTermination(code);
169
+
170
+ if (hasStateTermination) {
171
+ // While loop processes state that will eventually terminate
172
+ // Only flag if it has gas-heavy operations
173
+ if (this.hasGasHeavyOperations(code)) {
174
+ this.reportPotentiallyUnboundedWhile(node);
175
+ }
176
+ this.currentLoopIsUnbounded = true;
177
+ return;
178
+ }
179
+
180
+ // Check for infinite loop patterns (truly dangerous)
181
+ const hasInfinitePattern = this.hasInfiniteLoopPattern(code);
182
+
183
+ if (hasInfinitePattern) {
184
+ this.reportInfiniteLoop(node);
185
+ this.currentLoopIsUnbounded = true;
186
+ return;
187
+ }
188
+
189
+ // Default: flag if no clear termination condition
190
+ this.currentLoopIsUnbounded = true;
191
+ if (this.hasGasHeavyOperations(code)) {
192
+ this.reportUnboundedWhileLoop(node);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Analyze loop bounds from for-loop structure
198
+ */
199
+ analyzeLoopBounds(node, code, funcCode) {
200
+ const result = {
201
+ hasBound: false,
202
+ boundValue: Infinity,
203
+ boundSource: 'unknown'
204
+ };
205
+
206
+ // Pattern 1: Explicit numeric bound (for i = 0; i < 10; i++)
207
+ const numericBound = code.match(/[<>=]\s*(\d+)/);
208
+ if (numericBound) {
209
+ result.hasBound = true;
210
+ result.boundValue = parseInt(numericBound[1]);
211
+ result.boundSource = 'literal';
212
+ return result;
213
+ }
214
+
215
+ // Pattern 2: Constant bound (for i = 0; i < MAX_ITERATIONS; i++)
216
+ const constantPattern = /[<>=]\s*([A-Z_][A-Z0-9_]*)/;
217
+ const constantMatch = code.match(constantPattern);
218
+ if (constantMatch) {
219
+ const constantName = constantMatch[1];
220
+ // Try to find constant definition
221
+ const constantDef = funcCode.match(new RegExp(`${constantName}\\s*=\\s*(\\d+)`));
222
+ if (constantDef) {
223
+ result.hasBound = true;
224
+ result.boundValue = parseInt(constantDef[1]);
225
+ result.boundSource = 'constant';
226
+ return result;
227
+ }
228
+ // Known safe constants
229
+ if (/^MAX_|^LIMIT_|^BATCH_SIZE/i.test(constantName)) {
230
+ result.hasBound = true;
231
+ result.boundValue = this.WARNING_ITERATION_LIMIT; // Assume reasonable
232
+ result.boundSource = 'constant';
233
+ return result;
234
+ }
235
+ }
236
+
237
+ // Pattern 3: Min/Math.min bound
238
+ if (/Math\.min|min\s*\(/i.test(code)) {
239
+ result.hasBound = true;
240
+ result.boundValue = this.WARNING_ITERATION_LIMIT;
241
+ result.boundSource = 'min-function';
242
+ return result;
243
+ }
244
+
245
+ // Pattern 4: Parameter with require check
246
+ const paramBound = code.match(/[<>=]\s*(\w+)/);
247
+ if (paramBound) {
248
+ const boundVar = paramBound[1];
249
+ // Check if there's a require limiting this parameter
250
+ const requirePattern = new RegExp(`require\\s*\\([^;]*${boundVar}[^;]*[<>=]\\s*(\\d+)`);
251
+ const requireMatch = funcCode.match(requirePattern);
252
+ if (requireMatch) {
253
+ result.hasBound = true;
254
+ result.boundValue = parseInt(requireMatch[1]);
255
+ result.boundSource = 'require-bounded';
256
+ return result;
257
+ }
258
+ }
259
+
260
+ return result;
261
+ }
262
+
263
+ /**
264
+ * Analyze while loop bounds
265
+ */
266
+ analyzeWhileBounds(code, funcCode) {
267
+ const result = {
268
+ hasBound: false,
269
+ boundValue: Infinity
270
+ };
271
+
272
+ // Check for counter increment patterns
273
+ const counterPatterns = [
274
+ /(\w+)\s*\+\+/, // i++
275
+ /(\w+)\s*\+=\s*1/, // i += 1
276
+ /(\w+)\s*=\s*\1\s*\+\s*1/ // i = i + 1
277
+ ];
278
+
279
+ let counterVar = null;
280
+ for (const pattern of counterPatterns) {
281
+ const match = code.match(pattern);
282
+ if (match) {
283
+ counterVar = match[1];
284
+ break;
285
+ }
286
+ }
287
+
288
+ if (counterVar) {
289
+ // Find the bound for this counter
290
+ const boundPattern = new RegExp(`${counterVar}\\s*[<>=]+\\s*(\\d+|[A-Z_]+)`);
291
+ const boundMatch = code.match(boundPattern);
292
+
293
+ if (boundMatch) {
294
+ const boundValue = parseInt(boundMatch[1]);
295
+ if (!isNaN(boundValue)) {
296
+ result.hasBound = true;
297
+ result.boundValue = boundValue;
298
+ } else {
299
+ // It's a constant, assume bounded
300
+ result.hasBound = true;
301
+ result.boundValue = this.WARNING_ITERATION_LIMIT;
302
+ }
303
+ }
304
+ }
305
+
306
+ return result;
307
+ }
308
+
309
+ /**
310
+ * Check for counter-based bounds in while loops
311
+ */
312
+ hasCounterBasedBound(code) {
313
+ // Look for patterns like: while (i < limit) with i++ inside
314
+ const hasComparison = /while\s*\([^)]*[<>=]/i.test(code);
315
+ const hasIncrement = /\+\+|\+=\s*1|=\s*\w+\s*\+\s*1/i.test(code);
316
+
317
+ return hasComparison && hasIncrement;
318
+ }
319
+
320
+ /**
321
+ * Check for state-based termination
322
+ */
323
+ hasStateBasedTermination(code) {
324
+ // Queue/stack processing patterns
325
+ const statePatterns = [
326
+ /\.length\s*>\s*0/, // while (queue.length > 0)
327
+ /\.pop\s*\(/, // with pop operation
328
+ /\.shift\s*\(/, // with shift operation
329
+ /isEmpty/i, // isEmpty check
330
+ ];
331
+
332
+ return statePatterns.some(p => p.test(code));
333
+ }
334
+
335
+ /**
336
+ * Check for infinite loop patterns
337
+ */
338
+ hasInfiniteLoopPattern(code) {
339
+ // while(true), while(1), for(;;)
340
+ return /while\s*\(\s*(true|1)\s*\)|for\s*\(\s*;\s*;\s*\)/.test(code);
341
+ }
342
+
343
+ /**
344
+ * Analyze if loop iterates over dynamic, user-controllable array
345
+ */
346
+ analyzeDynamicArrayIteration(code, funcCode) {
347
+ const result = {
348
+ iteratesOverDynamicArray: false,
349
+ arrayName: null,
350
+ isUserControllable: false,
351
+ hasSafeGuards: false
352
+ };
353
+
354
+ // Check for .length iteration
355
+ const lengthMatch = code.match(/(\w+)\.length/);
356
+ if (!lengthMatch) return result;
357
+
358
+ result.iteratesOverDynamicArray = true;
359
+ result.arrayName = lengthMatch[1];
360
+
361
+ // Check if array is user-controllable
362
+ const userControllablePatterns = [
363
+ /public\s+\w+\[\]/, // public array
364
+ /push.*msg\.sender/, // users can add themselves
365
+ /push.*external/, // external function can add
366
+ /users|addresses|recipients/i // named like user list
367
+ ];
368
+
369
+ result.isUserControllable = userControllablePatterns.some(p => p.test(funcCode));
370
+
371
+ // Check for safe guards
372
+ const safeGuardPatterns = [
373
+ /require.*\.length\s*[<>=]/, // Length check in require
374
+ /Math\.min|min\s*\(/, // Min function for batching
375
+ /start.*end|offset.*limit/i, // Pagination parameters
376
+ /batch/i, // Batch processing
377
+ /MAX_|LIMIT_/, // Named limits
378
+ ];
379
+
380
+ result.hasSafeGuards = safeGuardPatterns.some(p => p.test(funcCode));
381
+
382
+ return result;
383
+ }
384
+
385
+ /**
386
+ * Check if code contains gas-heavy operations
387
+ */
388
+ hasGasHeavyOperations(code) {
389
+ const gasHeavyPatterns = [
390
+ /\.transfer\s*\(/,
391
+ /\.call\s*\{/,
392
+ /\.call\s*\(/,
393
+ /\.send\s*\(/,
394
+ /\.delegatecall/,
395
+ /delete\s+\w+\[/, // Storage deletion
396
+ /\w+\[\w+\]\s*=/, // Storage write in loop
397
+ ];
398
+
399
+ return gasHeavyPatterns.some(p => p.test(code));
400
+ }
401
+
402
+ checkArrayPush(node) {
403
+ const funcCode = this.currentFunctionNode ?
404
+ this.getCodeSnippet(this.currentFunctionNode.loc) :
405
+ this.sourceLines.slice(Math.max(0, node.loc.start.line - 10), node.loc.start.line + 5).join('\n');
406
+
407
+ // Check for size limits
408
+ const hasLimit = /require[^;]*length\s*[<]=?\s*\d+|MAX_|LIMIT_/i.test(funcCode);
409
+
410
+ // Check if push is user-controllable
411
+ const isUserTriggered = /external|public/i.test(funcCode) &&
412
+ !(/onlyOwner|onlyAdmin|onlyRole/i.test(funcCode));
413
+
414
+ if (!hasLimit && isUserTriggered) {
415
+ // Check if array is ever iterated
416
+ const arrayName = this.findArrayName(node);
417
+ const isIterated = arrayName && new RegExp(`for[^}]*${arrayName}\\.length`).test(this.sourceCode);
418
+
419
+ if (isIterated) {
420
+ this.reportUnboundedArrayGrowth(node, arrayName);
421
+ }
422
+ }
423
+ }
424
+
425
+ findArrayName(pushNode) {
426
+ if (pushNode.expression && pushNode.expression.expression) {
427
+ const expr = pushNode.expression.expression;
428
+ if (expr.type === 'Identifier') {
429
+ return expr.name;
430
+ }
431
+ }
432
+ return null;
433
+ }
434
+
435
+ reportUnboundedIteration(node, analysis) {
436
+ this.addFinding({
437
+ title: 'Unbounded Loop Over User-Controlled Array',
438
+ description: `Loop iterates over '${analysis.arrayName}' array which can grow without bounds. If users can add elements via external calls, gas costs could exceed block limit causing permanent DoS.`,
439
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
440
+ line: node.loc ? node.loc.start.line : 0,
441
+ column: node.loc ? node.loc.start.column : 0,
442
+ code: this.getCodeSnippet(node.loc),
443
+ severity: 'HIGH',
444
+ confidence: 'HIGH',
445
+ exploitable: true,
446
+ exploitabilityScore: 80,
447
+ attackVector: 'gas-exhaustion-dos',
448
+ recommendation: 'Implement pagination (start/end indices), set maximum array size, or use pull payment pattern where users withdraw individually.',
449
+ references: [
450
+ 'https://swcregistry.io/docs/SWC-128',
451
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
452
+ ],
453
+ foundryPoC: this.generateDoSPoC(analysis.arrayName)
454
+ });
455
+ }
456
+
457
+ reportBoundedButExpensive(node, boundAnalysis) {
458
+ this.addFinding({
459
+ title: 'Gas-Expensive Loop',
460
+ description: `Loop has bound of ${boundAnalysis.boundValue} iterations with gas-heavy operations. While bounded, this could still be expensive and approach gas limits.`,
461
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
462
+ line: node.loc ? node.loc.start.line : 0,
463
+ column: node.loc ? node.loc.start.column : 0,
464
+ code: this.getCodeSnippet(node.loc),
465
+ severity: 'MEDIUM',
466
+ confidence: 'MEDIUM',
467
+ exploitable: false,
468
+ exploitabilityScore: 20,
469
+ recommendation: 'Consider reducing maximum iterations, batching operations, or using pull payment pattern for transfers.',
470
+ references: [
471
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
472
+ ]
473
+ });
474
+ }
475
+
476
+ reportGasHeavyLoop(node) {
477
+ this.addFinding({
478
+ title: 'Gas-Heavy Operations in Unbounded Loop',
479
+ description: `External calls or storage operations inside a loop without explicit bounds. Each iteration costs significant gas, risking block gas limit exhaustion.`,
480
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
481
+ line: node.loc ? node.loc.start.line : 0,
482
+ column: node.loc ? node.loc.start.column : 0,
483
+ code: this.getCodeSnippet(node.loc),
484
+ severity: 'HIGH',
485
+ confidence: 'HIGH',
486
+ exploitable: true,
487
+ exploitabilityScore: 75,
488
+ attackVector: 'gas-exhaustion-dos',
489
+ recommendation: 'Add explicit iteration limits. Move external calls outside loops. Use pull payment pattern. Batch storage updates.',
490
+ references: [
491
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
492
+ ]
493
+ });
494
+ }
495
+
496
+ reportExternalCallInLoop(node) {
497
+ this.addFinding({
498
+ title: 'External Call in Unbounded Loop',
499
+ description: `External call (.call, .transfer, .send) inside an unbounded loop. A single failing recipient can block all subsequent transfers (DoS), or gas costs could exceed block limit.`,
500
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
501
+ line: node.loc ? node.loc.start.line : 0,
502
+ column: node.loc ? node.loc.start.column : 0,
503
+ code: this.getCodeSnippet(node.loc),
504
+ severity: 'CRITICAL',
505
+ confidence: 'HIGH',
506
+ exploitable: true,
507
+ exploitabilityScore: 90,
508
+ attackVector: 'gas-exhaustion-dos',
509
+ recommendation: 'Use pull payment pattern (recipients withdraw individually). If push is required: add try/catch, continue on failure, and track failed transfers for retry.',
510
+ references: [
511
+ 'https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/',
512
+ 'https://docs.openzeppelin.com/contracts/4.x/api/security#PullPayment'
513
+ ],
514
+ foundryPoC: this.generateExternalCallDoSPoC()
515
+ });
516
+ }
517
+
518
+ reportUnboundedWhileLoop(node) {
519
+ this.addFinding({
520
+ title: 'Unbounded While Loop with Gas-Heavy Operations',
521
+ description: `While loop without clear termination bounds contains operations that could exhaust gas. The loop may not terminate or could consume excessive gas.`,
522
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
523
+ line: node.loc ? node.loc.start.line : 0,
524
+ column: node.loc ? node.loc.start.column : 0,
525
+ code: this.getCodeSnippet(node.loc),
526
+ severity: 'HIGH',
527
+ confidence: 'MEDIUM',
528
+ exploitable: true,
529
+ exploitabilityScore: 65,
530
+ recommendation: 'Add explicit iteration counter with maximum limit. Consider converting to bounded for-loop. Add gas checks (gasleft()) if processing must continue across transactions.',
531
+ references: [
532
+ 'https://swcregistry.io/docs/SWC-128'
533
+ ]
534
+ });
535
+ }
536
+
537
+ reportPotentiallyUnboundedWhile(node) {
538
+ this.addFinding({
539
+ title: 'State-Processing While Loop',
540
+ description: `While loop processes state (queue/stack pattern) with gas-heavy operations. While logically bounded by state, could still exhaust gas with large state.`,
541
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
542
+ line: node.loc ? node.loc.start.line : 0,
543
+ column: node.loc ? node.loc.start.column : 0,
544
+ code: this.getCodeSnippet(node.loc),
545
+ severity: 'MEDIUM',
546
+ confidence: 'MEDIUM',
547
+ exploitable: true,
548
+ exploitabilityScore: 40,
549
+ recommendation: 'Add maximum iteration limit per transaction. Consider processing in batches across multiple transactions.',
550
+ references: [
551
+ 'https://swcregistry.io/docs/SWC-128'
552
+ ]
553
+ });
554
+ }
555
+
556
+ reportInfiniteLoop(node) {
557
+ this.addFinding({
558
+ title: 'Potential Infinite Loop',
559
+ description: `Loop with while(true) or for(;;) pattern detected. This will always consume all available gas unless explicitly broken.`,
560
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
561
+ line: node.loc ? node.loc.start.line : 0,
562
+ column: node.loc ? node.loc.start.column : 0,
563
+ code: this.getCodeSnippet(node.loc),
564
+ severity: 'CRITICAL',
565
+ confidence: 'HIGH',
566
+ exploitable: true,
567
+ exploitabilityScore: 95,
568
+ attackVector: 'infinite-loop-dos',
569
+ recommendation: 'Add explicit break conditions and maximum iteration counter. Verify all code paths lead to termination.',
570
+ references: [
571
+ 'https://swcregistry.io/docs/SWC-128'
572
+ ]
573
+ });
574
+ }
575
+
576
+ reportUnboundedArrayGrowth(node, arrayName) {
577
+ this.addFinding({
578
+ title: 'Unbounded Array Growth',
579
+ description: `Array '${arrayName || 'unknown'}' can grow without limits via user-accessible function, and is later iterated. Attackers can add elements until iteration exceeds block gas limit, causing permanent DoS.`,
580
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
581
+ line: node.loc ? node.loc.start.line : 0,
582
+ column: node.loc ? node.loc.start.column : 0,
583
+ code: this.getCodeSnippet(node.loc),
584
+ severity: 'HIGH',
585
+ confidence: 'HIGH',
586
+ exploitable: true,
587
+ exploitabilityScore: 85,
588
+ attackVector: 'unbounded-growth-dos',
589
+ recommendation: 'Add maximum array size with require check. Use mapping with counter instead of array if iteration not needed. Implement removal mechanism.',
590
+ references: [
591
+ 'https://swcregistry.io/docs/SWC-128'
592
+ ],
593
+ foundryPoC: this.generateArrayGrowthPoC(arrayName)
594
+ });
595
+ }
596
+
597
+ generateDoSPoC(arrayName) {
598
+ return `// SPDX-License-Identifier: MIT
599
+ pragma solidity ^0.8.0;
600
+
601
+ import "forge-std/Test.sol";
602
+
603
+ /**
604
+ * Proof of Concept: Gas Exhaustion DoS via Unbounded Loop
605
+ * Target array: ${arrayName || 'users'}
606
+ */
607
+ contract GasExhaustionExploit is Test {
608
+ address constant TARGET = address(0);
609
+
610
+ function testExploit() public {
611
+ // Step 1: Grow the array to exceed gas limits
612
+ // Each push adds an element that will be iterated
613
+ for (uint i = 0; i < 10000; i++) {
614
+ // TARGET.addUser(address(uint160(i)));
615
+ }
616
+
617
+ // Step 2: Attempt to call function that iterates
618
+ // This should fail with out-of-gas
619
+ // vm.expectRevert();
620
+ // TARGET.processAllUsers();
621
+
622
+ // The contract is now permanently DoS'd for this function
623
+ }
624
+ }`;
625
+ }
626
+
627
+ generateExternalCallDoSPoC() {
628
+ return `// SPDX-License-Identifier: MIT
629
+ pragma solidity ^0.8.0;
630
+
631
+ import "forge-std/Test.sol";
632
+
633
+ /**
634
+ * Proof of Concept: DoS via Malicious Recipient in Loop
635
+ */
636
+ contract MaliciousRecipient {
637
+ // Consume all gas or revert on receive
638
+ receive() external payable {
639
+ // Option 1: Infinite loop
640
+ while(true) {}
641
+
642
+ // Option 2: Simply revert
643
+ // revert("DoS");
644
+ }
645
+ }
646
+
647
+ contract ExternalCallLoopExploit is Test {
648
+ function testExploit() public {
649
+ // Deploy malicious recipient
650
+ MaliciousRecipient malicious = new MaliciousRecipient();
651
+
652
+ // Add malicious address to the payment queue
653
+ // TARGET.addRecipient(address(malicious));
654
+
655
+ // Now any call to distribute payments will fail
656
+ // Either consuming all gas or reverting
657
+
658
+ // vm.expectRevert();
659
+ // TARGET.distributePayments();
660
+ }
661
+ }`;
662
+ }
663
+
664
+ generateArrayGrowthPoC(arrayName) {
665
+ return `// SPDX-License-Identifier: MIT
666
+ pragma solidity ^0.8.0;
667
+
668
+ import "forge-std/Test.sol";
669
+
670
+ /**
671
+ * Proof of Concept: Permanent DoS via Array Growth
672
+ * Target array: ${arrayName || 'array'}
673
+ */
674
+ contract ArrayGrowthExploit is Test {
675
+ address constant TARGET = address(0);
676
+
677
+ function testExploit() public {
678
+ uint256 initialGas = gasleft();
679
+
680
+ // Keep adding elements until we approach gas limits
681
+ // Each element makes future iterations more expensive
682
+
683
+ uint256 elementsAdded = 0;
684
+ while (gasleft() > 100000) {
685
+ // TARGET.push(someValue);
686
+ elementsAdded++;
687
+
688
+ if (elementsAdded > 50000) break; // Safety limit for test
689
+ }
690
+
691
+ console.log("Elements added:", elementsAdded);
692
+
693
+ // Now try to call function that iterates over the array
694
+ // vm.expectRevert(); // Should fail with out-of-gas
695
+ // TARGET.processArray();
696
+ }
697
+ }`;
698
+ }
699
+ }
700
+
701
+ module.exports = GasGriefingDetector;