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,465 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Token Standard Compliance Detector
5
+ * Ensures tokens strictly follow ERC standards (ERC20, ERC721, ERC1155)
6
+ *
7
+ * Detects:
8
+ * - Missing required functions for ERC standards
9
+ * - Incorrect function signatures
10
+ * - Missing events
11
+ * - Non-standard return values
12
+ * - Missing approvals/transfers
13
+ * - Incorrect behavior patterns
14
+ */
15
+ class TokenStandardComplianceDetector extends BaseDetector {
16
+ constructor() {
17
+ super(
18
+ 'Token Standard Compliance',
19
+ 'Detects violations of ERC token standards (ERC20, ERC721, ERC1155)',
20
+ 'HIGH'
21
+ );
22
+ this.currentContract = null;
23
+ this.tokenStandard = null; // 'ERC20', 'ERC721', 'ERC1155', or null
24
+ this.requiredFunctions = {
25
+ ERC20: ['totalSupply', 'balanceOf', 'transfer', 'transferFrom', 'approve', 'allowance'],
26
+ ERC721: ['balanceOf', 'ownerOf', 'safeTransferFrom', 'transferFrom', 'approve', 'setApprovalForAll', 'getApproved', 'isApprovedForAll'],
27
+ ERC1155: ['balanceOf', 'balanceOfBatch', 'setApprovalForAll', 'isApprovedForAll', 'safeTransferFrom', 'safeBatchTransferFrom']
28
+ };
29
+ this.requiredEvents = {
30
+ ERC20: ['Transfer', 'Approval'],
31
+ ERC721: ['Transfer', 'Approval', 'ApprovalForAll'],
32
+ ERC1155: ['TransferSingle', 'TransferBatch', 'ApprovalForAll', 'URI']
33
+ };
34
+ this.foundFunctions = new Set();
35
+ this.foundEvents = new Set();
36
+ }
37
+
38
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
39
+ this.findings = [];
40
+ this.ast = ast;
41
+ this.sourceCode = sourceCode;
42
+ this.fileName = fileName;
43
+ this.sourceLines = sourceCode.split('\n');
44
+ this.cfg = cfg;
45
+ this.dataFlow = dataFlow;
46
+
47
+ // Reset per-file state
48
+ this.tokenStandard = null;
49
+ this.foundFunctions.clear();
50
+ this.foundEvents.clear();
51
+ this.currentContract = null;
52
+
53
+ // Detect which standard this contract implements
54
+ this.detectTokenStandard(sourceCode);
55
+
56
+ if (!this.tokenStandard) {
57
+ // Not a token contract, skip
58
+ return this.findings;
59
+ }
60
+
61
+ this.traverse(ast);
62
+
63
+ // Post-traversal analysis
64
+ this.analyzeCompliance();
65
+
66
+ return this.findings;
67
+ }
68
+
69
+ visitContractDefinition(node) {
70
+ this.currentContract = node.name;
71
+ this.foundFunctions.clear();
72
+ this.foundEvents.clear();
73
+ }
74
+
75
+ visitFunctionDefinition(node) {
76
+ const funcName = node.name || '';
77
+
78
+ if (this.tokenStandard) {
79
+ // Check if this is a required function
80
+ const required = this.requiredFunctions[this.tokenStandard] || [];
81
+ if (required.includes(funcName)) {
82
+ this.foundFunctions.add(funcName);
83
+
84
+ // Validate function signature and behavior
85
+ this.validateFunction(node, funcName);
86
+ }
87
+ }
88
+ }
89
+
90
+ visitEventDefinition(node) {
91
+ const eventName = node.name || '';
92
+
93
+ if (this.tokenStandard) {
94
+ const required = this.requiredEvents[this.tokenStandard] || [];
95
+ if (required.includes(eventName)) {
96
+ this.foundEvents.add(eventName);
97
+
98
+ // Validate event signature
99
+ this.validateEvent(node, eventName);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Detect which ERC standard this contract implements
106
+ */
107
+ detectTokenStandard(sourceCode) {
108
+ // Strip imports/comments so "ERC20" in import paths doesn't misclassify non-token contracts
109
+ const stripped = sourceCode
110
+ .replace(/^\s*import[^;]*;/gm, '')
111
+ .replace(/\/\/.*$/gm, '')
112
+ .replace(/\/\*[\s\S]*?\*\//g, '');
113
+
114
+ const codeLower = stripped.toLowerCase();
115
+
116
+ // Check for explicit ERC standard inheritance or interface implementation
117
+ const hasERCInterface =
118
+ /contract\s+\w+\s+(?:is|implements)\s+.*\b(IERC|ERC)\d+\b/i.test(stripped) ||
119
+ /contract\s+\w+\s+(?:is|implements)\s+.*\bERC\b/i.test(stripped);
120
+
121
+ // Check for ERC1155 (most specific)
122
+ if (codeLower.includes('erc1155') ||
123
+ codeLower.includes('multitoken') ||
124
+ (codeLower.includes('balanceofbatch') && codeLower.includes('safebatchtransferfrom'))) {
125
+ this.tokenStandard = 'ERC1155';
126
+ return;
127
+ }
128
+
129
+ // Check for ERC721
130
+ if (codeLower.includes('erc721') ||
131
+ (codeLower.includes('nft') && codeLower.includes('ownerof')) ||
132
+ (codeLower.includes('ownerof') && codeLower.includes('tokenuri')) ||
133
+ (codeLower.includes('setapprovalforall') && codeLower.includes('ownerof'))) {
134
+ this.tokenStandard = 'ERC721';
135
+ return;
136
+ }
137
+
138
+ // Check for ERC20 - need multiple indicators to avoid false positives
139
+ // Must have both transfer/transferFrom AND approve/allowance patterns
140
+ const hasTransfer = /function\s+transfer/i.test(sourceCode) || /function\s+transferFrom/i.test(sourceCode);
141
+ const hasApprove = /function\s+approve/i.test(sourceCode) || /function\s+allowance/i.test(sourceCode);
142
+ const hasBalanceOf = /function\s+balanceOf/i.test(sourceCode);
143
+ const hasTotalSupply = /function\s+totalSupply/i.test(sourceCode);
144
+
145
+ // Only treat as ERC20 if it is explicitly implemented/inherited, or declares core ERC20 functions
146
+ if (hasERCInterface) {
147
+ this.tokenStandard = 'ERC20';
148
+ return;
149
+ }
150
+
151
+ // Only detect ERC20 if it has multiple token functions (not just one)
152
+ if ((hasTransfer && hasApprove && hasBalanceOf) ||
153
+ (hasTransfer && hasTotalSupply && hasBalanceOf)) {
154
+ this.tokenStandard = 'ERC20';
155
+ return;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Validate function implementation
161
+ */
162
+ validateFunction(node, funcName) {
163
+ const funcCode = this.getCodeSnippet(node.loc);
164
+ const funcCodeLower = funcCode.toLowerCase();
165
+
166
+ // ERC20 specific validations
167
+ if (this.tokenStandard === 'ERC20') {
168
+ this.validateERC20Function(node, funcName, funcCode);
169
+ }
170
+
171
+ // ERC721 specific validations
172
+ if (this.tokenStandard === 'ERC721') {
173
+ this.validateERC721Function(node, funcName, funcCode);
174
+ }
175
+
176
+ // ERC1155 specific validations
177
+ if (this.tokenStandard === 'ERC1155') {
178
+ this.validateERC1155Function(node, funcName, funcCode);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Validate ERC20 function
184
+ */
185
+ validateERC20Function(node, funcName, code) {
186
+ const codeLower = code.toLowerCase();
187
+
188
+ // transfer/transferFrom must emit Transfer event
189
+ if ((funcName === 'transfer' || funcName === 'transferFrom') &&
190
+ !codeLower.includes('emit transfer')) {
191
+ this.addFinding({
192
+ title: 'ERC20 Transfer Missing Transfer Event',
193
+ description: `ERC20 function '${funcName}' does not emit Transfer event. This violates ERC20 standard and breaks compatibility with wallets, DEXs, and other contracts expecting the event.`,
194
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
195
+ line: node.loc ? node.loc.start.line : 0,
196
+ column: node.loc ? node.loc.start.column : 0,
197
+ code: this.getCodeSnippet(node.loc),
198
+ severity: 'HIGH',
199
+ confidence: 'HIGH',
200
+ exploitable: false,
201
+ exploitabilityScore: 30,
202
+ attackVector: 'standard-compliance',
203
+ recommendation: 'Emit Transfer event: emit Transfer(from, to, amount);',
204
+ references: [
205
+ 'https://eips.ethereum.org/EIPS/eip-20',
206
+ 'https://swcregistry.io/docs/SWC-140'
207
+ ]
208
+ });
209
+ }
210
+
211
+ // approve must emit Approval event
212
+ if (funcName === 'approve' && !codeLower.includes('emit approval')) {
213
+ this.addFinding({
214
+ title: 'ERC20 Approve Missing Approval Event',
215
+ description: `ERC20 function 'approve' does not emit Approval event. This violates ERC20 standard.`,
216
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
217
+ line: node.loc ? node.loc.start.line : 0,
218
+ column: node.loc ? node.loc.start.column : 0,
219
+ code: this.getCodeSnippet(node.loc),
220
+ severity: 'HIGH',
221
+ confidence: 'HIGH',
222
+ exploitable: false,
223
+ exploitabilityScore: 30,
224
+ attackVector: 'standard-compliance',
225
+ recommendation: 'Emit Approval event: emit Approval(owner, spender, amount);',
226
+ references: [
227
+ 'https://eips.ethereum.org/EIPS/eip-20'
228
+ ]
229
+ });
230
+ }
231
+
232
+ // Check for non-standard return values (should return bool)
233
+ if ((funcName === 'transfer' || funcName === 'transferFrom' || funcName === 'approve') &&
234
+ node.returnParameters && node.returnParameters.length === 0) {
235
+ // Some ERC20 implementations don't return bool, but it's non-standard
236
+ this.addFinding({
237
+ title: 'ERC20 Function Missing Return Value',
238
+ description: `ERC20 function '${funcName}' should return bool according to standard. Missing return value may break compatibility with some contracts.`,
239
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
240
+ line: node.loc ? node.loc.start.line : 0,
241
+ column: node.loc ? node.loc.start.column : 0,
242
+ code: this.getCodeSnippet(node.loc),
243
+ severity: 'MEDIUM',
244
+ confidence: 'MEDIUM',
245
+ exploitable: false,
246
+ exploitabilityScore: 20,
247
+ attackVector: 'standard-compliance',
248
+ recommendation: 'Add return bool to function signature: function transfer(...) public returns (bool)',
249
+ references: [
250
+ 'https://eips.ethereum.org/EIPS/eip-20'
251
+ ]
252
+ });
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Validate ERC721 function
258
+ */
259
+ validateERC721Function(node, funcName, code) {
260
+ const codeLower = code.toLowerCase();
261
+
262
+ // transferFrom/safeTransferFrom must emit Transfer event
263
+ if ((funcName === 'transferFrom' || funcName === 'safeTransferFrom') &&
264
+ !codeLower.includes('emit transfer')) {
265
+ this.addFinding({
266
+ title: 'ERC721 Transfer Missing Transfer Event',
267
+ description: `ERC721 function '${funcName}' does not emit Transfer event. This violates ERC721 standard.`,
268
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
269
+ line: node.loc ? node.loc.start.line : 0,
270
+ column: node.loc ? node.loc.start.column : 0,
271
+ code: this.getCodeSnippet(node.loc),
272
+ severity: 'HIGH',
273
+ confidence: 'HIGH',
274
+ exploitable: false,
275
+ exploitabilityScore: 30,
276
+ attackVector: 'standard-compliance',
277
+ recommendation: 'Emit Transfer event: emit Transfer(from, to, tokenId);',
278
+ references: [
279
+ 'https://eips.ethereum.org/EIPS/eip-721'
280
+ ]
281
+ });
282
+ }
283
+
284
+ // approve must emit Approval event
285
+ if (funcName === 'approve' && !codeLower.includes('emit approval')) {
286
+ this.addFinding({
287
+ title: 'ERC721 Approve Missing Approval Event',
288
+ description: `ERC721 function 'approve' does not emit Approval event. This violates ERC721 standard.`,
289
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
290
+ line: node.loc ? node.loc.start.line : 0,
291
+ column: node.loc ? node.loc.start.column : 0,
292
+ code: this.getCodeSnippet(node.loc),
293
+ severity: 'HIGH',
294
+ confidence: 'HIGH',
295
+ exploitable: false,
296
+ exploitabilityScore: 30,
297
+ attackVector: 'standard-compliance',
298
+ recommendation: 'Emit Approval event: emit Approval(owner, approved, tokenId);',
299
+ references: [
300
+ 'https://eips.ethereum.org/EIPS/eip-721'
301
+ ]
302
+ });
303
+ }
304
+
305
+ // setApprovalForAll must emit ApprovalForAll event
306
+ if (funcName === 'setApprovalForAll' && !codeLower.includes('emit approvalforall')) {
307
+ this.addFinding({
308
+ title: 'ERC721 setApprovalForAll Missing ApprovalForAll Event',
309
+ description: `ERC721 function 'setApprovalForAll' does not emit ApprovalForAll event. This violates ERC721 standard.`,
310
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
311
+ line: node.loc ? node.loc.start.line : 0,
312
+ column: node.loc ? node.loc.start.column : 0,
313
+ code: this.getCodeSnippet(node.loc),
314
+ severity: 'HIGH',
315
+ confidence: 'HIGH',
316
+ exploitable: false,
317
+ exploitabilityScore: 30,
318
+ attackVector: 'standard-compliance',
319
+ recommendation: 'Emit ApprovalForAll event: emit ApprovalForAll(owner, operator, approved);',
320
+ references: [
321
+ 'https://eips.ethereum.org/EIPS/eip-721'
322
+ ]
323
+ });
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Validate ERC1155 function
329
+ */
330
+ validateERC1155Function(node, funcName, code) {
331
+ const codeLower = code.toLowerCase();
332
+
333
+ // safeTransferFrom must emit TransferSingle event
334
+ if (funcName === 'safeTransferFrom' && !codeLower.includes('emit transfersingle')) {
335
+ this.addFinding({
336
+ title: 'ERC1155 safeTransferFrom Missing TransferSingle Event',
337
+ description: `ERC1155 function 'safeTransferFrom' does not emit TransferSingle event. This violates ERC1155 standard.`,
338
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
339
+ line: node.loc ? node.loc.start.line : 0,
340
+ column: node.loc ? node.loc.start.column : 0,
341
+ code: this.getCodeSnippet(node.loc),
342
+ severity: 'HIGH',
343
+ confidence: 'HIGH',
344
+ exploitable: false,
345
+ exploitabilityScore: 30,
346
+ attackVector: 'standard-compliance',
347
+ recommendation: 'Emit TransferSingle event: emit TransferSingle(operator, from, to, id, value);',
348
+ references: [
349
+ 'https://eips.ethereum.org/EIPS/eip-1155'
350
+ ]
351
+ });
352
+ }
353
+
354
+ // safeBatchTransferFrom must emit TransferBatch event
355
+ if (funcName === 'safeBatchTransferFrom' && !codeLower.includes('emit transferbatch')) {
356
+ this.addFinding({
357
+ title: 'ERC1155 safeBatchTransferFrom Missing TransferBatch Event',
358
+ description: `ERC1155 function 'safeBatchTransferFrom' does not emit TransferBatch event. This violates ERC1155 standard.`,
359
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
360
+ line: node.loc ? node.loc.start.line : 0,
361
+ column: node.loc ? node.loc.start.column : 0,
362
+ code: this.getCodeSnippet(node.loc),
363
+ severity: 'HIGH',
364
+ confidence: 'HIGH',
365
+ exploitable: false,
366
+ exploitabilityScore: 30,
367
+ attackVector: 'standard-compliance',
368
+ recommendation: 'Emit TransferBatch event: emit TransferBatch(operator, from, to, ids, values);',
369
+ references: [
370
+ 'https://eips.ethereum.org/EIPS/eip-1155'
371
+ ]
372
+ });
373
+ }
374
+
375
+ // setApprovalForAll must emit ApprovalForAll event
376
+ if (funcName === 'setApprovalForAll' && !codeLower.includes('emit approvalforall')) {
377
+ this.addFinding({
378
+ title: 'ERC1155 setApprovalForAll Missing ApprovalForAll Event',
379
+ description: `ERC1155 function 'setApprovalForAll' does not emit ApprovalForAll event. This violates ERC1155 standard.`,
380
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
381
+ line: node.loc ? node.loc.start.line : 0,
382
+ column: node.loc ? node.loc.start.column : 0,
383
+ code: this.getCodeSnippet(node.loc),
384
+ severity: 'HIGH',
385
+ confidence: 'HIGH',
386
+ exploitable: false,
387
+ exploitabilityScore: 30,
388
+ attackVector: 'standard-compliance',
389
+ recommendation: 'Emit ApprovalForAll event: emit ApprovalForAll(owner, operator, approved);',
390
+ references: [
391
+ 'https://eips.ethereum.org/EIPS/eip-1155'
392
+ ]
393
+ });
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Validate event signature
399
+ */
400
+ validateEvent(node, eventName) {
401
+ // Basic validation - events should have correct parameters
402
+ // This is a simplified check; full validation would require parameter matching
403
+ }
404
+
405
+ /**
406
+ * Post-traversal compliance analysis
407
+ */
408
+ analyzeCompliance() {
409
+ if (!this.tokenStandard) return;
410
+
411
+ const requiredFuncs = this.requiredFunctions[this.tokenStandard] || [];
412
+ const requiredEvents = this.requiredEvents[this.tokenStandard] || [];
413
+
414
+ // Check for missing required functions
415
+ const missingFunctions = requiredFuncs.filter(func => !this.foundFunctions.has(func));
416
+ if (missingFunctions.length > 0) {
417
+ this.addFinding({
418
+ title: `Missing Required ${this.tokenStandard} Functions`,
419
+ description: `Contract claims to implement ${this.tokenStandard} but is missing required functions: ${missingFunctions.join(', ')}. This breaks standard compliance and may cause integration issues.`,
420
+ location: `Contract: ${this.currentContract}`,
421
+ line: 1,
422
+ column: 0,
423
+ code: this.sourceCode.substring(0, 200),
424
+ severity: 'HIGH',
425
+ confidence: 'HIGH',
426
+ exploitable: false,
427
+ exploitabilityScore: 40,
428
+ attackVector: 'standard-compliance',
429
+ recommendation: `Implement all required ${this.tokenStandard} functions. Use OpenZeppelin's standard implementations as reference.`,
430
+ references: [
431
+ this.tokenStandard === 'ERC20' ? 'https://eips.ethereum.org/EIPS/eip-20' :
432
+ this.tokenStandard === 'ERC721' ? 'https://eips.ethereum.org/EIPS/eip-721' :
433
+ 'https://eips.ethereum.org/EIPS/eip-1155'
434
+ ]
435
+ });
436
+ }
437
+
438
+ // Check for missing required events
439
+ const missingEvents = requiredEvents.filter(event => !this.foundEvents.has(event));
440
+ if (missingEvents.length > 0) {
441
+ this.addFinding({
442
+ title: `Missing Required ${this.tokenStandard} Events`,
443
+ description: `Contract claims to implement ${this.tokenStandard} but is missing required events: ${missingEvents.join(', ')}. This breaks standard compliance.`,
444
+ location: `Contract: ${this.currentContract}`,
445
+ line: 1,
446
+ column: 0,
447
+ code: this.sourceCode.substring(0, 200),
448
+ severity: 'HIGH',
449
+ confidence: 'HIGH',
450
+ exploitable: false,
451
+ exploitabilityScore: 40,
452
+ attackVector: 'standard-compliance',
453
+ recommendation: `Declare all required ${this.tokenStandard} events. Events are essential for off-chain indexing and monitoring.`,
454
+ references: [
455
+ this.tokenStandard === 'ERC20' ? 'https://eips.ethereum.org/EIPS/eip-20' :
456
+ this.tokenStandard === 'ERC721' ? 'https://eips.ethereum.org/EIPS/eip-721' :
457
+ 'https://eips.ethereum.org/EIPS/eip-1155'
458
+ ]
459
+ });
460
+ }
461
+ }
462
+ }
463
+
464
+ module.exports = TokenStandardComplianceDetector;
465
+
@@ -0,0 +1,214 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ class UncheckedCallDetector extends BaseDetector {
4
+ constructor() {
5
+ super(
6
+ 'Unchecked External Call',
7
+ 'Detects external calls whose return values are not checked',
8
+ 'HIGH'
9
+ );
10
+ this.potentialIssues = [];
11
+ this.checkedVariables = new Set();
12
+ }
13
+
14
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
15
+ this.findings = [];
16
+ this.ast = ast;
17
+ this.sourceCode = sourceCode;
18
+ this.fileName = fileName;
19
+ this.sourceLines = sourceCode.split('\n');
20
+ this.cfg = cfg;
21
+ this.dataFlow = dataFlow;
22
+ this.potentialIssues = [];
23
+ this.checkedVariables = new Set();
24
+
25
+ // First pass: collect potential issues and checked variables
26
+ this.traverse(ast);
27
+
28
+ // Second pass: filter out false positives
29
+ this.potentialIssues = this.potentialIssues.filter(issue => {
30
+ return !this.checkedVariables.has(issue.variableName);
31
+ });
32
+
33
+ // Add remaining issues as findings
34
+ this.potentialIssues.forEach(issue => {
35
+ this.addFinding(issue.finding);
36
+ });
37
+
38
+ return this.findings;
39
+ }
40
+
41
+ visitFunctionDefinition(node) {
42
+ // Scan entire function for all require/assert/if statements
43
+ if (node.body) {
44
+ this.scanForChecks(node.body);
45
+ }
46
+ }
47
+
48
+ scanForChecks(node) {
49
+ if (!node) return;
50
+
51
+ // Check for require(success) or assert(success)
52
+ if (node.type === 'ExpressionStatement' && node.expression) {
53
+ const expr = node.expression;
54
+ if (expr.type === 'FunctionCall' && expr.expression) {
55
+ const funcName = expr.expression.name;
56
+ if (funcName === 'require' || funcName === 'assert') {
57
+ // Check first argument
58
+ if (expr.arguments && expr.arguments.length > 0) {
59
+ const arg = expr.arguments[0];
60
+ if (arg.type === 'Identifier') {
61
+ this.checkedVariables.add(arg.name);
62
+ }
63
+ // Handle !variable pattern
64
+ if (arg.type === 'UnaryOperation' && arg.operator === '!' && arg.subExpression && arg.subExpression.type === 'Identifier') {
65
+ this.checkedVariables.add(arg.subExpression.name);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ // Check for if (success) or if (!success)
73
+ if (node.type === 'IfStatement' && node.condition) {
74
+ if (node.condition.type === 'Identifier') {
75
+ this.checkedVariables.add(node.condition.name);
76
+ }
77
+ if (node.condition.type === 'UnaryOperation' && node.condition.operator === '!' && node.condition.subExpression && node.condition.subExpression.type === 'Identifier') {
78
+ this.checkedVariables.add(node.condition.subExpression.name);
79
+ }
80
+ // Recursively check inside if body
81
+ if (node.trueBody) this.scanForChecks(node.trueBody);
82
+ if (node.falseBody) this.scanForChecks(node.falseBody);
83
+ }
84
+
85
+ // Recursively check Block statements
86
+ if (node.type === 'Block' && node.statements) {
87
+ node.statements.forEach(stmt => this.scanForChecks(stmt));
88
+ }
89
+
90
+ // Check other nested structures
91
+ if (node.statements) {
92
+ node.statements.forEach(stmt => this.scanForChecks(stmt));
93
+ }
94
+ }
95
+
96
+ visitExpressionStatement(node) {
97
+ if (!node.expression) return;
98
+
99
+ const expr = node.expression;
100
+
101
+ // Check for call expressions
102
+ if (expr.type === 'FunctionCall') {
103
+ this.checkFunctionCall(expr, node);
104
+ }
105
+ }
106
+
107
+ checkFunctionCall(expr, parentNode) {
108
+ const code = this.getCodeSnippet(expr.loc);
109
+
110
+ // Check for low-level calls that should be checked
111
+ if (this.isLowLevelCall(code)) {
112
+ // If this is a standalone statement (not assigned or checked in if)
113
+ // it means the return value is ignored
114
+ if (parentNode.type === 'ExpressionStatement') {
115
+ // Determine severity based on call type
116
+ const isDelegatecall = code.includes('.delegatecall');
117
+ const hasValue = code.includes('{value:') || code.includes('{value :');
118
+
119
+ this.addFinding({
120
+ title: 'Unchecked Low-Level Call',
121
+ description: `Low-level call (${isDelegatecall ? 'delegatecall' : hasValue ? 'call with value' : 'call'}) return value is not checked. Failed calls will be silently ignored, potentially causing:\n` +
122
+ `- Fund loss (if sending ETH)\n` +
123
+ `- State inconsistency (balance decremented but transfer failed)\n` +
124
+ `- Silent failures in critical operations`,
125
+ location: this.getLocationString(expr.loc),
126
+ line: expr.loc ? expr.loc.start.line : 0,
127
+ column: expr.loc ? expr.loc.start.column : 0,
128
+ code: code,
129
+ severity: isDelegatecall ? 'CRITICAL' : 'HIGH',
130
+ confidence: 'HIGH', // Definite unchecked call
131
+ exploitable: true,
132
+ attackVector: 'unchecked-call',
133
+ recommendation: 'Always check the return value of low-level calls. Use require(success, "error message") or implement proper error handling.',
134
+ references: [
135
+ 'https://swcregistry.io/docs/SWC-104',
136
+ 'https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/'
137
+ ]
138
+ });
139
+ }
140
+ }
141
+
142
+ // Check for .send() calls
143
+ if (code.includes('.send(')) {
144
+ if (parentNode.type === 'ExpressionStatement') {
145
+ this.addFinding({
146
+ title: 'Unchecked Send Return Value',
147
+ description: 'The .send() function returns false on failure, but the return value is not checked. This causes fund loss - user balance is decremented but ETH transfer fails silently.',
148
+ location: this.getLocationString(expr.loc),
149
+ line: expr.loc ? expr.loc.start.line : 0,
150
+ column: expr.loc ? expr.loc.start.column : 0,
151
+ code: code,
152
+ severity: 'HIGH',
153
+ confidence: 'HIGH', // Definite unchecked send
154
+ exploitable: true,
155
+ attackVector: 'unchecked-call',
156
+ recommendation: 'Check the return value: require(recipient.send(amount), "Send failed"). Consider using .transfer() which reverts on failure, or .call{value: amount}() with proper checks.',
157
+ references: [
158
+ 'https://swcregistry.io/docs/SWC-104'
159
+ ]
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ visitVariableDeclarationStatement(node) {
166
+ // Check if a call's return value is assigned but never used
167
+ if (node.variables && node.variables.length > 0) {
168
+ const variable = node.variables[0];
169
+
170
+ // Handle null variables (from tuple destruction like "(bool success, ) = ...")
171
+ if (variable && variable.name && node.initialValue) {
172
+ const code = this.getCodeSnippet(node.initialValue.loc);
173
+
174
+ if (this.isLowLevelCall(code)) {
175
+ // Check if variable name suggests it should be checked (like 'success')
176
+ if (variable.name.toLowerCase().includes('success')) {
177
+ // Store as potential issue - will be filtered later
178
+ this.potentialIssues.push({
179
+ variableName: variable.name,
180
+ finding: {
181
+ title: 'Low-Level Call Return Value Not Checked',
182
+ description: `Low-level call result assigned to '${variable.name}' but never validated. Failed calls will be silently ignored.`,
183
+ location: this.getLocationString(node.loc),
184
+ line: node.loc ? node.loc.start.line : 0,
185
+ column: node.loc ? node.loc.start.column : 0,
186
+ code: this.getCodeSnippet(node.loc),
187
+ recommendation: 'Add validation: require(success, "Call failed") or if (!success) revert("Call failed")',
188
+ references: [
189
+ 'https://swcregistry.io/docs/SWC-104'
190
+ ]
191
+ }
192
+ });
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ isLowLevelCall(code) {
200
+ return code.includes('.call(') ||
201
+ code.includes('.call{') ||
202
+ code.includes('.delegatecall(') ||
203
+ code.includes('.delegatecall{') ||
204
+ code.includes('.staticcall(') ||
205
+ code.includes('.staticcall{');
206
+ }
207
+
208
+ getLocationString(loc) {
209
+ if (!loc || !loc.start) return 'Unknown';
210
+ return `Line ${loc.start.line}, Column ${loc.start.column}`;
211
+ }
212
+ }
213
+
214
+ module.exports = UncheckedCallDetector;