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,553 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Front-Running Vulnerability Detector (Enhanced)
|
|
5
|
+
* Detects patterns susceptible to MEV and front-running attacks
|
|
6
|
+
* with improved context awareness to reduce false positives.
|
|
7
|
+
*/
|
|
8
|
+
class FrontRunningDetector extends BaseDetector {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(
|
|
11
|
+
'Front-Running Vulnerability',
|
|
12
|
+
'Detects patterns vulnerable to MEV and front-running attacks',
|
|
13
|
+
'HIGH'
|
|
14
|
+
);
|
|
15
|
+
this.currentContract = null;
|
|
16
|
+
this.currentFunction = null;
|
|
17
|
+
this.currentFunctionNode = null;
|
|
18
|
+
this.contractCode = '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
22
|
+
this.findings = [];
|
|
23
|
+
this.ast = ast;
|
|
24
|
+
this.sourceCode = sourceCode;
|
|
25
|
+
this.fileName = fileName;
|
|
26
|
+
this.sourceLines = sourceCode.split('\n');
|
|
27
|
+
this.cfg = cfg;
|
|
28
|
+
this.dataFlow = dataFlow;
|
|
29
|
+
|
|
30
|
+
this.traverse(ast);
|
|
31
|
+
|
|
32
|
+
return this.findings;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
visitContractDefinition(node) {
|
|
36
|
+
this.currentContract = node.name;
|
|
37
|
+
this.contractCode = this.getCodeSnippet(node.loc);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
visitFunctionDefinition(node) {
|
|
41
|
+
this.currentFunction = node.name || 'constructor';
|
|
42
|
+
this.currentFunctionNode = node;
|
|
43
|
+
|
|
44
|
+
// Skip internal/private functions (not front-runnable by external actors)
|
|
45
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check function parameters for sensitive patterns
|
|
50
|
+
this.checkSwapFunction(node);
|
|
51
|
+
|
|
52
|
+
// Check function body for front-running patterns
|
|
53
|
+
if (node.body) {
|
|
54
|
+
this.checkFunctionBody(node);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check swap functions for proper slippage protection
|
|
60
|
+
*/
|
|
61
|
+
checkSwapFunction(node) {
|
|
62
|
+
const funcName = (node.name || '').toLowerCase();
|
|
63
|
+
const funcCode = node.body ? this.getCodeSnippet(node.loc) : '';
|
|
64
|
+
|
|
65
|
+
// Check if this is a swap-like function
|
|
66
|
+
const isSwapFunction = funcName.includes('swap') ||
|
|
67
|
+
funcName.includes('exchange') ||
|
|
68
|
+
funcName.includes('trade') ||
|
|
69
|
+
funcName.includes('buy') ||
|
|
70
|
+
funcName.includes('sell');
|
|
71
|
+
|
|
72
|
+
if (!isSwapFunction) return;
|
|
73
|
+
|
|
74
|
+
// Check for slippage protection in parameters
|
|
75
|
+
const hasSlippageProtection = this.checkSlippageProtection(node, funcCode);
|
|
76
|
+
|
|
77
|
+
if (!hasSlippageProtection.hasProtection) {
|
|
78
|
+
// Check if it calls an external swap with protection
|
|
79
|
+
const delegatesToProtected = this.delegatesToProtectedSwap(funcCode);
|
|
80
|
+
|
|
81
|
+
if (!delegatesToProtected) {
|
|
82
|
+
this.reportMissingSlippageProtection(node, hasSlippageProtection);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check for slippage protection mechanisms
|
|
89
|
+
*/
|
|
90
|
+
checkSlippageProtection(node, funcCode) {
|
|
91
|
+
const result = {
|
|
92
|
+
hasProtection: false,
|
|
93
|
+
hasMinAmount: false,
|
|
94
|
+
hasDeadline: false,
|
|
95
|
+
hasSlippageTolerance: false
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!node.parameters) return result;
|
|
99
|
+
|
|
100
|
+
// Check parameters
|
|
101
|
+
for (const param of node.parameters) {
|
|
102
|
+
const paramName = (param.name || '').toLowerCase();
|
|
103
|
+
|
|
104
|
+
if (paramName.includes('min') && (paramName.includes('amount') || paramName.includes('out') || paramName.includes('return'))) {
|
|
105
|
+
result.hasMinAmount = true;
|
|
106
|
+
}
|
|
107
|
+
if (paramName.includes('deadline') || paramName.includes('expiry') || paramName.includes('validuntil')) {
|
|
108
|
+
result.hasDeadline = true;
|
|
109
|
+
}
|
|
110
|
+
if (paramName.includes('slippage') || paramName.includes('tolerance')) {
|
|
111
|
+
result.hasSlippageTolerance = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check function body for slippage checks
|
|
116
|
+
if (funcCode) {
|
|
117
|
+
// require(amountOut >= minAmount) patterns
|
|
118
|
+
if (/require\s*\([^)]*>=\s*\w*(min|Min)/i.test(funcCode)) {
|
|
119
|
+
result.hasMinAmount = true;
|
|
120
|
+
}
|
|
121
|
+
// Deadline checks
|
|
122
|
+
if (/require\s*\([^)]*block\.timestamp\s*[<>=]/i.test(funcCode) ||
|
|
123
|
+
/require\s*\([^)]*deadline/i.test(funcCode)) {
|
|
124
|
+
result.hasDeadline = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
result.hasProtection = result.hasMinAmount || result.hasSlippageTolerance;
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if function delegates to a protected swap (e.g., Uniswap Router)
|
|
135
|
+
*/
|
|
136
|
+
delegatesToProtectedSwap(funcCode) {
|
|
137
|
+
// Known protected swap patterns
|
|
138
|
+
const protectedPatterns = [
|
|
139
|
+
/swapExactTokensForTokens\s*\([^)]*,\s*\w+\s*,/, // Uniswap V2 with minAmountOut
|
|
140
|
+
/swapExactETHForTokens\s*\{/,
|
|
141
|
+
/exactInputSingle\s*\(/, // Uniswap V3
|
|
142
|
+
/exactInput\s*\(/,
|
|
143
|
+
/\.swap\s*\([^)]*amountOutMin/i,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
return protectedPatterns.some(p => p.test(funcCode));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
checkFunctionBody(node) {
|
|
150
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
151
|
+
const funcName = (node.name || '').toLowerCase();
|
|
152
|
+
|
|
153
|
+
// 1. Check for weak commit-reveal patterns
|
|
154
|
+
this.checkCommitReveal(node, funcCode, funcName);
|
|
155
|
+
|
|
156
|
+
// 2. Check for auction patterns
|
|
157
|
+
this.checkAuctionPattern(node, funcCode, funcName);
|
|
158
|
+
|
|
159
|
+
// 3. Check for ERC20 approve (only in specific risky contexts)
|
|
160
|
+
this.checkApprovePattern(node, funcCode);
|
|
161
|
+
|
|
162
|
+
// 4. Check for signature replay issues
|
|
163
|
+
this.checkSignatureReplay(node, funcCode);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check for weak commit-reveal implementations
|
|
168
|
+
*/
|
|
169
|
+
checkCommitReveal(node, funcCode, funcName) {
|
|
170
|
+
// Must be a reveal function with hash verification
|
|
171
|
+
const isRevealFunction = funcName.includes('reveal') ||
|
|
172
|
+
(funcCode.includes('keccak256') && /commit|reveal|secret/i.test(funcCode));
|
|
173
|
+
|
|
174
|
+
if (!isRevealFunction) return;
|
|
175
|
+
|
|
176
|
+
// Check for proper block delay
|
|
177
|
+
const hasBlockDelay = /block\.number\s*[->]\s*commit.*block/i.test(funcCode) ||
|
|
178
|
+
/commitBlock.*\+\s*\d+/i.test(funcCode) ||
|
|
179
|
+
/require.*block\.number\s*>=?\s*\w+\s*\+\s*\d+/i.test(funcCode);
|
|
180
|
+
|
|
181
|
+
// Check for timestamp delay (less secure but still a protection)
|
|
182
|
+
const hasTimestampDelay = /block\.timestamp\s*>=?\s*\w+\s*\+\s*\d+/i.test(funcCode);
|
|
183
|
+
|
|
184
|
+
// Check for known secure patterns
|
|
185
|
+
const hasSecurePattern = /revealDeadline|commitPeriod|REVEAL_DELAY/i.test(funcCode);
|
|
186
|
+
|
|
187
|
+
if (!hasBlockDelay && !hasTimestampDelay && !hasSecurePattern) {
|
|
188
|
+
this.reportWeakCommitReveal(node);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check for vulnerable auction patterns
|
|
194
|
+
*/
|
|
195
|
+
checkAuctionPattern(node, funcCode, funcName) {
|
|
196
|
+
const isAuctionFunction = funcName.includes('bid') ||
|
|
197
|
+
funcName.includes('auction') ||
|
|
198
|
+
funcName.includes('offer');
|
|
199
|
+
|
|
200
|
+
if (!isAuctionFunction) return;
|
|
201
|
+
|
|
202
|
+
// Skip if it's just checking bids (view function)
|
|
203
|
+
if (node.stateMutability === 'view' || node.stateMutability === 'pure') {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check for sealed/commit patterns
|
|
208
|
+
const hasSealedBid = /commit|sealed|hash|blind/i.test(funcCode);
|
|
209
|
+
|
|
210
|
+
// Check for private mempool usage indicators
|
|
211
|
+
const hasPrivateSubmission = /flashbots|private|confidential/i.test(funcCode);
|
|
212
|
+
|
|
213
|
+
if (!hasSealedBid && !hasPrivateSubmission) {
|
|
214
|
+
// Check if bid amount is a direct parameter (front-runnable)
|
|
215
|
+
const hasBidAmountParam = node.parameters &&
|
|
216
|
+
node.parameters.some(p => /amount|value|bid/i.test(p.name || ''));
|
|
217
|
+
|
|
218
|
+
if (hasBidAmountParam || /msg\.value/.test(funcCode)) {
|
|
219
|
+
this.reportVisibleBidding(node);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check for ERC20 approve front-running (only flag risky patterns)
|
|
226
|
+
*/
|
|
227
|
+
checkApprovePattern(node, funcCode) {
|
|
228
|
+
// Only check if function contains approve
|
|
229
|
+
if (!funcCode.includes('.approve(')) return;
|
|
230
|
+
|
|
231
|
+
// Check for safe patterns that mitigate the issue
|
|
232
|
+
|
|
233
|
+
// Pattern 1: Uses increaseAllowance/decreaseAllowance instead
|
|
234
|
+
if (/increaseAllowance|decreaseAllowance/i.test(this.contractCode)) {
|
|
235
|
+
// Contract uses safe allowance patterns
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Pattern 2: Sets to 0 first, then to new value
|
|
240
|
+
const setsToZeroFirst = /\.approve\s*\([^,]+,\s*0\s*\)/i.test(funcCode) &&
|
|
241
|
+
/\.approve\s*\([^,]+,\s*[^0]/i.test(funcCode);
|
|
242
|
+
if (setsToZeroFirst) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Pattern 3: Uses SafeERC20
|
|
247
|
+
if (/safeApprove|safeIncreaseAllowance|forceApprove/i.test(funcCode)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Pattern 4: Initial approval (setting from 0)
|
|
252
|
+
// If the function is named like "initialize" or happens in constructor, it's likely safe
|
|
253
|
+
if (/constructor|initialize|init|setup/i.test(this.currentFunction)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Pattern 5: Approval to trusted addresses only (routers, etc)
|
|
258
|
+
if (/ROUTER|UNISWAP|SUSHISWAP|PANCAKE/i.test(funcCode)) {
|
|
259
|
+
// Approving to known routers - common safe pattern
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Only flag if this looks like user-facing allowance change
|
|
264
|
+
const funcName = this.currentFunction.toLowerCase();
|
|
265
|
+
const isUserFacing = /approve|allowance|permit/i.test(funcName) ||
|
|
266
|
+
node.visibility === 'external' ||
|
|
267
|
+
node.visibility === 'public';
|
|
268
|
+
|
|
269
|
+
if (isUserFacing) {
|
|
270
|
+
this.reportApprovalFrontRunning(node, funcCode);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check for signature replay vulnerabilities
|
|
276
|
+
*/
|
|
277
|
+
checkSignatureReplay(node, funcCode) {
|
|
278
|
+
// Check for signature verification
|
|
279
|
+
const hasSignatureVerification = /ecrecover|ECDSA\.recover|SignatureChecker/i.test(funcCode);
|
|
280
|
+
|
|
281
|
+
if (!hasSignatureVerification) return;
|
|
282
|
+
|
|
283
|
+
// Check for replay protection
|
|
284
|
+
const replayProtection = {
|
|
285
|
+
hasNonce: /nonce/i.test(funcCode),
|
|
286
|
+
hasDeadline: /deadline|expir|validUntil/i.test(funcCode),
|
|
287
|
+
hasChainId: /chainId|chainid|block\.chainid/i.test(funcCode),
|
|
288
|
+
marksUsed: /used\[|usedNonces|usedSignatures|invalidate/i.test(funcCode),
|
|
289
|
+
hasEIP712: /DOMAIN_SEPARATOR|_domainSeparator|EIP712/i.test(funcCode)
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const protectionCount = Object.values(replayProtection).filter(v => v).length;
|
|
293
|
+
|
|
294
|
+
// EIP712 typically includes chain ID and proper domain
|
|
295
|
+
if (replayProtection.hasEIP712) {
|
|
296
|
+
// Still check for nonce/deadline even with EIP712
|
|
297
|
+
if (!replayProtection.hasNonce && !replayProtection.marksUsed) {
|
|
298
|
+
this.reportSignatureReplayMissingNonce(node);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Need at least nonce OR marking as used, plus deadline/chainId
|
|
304
|
+
if (!replayProtection.hasNonce && !replayProtection.marksUsed) {
|
|
305
|
+
this.reportSignatureReplay(node, replayProtection);
|
|
306
|
+
} else if (!replayProtection.hasDeadline && !replayProtection.hasChainId) {
|
|
307
|
+
// Has nonce but missing other protections
|
|
308
|
+
this.reportSignatureReplayWeak(node, replayProtection);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
reportMissingSlippageProtection(node, analysis) {
|
|
313
|
+
const missingParts = [];
|
|
314
|
+
if (!analysis.hasMinAmount && !analysis.hasSlippageTolerance) {
|
|
315
|
+
missingParts.push('minimum output amount');
|
|
316
|
+
}
|
|
317
|
+
if (!analysis.hasDeadline) {
|
|
318
|
+
missingParts.push('deadline');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.addFinding({
|
|
322
|
+
title: 'Missing Slippage Protection in Swap',
|
|
323
|
+
description: `Function '${this.currentFunction}' performs token swaps without ${missingParts.join(' or ')}. Transactions can be sandwiched by MEV bots, resulting in users receiving fewer tokens than expected.`,
|
|
324
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
325
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
326
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
327
|
+
code: this.getCodeSnippet(node.loc),
|
|
328
|
+
severity: 'HIGH',
|
|
329
|
+
confidence: 'HIGH',
|
|
330
|
+
exploitable: true,
|
|
331
|
+
exploitabilityScore: 85,
|
|
332
|
+
attackVector: 'sandwich-attack',
|
|
333
|
+
recommendation: `Add 'minAmountOut' parameter and require that output >= minAmountOut. Add 'deadline' parameter and require block.timestamp <= deadline. Consider using a DEX aggregator with built-in MEV protection.`,
|
|
334
|
+
references: [
|
|
335
|
+
'https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/trading-from-a-smart-contract',
|
|
336
|
+
'https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest'
|
|
337
|
+
],
|
|
338
|
+
foundryPoC: this.generateSandwichPoC()
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
reportWeakCommitReveal(node) {
|
|
343
|
+
this.addFinding({
|
|
344
|
+
title: 'Weak Commit-Reveal Pattern',
|
|
345
|
+
description: `Commit-reveal implementation without sufficient block delay. Attackers watching the mempool can front-run reveal transactions in the same block by paying higher gas.`,
|
|
346
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
347
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
348
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
349
|
+
code: this.getCodeSnippet(node.loc),
|
|
350
|
+
severity: 'HIGH',
|
|
351
|
+
confidence: 'MEDIUM',
|
|
352
|
+
exploitable: true,
|
|
353
|
+
exploitabilityScore: 70,
|
|
354
|
+
attackVector: 'commit-reveal-frontrun',
|
|
355
|
+
recommendation: 'Require minimum block delay (e.g., 2+ blocks) between commit and reveal. Store commitBlock and require block.number >= commitBlock + DELAY. Consider using Flashbots Protect for private submission.',
|
|
356
|
+
references: [
|
|
357
|
+
'https://swcregistry.io/docs/SWC-114'
|
|
358
|
+
]
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
reportVisibleBidding(node) {
|
|
363
|
+
this.addFinding({
|
|
364
|
+
title: 'Visible Bid Amount - Front-Running Risk',
|
|
365
|
+
description: `Auction bid amount is visible in mempool before execution. Competitors can see bids and front-run with higher amounts, or miners can reorder transactions for profit.`,
|
|
366
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
367
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
368
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
369
|
+
code: this.getCodeSnippet(node.loc),
|
|
370
|
+
severity: 'MEDIUM',
|
|
371
|
+
confidence: 'MEDIUM',
|
|
372
|
+
exploitable: true,
|
|
373
|
+
exploitabilityScore: 60,
|
|
374
|
+
attackVector: 'auction-frontrun',
|
|
375
|
+
recommendation: 'Implement sealed-bid auction: (1) Commit phase: users submit hash(bid, salt), (2) Reveal phase: users reveal actual bid after commit deadline. Alternatively, use private transaction submission via Flashbots.',
|
|
376
|
+
references: [
|
|
377
|
+
'https://ethereum.org/en/developers/docs/mev/'
|
|
378
|
+
]
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
reportApprovalFrontRunning(node, funcCode) {
|
|
383
|
+
this.addFinding({
|
|
384
|
+
title: 'ERC20 Approve Race Condition',
|
|
385
|
+
description: `Direct use of approve() when changing non-zero allowances creates a race condition. A malicious spender can front-run the approve transaction to use both old and new allowances.`,
|
|
386
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
387
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
388
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
389
|
+
code: this.extractApproveCode(funcCode),
|
|
390
|
+
severity: 'MEDIUM',
|
|
391
|
+
confidence: 'MEDIUM',
|
|
392
|
+
exploitable: true,
|
|
393
|
+
exploitabilityScore: 45,
|
|
394
|
+
attackVector: 'approve-race-condition',
|
|
395
|
+
recommendation: 'Use increaseAllowance()/decreaseAllowance() from OpenZeppelin, or set allowance to 0 first with require(currentAllowance == 0 || newAllowance == 0). Consider using permit() for gasless approvals.',
|
|
396
|
+
references: [
|
|
397
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20-increaseAllowance-address-uint256-'
|
|
398
|
+
]
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
reportSignatureReplay(node, protection) {
|
|
403
|
+
this.addFinding({
|
|
404
|
+
title: 'Signature Replay Vulnerability',
|
|
405
|
+
description: `Signature verification without proper replay protection. Signed messages can be replayed multiple times (missing nonce) or across chains (missing chainId).`,
|
|
406
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
407
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
408
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
409
|
+
code: this.getCodeSnippet(node.loc),
|
|
410
|
+
severity: 'CRITICAL',
|
|
411
|
+
confidence: 'HIGH',
|
|
412
|
+
exploitable: true,
|
|
413
|
+
exploitabilityScore: 90,
|
|
414
|
+
attackVector: 'signature-replay',
|
|
415
|
+
recommendation: 'Implement EIP-712 typed data signing. Include: (1) nonce that increments per-signer, (2) deadline/expiry timestamp, (3) chainId from block.chainid. Track used signatures/nonces in mapping.',
|
|
416
|
+
references: [
|
|
417
|
+
'https://swcregistry.io/docs/SWC-121',
|
|
418
|
+
'https://eips.ethereum.org/EIPS/eip-712'
|
|
419
|
+
],
|
|
420
|
+
foundryPoC: this.generateSignatureReplayPoC()
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
reportSignatureReplayMissingNonce(node) {
|
|
425
|
+
this.addFinding({
|
|
426
|
+
title: 'Signature Missing Nonce/Usage Tracking',
|
|
427
|
+
description: `EIP-712 signature implementation without nonce or usage tracking. While EIP-712 provides domain separation, signatures can still be replayed if not invalidated after use.`,
|
|
428
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
429
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
430
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
431
|
+
code: this.getCodeSnippet(node.loc),
|
|
432
|
+
severity: 'HIGH',
|
|
433
|
+
confidence: 'MEDIUM',
|
|
434
|
+
exploitable: true,
|
|
435
|
+
exploitabilityScore: 70,
|
|
436
|
+
recommendation: 'Add incrementing nonce per-signer OR track used signature hashes in mapping. Include nonce in the signed struct.',
|
|
437
|
+
references: [
|
|
438
|
+
'https://eips.ethereum.org/EIPS/eip-712'
|
|
439
|
+
]
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
reportSignatureReplayWeak(node, protection) {
|
|
444
|
+
const missing = [];
|
|
445
|
+
if (!protection.hasDeadline) missing.push('deadline');
|
|
446
|
+
if (!protection.hasChainId) missing.push('chainId');
|
|
447
|
+
|
|
448
|
+
this.addFinding({
|
|
449
|
+
title: 'Weak Signature Replay Protection',
|
|
450
|
+
description: `Signature has nonce but missing ${missing.join(' and ')}. Signatures may be held indefinitely or replayed on other chains.`,
|
|
451
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
452
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
453
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
454
|
+
code: this.getCodeSnippet(node.loc),
|
|
455
|
+
severity: 'MEDIUM',
|
|
456
|
+
confidence: 'MEDIUM',
|
|
457
|
+
exploitable: true,
|
|
458
|
+
exploitabilityScore: 50,
|
|
459
|
+
recommendation: `Add ${missing.join(' and ')} to the signed message. Use block.chainid for cross-chain protection.`,
|
|
460
|
+
references: [
|
|
461
|
+
'https://eips.ethereum.org/EIPS/eip-712'
|
|
462
|
+
]
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
extractApproveCode(funcCode) {
|
|
467
|
+
// Extract just the approve-related lines
|
|
468
|
+
const lines = funcCode.split('\n');
|
|
469
|
+
const approveLines = lines.filter(l => /\.approve\s*\(/.test(l));
|
|
470
|
+
return approveLines.join('\n') || funcCode.substring(0, 150);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
generateSandwichPoC() {
|
|
474
|
+
return `// SPDX-License-Identifier: MIT
|
|
475
|
+
pragma solidity ^0.8.0;
|
|
476
|
+
|
|
477
|
+
import "forge-std/Test.sol";
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Proof of Concept: Sandwich Attack on Unprotected Swap
|
|
481
|
+
* Demonstrates how MEV bots profit from missing slippage protection
|
|
482
|
+
*/
|
|
483
|
+
contract SandwichAttackExploit is Test {
|
|
484
|
+
address constant TARGET = address(0); // Vulnerable contract
|
|
485
|
+
address constant DEX = address(0); // DEX being used
|
|
486
|
+
|
|
487
|
+
function testSandwichAttack() public {
|
|
488
|
+
// Attacker monitors mempool for unprotected swaps
|
|
489
|
+
|
|
490
|
+
// Step 1: FRONTRUN - Buy tokens before victim
|
|
491
|
+
// This increases the price
|
|
492
|
+
// DEX.swap(ETH_AMOUNT, 0); // No minOut needed for attacker
|
|
493
|
+
|
|
494
|
+
// Step 2: Victim's transaction executes at worse price
|
|
495
|
+
// (simulated - in reality this is the pending tx)
|
|
496
|
+
|
|
497
|
+
// Step 3: BACKRUN - Sell tokens after victim
|
|
498
|
+
// Attacker profits from price increase caused by victim
|
|
499
|
+
// DEX.swap(TOKENS_BOUGHT, 0);
|
|
500
|
+
|
|
501
|
+
// Profit = tokens received in step 3 - ETH spent in step 1
|
|
502
|
+
// Victim receives fewer tokens due to price manipulation
|
|
503
|
+
}
|
|
504
|
+
}`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
generateSignatureReplayPoC() {
|
|
508
|
+
return `// SPDX-License-Identifier: MIT
|
|
509
|
+
pragma solidity ^0.8.0;
|
|
510
|
+
|
|
511
|
+
import "forge-std/Test.sol";
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Proof of Concept: Signature Replay Attack
|
|
515
|
+
* Demonstrates replaying a valid signature multiple times
|
|
516
|
+
*/
|
|
517
|
+
contract SignatureReplayExploit is Test {
|
|
518
|
+
address constant TARGET = address(0);
|
|
519
|
+
|
|
520
|
+
function testSignatureReplay() public {
|
|
521
|
+
// Assume we have a valid signature for some action
|
|
522
|
+
bytes memory signature; // = captured from previous transaction
|
|
523
|
+
|
|
524
|
+
// First use - legitimate
|
|
525
|
+
// TARGET.executeWithSignature(data, signature);
|
|
526
|
+
|
|
527
|
+
// Replay 1 - should fail but succeeds without nonce
|
|
528
|
+
// TARGET.executeWithSignature(data, signature);
|
|
529
|
+
|
|
530
|
+
// Replay 2 - continues to succeed
|
|
531
|
+
// TARGET.executeWithSignature(data, signature);
|
|
532
|
+
|
|
533
|
+
// Attacker can replay indefinitely until signature is manually invalidated
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function testCrossChainReplay() public {
|
|
537
|
+
// On Mainnet, user signs transaction
|
|
538
|
+
bytes memory signature; // = signed on mainnet
|
|
539
|
+
|
|
540
|
+
// Without chainId in signature, same signature works on:
|
|
541
|
+
// - Polygon fork
|
|
542
|
+
// vm.chainId(137);
|
|
543
|
+
// TARGET.executeWithSignature(data, signature); // Works!
|
|
544
|
+
|
|
545
|
+
// - Arbitrum fork
|
|
546
|
+
// vm.chainId(42161);
|
|
547
|
+
// TARGET.executeWithSignature(data, signature); // Works!
|
|
548
|
+
}
|
|
549
|
+
}`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = FrontRunningDetector;
|