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.
- package/README.md +685 -0
- package/bin/web3crit +10 -0
- package/package.json +59 -0
- package/src/analyzers/control-flow.js +256 -0
- package/src/analyzers/data-flow.js +720 -0
- package/src/analyzers/exploit-chain.js +751 -0
- package/src/analyzers/immunefi-classifier.js +515 -0
- package/src/analyzers/poc-validator.js +396 -0
- package/src/analyzers/solodit-enricher.js +1122 -0
- package/src/cli.js +546 -0
- package/src/detectors/access-control-enhanced.js +458 -0
- package/src/detectors/base-detector.js +213 -0
- package/src/detectors/callback-reentrancy.js +362 -0
- package/src/detectors/cross-contract-reentrancy.js +697 -0
- package/src/detectors/delegatecall.js +167 -0
- package/src/detectors/deprecated-functions.js +62 -0
- package/src/detectors/flash-loan.js +408 -0
- package/src/detectors/frontrunning.js +553 -0
- package/src/detectors/gas-griefing.js +701 -0
- package/src/detectors/governance-attacks.js +366 -0
- package/src/detectors/integer-overflow.js +487 -0
- package/src/detectors/oracle-manipulation.js +524 -0
- package/src/detectors/permit-exploits.js +368 -0
- package/src/detectors/precision-loss.js +408 -0
- package/src/detectors/price-manipulation-advanced.js +548 -0
- package/src/detectors/proxy-vulnerabilities.js +651 -0
- package/src/detectors/readonly-reentrancy.js +473 -0
- package/src/detectors/rebasing-token-vault.js +416 -0
- package/src/detectors/reentrancy-enhanced.js +359 -0
- package/src/detectors/selfdestruct.js +259 -0
- package/src/detectors/share-manipulation.js +412 -0
- package/src/detectors/signature-replay.js +409 -0
- package/src/detectors/storage-collision.js +446 -0
- package/src/detectors/timestamp-dependence.js +494 -0
- package/src/detectors/toctou.js +427 -0
- package/src/detectors/token-standard-compliance.js +465 -0
- package/src/detectors/unchecked-call.js +214 -0
- package/src/detectors/vault-inflation.js +421 -0
- package/src/index.js +42 -0
- package/src/package-lock.json +2874 -0
- package/src/package.json +39 -0
- 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;
|