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,524 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Price Oracle Manipulation Detector
|
|
5
|
+
* Detects vulnerable oracle patterns that can be exploited via flash loans or market manipulation
|
|
6
|
+
*
|
|
7
|
+
* Attack vectors detected:
|
|
8
|
+
* 1. Spot price usage (getReserves, balanceOf ratio) - Flash loan manipulable
|
|
9
|
+
* 2. TWAP with insufficient window - Can be manipulated over time
|
|
10
|
+
* 3. Single oracle source - No redundancy
|
|
11
|
+
* 4. Stale price data - Missing freshness checks
|
|
12
|
+
* 5. Missing price bounds - No sanity checks on price
|
|
13
|
+
* 6. Chainlink-specific issues - Missing L2 sequencer checks
|
|
14
|
+
*/
|
|
15
|
+
class OracleManipulationDetector extends BaseDetector {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(
|
|
18
|
+
'Oracle Manipulation',
|
|
19
|
+
'Detects vulnerable price oracle patterns exploitable via flash loans',
|
|
20
|
+
'CRITICAL'
|
|
21
|
+
);
|
|
22
|
+
this.currentContract = null;
|
|
23
|
+
this.currentFunction = null;
|
|
24
|
+
this.oracleUsages = [];
|
|
25
|
+
this.chainlinkFeeds = [];
|
|
26
|
+
this.twapUsages = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
30
|
+
this.findings = [];
|
|
31
|
+
this.ast = ast;
|
|
32
|
+
this.sourceCode = sourceCode;
|
|
33
|
+
this.fileName = fileName;
|
|
34
|
+
this.sourceLines = sourceCode.split('\n');
|
|
35
|
+
this.cfg = cfg;
|
|
36
|
+
this.dataFlow = dataFlow;
|
|
37
|
+
this.oracleUsages = [];
|
|
38
|
+
this.chainlinkFeeds = [];
|
|
39
|
+
this.twapUsages = [];
|
|
40
|
+
|
|
41
|
+
this.traverse(ast);
|
|
42
|
+
|
|
43
|
+
// Analyze collected oracle patterns
|
|
44
|
+
this.analyzeOraclePatterns();
|
|
45
|
+
|
|
46
|
+
return this.findings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
visitContractDefinition(node) {
|
|
50
|
+
this.currentContract = node.name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
visitFunctionDefinition(node) {
|
|
54
|
+
this.currentFunction = node.name || 'constructor';
|
|
55
|
+
|
|
56
|
+
if (!node.body) return;
|
|
57
|
+
|
|
58
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
59
|
+
const funcName = (node.name || '').toLowerCase();
|
|
60
|
+
|
|
61
|
+
// Skip view functions that don't affect value
|
|
62
|
+
if (node.stateMutability === 'pure') return;
|
|
63
|
+
|
|
64
|
+
// Check for spot price patterns
|
|
65
|
+
this.detectSpotPriceUsage(funcCode, node);
|
|
66
|
+
|
|
67
|
+
// Check for TWAP usage
|
|
68
|
+
this.detectTWAPUsage(funcCode, node);
|
|
69
|
+
|
|
70
|
+
// Check for Chainlink usage
|
|
71
|
+
this.detectChainlinkUsage(funcCode, node);
|
|
72
|
+
|
|
73
|
+
// Check for price-dependent value calculations
|
|
74
|
+
this.detectPriceDependentOperations(funcCode, node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
detectSpotPriceUsage(funcCode, node) {
|
|
78
|
+
// Uniswap V2 spot price patterns - highly manipulable
|
|
79
|
+
const uniV2Patterns = [
|
|
80
|
+
{ pattern: /\.getReserves\s*\(\s*\)/, name: 'getReserves', risk: 'CRITICAL' },
|
|
81
|
+
{ pattern: /reserve0\s*[\/\*]\s*reserve1|reserve1\s*[\/\*]\s*reserve0/, name: 'reserve_ratio', risk: 'CRITICAL' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Uniswap V3 spot price - still manipulable within tick
|
|
85
|
+
const uniV3Patterns = [
|
|
86
|
+
{ pattern: /slot0\s*\(\s*\)/, name: 'slot0', risk: 'HIGH' },
|
|
87
|
+
{ pattern: /sqrtPriceX96/, name: 'sqrtPriceX96', risk: 'HIGH' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Balance ratio patterns - highly manipulable
|
|
91
|
+
const balancePatterns = [
|
|
92
|
+
{ pattern: /balanceOf[^]*\/[^]*balanceOf/, name: 'balance_ratio', risk: 'CRITICAL' },
|
|
93
|
+
{ pattern: /getBalance[^]*\/[^]*getBalance/, name: 'balance_ratio', risk: 'CRITICAL' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// AMM-specific price functions
|
|
97
|
+
const ammPatterns = [
|
|
98
|
+
{ pattern: /\.getAmountOut\s*\(/, name: 'getAmountOut', risk: 'HIGH' },
|
|
99
|
+
{ pattern: /\.getAmountsOut\s*\(/, name: 'getAmountsOut', risk: 'HIGH' },
|
|
100
|
+
{ pattern: /\.quote\s*\(/, name: 'quote', risk: 'HIGH' },
|
|
101
|
+
{ pattern: /calcSpotPrice/, name: 'calcSpotPrice', risk: 'CRITICAL' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const allPatterns = [...uniV2Patterns, ...uniV3Patterns, ...balancePatterns, ...ammPatterns];
|
|
105
|
+
|
|
106
|
+
for (const { pattern, name, risk } of allPatterns) {
|
|
107
|
+
if (pattern.test(funcCode)) {
|
|
108
|
+
// Check if it's used in a meaningful value calculation
|
|
109
|
+
const flowsToValue = this.flowsToValueOperation(funcCode);
|
|
110
|
+
|
|
111
|
+
this.oracleUsages.push({
|
|
112
|
+
function: this.currentFunction,
|
|
113
|
+
node: node,
|
|
114
|
+
type: 'spot_price',
|
|
115
|
+
source: name,
|
|
116
|
+
risk: risk,
|
|
117
|
+
flowsToValue: flowsToValue,
|
|
118
|
+
code: funcCode
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
detectTWAPUsage(funcCode, node) {
|
|
125
|
+
const twapPatterns = [
|
|
126
|
+
{ pattern: /observe\s*\(/, name: 'uniswap_observe', type: 'uniswap_v3' },
|
|
127
|
+
{ pattern: /consult\s*\(/, name: 'oracle_consult', type: 'generic' },
|
|
128
|
+
{ pattern: /TWAP|twap/, name: 'twap', type: 'generic' },
|
|
129
|
+
{ pattern: /cumulativePrice|priceCumulative/, name: 'cumulative', type: 'uniswap_v2' },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const { pattern, name, type } of twapPatterns) {
|
|
133
|
+
if (pattern.test(funcCode)) {
|
|
134
|
+
// Check TWAP window
|
|
135
|
+
const windowAnalysis = this.analyzeTWAPWindow(funcCode);
|
|
136
|
+
|
|
137
|
+
this.twapUsages.push({
|
|
138
|
+
function: this.currentFunction,
|
|
139
|
+
node: node,
|
|
140
|
+
source: name,
|
|
141
|
+
type: type,
|
|
142
|
+
windowSeconds: windowAnalysis.window,
|
|
143
|
+
windowSafe: windowAnalysis.safe,
|
|
144
|
+
code: funcCode
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
detectChainlinkUsage(funcCode, node) {
|
|
151
|
+
const chainlinkPatterns = [
|
|
152
|
+
/latestRoundData\s*\(\s*\)/,
|
|
153
|
+
/latestAnswer\s*\(\s*\)/,
|
|
154
|
+
/AggregatorV3Interface/,
|
|
155
|
+
/priceFeed/i,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
if (chainlinkPatterns.some(p => p.test(funcCode))) {
|
|
159
|
+
const analysis = this.analyzeChainlinkUsage(funcCode);
|
|
160
|
+
|
|
161
|
+
this.chainlinkFeeds.push({
|
|
162
|
+
function: this.currentFunction,
|
|
163
|
+
node: node,
|
|
164
|
+
checks: analysis,
|
|
165
|
+
code: funcCode
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
detectPriceDependentOperations(funcCode, node) {
|
|
171
|
+
// Check if price is used in critical operations
|
|
172
|
+
const criticalOps = [
|
|
173
|
+
/collateral\s*[\/\*]/i,
|
|
174
|
+
/liquidat/i,
|
|
175
|
+
/borrow/i,
|
|
176
|
+
/mint.*price|price.*mint/i,
|
|
177
|
+
/redeem.*price|price.*redeem/i,
|
|
178
|
+
/swap.*price|price.*swap/i,
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (criticalOps.some(p => p.test(funcCode))) {
|
|
182
|
+
// This function uses price in critical operations
|
|
183
|
+
// Will be cross-referenced with oracle findings
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
analyzeChainlinkUsage(funcCode) {
|
|
188
|
+
return {
|
|
189
|
+
checksRoundId: /roundId\s*[><=!]/i.test(funcCode) || /require.*roundId/i.test(funcCode),
|
|
190
|
+
checksTimestamp: /updatedAt|timestamp/i.test(funcCode) && /[><=]/i.test(funcCode),
|
|
191
|
+
checksStaleness: /block\.timestamp\s*-\s*updatedAt|updatedAt.*block\.timestamp/i.test(funcCode),
|
|
192
|
+
checksAnswer: /answer\s*[><=!]\s*0|require.*answer/i.test(funcCode),
|
|
193
|
+
checksSequencer: /sequencer|L2|isSequencerUp/i.test(funcCode),
|
|
194
|
+
usesLatestAnswer: /latestAnswer/i.test(funcCode), // Deprecated
|
|
195
|
+
hasHeartbeat: /heartbeat|HEARTBEAT|maxDelay/i.test(funcCode),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
analyzeTWAPWindow(funcCode) {
|
|
200
|
+
// Try to extract TWAP window
|
|
201
|
+
const windowPatterns = [
|
|
202
|
+
/(\d+)\s*(?:seconds|minutes|hours)/i,
|
|
203
|
+
/TWAP_WINDOW\s*=\s*(\d+)/i,
|
|
204
|
+
/window\s*=\s*(\d+)/i,
|
|
205
|
+
/period\s*=\s*(\d+)/i,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
let window = null;
|
|
209
|
+
for (const pattern of windowPatterns) {
|
|
210
|
+
const match = funcCode.match(pattern);
|
|
211
|
+
if (match) {
|
|
212
|
+
let value = parseInt(match[1]);
|
|
213
|
+
// Convert to seconds if needed
|
|
214
|
+
if (/minutes/i.test(match[0])) value *= 60;
|
|
215
|
+
if (/hours/i.test(match[0])) value *= 3600;
|
|
216
|
+
window = value;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Safe window is generally considered 30+ minutes for TWAP
|
|
222
|
+
// Less than 10 minutes is dangerous
|
|
223
|
+
return {
|
|
224
|
+
window: window,
|
|
225
|
+
safe: window === null || window >= 1800 // 30 minutes
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
flowsToValueOperation(funcCode) {
|
|
230
|
+
// Check if oracle result flows to value-affecting operation
|
|
231
|
+
const valueOps = [
|
|
232
|
+
/transferFrom|transfer|safeTransfer/i,
|
|
233
|
+
/mint\s*\(/i,
|
|
234
|
+
/burn\s*\(/i,
|
|
235
|
+
/liquidate/i,
|
|
236
|
+
/borrow/i,
|
|
237
|
+
/repay/i,
|
|
238
|
+
/\.call\s*\{.*value/i,
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
return valueOps.some(p => p.test(funcCode));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
analyzeOraclePatterns() {
|
|
245
|
+
// HARDENED: Use data flow analysis to verify oracle values flow to value-moving operations
|
|
246
|
+
// This significantly reduces false positives from pattern-only detection
|
|
247
|
+
const oracleValueFlows = this.dataFlow?.oracleValueFlows || [];
|
|
248
|
+
|
|
249
|
+
// Analyze spot price usages - require data flow proof
|
|
250
|
+
for (const usage of this.oracleUsages) {
|
|
251
|
+
// Check if we have data flow evidence that oracle flows to value operation
|
|
252
|
+
const hasDataFlowProof = oracleValueFlows.some(flow =>
|
|
253
|
+
flow.function.includes(usage.function) ||
|
|
254
|
+
flow.contract === this.currentContract
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Require either data flow proof OR strong heuristic evidence
|
|
258
|
+
const hasStrongEvidence = usage.flowsToValue && usage.risk === 'CRITICAL';
|
|
259
|
+
|
|
260
|
+
if (usage.risk === 'CRITICAL' && (hasDataFlowProof || hasStrongEvidence)) {
|
|
261
|
+
this.reportSpotPriceVulnerability(usage);
|
|
262
|
+
} else if (usage.risk === 'HIGH' && hasDataFlowProof) {
|
|
263
|
+
// Only report HIGH risk if we have data flow proof
|
|
264
|
+
this.reportHighRiskOracleUsage(usage);
|
|
265
|
+
}
|
|
266
|
+
// Skip reporting if no data flow proof - reduces false positives
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Analyze TWAP usages - only report very short windows
|
|
270
|
+
for (const usage of this.twapUsages) {
|
|
271
|
+
// Tighten: only report if window is dangerously short (< 10 minutes)
|
|
272
|
+
// 10-30 minute windows are marginal, not critical
|
|
273
|
+
if (usage.windowSeconds !== null && usage.windowSeconds < 600) {
|
|
274
|
+
this.reportShortTWAPWindow(usage);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Analyze Chainlink usages - only report missing critical checks
|
|
279
|
+
for (const feed of this.chainlinkFeeds) {
|
|
280
|
+
this.analyzeChainlinkVulnerabilities(feed);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
analyzeChainlinkVulnerabilities(feed) {
|
|
285
|
+
const { checks, node, code } = feed;
|
|
286
|
+
|
|
287
|
+
// Critical: Using deprecated latestAnswer
|
|
288
|
+
if (checks.usesLatestAnswer) {
|
|
289
|
+
this.addFinding({
|
|
290
|
+
title: 'Deprecated Chainlink latestAnswer() Usage',
|
|
291
|
+
description: `Function '${feed.function}' uses deprecated latestAnswer(). This function is deprecated and returns stale data without any indication of staleness. Use latestRoundData() instead.`,
|
|
292
|
+
location: `Contract: ${this.currentContract}, Function: ${feed.function}`,
|
|
293
|
+
line: node.loc?.start?.line || 0,
|
|
294
|
+
column: node.loc?.start?.column || 0,
|
|
295
|
+
code: code.substring(0, 200),
|
|
296
|
+
severity: 'CRITICAL',
|
|
297
|
+
confidence: 'HIGH',
|
|
298
|
+
exploitable: true,
|
|
299
|
+
exploitabilityScore: 85,
|
|
300
|
+
attackVector: 'stale-price-exploitation',
|
|
301
|
+
recommendation: `Replace latestAnswer() with latestRoundData() and add proper checks:
|
|
302
|
+
(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
|
|
303
|
+
require(answer > 0, "Invalid price");
|
|
304
|
+
require(updatedAt > block.timestamp - MAX_DELAY, "Stale price");
|
|
305
|
+
require(answeredInRound >= roundId, "Stale round");`,
|
|
306
|
+
references: [
|
|
307
|
+
'https://docs.chain.link/data-feeds/api-reference'
|
|
308
|
+
]
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// High: No staleness check
|
|
313
|
+
if (!checks.checksStaleness && !checks.checksTimestamp) {
|
|
314
|
+
this.addFinding({
|
|
315
|
+
title: 'Missing Chainlink Staleness Check',
|
|
316
|
+
description: `Function '${feed.function}' uses Chainlink price feed without checking for stale data. During network congestion or oracle issues, stale prices can be exploited for arbitrage or liquidation attacks.`,
|
|
317
|
+
location: `Contract: ${this.currentContract}, Function: ${feed.function}`,
|
|
318
|
+
line: node.loc?.start?.line || 0,
|
|
319
|
+
column: node.loc?.start?.column || 0,
|
|
320
|
+
code: code.substring(0, 200),
|
|
321
|
+
severity: 'HIGH',
|
|
322
|
+
confidence: 'HIGH',
|
|
323
|
+
exploitable: true,
|
|
324
|
+
exploitabilityScore: 75,
|
|
325
|
+
attackVector: 'stale-price-exploitation',
|
|
326
|
+
recommendation: `Add staleness check:
|
|
327
|
+
require(block.timestamp - updatedAt < MAX_STALENESS, "Price is stale");
|
|
328
|
+
|
|
329
|
+
Where MAX_STALENESS is set based on the feed's heartbeat (e.g., 3600 for 1-hour heartbeat feeds).`,
|
|
330
|
+
references: [
|
|
331
|
+
'https://docs.chain.link/data-feeds#check-the-timestamp-of-the-latest-answer'
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// High: No answer validation
|
|
337
|
+
if (!checks.checksAnswer) {
|
|
338
|
+
this.addFinding({
|
|
339
|
+
title: 'Missing Chainlink Answer Validation',
|
|
340
|
+
description: `Function '${feed.function}' uses Chainlink price without validating the answer. A zero or negative price (which can occur during circuit breakers) could cause division by zero or incorrect calculations.`,
|
|
341
|
+
location: `Contract: ${this.currentContract}, Function: ${feed.function}`,
|
|
342
|
+
line: node.loc?.start?.line || 0,
|
|
343
|
+
column: node.loc?.start?.column || 0,
|
|
344
|
+
code: code.substring(0, 200),
|
|
345
|
+
severity: 'HIGH',
|
|
346
|
+
confidence: 'HIGH',
|
|
347
|
+
exploitable: true,
|
|
348
|
+
exploitabilityScore: 70,
|
|
349
|
+
attackVector: 'invalid-price-exploitation',
|
|
350
|
+
recommendation: `Add answer validation:
|
|
351
|
+
require(answer > 0, "Invalid price from oracle");`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Medium: No L2 sequencer check (for L2 deployments)
|
|
356
|
+
if (!checks.checksSequencer && this.mightBeL2Contract()) {
|
|
357
|
+
this.addFinding({
|
|
358
|
+
title: 'Missing L2 Sequencer Uptime Check',
|
|
359
|
+
description: `Contract may be deployed on L2 but doesn't check sequencer uptime. When the L2 sequencer is down, price feeds are not updated and can become stale without the staleness check detecting it.`,
|
|
360
|
+
location: `Contract: ${this.currentContract}, Function: ${feed.function}`,
|
|
361
|
+
line: node.loc?.start?.line || 0,
|
|
362
|
+
column: node.loc?.start?.column || 0,
|
|
363
|
+
code: code.substring(0, 200),
|
|
364
|
+
severity: 'MEDIUM',
|
|
365
|
+
confidence: 'LOW',
|
|
366
|
+
exploitable: true,
|
|
367
|
+
exploitabilityScore: 50,
|
|
368
|
+
attackVector: 'l2-sequencer-exploitation',
|
|
369
|
+
recommendation: `For L2 deployments, add sequencer uptime feed check:
|
|
370
|
+
(, int256 answer, uint256 startedAt,,) = sequencerUptimeFeed.latestRoundData();
|
|
371
|
+
bool isSequencerUp = answer == 0;
|
|
372
|
+
require(isSequencerUp, "Sequencer is down");
|
|
373
|
+
require(block.timestamp - startedAt > GRACE_PERIOD, "Grace period not over");`,
|
|
374
|
+
references: [
|
|
375
|
+
'https://docs.chain.link/data-feeds/l2-sequencer-feeds'
|
|
376
|
+
]
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
reportSpotPriceVulnerability(usage) {
|
|
382
|
+
this.addFinding({
|
|
383
|
+
title: 'Flash Loan Exploitable Spot Price Oracle',
|
|
384
|
+
description: `Function '${usage.function}' uses ${usage.source} for price calculation which can be manipulated within a single transaction via flash loans.
|
|
385
|
+
|
|
386
|
+
Attack scenario:
|
|
387
|
+
1. Attacker takes flash loan of large amount
|
|
388
|
+
2. Manipulates spot price (e.g., large swap shifts reserves)
|
|
389
|
+
3. Executes target function at manipulated price
|
|
390
|
+
4. Reverts manipulation and repays flash loan
|
|
391
|
+
5. Profits from price discrepancy
|
|
392
|
+
|
|
393
|
+
This pattern has caused losses exceeding $300M in DeFi exploits.`,
|
|
394
|
+
location: `Contract: ${this.currentContract}, Function: ${usage.function}`,
|
|
395
|
+
line: usage.node.loc?.start?.line || 0,
|
|
396
|
+
column: usage.node.loc?.start?.column || 0,
|
|
397
|
+
code: usage.code.substring(0, 300),
|
|
398
|
+
severity: 'CRITICAL',
|
|
399
|
+
confidence: 'HIGH',
|
|
400
|
+
exploitable: true,
|
|
401
|
+
exploitabilityScore: 95,
|
|
402
|
+
attackVector: 'flash-loan-oracle-manipulation',
|
|
403
|
+
recommendation: `Replace spot price with manipulation-resistant oracle:
|
|
404
|
+
1. Use Chainlink price feeds for standard assets
|
|
405
|
+
2. Use Uniswap V3 TWAP with 30+ minute window
|
|
406
|
+
3. Use multiple oracle sources and take median
|
|
407
|
+
4. Add price deviation checks against reference oracle
|
|
408
|
+
|
|
409
|
+
Example TWAP usage:
|
|
410
|
+
uint32[] memory secondsAgos = new uint32[](2);
|
|
411
|
+
secondsAgos[0] = 1800; // 30 minutes ago
|
|
412
|
+
secondsAgos[1] = 0; // now
|
|
413
|
+
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
|
|
414
|
+
int24 avgTick = int24((tickCumulatives[1] - tickCumulatives[0]) / 1800);`,
|
|
415
|
+
references: [
|
|
416
|
+
'https://samczsun.com/so-you-want-to-use-a-price-oracle/',
|
|
417
|
+
'https://docs.uniswap.org/concepts/protocol/oracle'
|
|
418
|
+
],
|
|
419
|
+
foundryPoC: this.generateFlashLoanOraclePoC(usage)
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
reportHighRiskOracleUsage(usage) {
|
|
424
|
+
this.addFinding({
|
|
425
|
+
title: 'High-Risk Oracle Price Source',
|
|
426
|
+
description: `Function '${usage.function}' uses ${usage.source} which is manipulable under certain conditions. While not as easily exploited as spot prices, this can still be manipulated with sufficient capital or over multiple blocks.`,
|
|
427
|
+
location: `Contract: ${this.currentContract}, Function: ${usage.function}`,
|
|
428
|
+
line: usage.node.loc?.start?.line || 0,
|
|
429
|
+
column: usage.node.loc?.start?.column || 0,
|
|
430
|
+
code: usage.code.substring(0, 200),
|
|
431
|
+
severity: 'HIGH',
|
|
432
|
+
confidence: 'MEDIUM',
|
|
433
|
+
exploitable: true,
|
|
434
|
+
exploitabilityScore: 65,
|
|
435
|
+
attackVector: 'oracle-manipulation',
|
|
436
|
+
recommendation: `Add additional safeguards:
|
|
437
|
+
1. Compare against secondary oracle source
|
|
438
|
+
2. Add price deviation bounds (e.g., max 5% change)
|
|
439
|
+
3. Add time delay for price-sensitive operations
|
|
440
|
+
4. Consider using Chainlink for primary price source`
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
reportShortTWAPWindow(usage) {
|
|
445
|
+
this.addFinding({
|
|
446
|
+
title: 'TWAP Window Too Short',
|
|
447
|
+
description: `Function '${usage.function}' uses TWAP with window of ${usage.windowSeconds || 'unknown'} seconds. Short TWAP windows can be manipulated by sustaining price manipulation across multiple blocks.
|
|
448
|
+
|
|
449
|
+
A well-funded attacker can manipulate prices for several minutes, making short TWAPs vulnerable.`,
|
|
450
|
+
location: `Contract: ${this.currentContract}, Function: ${usage.function}`,
|
|
451
|
+
line: usage.node.loc?.start?.line || 0,
|
|
452
|
+
column: usage.node.loc?.start?.column || 0,
|
|
453
|
+
code: usage.code.substring(0, 200),
|
|
454
|
+
severity: 'HIGH',
|
|
455
|
+
confidence: 'MEDIUM',
|
|
456
|
+
exploitable: true,
|
|
457
|
+
exploitabilityScore: 60,
|
|
458
|
+
attackVector: 'twap-manipulation',
|
|
459
|
+
recommendation: `Increase TWAP window to at least 30 minutes (1800 seconds):
|
|
460
|
+
uint32 constant TWAP_WINDOW = 1800; // 30 minutes
|
|
461
|
+
|
|
462
|
+
Longer windows are more resistant to manipulation but less responsive to legitimate price changes. Consider the tradeoff based on your use case.`
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
mightBeL2Contract() {
|
|
467
|
+
// Heuristics to detect if this might be an L2 contract
|
|
468
|
+
const l2Indicators = [
|
|
469
|
+
/optimism|arbitrum|polygon|zksync|base|scroll|linea/i,
|
|
470
|
+
/L2|layer2|layer-2/i,
|
|
471
|
+
];
|
|
472
|
+
return l2Indicators.some(p => p.test(this.sourceCode));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
generateFlashLoanOraclePoC(usage) {
|
|
476
|
+
return `// SPDX-License-Identifier: MIT
|
|
477
|
+
pragma solidity ^0.8.0;
|
|
478
|
+
|
|
479
|
+
import "forge-std/Test.sol";
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Proof of Concept: Flash Loan Oracle Manipulation
|
|
483
|
+
* Exploits ${usage.source} usage in ${usage.function}
|
|
484
|
+
*/
|
|
485
|
+
contract FlashLoanOracleExploit is Test {
|
|
486
|
+
// Interfaces would be defined here
|
|
487
|
+
// ILendingPool flashLoanProvider;
|
|
488
|
+
// IUniswapV2Pair targetPair;
|
|
489
|
+
// IVulnerableProtocol target;
|
|
490
|
+
|
|
491
|
+
function testExploit() public {
|
|
492
|
+
// Step 1: Take flash loan
|
|
493
|
+
uint256 loanAmount = 1_000_000e18; // Large amount
|
|
494
|
+
// flashLoanProvider.flashLoan(address(this), token, loanAmount, "");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function executeOperation(
|
|
498
|
+
address asset,
|
|
499
|
+
uint256 amount,
|
|
500
|
+
uint256 premium,
|
|
501
|
+
address initiator,
|
|
502
|
+
bytes calldata params
|
|
503
|
+
) external returns (bool) {
|
|
504
|
+
// Step 2: Manipulate the oracle
|
|
505
|
+
// For Uniswap V2 reserves manipulation:
|
|
506
|
+
// token.transfer(address(targetPair), amount);
|
|
507
|
+
// targetPair.sync(); // Updates reserves
|
|
508
|
+
|
|
509
|
+
// Step 3: Execute vulnerable function at manipulated price
|
|
510
|
+
// target.${usage.function}(...);
|
|
511
|
+
|
|
512
|
+
// Step 4: Reverse manipulation
|
|
513
|
+
// (swap back or let it auto-correct)
|
|
514
|
+
|
|
515
|
+
// Step 5: Repay flash loan
|
|
516
|
+
// IERC20(asset).transfer(msg.sender, amount + premium);
|
|
517
|
+
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
module.exports = OracleManipulationDetector;
|