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,412 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Share/Vault Manipulation Detector
5
+ * Detects vulnerabilities in vault/share-based systems:
6
+ * - First depositor / vault inflation attacks
7
+ * - Share price manipulation
8
+ * - Donation attacks
9
+ * - Rounding exploitation
10
+ *
11
+ * Immunefi Critical: Direct theft of depositor funds
12
+ */
13
+ class ShareManipulationDetector extends BaseDetector {
14
+ constructor() {
15
+ super(
16
+ 'Share Manipulation',
17
+ 'Detects vault share manipulation vulnerabilities for fund theft',
18
+ 'CRITICAL'
19
+ );
20
+ this.currentContract = null;
21
+ this.currentFunction = null;
22
+ this.vaultPatterns = [];
23
+ this.shareCalculations = [];
24
+ }
25
+
26
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
27
+ this.findings = [];
28
+ this.ast = ast;
29
+ this.sourceCode = sourceCode;
30
+ this.fileName = fileName;
31
+ this.sourceLines = sourceCode.split('\n');
32
+ this.cfg = cfg;
33
+ this.dataFlow = dataFlow;
34
+ this.vaultPatterns = [];
35
+ this.shareCalculations = [];
36
+
37
+ this.traverse(ast);
38
+ this.analyzeVaultPatterns();
39
+
40
+ return this.findings;
41
+ }
42
+
43
+ visitContractDefinition(node) {
44
+ this.currentContract = node.name;
45
+
46
+ // Identify vault-like contracts
47
+ const baseContracts = (node.baseContracts || []).map(b =>
48
+ b.baseName?.namePath || ''
49
+ ).join(' ');
50
+
51
+ const isVault = /Vault|ERC4626|Strategy|Pool|Staking|Yield/i.test(this.currentContract) ||
52
+ /Vault|ERC4626|Strategy|Pool|Staking|Yield/i.test(baseContracts);
53
+
54
+ if (isVault) {
55
+ this.vaultPatterns.push({
56
+ contract: this.currentContract,
57
+ node: node
58
+ });
59
+ }
60
+ }
61
+
62
+ visitFunctionDefinition(node) {
63
+ this.currentFunction = node.name || 'constructor';
64
+
65
+ if (!node.body) return;
66
+
67
+ const funcCode = this.getCodeSnippet(node.loc);
68
+ const funcName = (node.name || '').toLowerCase();
69
+
70
+ // Skip internal functions
71
+ if (node.visibility === 'private' || node.visibility === 'internal') {
72
+ return;
73
+ }
74
+
75
+ // Detect deposit/mint functions
76
+ if (/deposit|mint|stake|supply/i.test(funcName)) {
77
+ this.analyzeDepositFunction(funcCode, node);
78
+ }
79
+
80
+ // Detect withdraw/redeem functions
81
+ if (/withdraw|redeem|unstake|remove/i.test(funcName)) {
82
+ this.analyzeWithdrawFunction(funcCode, node);
83
+ }
84
+
85
+ // Detect share calculation functions
86
+ if (/convertToShares|convertToAssets|previewDeposit|previewMint|pricePerShare/i.test(funcName)) {
87
+ this.analyzeShareCalculation(funcCode, node);
88
+ }
89
+
90
+ // Detect general share/asset ratio calculations
91
+ this.detectSharePriceManipulation(funcCode, node);
92
+ }
93
+
94
+ /**
95
+ * Analyze deposit function for first depositor attacks
96
+ */
97
+ analyzeDepositFunction(funcCode, node) {
98
+ // Check for share minting logic
99
+ const hasShareMinting = /shares\s*=|_mint\s*\(|mint\s*\(/.test(funcCode);
100
+
101
+ if (!hasShareMinting) return;
102
+
103
+ // Check for first depositor protection
104
+ const hasFirstDepositorProtection =
105
+ /totalSupply\s*\(\s*\)\s*==\s*0.*?[+]|MINIMUM_LIQUIDITY|dead.*shares|_mint.*0x.*dead|virtualAssets|virtualShares/i.test(funcCode);
106
+
107
+ const hasMinDeposit = /require.*amount\s*>=|MIN_DEPOSIT|minimumDeposit/i.test(funcCode);
108
+
109
+ if (!hasFirstDepositorProtection && !hasMinDeposit) {
110
+ // Check if it's a division-based share calculation
111
+ const hasDivisionCalc = /\/\s*totalSupply|\/\s*totalAssets|\*\s*totalSupply.*\/|shares\s*=.*\//.test(funcCode);
112
+
113
+ if (hasDivisionCalc) {
114
+ this.addFinding({
115
+ title: 'First Depositor Vault Inflation Attack',
116
+ description: `Function '${this.currentFunction}' in vault '${this.currentContract}' is vulnerable to first depositor attack:
117
+
118
+ Attack scenario:
119
+ 1. Attacker is first depositor, deposits minimal amount (1 wei)
120
+ 2. Attacker receives 1 share (1:1 for first deposit)
121
+ 3. Attacker donates large amount directly to vault (transfers tokens)
122
+ 4. Share price inflates: 1 share = 1 wei + donation
123
+ 5. Victim deposits X tokens
124
+ 6. Due to rounding: victim receives 0 shares (X < share price)
125
+ 7. Attacker redeems 1 share, receives victim's deposit + original donation
126
+
127
+ This results in complete theft of victim deposits.`,
128
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
129
+ line: node.loc?.start?.line || 0,
130
+ column: node.loc?.start?.column || 0,
131
+ code: funcCode.substring(0, 400),
132
+ severity: 'CRITICAL',
133
+ confidence: 'HIGH',
134
+ exploitable: true,
135
+ exploitabilityScore: 95,
136
+ attackVector: 'first-depositor-inflation',
137
+ recommendation: `Implement first depositor protection:
138
+
139
+ 1. Virtual shares/assets (OpenZeppelin ERC4626 pattern):
140
+ function _decimalsOffset() internal pure returns (uint8) { return 3; }
141
+
142
+ 2. Minimum initial deposit:
143
+ require(totalSupply() > 0 || amount >= MIN_DEPOSIT);
144
+
145
+ 3. Dead shares on first deposit:
146
+ if (totalSupply() == 0) {
147
+ _mint(address(0xdead), MINIMUM_SHARES);
148
+ }
149
+
150
+ 4. Use internal accounting instead of balanceOf()`,
151
+ references: [
152
+ 'https://blog.openzeppelin.com/a-]]]novel-defense-against-erc4626-inflation-attacks',
153
+ 'https://docs.openzeppelin.com/contracts/4.x/erc4626'
154
+ ],
155
+ foundryPoC: this.generateFirstDepositorPoC()
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Analyze withdraw function for manipulation
163
+ */
164
+ analyzeWithdrawFunction(funcCode, node) {
165
+ // Check for rounding direction
166
+ const hasRoundDown = /mulDiv.*DOWN|roundDown|\/ totalSupply/i.test(funcCode);
167
+ const hasRoundUp = /mulDiv.*UP|roundUp|ceil/i.test(funcCode);
168
+
169
+ // Withdrawals should round down (in favor of vault)
170
+ if (hasRoundUp && !hasRoundDown) {
171
+ this.addFinding({
172
+ title: 'Withdrawal Rounds Up (Favors User)',
173
+ description: `Function '${this.currentFunction}' appears to round up on withdrawal calculations. This allows users to extract more value than entitled through repeated small withdrawals.`,
174
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
175
+ line: node.loc?.start?.line || 0,
176
+ code: funcCode.substring(0, 200),
177
+ severity: 'HIGH',
178
+ confidence: 'MEDIUM',
179
+ exploitable: true,
180
+ exploitabilityScore: 70,
181
+ attackVector: 'rounding-exploit',
182
+ recommendation: 'Round down for withdrawals (in favor of vault): assets = shares.mulDiv(totalAssets, totalSupply, Math.Rounding.Down)'
183
+ });
184
+ }
185
+
186
+ // Check for flash loan withdrawal
187
+ if (/balanceOf\s*\(address\s*\(this\)\)|\.balance/.test(funcCode)) {
188
+ if (!/internalBalance|_totalAssets|checkpoint/i.test(funcCode)) {
189
+ this.addFinding({
190
+ title: 'Withdrawal Uses Manipulable Balance',
191
+ description: `Function '${this.currentFunction}' calculates withdrawal amounts using real-time balance (balanceOf/balance) which can be manipulated via flash loans or donations.`,
192
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
193
+ line: node.loc?.start?.line || 0,
194
+ code: funcCode.substring(0, 200),
195
+ severity: 'HIGH',
196
+ confidence: 'HIGH',
197
+ exploitable: true,
198
+ exploitabilityScore: 80,
199
+ attackVector: 'balance-manipulation',
200
+ recommendation: 'Use internal accounting that tracks deposits/withdrawals rather than raw balanceOf(). Implement donation attack protection.'
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Analyze share calculation for manipulation vectors
208
+ */
209
+ analyzeShareCalculation(funcCode, node) {
210
+ // Division by totalSupply without protection
211
+ if (/\/\s*totalSupply\s*\(\s*\)/.test(funcCode)) {
212
+ if (!/totalSupply\s*\(\s*\)\s*==\s*0|totalSupply\s*>\s*0|virtualShares/i.test(funcCode)) {
213
+ this.addFinding({
214
+ title: 'Division by Zero Risk in Share Calculation',
215
+ description: `Function '${this.currentFunction}' divides by totalSupply without checking for zero. When totalSupply is 0, this reverts, potentially causing DoS or undefined behavior.`,
216
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
217
+ line: node.loc?.start?.line || 0,
218
+ code: funcCode.substring(0, 200),
219
+ severity: 'HIGH',
220
+ confidence: 'HIGH',
221
+ exploitable: true,
222
+ exploitabilityScore: 65,
223
+ attackVector: 'division-by-zero',
224
+ recommendation: 'Handle zero totalSupply case: return totalSupply == 0 ? assets : assets.mulDiv(totalSupply, totalAssets)'
225
+ });
226
+ }
227
+ }
228
+
229
+ // totalAssets from external call (manipulable)
230
+ if (/totalAssets\s*\(\s*\)/.test(funcCode)) {
231
+ // Check if totalAssets uses balanceOf
232
+ const totalAssetsPattern = /function\s+totalAssets[^{]*\{[^}]*balanceOf/;
233
+ if (totalAssetsPattern.test(this.sourceCode)) {
234
+ this.addFinding({
235
+ title: 'totalAssets() Uses Manipulable balanceOf()',
236
+ description: `Share calculations use totalAssets() which appears to use balanceOf(). Attacker can manipulate share price by donating tokens directly to the vault before/after key operations.`,
237
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
238
+ line: node.loc?.start?.line || 0,
239
+ code: funcCode.substring(0, 200),
240
+ severity: 'HIGH',
241
+ confidence: 'MEDIUM',
242
+ exploitable: true,
243
+ exploitabilityScore: 75,
244
+ attackVector: 'donation-attack',
245
+ recommendation: `Use internal accounting for totalAssets:
246
+ - Track deposits/withdrawals in state variable
247
+ - Add virtual assets offset: totalAssets() + 1
248
+ - Consider using balanceOf only as upper bound check`
249
+ });
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Detect share price manipulation patterns
256
+ */
257
+ detectSharePriceManipulation(funcCode, node) {
258
+ // Direct balance usage in share calculation
259
+ const shareCalculationWithBalance =
260
+ /shares.*=.*balanceOf|shares.*=.*\.balance|pricePerShare.*balanceOf/i.test(funcCode);
261
+
262
+ if (shareCalculationWithBalance) {
263
+ this.shareCalculations.push({
264
+ function: this.currentFunction,
265
+ node: node,
266
+ code: funcCode,
267
+ usesDirectBalance: true
268
+ });
269
+ }
270
+
271
+ // Exchange rate calculation
272
+ if (/exchangeRate|pricePerShare|sharePrice|getExchangeRate/i.test(this.currentFunction || '')) {
273
+ if (/balanceOf|\.balance/.test(funcCode)) {
274
+ this.addFinding({
275
+ title: 'Exchange Rate Manipulable via Donation',
276
+ description: `Function '${this.currentFunction}' calculates exchange rate using real-time balance. Attacker can sandwich victim transactions:
277
+
278
+ 1. Frontrun: Donate tokens to inflate exchange rate
279
+ 2. Victim deposits at inflated rate, receives fewer shares
280
+ 3. Backrun: Withdraw to capture victim's lost value`,
281
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
282
+ line: node.loc?.start?.line || 0,
283
+ code: funcCode.substring(0, 200),
284
+ severity: 'HIGH',
285
+ confidence: 'HIGH',
286
+ exploitable: true,
287
+ exploitabilityScore: 80,
288
+ attackVector: 'sandwich-donation',
289
+ recommendation: 'Use internal accounting. Add donation attack protection by separating "balance" from "accounting".'
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Cross-analyze vault patterns
297
+ */
298
+ analyzeVaultPatterns() {
299
+ // Check ERC4626 compliance issues
300
+ if (this.vaultPatterns.length > 0 && /ERC4626|Vault/i.test(this.sourceCode)) {
301
+ // Check for decimal offset (OZ protection)
302
+ const hasDecimalOffset = /_decimalsOffset|virtualAssets|virtualShares/i.test(this.sourceCode);
303
+
304
+ if (!hasDecimalOffset) {
305
+ // Already covered by first depositor finding, but add context
306
+ }
307
+
308
+ // Check for preview function accuracy
309
+ const hasPreview = /previewDeposit|previewMint|previewWithdraw|previewRedeem/i.test(this.sourceCode);
310
+ if (hasPreview) {
311
+ // Check if previews account for fees
312
+ const previewAccountsFees = /fee|slippage|preview.*fee/i.test(this.sourceCode);
313
+ if (!previewAccountsFees && /fee|Fee/i.test(this.sourceCode)) {
314
+ this.addFinding({
315
+ title: 'Preview Functions May Not Account for Fees',
316
+ description: `Vault implements fees but preview functions may not accurately reflect them. This can cause user transactions to fail or receive unexpected amounts.`,
317
+ location: `Contract: ${this.vaultPatterns[0].contract}`,
318
+ line: this.vaultPatterns[0].node.loc?.start?.line || 0,
319
+ severity: 'MEDIUM',
320
+ confidence: 'LOW',
321
+ exploitable: false,
322
+ exploitabilityScore: 40,
323
+ attackVector: 'preview-mismatch',
324
+ recommendation: 'Ensure preview functions return accurate values inclusive of fees per ERC4626 spec.'
325
+ });
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ generateFirstDepositorPoC() {
332
+ return `// SPDX-License-Identifier: MIT
333
+ pragma solidity ^0.8.0;
334
+
335
+ import "forge-std/Test.sol";
336
+ import "forge-std/console.sol";
337
+
338
+ interface IERC20 {
339
+ function transfer(address to, uint256 amount) external returns (bool);
340
+ function balanceOf(address account) external view returns (uint256);
341
+ }
342
+
343
+ interface IVault {
344
+ function deposit(uint256 assets, address receiver) external returns (uint256 shares);
345
+ function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
346
+ function totalSupply() external view returns (uint256);
347
+ function balanceOf(address account) external view returns (uint256);
348
+ }
349
+
350
+ contract FirstDepositorAttack is Test {
351
+ IVault vault;
352
+ IERC20 asset;
353
+
354
+ address attacker = address(0xBAD);
355
+ address victim = address(0xBEEF);
356
+
357
+ function setUp() public {
358
+ // Deploy vault and asset token
359
+ // vault = IVault(address(new VulnerableVault(address(asset))));
360
+ // Fund accounts
361
+ // deal(address(asset), attacker, 10000e18);
362
+ // deal(address(asset), victim, 1000e18);
363
+ }
364
+
365
+ function testFirstDepositorAttack() public {
366
+ uint256 victimDeposit = 1000e18;
367
+ uint256 donationAmount = 10000e18;
368
+
369
+ // Step 1: Attacker deposits 1 wei first
370
+ vm.startPrank(attacker);
371
+ // asset.approve(address(vault), type(uint256).max);
372
+ // vault.deposit(1, attacker);
373
+ console.log("Attacker shares after first deposit:", vault.balanceOf(attacker));
374
+ // Attacker has 1 share
375
+
376
+ // Step 2: Attacker donates large amount directly to vault
377
+ // asset.transfer(address(vault), donationAmount);
378
+ console.log("Vault balance after donation:", asset.balanceOf(address(vault)));
379
+ // Vault now has 1 + donationAmount tokens, still 1 share
380
+
381
+ vm.stopPrank();
382
+
383
+ // Step 3: Victim deposits
384
+ vm.startPrank(victim);
385
+ // asset.approve(address(vault), type(uint256).max);
386
+
387
+ // uint256 victimSharesBefore = vault.totalSupply();
388
+ // vault.deposit(victimDeposit, victim);
389
+ // uint256 victimShares = vault.balanceOf(victim);
390
+
391
+ console.log("Victim shares received:", vault.balanceOf(victim));
392
+ // Victim receives 0 shares due to rounding!
393
+ // victimDeposit / (1 + donationAmount) = 0 (rounds down)
394
+
395
+ vm.stopPrank();
396
+
397
+ // Step 4: Attacker redeems their 1 share
398
+ vm.startPrank(attacker);
399
+ // uint256 attackerAssets = vault.redeem(1, attacker, attacker);
400
+ // console.log("Attacker receives:", attackerAssets);
401
+ // Attacker gets: original donation + victim's deposit!
402
+ vm.stopPrank();
403
+
404
+ // Verify attack success
405
+ // assertEq(vault.balanceOf(victim), 0, "Victim should have 0 shares");
406
+ // assertGt(attackerAssets, donationAmount, "Attacker should profit");
407
+ }
408
+ }`;
409
+ }
410
+ }
411
+
412
+ module.exports = ShareManipulationDetector;