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,416 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Rebasing Token Vault Detector
5
+ *
6
+ * Detects critical vulnerabilities when vaults/pools integrate with rebasing tokens:
7
+ * - Aave aTokens (balance increases from interest)
8
+ * - Lido stETH (balance changes from staking rewards/slashing)
9
+ * - Ampleforth AMPL (global rebases)
10
+ * - Compound cTokens (exchange rate appreciation)
11
+ *
12
+ * Attack vectors:
13
+ * 1. Share dilution: Rebasing rewards go to vault, not depositors
14
+ * 2. First depositor + rebase: Attacker deposits 1 wei, waits for rebase, steals from next depositor
15
+ * 3. Accounting mismatch: Vault tracks nominal amounts, actual balance differs
16
+ * 4. Negative rebase: Slashing causes insufficient funds for withdrawals
17
+ *
18
+ * Immunefi Critical: Direct theft via accounting manipulation
19
+ */
20
+ class RebasingTokenVaultDetector extends BaseDetector {
21
+ constructor() {
22
+ super(
23
+ 'Rebasing Token Vault',
24
+ 'Detects vulnerabilities in vaults integrating rebasing tokens (aTokens, stETH, AMPL)',
25
+ 'CRITICAL'
26
+ );
27
+ this.currentContract = null;
28
+ this.currentFunction = null;
29
+ this.rebasingPatterns = [];
30
+ this.vaultPatterns = [];
31
+ }
32
+
33
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
34
+ this.findings = [];
35
+ this.ast = ast;
36
+ this.sourceCode = sourceCode;
37
+ this.fileName = fileName;
38
+ this.sourceLines = sourceCode.split('\n');
39
+ this.cfg = cfg;
40
+ this.dataFlow = dataFlow;
41
+ this.rebasingPatterns = [];
42
+ this.vaultPatterns = [];
43
+
44
+ // First pass: identify rebasing token usage and vault patterns
45
+ this.detectRebasingTokenUsage();
46
+ this.detectVaultPatterns();
47
+
48
+ // Traverse AST for detailed analysis
49
+ this.traverse(ast);
50
+
51
+ // Analyze interactions
52
+ this.analyzeRebasingVaultInteractions();
53
+
54
+ return this.findings;
55
+ }
56
+
57
+ /**
58
+ * Detect rebasing token interfaces/usage in code
59
+ */
60
+ detectRebasingTokenUsage() {
61
+ const code = this.sourceCode;
62
+
63
+ // Aave aToken patterns
64
+ const aavePatterns = [
65
+ { pattern: /IAToken|AToken|aToken/i, type: 'Aave aToken', risk: 'CRITICAL' },
66
+ { pattern: /IPool\.supply|lendingPool\.deposit/i, type: 'Aave deposit', risk: 'HIGH' },
67
+ { pattern: /scaledBalanceOf|getScaledUserBalanceAndSupply/i, type: 'Aave scaled balance', risk: 'MEDIUM' },
68
+ ];
69
+
70
+ // Lido stETH patterns
71
+ const lidoPatterns = [
72
+ { pattern: /IStETH|stETH|wstETH/i, type: 'Lido stETH', risk: 'CRITICAL' },
73
+ { pattern: /getSharesByPooledEth|getPooledEthByShares/i, type: 'Lido share conversion', risk: 'HIGH' },
74
+ { pattern: /submit\s*\(\s*\)/i, type: 'Lido staking', risk: 'MEDIUM' },
75
+ ];
76
+
77
+ // Ampleforth/elastic supply patterns
78
+ const elasticPatterns = [
79
+ { pattern: /IAMPL|Ampleforth|rebase/i, type: 'Elastic supply', risk: 'CRITICAL' },
80
+ { pattern: /scaledTotalSupply|scaledBalance/i, type: 'Scaled accounting', risk: 'HIGH' },
81
+ ];
82
+
83
+ // Compound cToken patterns
84
+ const compoundPatterns = [
85
+ { pattern: /ICToken|cToken|CErc20/i, type: 'Compound cToken', risk: 'HIGH' },
86
+ { pattern: /exchangeRateCurrent|exchangeRateStored/i, type: 'Compound exchange rate', risk: 'MEDIUM' },
87
+ { pattern: /mint\s*\(.*\)|redeem\s*\(.*\)|redeemUnderlying/i, type: 'Compound ops', risk: 'MEDIUM' },
88
+ ];
89
+
90
+ const allPatterns = [...aavePatterns, ...lidoPatterns, ...elasticPatterns, ...compoundPatterns];
91
+
92
+ for (const { pattern, type, risk } of allPatterns) {
93
+ if (pattern.test(code)) {
94
+ this.rebasingPatterns.push({ type, risk, pattern });
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Detect vault/share-based contract patterns
101
+ */
102
+ detectVaultPatterns() {
103
+ const code = this.sourceCode;
104
+
105
+ const vaultIndicators = [
106
+ /totalAssets|totalShares|totalSupply/i,
107
+ /convertToAssets|convertToShares|previewDeposit|previewWithdraw/i,
108
+ /deposit.*shares|withdraw.*assets/i,
109
+ /ERC4626|Vault|Pool|Strategy/i,
110
+ /function\s+deposit\s*\(|function\s+withdraw\s*\(/i,
111
+ ];
112
+
113
+ this.isVaultContract = vaultIndicators.some(p => p.test(code));
114
+ }
115
+
116
+ visitContractDefinition(node) {
117
+ this.currentContract = node.name;
118
+
119
+ // Check inheritance for vault patterns
120
+ const baseContracts = (node.baseContracts || [])
121
+ .map(b => b.baseName?.namePath || '')
122
+ .join(' ');
123
+
124
+ if (/ERC4626|Vault|Strategy|Pool/i.test(baseContracts)) {
125
+ this.isVaultContract = true;
126
+ }
127
+ }
128
+
129
+ visitFunctionDefinition(node) {
130
+ this.currentFunction = node.name || 'constructor';
131
+
132
+ if (!node.body) return;
133
+
134
+ const funcCode = this.getCodeSnippet(node.loc);
135
+ const funcName = (node.name || '').toLowerCase();
136
+
137
+ // Skip internal/private
138
+ if (node.visibility === 'private' || node.visibility === 'internal') {
139
+ return;
140
+ }
141
+
142
+ // Analyze deposit/withdraw for rebasing issues
143
+ if (/deposit|stake|supply/i.test(funcName)) {
144
+ this.analyzeDepositFunction(funcCode, node);
145
+ }
146
+
147
+ if (/withdraw|redeem|unstake/i.test(funcName)) {
148
+ this.analyzeWithdrawFunction(funcCode, node);
149
+ }
150
+
151
+ // Check for totalAssets using balanceOf (vulnerable to rebasing)
152
+ this.checkTotalAssetsImplementation(funcCode, node);
153
+
154
+ // Check for share calculation vulnerabilities
155
+ this.checkShareCalculation(funcCode, node);
156
+ }
157
+
158
+ /**
159
+ * Analyze deposit functions for rebasing vulnerabilities
160
+ */
161
+ analyzeDepositFunction(funcCode, node) {
162
+ if (this.rebasingPatterns.length === 0) return;
163
+
164
+ // Check if deposit uses raw balanceOf for share calculation
165
+ const usesBalanceOf = /balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(funcCode);
166
+ const usesTransferAmount = /amount|_amount|assets/i.test(funcCode);
167
+
168
+ // Vulnerable pattern: shares = amount * totalShares / balanceOf(this)
169
+ // If token rebases between deposit transactions, share calculation is wrong
170
+ if (usesBalanceOf && !this.hasRebaseProtection(funcCode)) {
171
+ const rebasingType = this.rebasingPatterns[0].type;
172
+
173
+ this.addFinding({
174
+ title: 'Rebasing Token Deposit Vulnerability',
175
+ description: `Deposit function '${this.currentFunction}' uses balanceOf for share calculation with ${rebasingType}.\n\n` +
176
+ `Attack scenario:\n` +
177
+ `1. Attacker deposits 1 wei, gets 1 share\n` +
178
+ `2. ${rebasingType} rebases (interest accrues)\n` +
179
+ `3. Vault's balanceOf increases WITHOUT new deposits\n` +
180
+ `4. Next depositor's shares = amount * 1 / (1 + rebaseAmount)\n` +
181
+ `5. Depositor gets fewer shares than deserved\n` +
182
+ `6. Attacker withdraws with inflated share value\n\n` +
183
+ `This is a variant of the ERC4626 inflation attack specific to rebasing tokens.`,
184
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
185
+ line: node.loc?.start?.line || 0,
186
+ column: node.loc?.start?.column || 0,
187
+ code: funcCode.substring(0, 400),
188
+ severity: 'CRITICAL',
189
+ confidence: 'HIGH',
190
+ exploitable: true,
191
+ exploitabilityScore: 95,
192
+ attackVector: 'rebasing-token-vault',
193
+ recommendation: `For ${rebasingType}:\n` +
194
+ `1. Use internal accounting (track deposited amounts, not balanceOf)\n` +
195
+ `2. For Aave: Use scaledBalanceOf instead of balanceOf\n` +
196
+ `3. For Lido: Use wstETH (non-rebasing wrapper) or track shares\n` +
197
+ `4. Add virtual shares/assets offset (OZ recommendation)\n` +
198
+ `5. Consider fee-on-transfer style balance checking`,
199
+ references: [
200
+ 'https://docs.aave.com/developers/tokens/atoken',
201
+ 'https://docs.lido.fi/contracts/wsteth',
202
+ 'https://blog.openzeppelin.com/a-]novel-defense-against-erc4626-inflation-attacks'
203
+ ],
204
+ foundryPoC: this.generateRebasingDepositPoC(rebasingType)
205
+ });
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Analyze withdraw functions for rebasing vulnerabilities
211
+ */
212
+ analyzeWithdrawFunction(funcCode, node) {
213
+ if (this.rebasingPatterns.length === 0) return;
214
+
215
+ // Check for insufficient balance handling during negative rebases
216
+ const checksBalance = /require\s*\(.*balance|if\s*\(.*balance\s*</i.test(funcCode);
217
+ const hasSlashingProtection = /slashing|negativeRebase|minBalance/i.test(funcCode);
218
+
219
+ // Vulnerable to Lido slashing events
220
+ if (this.rebasingPatterns.some(p => p.type.includes('Lido')) && !hasSlashingProtection) {
221
+ this.addFinding({
222
+ title: 'Negative Rebase (Slashing) Not Handled',
223
+ description: `Withdraw function '${this.currentFunction}' integrates Lido stETH but doesn't handle slashing scenarios.\n\n` +
224
+ `Risk:\n` +
225
+ `1. Validator slashing event occurs\n` +
226
+ `2. stETH balance decreases (negative rebase)\n` +
227
+ `3. Vault has insufficient tokens for all withdrawal claims\n` +
228
+ `4. Last withdrawers cannot withdraw (bank run)\n` +
229
+ `5. Or protocol becomes insolvent\n\n` +
230
+ `This occurred during Ethereum merge testing when stETH depegged.`,
231
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
232
+ line: node.loc?.start?.line || 0,
233
+ column: node.loc?.start?.column || 0,
234
+ code: funcCode.substring(0, 300),
235
+ severity: 'HIGH',
236
+ confidence: 'MEDIUM',
237
+ exploitable: true,
238
+ exploitabilityScore: 70,
239
+ attackVector: 'rebasing-token-vault',
240
+ recommendation: `1. Track user shares, not absolute amounts\n` +
241
+ `2. Implement pro-rata withdrawal during undercollateralization\n` +
242
+ `3. Add slashing buffer/insurance mechanism\n` +
243
+ `4. Use wstETH which abstracts rebasing complexity`,
244
+ references: [
245
+ 'https://docs.lido.fi/guides/steth-integration-guide'
246
+ ]
247
+ });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Check totalAssets implementation for rebasing issues
253
+ */
254
+ checkTotalAssetsImplementation(funcCode, node) {
255
+ const funcName = (node.name || '').toLowerCase();
256
+
257
+ if (funcName === 'totalassets' || funcName === '_totalassets') {
258
+ // Check if it just returns balanceOf (vulnerable with rebasing tokens)
259
+ if (/return\s+\w+\.balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(funcCode)) {
260
+ if (this.rebasingPatterns.length > 0) {
261
+ this.addFinding({
262
+ title: 'totalAssets Uses balanceOf With Rebasing Token',
263
+ description: `totalAssets() returns raw balanceOf with rebasing token integration.\n\n` +
264
+ `Problem: Rebasing tokens change balance without transfers:\n` +
265
+ `- Aave aTokens: Balance increases from interest\n` +
266
+ `- Lido stETH: Balance changes from rewards/slashing\n` +
267
+ `- AMPL: Global rebases change all balances\n\n` +
268
+ `Impact:\n` +
269
+ `- Share price manipulation via rebase timing\n` +
270
+ `- Sandwich attacks around rebase events\n` +
271
+ `- First depositor attacks amplified by rebases`,
272
+ location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
273
+ line: node.loc?.start?.line || 0,
274
+ code: funcCode.substring(0, 200),
275
+ severity: 'HIGH',
276
+ confidence: 'HIGH',
277
+ exploitable: true,
278
+ exploitabilityScore: 80,
279
+ attackVector: 'rebasing-token-vault',
280
+ recommendation: `1. Use internal accounting that tracks deposits/withdrawals\n` +
281
+ `2. For Aave: totalAssets = aToken.scaledBalanceOf(this) * liquidityIndex\n` +
282
+ `3. For Lido: Track shares instead of stETH balance\n` +
283
+ `4. Add virtual offset for inflation protection`
284
+ });
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Check share calculations for rebasing-specific issues
292
+ */
293
+ checkShareCalculation(funcCode, node) {
294
+ // Check for division without rebasing consideration
295
+ const hasDivision = /totalSupply\s*[><=!]\s*0\s*\?\s*.*\s*:\s*.*\s*\*\s*totalSupply\s*\/\s*totalAssets/i.test(funcCode) ||
296
+ /amount\s*\*\s*totalSupply\s*\/\s*totalAssets/i.test(funcCode);
297
+
298
+ if (hasDivision && this.rebasingPatterns.length > 0 && !this.hasRebaseProtection(funcCode)) {
299
+ // Already covered by deposit analysis, skip duplicate
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Check if function has rebasing-specific protections
305
+ */
306
+ hasRebaseProtection(funcCode) {
307
+ const protections = [
308
+ /scaledBalance|scaledTotalSupply/i, // Aave scaled values
309
+ /wstETH|wrap.*stETH/i, // Lido wrapped version
310
+ /virtualAssets|virtualShares/i, // OZ inflation protection
311
+ /internalBalance|_totalDeposited/i, // Internal accounting
312
+ /checkpoint|snapshot/i, // Historical tracking
313
+ ];
314
+
315
+ return protections.some(p => p.test(funcCode));
316
+ }
317
+
318
+ /**
319
+ * Analyze overall rebasing + vault interaction
320
+ */
321
+ analyzeRebasingVaultInteractions() {
322
+ if (!this.isVaultContract || this.rebasingPatterns.length === 0) {
323
+ return;
324
+ }
325
+
326
+ // Check if contract uses rebasing tokens without proper handling
327
+ const code = this.sourceCode;
328
+
329
+ // Look for dangerous combinations
330
+ const hasDirectBalanceOf = /\.balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(code);
331
+ const hasShareMath = /totalSupply\s*[/*]/i.test(code);
332
+ const hasProtection = this.hasRebaseProtection(code);
333
+
334
+ if (hasDirectBalanceOf && hasShareMath && !hasProtection) {
335
+ // General warning about rebasing integration
336
+ const tokenTypes = this.rebasingPatterns.map(p => p.type).join(', ');
337
+
338
+ this.addFinding({
339
+ title: 'Vault Uses Rebasing Tokens Without Proper Accounting',
340
+ description: `Contract '${this.currentContract}' appears to be a vault that integrates rebasing tokens (${tokenTypes}) ` +
341
+ `but uses direct balanceOf for share calculations.\n\n` +
342
+ `This is a critical vulnerability class that has caused multiple exploits:\n` +
343
+ `- Rebasing rewards get trapped in contract (not distributed to depositors)\n` +
344
+ `- Share price manipulation via rebase timing/MEV\n` +
345
+ `- First depositor attacks amplified\n` +
346
+ `- Potential insolvency during negative rebases (Lido slashing)`,
347
+ location: `Contract: ${this.currentContract}`,
348
+ line: 1,
349
+ severity: 'CRITICAL',
350
+ confidence: 'MEDIUM',
351
+ exploitable: true,
352
+ exploitabilityScore: 85,
353
+ attackVector: 'rebasing-token-vault',
354
+ recommendation: `CRITICAL: Review rebasing token integration:\n` +
355
+ `1. Never use balanceOf for share calculations with rebasing tokens\n` +
356
+ `2. Implement internal deposit tracking\n` +
357
+ `3. For Aave: Use scaledBalanceOf and rayMul with liquidityIndex\n` +
358
+ `4. For Lido: Use wstETH or track shares directly\n` +
359
+ `5. Add virtual offset (10^decimals initial shares) per OZ recommendation`,
360
+ references: [
361
+ 'https://docs.aave.com/developers/guides/interest-bearing-tokens',
362
+ 'https://docs.lido.fi/guides/steth-integration-guide',
363
+ 'https://blog.openzeppelin.com/a-novel-defense-against-erc4626-inflation-attacks'
364
+ ]
365
+ });
366
+ }
367
+ }
368
+
369
+ generateRebasingDepositPoC(tokenType) {
370
+ return `// SPDX-License-Identifier: MIT
371
+ pragma solidity ^0.8.0;
372
+
373
+ import "forge-std/Test.sol";
374
+
375
+ /**
376
+ * PoC: Rebasing Token Vault Inflation Attack
377
+ * Target: ${tokenType} integrated vault
378
+ *
379
+ * Demonstrates how rebasing amplifies share manipulation
380
+ */
381
+ contract RebasingVaultExploit is Test {
382
+ // IVault vault;
383
+ // IERC20 rebasingToken; // aToken, stETH, etc.
384
+
385
+ function testRebasingInflationAttack() public {
386
+ // Setup: Deploy vault with ${tokenType}
387
+
388
+ // Step 1: Attacker deposits minimal amount (1 wei)
389
+ // uint256 attackerShares = vault.deposit(1, attacker);
390
+ // assert(attackerShares == 1); // Gets 1 share
391
+
392
+ // Step 2: Wait for rebase event
393
+ // For Aave: Interest accrues
394
+ // For Lido: Staking rewards distribute
395
+ // For AMPL: Global rebase occurs
396
+ // vm.warp(block.timestamp + 1 days);
397
+
398
+ // Simulate rebase: vault's balanceOf increases
399
+ // deal(address(rebasingToken), address(vault), 1000e18);
400
+
401
+ // Step 3: Victim deposits 500e18
402
+ // uint256 victimShares = vault.deposit(500e18, victim);
403
+ // Expected: ~500e18 shares
404
+ // Actual: 500e18 * 1 / 1000e18 = 0 shares (or dust)
405
+
406
+ // Step 4: Attacker withdraws all
407
+ // uint256 attackerAssets = vault.redeem(1, attacker, attacker);
408
+ // Attacker gets: ~1000e18 + 500e18 = 1500e18 assets!
409
+
410
+ // Result: Attacker stole victim's deposit via rebase timing
411
+ }
412
+ }`;
413
+ }
414
+ }
415
+
416
+ module.exports = RebasingTokenVaultDetector;