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,421 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * ERC4626 Vault Inflation Attack Detector
5
+ * Detects first depositor attacks, donation attacks, and share manipulation
6
+ *
7
+ * Attack vectors detected:
8
+ * 1. First Depositor Attack - Attacker deposits 1 wei, donates tokens, inflating share price
9
+ * 2. Donation Attack - Direct token transfer to vault manipulates share calculations
10
+ * 3. Share Inflation - Division rounding exploits in share/asset calculations
11
+ * 4. Empty Vault Manipulation - Special case handling when totalSupply = 0
12
+ */
13
+ class VaultInflationDetector extends BaseDetector {
14
+ constructor() {
15
+ super(
16
+ 'Vault Inflation Attack',
17
+ 'Detects ERC4626 vault inflation, first depositor, and donation attacks',
18
+ 'CRITICAL'
19
+ );
20
+ this.currentContract = null;
21
+ this.currentFunction = null;
22
+ this.isVaultContract = false;
23
+ this.hasVirtualShares = false;
24
+ this.hasVirtualAssets = false;
25
+ this.hasMinDeposit = false;
26
+ this.depositFunction = null;
27
+ this.withdrawFunction = null;
28
+ this.shareCalculations = [];
29
+ }
30
+
31
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
32
+ this.findings = [];
33
+ this.ast = ast;
34
+ this.sourceCode = sourceCode;
35
+ this.fileName = fileName;
36
+ this.sourceLines = sourceCode.split('\n');
37
+ this.cfg = cfg;
38
+ this.dataFlow = dataFlow;
39
+ this.shareCalculations = [];
40
+
41
+ // First pass: determine if this is a vault contract
42
+ this.traverse(ast);
43
+
44
+ // Second pass: analyze vault-specific vulnerabilities
45
+ if (this.isVaultContract) {
46
+ this.analyzeVaultVulnerabilities();
47
+ }
48
+
49
+ return this.findings;
50
+ }
51
+
52
+ visitContractDefinition(node) {
53
+ this.currentContract = node.name;
54
+ this.isVaultContract = false;
55
+ this.hasVirtualShares = false;
56
+ this.hasVirtualAssets = false;
57
+ this.hasMinDeposit = false;
58
+
59
+ // Check if contract inherits from ERC4626 or implements vault pattern
60
+ const contractCode = this.getCodeSnippet(node.loc);
61
+
62
+ // ERC4626 inheritance patterns
63
+ const vaultPatterns = [
64
+ /ERC4626/i,
65
+ /Vault/i,
66
+ /is\s+.*Vault/i,
67
+ /totalAssets/i,
68
+ /convertToShares/i,
69
+ /convertToAssets/i,
70
+ ];
71
+
72
+ this.isVaultContract = vaultPatterns.some(p => p.test(contractCode));
73
+
74
+ // Check for protection mechanisms
75
+ this.hasVirtualShares = /virtualShares|_decimalsOffset|VIRTUAL_SHARES/i.test(contractCode);
76
+ this.hasVirtualAssets = /virtualAssets|VIRTUAL_ASSETS/i.test(contractCode);
77
+ this.hasMinDeposit = /minDeposit|MIN_DEPOSIT|minimumDeposit/i.test(contractCode);
78
+ }
79
+
80
+ visitFunctionDefinition(node) {
81
+ this.currentFunction = node.name || 'constructor';
82
+
83
+ if (!this.isVaultContract) return;
84
+
85
+ const funcName = (node.name || '').toLowerCase();
86
+ const funcCode = node.body ? this.getCodeSnippet(node.loc) : '';
87
+
88
+ // Track deposit/withdraw functions
89
+ if (funcName.includes('deposit') || funcName === 'mint') {
90
+ this.depositFunction = { node, code: funcCode };
91
+ }
92
+ if (funcName.includes('withdraw') || funcName === 'redeem') {
93
+ this.withdrawFunction = { node, code: funcCode };
94
+ }
95
+
96
+ // Track share calculation functions
97
+ if (funcName.includes('converttoshares') || funcName.includes('converttoassets') ||
98
+ funcName.includes('previewdeposit') || funcName.includes('previewmint') ||
99
+ funcName.includes('previewwithdraw') || funcName.includes('previewredeem')) {
100
+ this.analyzeShareCalculation(node, funcCode);
101
+ }
102
+ }
103
+
104
+ visitBinaryOperation(node) {
105
+ if (!this.isVaultContract) return;
106
+
107
+ // Track division operations in share calculations
108
+ if (node.operator === '/') {
109
+ const code = this.getCodeSnippet(node.loc);
110
+ const leftCode = this.getCodeSnippet(node.left?.loc);
111
+ const rightCode = this.getCodeSnippet(node.right?.loc);
112
+
113
+ // Check for share/asset division patterns
114
+ if (this.isShareRelatedDivision(code, leftCode, rightCode)) {
115
+ this.shareCalculations.push({
116
+ loc: node.loc,
117
+ code: code,
118
+ type: 'division',
119
+ left: leftCode,
120
+ right: rightCode
121
+ });
122
+ }
123
+ }
124
+ }
125
+
126
+ analyzeShareCalculation(node, funcCode) {
127
+ // Check for vulnerable patterns in share calculations
128
+
129
+ // Pattern 1: Division without rounding protection
130
+ if (funcCode.includes('/') && !funcCode.includes('mulDiv')) {
131
+ this.shareCalculations.push({
132
+ function: node.name,
133
+ code: funcCode,
134
+ type: 'manual_division',
135
+ vulnerable: !this.hasRoundingProtection(funcCode)
136
+ });
137
+ }
138
+
139
+ // Pattern 2: No check for zero totalSupply
140
+ if (!funcCode.includes('totalSupply() == 0') &&
141
+ !funcCode.includes('totalSupply() > 0') &&
142
+ !funcCode.includes('supply == 0') &&
143
+ !funcCode.includes('supply > 0')) {
144
+ this.shareCalculations.push({
145
+ function: node.name,
146
+ code: funcCode,
147
+ type: 'no_zero_check',
148
+ vulnerable: true
149
+ });
150
+ }
151
+ }
152
+
153
+ analyzeVaultVulnerabilities() {
154
+ // Check 1: First Depositor Attack vulnerability
155
+ if (!this.hasVirtualShares && !this.hasVirtualAssets && !this.hasMinDeposit) {
156
+ this.reportFirstDepositorVulnerability();
157
+ }
158
+
159
+ // Check 2: Donation Attack vulnerability
160
+ if (!this.hasDonationProtection()) {
161
+ this.reportDonationAttackVulnerability();
162
+ }
163
+
164
+ // Check 3: Share calculation precision issues
165
+ this.shareCalculations.forEach(calc => {
166
+ if (calc.vulnerable) {
167
+ if (calc.type === 'no_zero_check') {
168
+ this.reportZeroSupplyVulnerability(calc);
169
+ } else if (calc.type === 'manual_division') {
170
+ this.reportPrecisionLossVulnerability(calc);
171
+ }
172
+ }
173
+ });
174
+
175
+ // Check 4: Empty vault edge cases
176
+ if (!this.hasEmptyVaultProtection()) {
177
+ this.reportEmptyVaultVulnerability();
178
+ }
179
+ }
180
+
181
+ hasRoundingProtection(code) {
182
+ // Check for mulDiv or similar rounding-safe operations
183
+ const roundingPatterns = [
184
+ /mulDiv/i,
185
+ /FullMath/i,
186
+ /roundUp|roundDown/i,
187
+ /Math\.ceil|Math\.floor/i,
188
+ /\+ 1\s*\)|1 \+/, // Adding 1 for rounding
189
+ ];
190
+
191
+ return roundingPatterns.some(p => p.test(code));
192
+ }
193
+
194
+ hasDonationProtection() {
195
+ // Check for mechanisms that prevent donation attacks
196
+ const protectionPatterns = [
197
+ /virtualAssets/i,
198
+ /virtualShares/i,
199
+ /\_decimalsOffset/i,
200
+ /balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)\s*-/, // Tracking internal balance
201
+ /internalBalance/i,
202
+ /lastBalance/i,
203
+ /storedBalance/i,
204
+ ];
205
+
206
+ return protectionPatterns.some(p => p.test(this.sourceCode));
207
+ }
208
+
209
+ hasEmptyVaultProtection() {
210
+ // Check for empty vault handling
211
+ if (!this.depositFunction) return true;
212
+
213
+ const depositCode = this.depositFunction.code;
214
+
215
+ const protectionPatterns = [
216
+ /require\s*\(\s*totalSupply\(\)\s*[>!]/i,
217
+ /if\s*\(\s*totalSupply\(\)\s*==\s*0/i,
218
+ /supply\s*==\s*0\s*\?/i, // Ternary check
219
+ /firstDeposit/i,
220
+ /initialDeposit/i,
221
+ /deadShares/i, // Burn initial shares pattern
222
+ ];
223
+
224
+ return protectionPatterns.some(p => p.test(depositCode));
225
+ }
226
+
227
+ isShareRelatedDivision(code, left, right) {
228
+ const shareTerms = ['share', 'asset', 'supply', 'balance', 'totalassets', 'totalsupply'];
229
+ const combined = `${code} ${left} ${right}`.toLowerCase();
230
+ return shareTerms.some(term => combined.includes(term));
231
+ }
232
+
233
+ reportFirstDepositorVulnerability() {
234
+ this.addFinding({
235
+ title: 'First Depositor / Vault Inflation Attack',
236
+ description: `Contract '${this.currentContract}' implements ERC4626 vault pattern without protection against first depositor attack. An attacker can:
237
+ 1. Deposit 1 wei as the first depositor to get 1 share
238
+ 2. Donate (directly transfer) large amount of tokens to vault
239
+ 3. This inflates share price so next depositor's deposit rounds down to 0 shares
240
+ 4. Attacker withdraws, taking victim's deposit
241
+
242
+ This attack has caused >$100M in losses across DeFi protocols.`,
243
+ location: `Contract: ${this.currentContract}`,
244
+ line: 1,
245
+ column: 0,
246
+ code: this.sourceLines.slice(0, 10).join('\n'),
247
+ severity: 'CRITICAL',
248
+ confidence: 'HIGH',
249
+ exploitable: true,
250
+ exploitabilityScore: 95,
251
+ attackVector: 'first-depositor-attack',
252
+ recommendation: `Implement one or more of these mitigations:
253
+ 1. Virtual shares/assets offset (OpenZeppelin ERC4626 pattern): Add virtual offset to calculations
254
+ 2. Minimum deposit amount: Require minimum first deposit (e.g., 1000 tokens)
255
+ 3. Dead shares: Burn small amount of shares on first deposit to address(1)
256
+ 4. Internal balance tracking: Track deposited amount separately from balance
257
+
258
+ Example (OpenZeppelin pattern):
259
+ function _decimalsOffset() internal pure override returns (uint8) {
260
+ return 3; // Adds virtual 1000 shares/assets offset
261
+ }`,
262
+ references: [
263
+ 'https://blog.openzeppelin.com/a-]novel-defense-against-erc4626-inflation-attacks',
264
+ 'https://docs.openzeppelin.com/contracts/4.x/erc4626',
265
+ 'https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706'
266
+ ],
267
+ foundryPoC: this.generateFirstDepositorPoC()
268
+ });
269
+ }
270
+
271
+ reportDonationAttackVulnerability() {
272
+ this.addFinding({
273
+ title: 'Donation Attack Vulnerability',
274
+ description: `Contract '${this.currentContract}' uses balanceOf(address(this)) or similar for share calculations without tracking internal deposits. An attacker can directly transfer tokens to manipulate share prices.
275
+
276
+ Attack scenario:
277
+ 1. Attacker monitors mempool for large deposits
278
+ 2. Front-runs with direct token transfer (donation) to vault
279
+ 3. Victim's deposit receives fewer shares due to inflated totalAssets
280
+ 4. Attacker back-runs by withdrawing, extracting donated value`,
281
+ location: `Contract: ${this.currentContract}`,
282
+ line: 1,
283
+ column: 0,
284
+ code: this.sourceLines.slice(0, 10).join('\n'),
285
+ severity: 'HIGH',
286
+ confidence: 'MEDIUM',
287
+ exploitable: true,
288
+ exploitabilityScore: 75,
289
+ attackVector: 'donation-attack',
290
+ recommendation: `Track internal balance separately from actual balance:
291
+ 1. Use internal accounting (internalBalance) for share calculations
292
+ 2. Or use virtual offset that dominates small donations
293
+ 3. Or add sweep function to handle unexpected balance increases
294
+
295
+ Example:
296
+ uint256 internal _totalDeposited;
297
+ function totalAssets() public view returns (uint256) {
298
+ return _totalDeposited; // Not balanceOf(address(this))
299
+ }`,
300
+ references: [
301
+ 'https://mixbytes.io/blog/overview-of-the-inflation-attack'
302
+ ]
303
+ });
304
+ }
305
+
306
+ reportZeroSupplyVulnerability(calc) {
307
+ this.addFinding({
308
+ title: 'Missing Zero Supply Check in Share Calculation',
309
+ description: `Function '${calc.function}' performs share calculations without checking for totalSupply == 0. This can lead to division by zero or unexpected behavior for first depositor.`,
310
+ location: `Contract: ${this.currentContract}, Function: ${calc.function}`,
311
+ line: calc.loc?.start?.line || 0,
312
+ column: calc.loc?.start?.column || 0,
313
+ code: calc.code?.substring(0, 200),
314
+ severity: 'HIGH',
315
+ confidence: 'HIGH',
316
+ exploitable: true,
317
+ exploitabilityScore: 70,
318
+ attackVector: 'share-calculation-edge-case',
319
+ recommendation: `Add explicit check for zero supply:
320
+ if (totalSupply() == 0) {
321
+ return assets; // 1:1 ratio for first deposit
322
+ }
323
+ return assets.mulDiv(totalSupply(), totalAssets(), rounding);`
324
+ });
325
+ }
326
+
327
+ reportPrecisionLossVulnerability(calc) {
328
+ this.addFinding({
329
+ title: 'Precision Loss in Share Calculation',
330
+ description: `Function '${calc.function}' uses manual division for share calculations without rounding protection. Integer division truncates, allowing attackers to exploit rounding in their favor.`,
331
+ location: `Contract: ${this.currentContract}, Function: ${calc.function}`,
332
+ line: calc.loc?.start?.line || 0,
333
+ column: calc.loc?.start?.column || 0,
334
+ code: calc.code?.substring(0, 200),
335
+ severity: 'MEDIUM',
336
+ confidence: 'MEDIUM',
337
+ exploitable: true,
338
+ exploitabilityScore: 50,
339
+ attackVector: 'rounding-exploit',
340
+ recommendation: `Use mulDiv with explicit rounding direction:
341
+ // For deposits (round down - favor vault)
342
+ shares = assets.mulDiv(totalSupply(), totalAssets(), Math.Rounding.Down);
343
+ // For withdrawals (round down - favor vault)
344
+ assets = shares.mulDiv(totalAssets(), totalSupply(), Math.Rounding.Down);`
345
+ });
346
+ }
347
+
348
+ reportEmptyVaultVulnerability() {
349
+ this.addFinding({
350
+ title: 'Empty Vault Edge Case Not Handled',
351
+ description: `Deposit function does not properly handle the empty vault case (totalSupply == 0). First depositor can manipulate initial share price.`,
352
+ location: `Contract: ${this.currentContract}`,
353
+ line: this.depositFunction?.node?.loc?.start?.line || 0,
354
+ column: 0,
355
+ code: this.depositFunction?.code?.substring(0, 200) || '',
356
+ severity: 'HIGH',
357
+ confidence: 'MEDIUM',
358
+ exploitable: true,
359
+ exploitabilityScore: 70,
360
+ attackVector: 'empty-vault-manipulation',
361
+ recommendation: `Handle first deposit specially:
362
+ 1. Burn dead shares: mint shares to address(1) on first deposit
363
+ 2. Enforce minimum deposit for first depositor
364
+ 3. Use virtual offset (OpenZeppelin pattern)`
365
+ });
366
+ }
367
+
368
+ generateFirstDepositorPoC() {
369
+ return `// SPDX-License-Identifier: MIT
370
+ pragma solidity ^0.8.0;
371
+
372
+ import "forge-std/Test.sol";
373
+ import "forge-std/console.sol";
374
+
375
+ /**
376
+ * Proof of Concept: First Depositor / Vault Inflation Attack
377
+ * This demonstrates stealing funds from the second depositor
378
+ */
379
+ contract FirstDepositorExploit is Test {
380
+ // Target vault and underlying token
381
+ // IERC4626 vault;
382
+ // IERC20 token;
383
+
384
+ address attacker = address(0x1);
385
+ address victim = address(0x2);
386
+
387
+ function testFirstDepositorAttack() public {
388
+ uint256 victimDeposit = 10000e18; // Victim wants to deposit 10,000 tokens
389
+
390
+ // Step 1: Attacker front-runs, deposits minimum amount
391
+ vm.startPrank(attacker);
392
+ // vault.deposit(1, attacker); // Deposit 1 wei, get 1 share
393
+ vm.stopPrank();
394
+
395
+ // Step 2: Attacker donates tokens directly (not through deposit)
396
+ vm.prank(attacker);
397
+ // token.transfer(address(vault), victimDeposit - 1);
398
+
399
+ // Now: totalAssets = victimDeposit, totalShares = 1
400
+ // Share price = victimDeposit / 1
401
+
402
+ // Step 3: Victim deposits
403
+ vm.prank(victim);
404
+ // uint256 victimShares = vault.deposit(victimDeposit, victim);
405
+
406
+ // Victim receives: victimDeposit * 1 / victimDeposit = 1 share (rounds down to 0 or 1)
407
+
408
+ // Step 4: Attacker withdraws
409
+ vm.prank(attacker);
410
+ // uint256 attackerReceived = vault.redeem(1, attacker, attacker);
411
+
412
+ // Attacker gets ~50% of victim's deposit
413
+ // console.log("Victim deposited:", victimDeposit);
414
+ // console.log("Victim shares:", victimShares);
415
+ // console.log("Attacker profit:", attackerReceived - 1);
416
+ }
417
+ }`;
418
+ }
419
+ }
420
+
421
+ module.exports = VaultInflationDetector;
package/src/index.js ADDED
@@ -0,0 +1,42 @@
1
+ const Web3CRITScanner = require('./scanner');
2
+
3
+ // Export main scanner class
4
+ module.exports = Web3CRITScanner;
5
+
6
+ // Export individual detectors for advanced usage
7
+ module.exports.detectors = {
8
+ ReentrancyDetector: require('./detectors/reentrancy'),
9
+ IntegerOverflowDetector: require('./detectors/integer-overflow'),
10
+ AccessControlDetector: require('./detectors/access-control'),
11
+ UncheckedCallDetector: require('./detectors/unchecked-call'),
12
+ DelegateCallDetector: require('./detectors/delegatecall'),
13
+ FrontRunningDetector: require('./detectors/frontrunning'),
14
+ TimestampDependenceDetector: require('./detectors/timestamp'),
15
+ LogicBugDetector: require('./detectors/logic-bugs'),
16
+ UnprotectedSelfdestructDetector: require('./detectors/selfdestruct'),
17
+ PriceFeedManipulationDetector: require('./detectors/price-feed'),
18
+ BaseDetector: require('./detectors/base-detector')
19
+ };
20
+
21
+ // Convenience function for quick scanning
22
+ module.exports.scan = async function(pathOrSource, options = {}) {
23
+ const scanner = new Web3CRITScanner(options);
24
+
25
+ try {
26
+ const stats = require('fs').statSync(pathOrSource);
27
+
28
+ if (stats.isDirectory()) {
29
+ await scanner.scanDirectory(pathOrSource);
30
+ } else if (stats.isFile()) {
31
+ await scanner.scanFile(pathOrSource);
32
+ }
33
+ } catch (err) {
34
+ // Assume it's source code if not a valid path
35
+ await scanner.scanSource(pathOrSource);
36
+ }
37
+
38
+ return scanner.getFindings();
39
+ };
40
+
41
+ // Export version
42
+ module.exports.version = require('./package.json').version;