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,408 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Precision Loss Detector
5
+ * Detects mathematical operations that cause precision loss:
6
+ * - Division before multiplication (truncation)
7
+ * - Rounding errors in financial calculations
8
+ * - Loss of precision in decimal conversions
9
+ *
10
+ * Classification:
11
+ * - CRITICAL: Division before multiplication in token/price calculations
12
+ * - HIGH: Rounding errors affecting user balances
13
+ * - MEDIUM: Precision loss in non-financial contexts
14
+ */
15
+ class PrecisionLossDetector extends BaseDetector {
16
+ constructor() {
17
+ super(
18
+ 'Precision Loss',
19
+ 'Detects mathematical operations causing precision loss',
20
+ 'HIGH'
21
+ );
22
+ this.currentContract = null;
23
+ this.currentFunction = null;
24
+ this.currentFunctionNode = null;
25
+ this.variableTypes = new Map();
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
+ this.cfg = cfg;
35
+ this.dataFlow = dataFlow;
36
+
37
+ this.traverse(ast);
38
+
39
+ return this.findings;
40
+ }
41
+
42
+ visitContractDefinition(node) {
43
+ this.currentContract = node.name;
44
+ this.variableTypes.clear();
45
+
46
+ // Track state variable types
47
+ if (node.subNodes) {
48
+ for (const subNode of node.subNodes) {
49
+ if (subNode.type === 'StateVariableDeclaration') {
50
+ this.trackVariableType(subNode);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ visitFunctionDefinition(node) {
57
+ this.currentFunction = node.name || 'constructor';
58
+ this.currentFunctionNode = node;
59
+
60
+ // Skip view/pure functions for critical financial issues (they don't modify state)
61
+ // but still check for precision issues
62
+ if (node.body) {
63
+ this.analyzeFunction(node);
64
+ }
65
+ }
66
+
67
+ trackVariableType(node) {
68
+ if (node.variables) {
69
+ for (const variable of node.variables) {
70
+ if (variable.name && variable.typeName) {
71
+ this.variableTypes.set(variable.name, this.getTypeName(variable.typeName));
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ getTypeName(typeNode) {
78
+ if (!typeNode) return 'unknown';
79
+ if (typeNode.type === 'ElementaryTypeName') {
80
+ return typeNode.name;
81
+ }
82
+ if (typeNode.type === 'UserDefinedTypeName') {
83
+ return typeNode.namePath;
84
+ }
85
+ if (typeNode.type === 'ArrayTypeName') {
86
+ return this.getTypeName(typeNode.baseTypeName) + '[]';
87
+ }
88
+ return 'unknown';
89
+ }
90
+
91
+ analyzeFunction(funcNode) {
92
+ const funcCode = this.getCodeSnippet(funcNode.loc);
93
+
94
+ // Pattern 1: Division before multiplication (most common precision loss)
95
+ this.detectDivisionBeforeMultiplication(funcNode, funcCode);
96
+
97
+ // Pattern 2: Direct division without scaling
98
+ this.detectUnscaledDivision(funcNode, funcCode);
99
+
100
+ // Pattern 3: Decimal conversion issues
101
+ this.detectDecimalConversionIssues(funcNode, funcCode);
102
+
103
+ // Pattern 4: Percentage calculations losing precision
104
+ this.detectPercentageIssues(funcNode, funcCode);
105
+ }
106
+
107
+ /**
108
+ * Detect division before multiplication pattern
109
+ * Example: a / b * c should be a * c / b
110
+ */
111
+ detectDivisionBeforeMultiplication(funcNode, funcCode) {
112
+ // Pattern: expression / value * anotherValue
113
+ // This truncates the intermediate result before multiplication
114
+ const divBeforeMulPattern = /(\w+)\s*\/\s*(\w+)\s*\*\s*(\w+)/g;
115
+ let match;
116
+
117
+ while ((match = divBeforeMulPattern.exec(funcCode)) !== null) {
118
+ const [fullMatch, dividend, divisor, multiplier] = match;
119
+
120
+ // Check if this is in a financial context
121
+ const isFinancial = this.isFinancialContext(funcCode, match.index);
122
+
123
+ // Skip if using safe multiplication patterns
124
+ if (this.usesSafeMathPattern(funcCode)) {
125
+ continue;
126
+ }
127
+
128
+ // Calculate the line number
129
+ const linesBefore = funcCode.substring(0, match.index).split('\n').length;
130
+ const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
131
+
132
+ this.addFinding({
133
+ title: 'Division Before Multiplication',
134
+ description: `Expression '${fullMatch}' performs division before multiplication, causing precision loss due to integer truncation. In Solidity, (a/b)*c loses precision compared to (a*c)/b.`,
135
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
136
+ line: line,
137
+ column: 0,
138
+ code: this.extractContext(funcCode, match.index),
139
+ severity: isFinancial ? 'CRITICAL' : 'HIGH',
140
+ confidence: 'HIGH',
141
+ exploitable: isFinancial,
142
+ exploitabilityScore: isFinancial ? 85 : 65,
143
+ attackVector: 'precision-loss',
144
+ recommendation: `Reorder to multiply before dividing: (${dividend} * ${multiplier}) / ${divisor}. For percentage calculations, multiply by the percentage first, then divide by 100 or the base.`,
145
+ references: [
146
+ 'https://github.com/crytic/slither/wiki/Detector-Documentation#divide-before-multiply',
147
+ 'https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/integer-division/'
148
+ ],
149
+ foundryPoC: isFinancial ? this.generateDivBeforeMulPoC(dividend, divisor, multiplier) : undefined
150
+ });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Detect division without proper scaling
156
+ * Example: amount / totalSupply without WAD/RAY scaling
157
+ */
158
+ detectUnscaledDivision(funcNode, funcCode) {
159
+ // Look for ratio calculations without scaling
160
+ const ratioPatterns = [
161
+ { pattern: /(\w+)\s*\/\s*totalSupply/gi, context: 'share calculation' },
162
+ { pattern: /(\w+)\s*\/\s*(\w+Supply)/gi, context: 'supply ratio' },
163
+ { pattern: /amount\s*\/\s*(\w+)/gi, context: 'amount division' },
164
+ { pattern: /balance\s*\/\s*(\w+)/gi, context: 'balance division' },
165
+ ];
166
+
167
+ for (const { pattern, context } of ratioPatterns) {
168
+ let match;
169
+ pattern.lastIndex = 0; // Reset regex state
170
+
171
+ while ((match = pattern.exec(funcCode)) !== null) {
172
+ // Check if there's scaling (WAD, RAY, 1e18, etc.)
173
+ const hasScaling = this.hasProperScaling(funcCode, match.index);
174
+
175
+ if (!hasScaling && !this.isSafeContext(funcCode, match.index)) {
176
+ const linesBefore = funcCode.substring(0, match.index).split('\n').length;
177
+ const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
178
+
179
+ this.addFinding({
180
+ title: 'Unscaled Division May Lose Precision',
181
+ description: `Division in ${context} without precision scaling. When dividing integers, small amounts may round to zero. Consider using WAD (1e18) or RAY (1e27) scaling.`,
182
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
183
+ line: line,
184
+ column: 0,
185
+ code: this.extractContext(funcCode, match.index),
186
+ severity: 'MEDIUM',
187
+ confidence: 'MEDIUM',
188
+ exploitable: true,
189
+ exploitabilityScore: 55,
190
+ attackVector: 'precision-loss',
191
+ recommendation: 'Scale the numerator before division: (amount * 1e18) / totalSupply. Use fixed-point math libraries like DSMath or PRBMath for precise calculations.',
192
+ references: [
193
+ 'https://github.com/dapphub/ds-math',
194
+ 'https://github.com/PaulRBerg/prb-math'
195
+ ]
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Detect decimal conversion issues between tokens with different decimals
204
+ */
205
+ detectDecimalConversionIssues(funcNode, funcCode) {
206
+ // Check for operations mixing decimals
207
+ const decimalPatterns = [
208
+ /decimals\s*\(\s*\)/gi,
209
+ /10\s*\*\*\s*(\w+)/gi,
210
+ /1e(\d+)/gi,
211
+ ];
212
+
213
+ const hasDecimalHandling = decimalPatterns.some(p => p.test(funcCode));
214
+
215
+ // If function deals with multiple tokens and doesn't handle decimals properly
216
+ if (funcCode.includes('token') && funcCode.includes('/')) {
217
+ const multiTokenPattern = /token[A-Z0-9]?\.balanceOf|IERC20\([^)]+\)\.balanceOf/gi;
218
+ const tokenMatches = funcCode.match(multiTokenPattern);
219
+
220
+ if (tokenMatches && tokenMatches.length > 1 && !hasDecimalHandling) {
221
+ this.addFinding({
222
+ title: 'Missing Decimal Normalization',
223
+ description: `Function operates on multiple tokens without decimal normalization. Tokens with different decimals (e.g., USDC=6, DAI=18) will produce incorrect calculations.`,
224
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
225
+ line: funcNode.loc?.start?.line || 0,
226
+ column: 0,
227
+ code: this.extractContext(funcCode, 0),
228
+ severity: 'HIGH',
229
+ confidence: 'MEDIUM',
230
+ exploitable: true,
231
+ exploitabilityScore: 70,
232
+ attackVector: 'precision-loss',
233
+ recommendation: 'Normalize all token amounts to a common decimal base (typically 18 decimals) before performing calculations. Use: normalizedAmount = amount * (10 ** (18 - tokenDecimals)).',
234
+ references: [
235
+ 'https://consensys.github.io/smart-contract-best-practices/development-recommendations/token-specific/token-normalization/'
236
+ ]
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Detect percentage calculation issues
244
+ */
245
+ detectPercentageIssues(funcNode, funcCode) {
246
+ // Common problematic percentage patterns
247
+ const percentPatterns = [
248
+ // amount * percentage / 100 where percentage might be small
249
+ { pattern: /(\w+)\s*\*\s*(\w+)\s*\/\s*100\b/gi, risk: 'low percentage precision' },
250
+ // Basis points without proper handling
251
+ { pattern: /(\w+)\s*\*\s*(\w+)\s*\/\s*10000\b/gi, risk: 'basis points precision' },
252
+ // Fee calculation that might round to zero
253
+ { pattern: /fee\s*=\s*(\w+)\s*\*\s*(\w+)\s*\/\s*(\w+)/gi, risk: 'fee rounding' },
254
+ ];
255
+
256
+ for (const { pattern, risk } of percentPatterns) {
257
+ pattern.lastIndex = 0;
258
+ let match;
259
+
260
+ while ((match = pattern.exec(funcCode)) !== null) {
261
+ // Check if there's a minimum check or rounding protection
262
+ const hasMinCheck = /require\s*\([^)]*>\s*0|Math\.max/i.test(funcCode);
263
+
264
+ if (!hasMinCheck) {
265
+ const linesBefore = funcCode.substring(0, match.index).split('\n').length;
266
+ const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
267
+
268
+ this.addFinding({
269
+ title: 'Percentage Calculation May Round to Zero',
270
+ description: `Percentage calculation (${risk}) may produce zero for small amounts. Without minimum value protection, users could receive zero fees/rewards or bypass fee payments.`,
271
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
272
+ line: line,
273
+ column: 0,
274
+ code: this.extractContext(funcCode, match.index),
275
+ severity: 'MEDIUM',
276
+ confidence: 'MEDIUM',
277
+ exploitable: true,
278
+ exploitabilityScore: 50,
279
+ attackVector: 'precision-loss',
280
+ recommendation: 'Add minimum value check: require(result > 0) or use Math.max(1, result). For fees, consider a minimum fee constant.',
281
+ references: [
282
+ 'https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/integer-division/'
283
+ ]
284
+ });
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Check if context involves financial calculations
292
+ */
293
+ isFinancialContext(code, position) {
294
+ const context = code.substring(Math.max(0, position - 100), position + 100).toLowerCase();
295
+ const financialKeywords = [
296
+ 'balance', 'amount', 'price', 'rate', 'fee', 'reward', 'stake',
297
+ 'yield', 'interest', 'collateral', 'debt', 'liquidity', 'share',
298
+ 'token', 'mint', 'burn', 'transfer', 'swap', 'exchange'
299
+ ];
300
+ return financialKeywords.some(kw => context.includes(kw));
301
+ }
302
+
303
+ /**
304
+ * Check if code uses SafeMath or similar patterns
305
+ */
306
+ usesSafeMathPattern(code) {
307
+ return /\.mul\(|\.div\(|mulDiv|FullMath|PRBMath|DSMath/i.test(code);
308
+ }
309
+
310
+ /**
311
+ * Check if division has proper scaling
312
+ */
313
+ hasProperScaling(code, position) {
314
+ const context = code.substring(Math.max(0, position - 50), Math.min(code.length, position + 100));
315
+ return /1e18|1e27|WAD|RAY|PRECISION|SCALE|10\s*\*\*\s*(18|27)/i.test(context);
316
+ }
317
+
318
+ /**
319
+ * Check if context is safe (e.g., already validated)
320
+ */
321
+ isSafeContext(code, position) {
322
+ const context = code.substring(Math.max(0, position - 100), position);
323
+ // Check for prior validation
324
+ return /require\s*\([^)]*>\s*0|if\s*\([^)]*>\s*0/i.test(context);
325
+ }
326
+
327
+ /**
328
+ * Extract relevant code context around a position
329
+ */
330
+ extractContext(code, position) {
331
+ const lines = code.split('\n');
332
+ const linesBefore = code.substring(0, position).split('\n').length - 1;
333
+
334
+ const start = Math.max(0, linesBefore - 1);
335
+ const end = Math.min(lines.length, linesBefore + 3);
336
+
337
+ return lines.slice(start, end).join('\n');
338
+ }
339
+
340
+ /**
341
+ * Generate Foundry PoC for division before multiplication
342
+ */
343
+ generateDivBeforeMulPoC(dividend, divisor, multiplier) {
344
+ return `// SPDX-License-Identifier: MIT
345
+ pragma solidity ^0.8.0;
346
+
347
+ import "forge-std/Test.sol";
348
+
349
+ /**
350
+ * Proof of Concept: Division Before Multiplication Precision Loss
351
+ * Demonstrates how (a/b)*c loses precision vs (a*c)/b
352
+ */
353
+ contract PrecisionLossExploit is Test {
354
+ function testDivisionBeforeMultiplication() public {
355
+ // Example values that demonstrate precision loss
356
+ uint256 ${dividend} = 1000;
357
+ uint256 ${divisor} = 3;
358
+ uint256 ${multiplier} = 7;
359
+
360
+ // Vulnerable calculation (division first - loses precision)
361
+ uint256 vulnerable = (${dividend} / ${divisor}) * ${multiplier};
362
+ // 1000 / 3 = 333 (truncated)
363
+ // 333 * 7 = 2331
364
+
365
+ // Correct calculation (multiplication first)
366
+ uint256 correct = (${dividend} * ${multiplier}) / ${divisor};
367
+ // 1000 * 7 = 7000
368
+ // 7000 / 3 = 2333
369
+
370
+ console.log("Vulnerable result:", vulnerable);
371
+ console.log("Correct result:", correct);
372
+ console.log("Precision lost:", correct - vulnerable);
373
+
374
+ // The difference of 2 units may seem small, but:
375
+ // - At scale (millions of tokens), this becomes significant
376
+ // - Repeated operations compound the error
377
+ // - Attackers can exploit small amounts to always round in their favor
378
+
379
+ assertGt(correct, vulnerable, "Precision loss demonstrated");
380
+ }
381
+
382
+ function testExploitSmallAmounts() public {
383
+ // Exploit: Process many small amounts that round to zero
384
+ uint256 amount = 99;
385
+ uint256 feePercent = 1;
386
+ uint256 base = 100;
387
+
388
+ // Vulnerable: fee rounds to zero
389
+ uint256 fee = (amount / base) * feePercent; // 0
390
+ assertEq(fee, 0, "Fee is zero - attacker pays no fees");
391
+
392
+ // Correct: fee is calculated properly
393
+ uint256 correctFee = (amount * feePercent) / base; // 0 still, but...
394
+
395
+ // With slightly larger amount
396
+ amount = 100;
397
+ fee = (amount / base) * feePercent; // 1
398
+ correctFee = (amount * feePercent) / base; // 1
399
+
400
+ // Attacker processes 100 transactions of 99 each: 0 total fees
401
+ // vs 1 transaction of 9900: 99 fees
402
+ console.log("Exploit: Split to avoid fees");
403
+ }
404
+ }`;
405
+ }
406
+ }
407
+
408
+ module.exports = PrecisionLossDetector;