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,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;
|