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,697 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Cross-Contract Reentrancy Detector
5
+ * Detects complex reentrancy attacks involving multiple contracts
6
+ *
7
+ * Detects:
8
+ * - Reentrancy across multiple contracts in same transaction
9
+ * - State changes in one contract affecting another
10
+ * - External calls that can trigger state changes in related contracts
11
+ * - Missing reentrancy guards in cross-contract interactions
12
+ * - Reentrancy via delegatecall patterns
13
+ * - Reentrancy in multi-step protocols
14
+ */
15
+ class CrossContractReentrancyDetector extends BaseDetector {
16
+ constructor() {
17
+ super(
18
+ 'Cross-Contract Reentrancy',
19
+ 'Detects reentrancy attacks involving multiple contracts and complex state interactions',
20
+ 'CRITICAL'
21
+ );
22
+ this.currentContract = null;
23
+ this.externalCalls = [];
24
+ this.stateChanges = [];
25
+ this.contractInteractions = new Map(); // contract -> [functions called]
26
+ this.cfg = null;
27
+ this.dataFlow = null;
28
+ }
29
+
30
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
31
+ this.findings = [];
32
+ this.ast = ast;
33
+ this.sourceCode = sourceCode;
34
+ this.fileName = fileName;
35
+ this.sourceLines = sourceCode.split('\n');
36
+ this.cfg = cfg;
37
+ this.dataFlow = dataFlow;
38
+
39
+ this.traverse(ast);
40
+
41
+ // Post-traversal analysis
42
+ this.analyzeCrossContractReentrancy();
43
+
44
+ return this.findings;
45
+ }
46
+
47
+ visitContractDefinition(node) {
48
+ this.currentContract = node.name;
49
+ this.externalCalls = [];
50
+ this.stateChanges = [];
51
+ this.contractInteractions = new Map();
52
+ }
53
+
54
+ visitFunctionDefinition(node) {
55
+ const funcName = node.name || '';
56
+ const funcCode = this.getCodeSnippet(node.loc);
57
+
58
+ // Skip private/internal functions (not directly exploitable)
59
+ if (node.visibility === 'private' || node.visibility === 'internal') {
60
+ return;
61
+ }
62
+
63
+ // Reset per-function tracking
64
+ const functionExternalCalls = [];
65
+ const functionStateChanges = [];
66
+ const functionInteractions = new Set();
67
+
68
+ // Analyze function body - use both AST and code analysis
69
+ this.analyzeFunctionBody(node, functionExternalCalls, functionStateChanges, functionInteractions);
70
+
71
+ // Also do direct AST traversal for interface calls
72
+ if (node.body && node.body.statements) {
73
+ this.analyzeFunctionBodyAST(node.body.statements, functionExternalCalls, functionStateChanges, functionInteractions);
74
+ }
75
+
76
+ // Store for cross-contract analysis
77
+ if (functionExternalCalls.length > 0 || functionStateChanges.length > 0) {
78
+ this.externalCalls.push({
79
+ function: funcName,
80
+ calls: functionExternalCalls,
81
+ stateChanges: functionStateChanges,
82
+ interactions: Array.from(functionInteractions),
83
+ node: node
84
+ });
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Direct AST analysis for interface calls
90
+ */
91
+ analyzeFunctionBodyAST(statements, externalCalls, stateChanges, interactions) {
92
+ for (let i = 0; i < statements.length; i++) {
93
+ const stmt = statements[i];
94
+
95
+ // Check for interface calls: contractA.withdraw() pattern
96
+ if (stmt.type === 'ExpressionStatement' && stmt.expression) {
97
+ const expr = stmt.expression;
98
+ if (expr.type === 'FunctionCall' && expr.expression && expr.expression.type === 'MemberAccess') {
99
+ const memberAccess = expr.expression;
100
+ if (memberAccess.expression && memberAccess.expression.type === 'Identifier') {
101
+ const varName = memberAccess.expression.name;
102
+ // Check if it's a contract variable (not a built-in)
103
+ if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(varName.toLowerCase())) {
104
+ // This is an interface call
105
+ if (!externalCalls.some(c => c.index === i)) {
106
+ const stmtCode = this.getCodeSnippet(stmt.loc);
107
+ externalCalls.push({
108
+ type: 'external',
109
+ target: varName,
110
+ statement: stmt,
111
+ code: stmtCode || `${varName}.${memberAccess.memberName}()`,
112
+ index: i
113
+ });
114
+ interactions.add(varName);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ // Check for state changes: balances[user] = value
122
+ if (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'Assignment') {
123
+ const assignment = stmt.expression;
124
+ if (assignment.left) {
125
+ // Check if left side is a mapping access or state variable
126
+ const leftCode = this.getCodeSnippet(assignment.left.loc);
127
+ if (leftCode && (leftCode.includes('balances[') || leftCode.includes('['))) {
128
+ if (!stateChanges.some(s => s.index === i)) {
129
+ stateChanges.push({
130
+ type: 'balance',
131
+ statement: stmt,
132
+ code: this.getCodeSnippet(stmt.loc),
133
+ index: i
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Analyze function body for external calls and state changes
144
+ */
145
+ analyzeFunctionBody(node, externalCalls, stateChanges, interactions) {
146
+ if (!node.body || !node.body.statements) return;
147
+
148
+ const statements = node.body.statements;
149
+ let externalCallBeforeStateUpdate = false;
150
+ let stateUpdateAfterExternalCall = false;
151
+
152
+ for (let i = 0; i < statements.length; i++) {
153
+ const stmt = statements[i];
154
+ let stmtCode = this.getCodeSnippet(stmt.loc);
155
+
156
+ // If code snippet is too short, try to get more context from surrounding lines
157
+ if (stmtCode.length < 10 && stmt.loc) {
158
+ const lineNum = stmt.loc.start.line;
159
+ if (lineNum > 0 && lineNum <= this.sourceLines.length) {
160
+ stmtCode = this.sourceLines[lineNum - 1] || stmtCode;
161
+ }
162
+ }
163
+
164
+ const stmtCodeLower = stmtCode.toLowerCase();
165
+
166
+ // Detect external calls
167
+ if (this.isExternalCall(stmt, stmtCode)) {
168
+ const callTarget = this.getCallTarget(stmtCode);
169
+ externalCalls.push({
170
+ type: this.getCallType(stmtCode),
171
+ target: callTarget,
172
+ statement: stmt,
173
+ code: stmtCode,
174
+ index: i
175
+ });
176
+
177
+ // Track which contract is being called
178
+ if (callTarget) {
179
+ interactions.add(callTarget);
180
+ } else {
181
+ // Even if we can't extract the target, mark as external call
182
+ interactions.add('external contract');
183
+ }
184
+
185
+ externalCallBeforeStateUpdate = true;
186
+ }
187
+
188
+ // Also check AST structure directly for interface calls (e.g., contractA.withdraw())
189
+ // This is a more reliable way to detect interface calls
190
+ if (stmt.type === 'ExpressionStatement' && stmt.expression) {
191
+ const expr = stmt.expression;
192
+ if (expr.type === 'FunctionCall' && expr.expression) {
193
+ // Check if it's a member access (variable.function())
194
+ if (expr.expression.type === 'MemberAccess') {
195
+ const memberAccess = expr.expression;
196
+ // Check if the base is an identifier (contract variable)
197
+ if (memberAccess.expression && memberAccess.expression.type === 'Identifier') {
198
+ const varName = memberAccess.expression.name;
199
+ // Check if it's a contract variable (not a built-in)
200
+ if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(varName.toLowerCase())) {
201
+ // This is an interface call - add it if not already added
202
+ if (!externalCalls.some(c => c.index === i)) {
203
+ externalCalls.push({
204
+ type: 'external',
205
+ target: varName,
206
+ statement: stmt,
207
+ code: stmtCode,
208
+ index: i
209
+ });
210
+ interactions.add(varName);
211
+ externalCallBeforeStateUpdate = true;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // Detect state changes
220
+ if (this.isStateChange(stmt, stmtCode)) {
221
+ stateChanges.push({
222
+ type: this.getStateChangeType(stmtCode),
223
+ statement: stmt,
224
+ code: stmtCode,
225
+ index: i
226
+ });
227
+
228
+ // If we had an external call before, this is a reentrancy pattern
229
+ if (externalCallBeforeStateUpdate) {
230
+ stateUpdateAfterExternalCall = true;
231
+ }
232
+ }
233
+ }
234
+
235
+ // Check for cross-contract reentrancy pattern
236
+ // Report if there's at least one external call and state change after it
237
+ if (externalCalls.length > 0 && stateChanges.length > 0) {
238
+ // Check if any state change happens after an external call
239
+ const hasStateAfterCall = stateChanges.some(stateChange => {
240
+ return externalCalls.some(call => stateChange.index > call.index);
241
+ });
242
+
243
+ if (hasStateAfterCall) {
244
+ this.checkCrossContractReentrancy(
245
+ node,
246
+ externalCalls,
247
+ stateChanges,
248
+ interactions
249
+ );
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Check for cross-contract reentrancy vulnerabilities
256
+ */
257
+ checkCrossContractReentrancy(node, externalCalls, stateChanges, interactions) {
258
+ const funcName = node.name || '';
259
+ const funcCode = this.getCodeSnippet(node.loc);
260
+
261
+ // Check if function has reentrancy guard
262
+ const hasReentrancyGuard = this.hasReentrancyGuard(funcCode, node);
263
+
264
+ // Check for cross-contract patterns
265
+ // Report if there's at least one external call and state update after it
266
+ if (externalCalls.length > 0 && stateChanges.length > 0) {
267
+ // Check if state is updated after external call
268
+ const hasStateAfterCall = stateChanges.some(stateChange => {
269
+ return externalCalls.some(call => stateChange.index > call.index);
270
+ });
271
+
272
+ if (hasStateAfterCall && !hasReentrancyGuard) {
273
+ // Check if there are multiple contracts or just one
274
+ const contractList = interactions.size > 0 ? Array.from(interactions).join(', ') : 'external contract(s)';
275
+ const isMultiContract = interactions.size > 1;
276
+
277
+ this.addFinding({
278
+ title: isMultiContract ? 'Cross-Contract Reentrancy Vulnerability' : 'Reentrancy Vulnerability (External Call Before State Update)',
279
+ description: `Function '${funcName}' makes external calls to ${contractList} before updating state. An attacker can exploit this by having the called contract call back into this function while state is still inconsistent.`,
280
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
281
+ line: node.loc ? node.loc.start.line : 0,
282
+ column: node.loc ? node.loc.start.column : 0,
283
+ code: this.getCodeSnippet(node.loc),
284
+ severity: 'CRITICAL',
285
+ confidence: 'HIGH',
286
+ exploitable: true,
287
+ exploitabilityScore: 90,
288
+ attackVector: 'cross-contract-reentrancy',
289
+ recommendation: 'Apply reentrancy guard (nonReentrant modifier) or use Checks-Effects-Interactions pattern. Update all state before making external calls. Consider using internal functions for state updates.',
290
+ references: [
291
+ 'https://swcregistry.io/docs/SWC-107',
292
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/',
293
+ 'https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard'
294
+ ],
295
+ foundryPoC: this.generateCrossContractReentrancyPoC(this.currentContract, funcName, Array.from(interactions))
296
+ });
297
+ }
298
+ }
299
+
300
+ // Check for delegatecall reentrancy
301
+ const hasDelegatecall = externalCalls.some(call => call.type === 'delegatecall');
302
+ if (hasDelegatecall && !hasReentrancyGuard) {
303
+ this.addFinding({
304
+ title: 'Delegatecall Reentrancy Vulnerability',
305
+ description: `Function '${funcName}' uses delegatecall which can be exploited for reentrancy. The called contract can call back into this contract with elevated privileges.`,
306
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
307
+ line: node.loc ? node.loc.start.line : 0,
308
+ column: node.loc ? node.loc.start.column : 0,
309
+ code: this.getCodeSnippet(node.loc),
310
+ severity: 'CRITICAL',
311
+ confidence: 'HIGH',
312
+ exploitable: true,
313
+ exploitabilityScore: 95,
314
+ attackVector: 'delegatecall-reentrancy',
315
+ recommendation: 'Never use delegatecall with user-controlled addresses. If delegatecall is necessary, apply strict reentrancy guards and validate the target contract.',
316
+ references: [
317
+ 'https://swcregistry.io/docs/SWC-112',
318
+ 'https://swcregistry.io/docs/SWC-107'
319
+ ],
320
+ foundryPoC: this.generateDelegatecallReentrancyPoC(this.currentContract, funcName)
321
+ });
322
+ }
323
+
324
+ // Check for state dependency across contracts
325
+ if (this.hasStateDependency(externalCalls, stateChanges)) {
326
+ if (!hasReentrancyGuard) {
327
+ this.addFinding({
328
+ title: 'State-Dependent Cross-Contract Reentrancy',
329
+ description: `Function '${funcName}' reads state from one contract and writes to another, creating a reentrancy vector. An attacker can manipulate the state between the read and write.`,
330
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
331
+ line: node.loc ? node.loc.start.line : 0,
332
+ column: node.loc ? node.loc.start.column : 0,
333
+ code: this.getCodeSnippet(node.loc),
334
+ severity: 'CRITICAL',
335
+ confidence: 'MEDIUM',
336
+ exploitable: true,
337
+ exploitabilityScore: 85,
338
+ attackVector: 'state-dependent-reentrancy',
339
+ recommendation: 'Cache state values before external calls. Update all state before making external calls. Use internal functions to separate state updates from external interactions.',
340
+ references: [
341
+ 'https://swcregistry.io/docs/SWC-107',
342
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/'
343
+ ]
344
+ });
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Check if statement is an external call
351
+ */
352
+ isExternalCall(stmt, code) {
353
+ const codeLower = code.toLowerCase();
354
+
355
+ // External call patterns
356
+ const callPatterns = [
357
+ /\.call\s*\(/i,
358
+ /\.delegatecall\s*\(/i,
359
+ /\.send\s*\(/i,
360
+ /\.transfer\s*\(/i,
361
+ /\.callcode\s*\(/i,
362
+ /external\s+contract/i,
363
+ /interface\s+\w+\s*\(/i
364
+ ];
365
+
366
+ // Check for call patterns in code first (most reliable)
367
+ if (callPatterns.some(pattern => pattern.test(code))) {
368
+ return true;
369
+ }
370
+
371
+ // Check for interface/contract calls (e.g., contractA.withdraw(), contractB.deposit())
372
+ // These are function calls on contract variables
373
+ if (stmt.type === 'ExpressionStatement' && stmt.expression) {
374
+ const expr = stmt.expression;
375
+
376
+ // Direct function call on a variable (interface call)
377
+ if (expr.type === 'FunctionCall' && expr.expression) {
378
+ // Check if it's a member access (contract.method())
379
+ if (expr.expression.type === 'MemberAccess') {
380
+ // Check if the base is an identifier (variable name)
381
+ if (expr.expression.expression && expr.expression.expression.type === 'Identifier') {
382
+ const varName = expr.expression.expression.name;
383
+ // Skip built-in variables
384
+ if (!['msg', 'tx', 'block', 'this', 'address', 'abi'].includes(varName.toLowerCase())) {
385
+ // This is likely an external contract call
386
+ return true;
387
+ }
388
+ }
389
+ // Even without identifier check, member access with function call is likely external
390
+ return true;
391
+ }
392
+ }
393
+ }
394
+
395
+ // Also check for interface calls by looking at the code directly
396
+ // Pattern: variableName.functionName() where variableName is a contract interface
397
+ // This catches patterns like: contractA.withdraw(), contractB.deposit{value: ...}()
398
+ // Match: contractA.withdraw() or contractB.deposit{value: ...}()
399
+ const interfaceCallPattern = /(\w+)\.(\w+)\s*[\{\(]/;
400
+ if (interfaceCallPattern.test(code)) {
401
+ const match = code.match(interfaceCallPattern);
402
+ if (match && match[1]) {
403
+ const varName = match[1].toLowerCase();
404
+ // Skip built-in variables and common Solidity keywords
405
+ if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string', 'uint', 'int', 'bool', 'mapping', 'array'].includes(varName)) {
406
+ // Check if it looks like a contract variable (camelCase, not a type)
407
+ // Contract variables typically start with lowercase and are not Solidity types
408
+ if (varName[0] === varName[0].toLowerCase() && varName.length > 1) {
409
+ return true;
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ return false;
416
+ }
417
+
418
+ /**
419
+ * Get type of external call
420
+ */
421
+ getCallType(code) {
422
+ const codeLower = code.toLowerCase();
423
+ if (codeLower.includes('delegatecall')) return 'delegatecall';
424
+ if (codeLower.includes('.call(')) return 'call';
425
+ if (codeLower.includes('.send(')) return 'send';
426
+ if (codeLower.includes('.transfer(')) return 'transfer';
427
+ return 'external';
428
+ }
429
+
430
+ /**
431
+ * Extract call target from code
432
+ */
433
+ getCallTarget(code) {
434
+ // Try to extract contract/address being called
435
+ // Pattern: contractVariable.function() or address.call()
436
+ const patterns = [
437
+ /(\w+)\.call\s*\(/i,
438
+ /(\w+)\.delegatecall\s*\(/i,
439
+ /(\w+)\.transfer\s*\(/i,
440
+ /(\w+)\.send\s*\(/i,
441
+ /(\w+)\.\w+\s*\{/i, // contractVariable.function{value: ...}() - interface calls with value
442
+ /(\w+)\.\w+\s*\(/i // contractVariable.function() - interface calls
443
+ ];
444
+
445
+ // Try patterns in order - more specific first
446
+ for (const pattern of patterns) {
447
+ const match = code.match(pattern);
448
+ if (match && match[1]) {
449
+ const target = match[1];
450
+ const targetLower = target.toLowerCase();
451
+ // Skip common keywords and built-ins
452
+ if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(targetLower)) {
453
+ // Check if it looks like a contract variable
454
+ // Contract variables are typically camelCase
455
+ if (target[0] === target[0].toLowerCase()) {
456
+ return target;
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ return null;
463
+ }
464
+
465
+ /**
466
+ * Check if statement is a state change
467
+ */
468
+ isStateChange(stmt, code) {
469
+ const codeLower = code.toLowerCase();
470
+
471
+ // Skip variable declarations (local variables)
472
+ if (stmt.type === 'VariableDeclarationStatement') {
473
+ // Only count if it's a state variable assignment
474
+ // State variables are typically declared at contract level, not in functions
475
+ // But we can check if it's modifying a mapping or state variable
476
+ if (codeLower.includes('balances[') || codeLower.includes('allowance[') ||
477
+ codeLower.includes('mapping[') || codeLower.match(/\w+\[.*\]\s*=/)) {
478
+ // This is modifying a state variable through mapping
479
+ return true;
480
+ }
481
+ return false; // Local variable declaration, not a state change
482
+ }
483
+
484
+ // State change patterns - assignments to state variables
485
+ const stateChangePatterns = [
486
+ /balances\[.*\]\s*=/i, // balances[user] = ...
487
+ /allowance\[.*\]\s*=/i, // allowance[from][spender] = ...
488
+ /mapping\[.*\]\s*=/i, // mapping assignments
489
+ /\+\+/, // Increment
490
+ /--/, // Decrement
491
+ /\+\s*=/, // Add assign
492
+ /-\s*=/ // Subtract assign
493
+ ];
494
+
495
+ // Check for state variable assignments (ExpressionStatement with Assignment)
496
+ if (stmt.type === 'ExpressionStatement' && stmt.expression) {
497
+ const expr = stmt.expression;
498
+ if (expr.type === 'Assignment') {
499
+ // Check if left side is a state variable (mapping, storage variable)
500
+ const leftSide = this.getCodeSnippet(expr.left ? expr.left.loc : null);
501
+ if (leftSide && (leftSide.includes('balances') || leftSide.includes('allowance') ||
502
+ leftSide.includes('mapping') || stateChangePatterns.some(p => p.test(leftSide)))) {
503
+ return true;
504
+ }
505
+ }
506
+ if (expr.type === 'UnaryOperation') {
507
+ // ++ or -- operations
508
+ return /\+\+|--/.test(code);
509
+ }
510
+ }
511
+
512
+ return false;
513
+ }
514
+
515
+ /**
516
+ * Get type of state change
517
+ */
518
+ getStateChangeType(code) {
519
+ const codeLower = code.toLowerCase();
520
+ if (codeLower.includes('balance')) return 'balance';
521
+ if (codeLower.includes('mapping')) return 'mapping';
522
+ if (codeLower.includes('array')) return 'array';
523
+ return 'state';
524
+ }
525
+
526
+ /**
527
+ * Check if function has reentrancy guard
528
+ */
529
+ hasReentrancyGuard(code, node) {
530
+ const codeLower = code.toLowerCase();
531
+
532
+ // Check for nonReentrant modifier
533
+ if (node.modifiers) {
534
+ const hasNonReentrant = node.modifiers.some(m =>
535
+ m.name && m.name.toLowerCase().includes('nonreentrant')
536
+ );
537
+ if (hasNonReentrant) return true;
538
+ }
539
+
540
+ // Check for reentrancy guard patterns
541
+ const guardPatterns = [
542
+ /nonReentrant/i,
543
+ /reentrancyGuard/i,
544
+ /_status\s*==\s*_NOT_ENTERED/i,
545
+ /require\s*\(\s*.*reentrant/i
546
+ ];
547
+
548
+ return guardPatterns.some(pattern => pattern.test(codeLower));
549
+ }
550
+
551
+ /**
552
+ * Check for state dependency across contracts
553
+ */
554
+ hasStateDependency(externalCalls, stateChanges) {
555
+ // If we read from one contract and write to another, that's a dependency
556
+ if (externalCalls.length > 0 && stateChanges.length > 0) {
557
+ // Check if external calls read state
558
+ const readsState = externalCalls.some(call => {
559
+ const callCode = call.code.toLowerCase();
560
+ return callCode.includes('balance') ||
561
+ callCode.includes('get') ||
562
+ callCode.includes('view') ||
563
+ callCode.includes('read');
564
+ });
565
+
566
+ return readsState;
567
+ }
568
+
569
+ return false;
570
+ }
571
+
572
+ /**
573
+ * Post-traversal analysis
574
+ */
575
+ analyzeCrossContractReentrancy() {
576
+ // Analyze interactions between contracts
577
+ if (this.externalCalls.length === 0) return;
578
+
579
+ // Group by function
580
+ const functionGroups = new Map();
581
+ this.externalCalls.forEach(callInfo => {
582
+ if (!functionGroups.has(callInfo.function)) {
583
+ functionGroups.set(callInfo.function, []);
584
+ }
585
+ functionGroups.get(callInfo.function).push(callInfo);
586
+ });
587
+
588
+ // Check each function for cross-contract patterns
589
+ functionGroups.forEach((callInfos, funcName) => {
590
+ const allInteractions = new Set();
591
+ callInfos.forEach(info => {
592
+ info.interactions.forEach(interaction => allInteractions.add(interaction));
593
+ });
594
+
595
+ if (allInteractions.size > 1) {
596
+ // Multiple contracts - potential cross-contract reentrancy
597
+ // Already handled in checkCrossContractReentrancy
598
+ }
599
+ });
600
+ }
601
+
602
+ /**
603
+ * Generate Foundry PoC for cross-contract reentrancy
604
+ */
605
+ generateCrossContractReentrancyPoC(contractName, funcName, interactions) {
606
+ return `// SPDX-License-Identifier: MIT
607
+ pragma solidity ^0.8.0;
608
+
609
+ import "forge-std/Test.sol";
610
+
611
+ /**
612
+ * Proof of Concept: Cross-Contract Reentrancy Attack
613
+ * Target: ${contractName}.${funcName}()
614
+ * Attack Vector: Reentrancy via multiple contracts
615
+ */
616
+ contract CrossContractReentrancyExploit is Test {
617
+ address constant TARGET = address(0); // ${contractName} address
618
+ address constant CONTRACT_A = address(0); // ${interactions[0] || 'ContractA'}
619
+ address constant CONTRACT_B = address(0); // ${interactions[1] || 'ContractB'}
620
+
621
+ AttackerContract attacker;
622
+
623
+ function setUp() public {
624
+ attacker = new AttackerContract();
625
+ }
626
+
627
+ function testExploit() public {
628
+ // 1. Setup: Attacker has funds in both contracts
629
+ // 2. Call vulnerable function which interacts with Contract A
630
+ // ${contractName}(TARGET).${funcName}(...);
631
+
632
+ // 3. Contract A's callback triggers interaction with Contract B
633
+ // 4. Contract B's callback re-enters ${contractName}.${funcName}()
634
+ // 5. State is inconsistent, attacker benefits
635
+
636
+ // Assert exploit succeeded
637
+ // assertGt(attacker.balance, initialBalance);
638
+ }
639
+ }
640
+
641
+ contract AttackerContract {
642
+ function onCallback() external {
643
+ // Re-enter target contract
644
+ // Or trigger another contract to re-enter
645
+ }
646
+ }`;
647
+ }
648
+
649
+ /**
650
+ * Generate Foundry PoC for delegatecall reentrancy
651
+ */
652
+ generateDelegatecallReentrancyPoC(contractName, funcName) {
653
+ return `// SPDX-License-Identifier: MIT
654
+ pragma solidity ^0.8.0;
655
+
656
+ import "forge-std/Test.sol";
657
+
658
+ /**
659
+ * Proof of Concept: Delegatecall Reentrancy Attack
660
+ * Target: ${contractName}.${funcName}()
661
+ * Attack Vector: Reentrancy via delegatecall
662
+ */
663
+ contract DelegatecallReentrancyExploit is Test {
664
+ address constant TARGET = address(0); // ${contractName} address
665
+ MaliciousImplementation maliciousImpl;
666
+
667
+ function setUp() public {
668
+ maliciousImpl = new MaliciousImplementation();
669
+ }
670
+
671
+ function testExploit() public {
672
+ // 1. Deploy malicious implementation
673
+ // 2. Call ${funcName}() which delegatecalls to malicious contract
674
+ // ${contractName}(TARGET).${funcName}(address(maliciousImpl), ...);
675
+
676
+ // 3. Malicious contract executes in target's context
677
+ // 4. Malicious contract calls back into target
678
+ // 5. Reentrancy occurs with elevated privileges
679
+
680
+ // Assert exploit succeeded
681
+ }
682
+ }
683
+
684
+ contract MaliciousImplementation {
685
+ address target;
686
+
687
+ function maliciousFunction() external {
688
+ // Execute in target's storage context
689
+ // Call back into target for reentrancy
690
+ // ${contractName}(target).vulnerableFunction();
691
+ }
692
+ }`;
693
+ }
694
+ }
695
+
696
+ module.exports = CrossContractReentrancyDetector;
697
+