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,494 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Timestamp Dependence Detector (Enhanced)
|
|
5
|
+
* Detects dangerous reliance on block.timestamp with time-scale awareness
|
|
6
|
+
* to reduce false positives for legitimate long-term calculations.
|
|
7
|
+
*/
|
|
8
|
+
class TimestampDependenceDetector extends BaseDetector {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(
|
|
11
|
+
'Timestamp Dependence',
|
|
12
|
+
'Detects dangerous reliance on block.timestamp',
|
|
13
|
+
'MEDIUM'
|
|
14
|
+
);
|
|
15
|
+
this.currentContract = null;
|
|
16
|
+
this.currentFunction = null;
|
|
17
|
+
this.currentFunctionNode = null;
|
|
18
|
+
this.timestampUsages = [];
|
|
19
|
+
|
|
20
|
+
// Miner manipulation window in seconds (~15 seconds)
|
|
21
|
+
this.MANIPULATION_WINDOW = 15;
|
|
22
|
+
// Safe threshold - windows longer than this have minimal manipulation impact
|
|
23
|
+
this.SAFE_WINDOW_THRESHOLD = 3600; // 1 hour in seconds
|
|
24
|
+
// Very safe threshold - negligible impact
|
|
25
|
+
this.VERY_SAFE_THRESHOLD = 86400; // 1 day in seconds
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
29
|
+
this.findings = [];
|
|
30
|
+
this.ast = ast;
|
|
31
|
+
this.sourceCode = sourceCode;
|
|
32
|
+
this.fileName = fileName;
|
|
33
|
+
this.sourceLines = sourceCode.split('\n');
|
|
34
|
+
this.timestampUsages = [];
|
|
35
|
+
|
|
36
|
+
this.traverse(ast);
|
|
37
|
+
|
|
38
|
+
return this.findings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
visitContractDefinition(node) {
|
|
42
|
+
this.currentContract = node.name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visitFunctionDefinition(node) {
|
|
46
|
+
this.currentFunction = node.name || 'constructor';
|
|
47
|
+
this.currentFunctionNode = node;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
visitMemberAccess(node) {
|
|
51
|
+
// Check for block.timestamp usage
|
|
52
|
+
if (node.expression &&
|
|
53
|
+
node.expression.type === 'Identifier' &&
|
|
54
|
+
node.expression.name === 'block') {
|
|
55
|
+
|
|
56
|
+
if (node.memberName === 'timestamp' || node.memberName === 'number') {
|
|
57
|
+
this.analyzeTimestampUsage(node);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Also check for 'now' keyword (deprecated but still used)
|
|
62
|
+
if (node.memberName === 'now') {
|
|
63
|
+
this.reportNowUsage(node);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
visitIdentifier(node) {
|
|
68
|
+
// Check for direct 'now' usage (deprecated)
|
|
69
|
+
if (node.name === 'now') {
|
|
70
|
+
this.reportNowUsage(node);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
analyzeTimestampUsage(node) {
|
|
75
|
+
// Get surrounding context
|
|
76
|
+
const parentContext = this.getParentContext(node);
|
|
77
|
+
const code = this.getCodeSnippet(node.loc);
|
|
78
|
+
|
|
79
|
+
// 1. CRITICAL: Randomness based on timestamp - always dangerous
|
|
80
|
+
if (this.isUsedForRandomness(parentContext)) {
|
|
81
|
+
this.reportTimestampRandomness(node);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. HIGH: Equality comparison with timestamp - usually a bug
|
|
86
|
+
if (this.isEqualityComparison(parentContext)) {
|
|
87
|
+
this.reportTimestampEquality(node);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Analyze time windows with scale awareness
|
|
92
|
+
const timeWindowAnalysis = this.analyzeTimeWindow(parentContext);
|
|
93
|
+
|
|
94
|
+
if (timeWindowAnalysis.hasTimeWindow) {
|
|
95
|
+
if (timeWindowAnalysis.isCritical) {
|
|
96
|
+
this.reportCriticalTimeWindow(node, timeWindowAnalysis);
|
|
97
|
+
return;
|
|
98
|
+
} else if (timeWindowAnalysis.isRisky) {
|
|
99
|
+
this.reportShortTimeWindow(node, timeWindowAnalysis);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Safe time windows (>1 hour) are not reported
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Financial calculations - analyze impact scale
|
|
106
|
+
if (this.isFinancialCalculation(parentContext)) {
|
|
107
|
+
const financialRisk = this.analyzeFinancialImpact(parentContext);
|
|
108
|
+
if (financialRisk.isSignificant) {
|
|
109
|
+
this.reportTimestampFinancial(node, financialRisk);
|
|
110
|
+
}
|
|
111
|
+
// Don't report low-impact financial calculations
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 5. Access control - analyze the lock duration
|
|
116
|
+
if (this.isAccessControl(parentContext)) {
|
|
117
|
+
const accessRisk = this.analyzeAccessControlRisk(parentContext);
|
|
118
|
+
if (accessRisk.isExploitable) {
|
|
119
|
+
this.reportTimestampAccessControl(node, accessRisk);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getParentContext(node) {
|
|
126
|
+
// Get lines around the timestamp usage
|
|
127
|
+
if (!node.loc) return '';
|
|
128
|
+
const startLine = Math.max(0, node.loc.start.line - 5);
|
|
129
|
+
const endLine = Math.min(this.sourceLines.length, node.loc.start.line + 5);
|
|
130
|
+
return this.sourceLines.slice(startLine, endLine).join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isUsedForRandomness(context) {
|
|
134
|
+
const randomKeywords = ['random', 'seed', 'entropy'];
|
|
135
|
+
const contextLower = context.toLowerCase();
|
|
136
|
+
|
|
137
|
+
// Must be in a randomness context AND use hashing
|
|
138
|
+
const hasRandomContext = randomKeywords.some(kw => contextLower.includes(kw));
|
|
139
|
+
const hasHashing = /keccak256|sha3|hash/i.test(context);
|
|
140
|
+
|
|
141
|
+
return hasRandomContext || (hasHashing && /timestamp|block\.number/i.test(context));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
isEqualityComparison(context) {
|
|
145
|
+
// Check for exact equality with timestamp (common bug)
|
|
146
|
+
// Look for patterns like: timestamp == value or value == timestamp
|
|
147
|
+
const exactEquality = /block\.timestamp\s*==|==\s*block\.timestamp/i.test(context);
|
|
148
|
+
return exactEquality;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Analyze time window with awareness of manipulation impact
|
|
153
|
+
*/
|
|
154
|
+
analyzeTimeWindow(context) {
|
|
155
|
+
const result = {
|
|
156
|
+
hasTimeWindow: false,
|
|
157
|
+
windowSize: 0,
|
|
158
|
+
isCritical: false,
|
|
159
|
+
isRisky: false,
|
|
160
|
+
reason: ''
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Extract time constants - handle Solidity time units
|
|
164
|
+
const timeUnits = {
|
|
165
|
+
'seconds': 1,
|
|
166
|
+
'second': 1,
|
|
167
|
+
'minutes': 60,
|
|
168
|
+
'minute': 60,
|
|
169
|
+
'hours': 3600,
|
|
170
|
+
'hour': 3600,
|
|
171
|
+
'days': 86400,
|
|
172
|
+
'day': 86400,
|
|
173
|
+
'weeks': 604800,
|
|
174
|
+
'week': 604800
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Pattern: number followed by optional time unit
|
|
178
|
+
const timePattern = /(\d+)\s*(seconds?|minutes?|hours?|days?|weeks?)?/gi;
|
|
179
|
+
let match;
|
|
180
|
+
let smallestWindow = Infinity;
|
|
181
|
+
|
|
182
|
+
while ((match = timePattern.exec(context)) !== null) {
|
|
183
|
+
const value = parseInt(match[1]);
|
|
184
|
+
const unit = match[2] ? match[2].toLowerCase() : 'seconds';
|
|
185
|
+
const multiplier = timeUnits[unit] || 1;
|
|
186
|
+
const windowInSeconds = value * multiplier;
|
|
187
|
+
|
|
188
|
+
if (windowInSeconds > 0 && windowInSeconds < smallestWindow) {
|
|
189
|
+
smallestWindow = windowInSeconds;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (smallestWindow !== Infinity) {
|
|
194
|
+
result.hasTimeWindow = true;
|
|
195
|
+
result.windowSize = smallestWindow;
|
|
196
|
+
|
|
197
|
+
// Calculate manipulation impact
|
|
198
|
+
const manipulationPercent = (this.MANIPULATION_WINDOW / smallestWindow) * 100;
|
|
199
|
+
|
|
200
|
+
if (smallestWindow < 60) {
|
|
201
|
+
// Less than 1 minute - critical
|
|
202
|
+
result.isCritical = true;
|
|
203
|
+
result.reason = `Time window of ${smallestWindow}s can be significantly manipulated (${manipulationPercent.toFixed(1)}% of window)`;
|
|
204
|
+
} else if (smallestWindow < this.SAFE_WINDOW_THRESHOLD) {
|
|
205
|
+
// Between 1 minute and 1 hour - risky
|
|
206
|
+
result.isRisky = true;
|
|
207
|
+
result.reason = `Time window of ${this.formatDuration(smallestWindow)} has ${manipulationPercent.toFixed(1)}% manipulation risk`;
|
|
208
|
+
}
|
|
209
|
+
// >= 1 hour is considered safe (less than 0.4% manipulation)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Analyze if financial calculation has meaningful impact from timestamp manipulation
|
|
217
|
+
*/
|
|
218
|
+
analyzeFinancialImpact(context) {
|
|
219
|
+
const result = {
|
|
220
|
+
isSignificant: false,
|
|
221
|
+
impactReason: '',
|
|
222
|
+
confidence: 'LOW'
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const contextLower = context.toLowerCase();
|
|
226
|
+
|
|
227
|
+
// Check if this is a long-term calculation (days/weeks/years)
|
|
228
|
+
const longTermPatterns = /days?|weeks?|years?|annual|monthly/i;
|
|
229
|
+
const isLongTerm = longTermPatterns.test(context);
|
|
230
|
+
|
|
231
|
+
// Check for high-value operations
|
|
232
|
+
const highValueOps = /liquidat|collateral|borrow|lend|stake|unstake/i;
|
|
233
|
+
const isHighValue = highValueOps.test(context);
|
|
234
|
+
|
|
235
|
+
// Short-term, high-frequency rewards are risky
|
|
236
|
+
const shortTermRewards = /reward|claim|harvest/i.test(context) &&
|
|
237
|
+
/block|second|minute/i.test(context);
|
|
238
|
+
|
|
239
|
+
if (shortTermRewards) {
|
|
240
|
+
result.isSignificant = true;
|
|
241
|
+
result.impactReason = 'Short-term reward calculation where 15s manipulation could affect immediate payouts';
|
|
242
|
+
result.confidence = 'MEDIUM';
|
|
243
|
+
} else if (isHighValue && !isLongTerm) {
|
|
244
|
+
result.isSignificant = true;
|
|
245
|
+
result.impactReason = 'High-value financial operation without long-term averaging';
|
|
246
|
+
result.confidence = 'MEDIUM';
|
|
247
|
+
}
|
|
248
|
+
// Long-term APY/interest calculations are not significant
|
|
249
|
+
// 15 seconds out of 365 days = 0.0000005% impact
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Analyze access control timestamp risk
|
|
256
|
+
*/
|
|
257
|
+
analyzeAccessControlRisk(context) {
|
|
258
|
+
const result = {
|
|
259
|
+
isExploitable: false,
|
|
260
|
+
reason: '',
|
|
261
|
+
severity: 'MEDIUM'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Check for timelock patterns
|
|
265
|
+
const timelockPatterns = /unlock|lock|delay|cliff|vesting/i;
|
|
266
|
+
const hasTimelock = timelockPatterns.test(context);
|
|
267
|
+
|
|
268
|
+
// Extract the timelock duration if possible
|
|
269
|
+
const timeAnalysis = this.analyzeTimeWindow(context);
|
|
270
|
+
|
|
271
|
+
if (hasTimelock && timeAnalysis.hasTimeWindow) {
|
|
272
|
+
if (timeAnalysis.windowSize < 3600) {
|
|
273
|
+
// Less than 1 hour timelock - exploitable
|
|
274
|
+
result.isExploitable = true;
|
|
275
|
+
result.reason = `Timelock of ${this.formatDuration(timeAnalysis.windowSize)} can be bypassed via ~15s timestamp manipulation`;
|
|
276
|
+
result.severity = 'HIGH';
|
|
277
|
+
} else if (timeAnalysis.windowSize < 86400) {
|
|
278
|
+
// 1-24 hour timelock - minor risk
|
|
279
|
+
result.isExploitable = true;
|
|
280
|
+
result.reason = `Timelock could be slightly shortened via timestamp manipulation (minor impact)`;
|
|
281
|
+
result.severity = 'LOW';
|
|
282
|
+
}
|
|
283
|
+
// Timelocks >= 1 day are effectively safe
|
|
284
|
+
} else if (!hasTimelock) {
|
|
285
|
+
// Generic timestamp access control without clear timelock
|
|
286
|
+
// Only flag if it appears to be a short window check
|
|
287
|
+
if (timeAnalysis.isCritical || timeAnalysis.isRisky) {
|
|
288
|
+
result.isExploitable = true;
|
|
289
|
+
result.reason = 'Short-duration timestamp check can be manipulated';
|
|
290
|
+
result.severity = 'MEDIUM';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
isFinancialCalculation(context) {
|
|
298
|
+
const financialKeywords = ['interest', 'rate', 'reward', 'yield', 'apr', 'apy', 'compound', 'stake', 'earn'];
|
|
299
|
+
const contextLower = context.toLowerCase();
|
|
300
|
+
return financialKeywords.some(kw => contextLower.includes(kw));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
isAccessControl(context) {
|
|
304
|
+
// More specific: must be in a require/if with comparison
|
|
305
|
+
const hasCondition = /require\s*\(|if\s*\(/i.test(context);
|
|
306
|
+
const hasComparison = /[<>=]/.test(context);
|
|
307
|
+
return hasCondition && hasComparison;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
formatDuration(seconds) {
|
|
311
|
+
if (seconds < 60) return `${seconds} seconds`;
|
|
312
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)} minutes`;
|
|
313
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)} hours`;
|
|
314
|
+
return `${Math.round(seconds / 86400)} days`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
reportNowUsage(node) {
|
|
318
|
+
this.addFinding({
|
|
319
|
+
title: 'Deprecated "now" Keyword Usage',
|
|
320
|
+
description: `Using deprecated 'now' keyword which is alias for block.timestamp. This should be replaced with block.timestamp for clarity.`,
|
|
321
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
322
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
323
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
324
|
+
code: this.getCodeSnippet(node.loc),
|
|
325
|
+
severity: 'INFO',
|
|
326
|
+
confidence: 'HIGH',
|
|
327
|
+
exploitable: false,
|
|
328
|
+
recommendation: 'Replace "now" with "block.timestamp" for Solidity 0.7+ compatibility.',
|
|
329
|
+
references: [
|
|
330
|
+
'https://docs.soliditylang.org/en/latest/units-and-global-variables.html'
|
|
331
|
+
]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
reportTimestampRandomness(node) {
|
|
336
|
+
this.addFinding({
|
|
337
|
+
title: 'Timestamp Used for Randomness',
|
|
338
|
+
description: `Block timestamp or block number used in randomness generation. Miners/validators can manipulate block.timestamp within ~15 seconds to influence outcomes.`,
|
|
339
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
340
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
341
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
342
|
+
code: this.getCodeSnippet(node.loc),
|
|
343
|
+
severity: 'CRITICAL',
|
|
344
|
+
confidence: 'HIGH',
|
|
345
|
+
exploitable: true,
|
|
346
|
+
exploitabilityScore: 90,
|
|
347
|
+
attackVector: 'miner-timestamp-manipulation',
|
|
348
|
+
recommendation: 'Use Chainlink VRF for verifiable randomness. Never use block.timestamp, block.number, or blockhash for randomness in applications with economic value.',
|
|
349
|
+
references: [
|
|
350
|
+
'https://swcregistry.io/docs/SWC-120',
|
|
351
|
+
'https://docs.chain.link/vrf'
|
|
352
|
+
],
|
|
353
|
+
foundryPoC: this.generateRandomnessPoC()
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
reportTimestampEquality(node) {
|
|
358
|
+
this.addFinding({
|
|
359
|
+
title: 'Timestamp Equality Comparison',
|
|
360
|
+
description: `Exact equality comparison (==) with block.timestamp. This condition is unreliable - the exact timestamp value is unpredictable and may never match.`,
|
|
361
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
362
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
363
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
364
|
+
code: this.getCodeSnippet(node.loc),
|
|
365
|
+
severity: 'HIGH',
|
|
366
|
+
confidence: 'HIGH',
|
|
367
|
+
exploitable: false,
|
|
368
|
+
exploitabilityScore: 0,
|
|
369
|
+
recommendation: 'Use range comparisons (>=, <=) instead of equality. Example: require(block.timestamp >= deadline) instead of require(block.timestamp == deadline).',
|
|
370
|
+
references: [
|
|
371
|
+
'https://swcregistry.io/docs/SWC-116'
|
|
372
|
+
]
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
reportCriticalTimeWindow(node, analysis) {
|
|
377
|
+
this.addFinding({
|
|
378
|
+
title: 'Critical: Very Short Time Window',
|
|
379
|
+
description: `${analysis.reason}. A miner can manipulate timestamps by ~15 seconds, which is a significant portion of this window.`,
|
|
380
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
381
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
382
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
383
|
+
code: this.getCodeSnippet(node.loc),
|
|
384
|
+
severity: 'HIGH',
|
|
385
|
+
confidence: 'HIGH',
|
|
386
|
+
exploitable: true,
|
|
387
|
+
exploitabilityScore: 75,
|
|
388
|
+
attackVector: 'timestamp-manipulation',
|
|
389
|
+
recommendation: 'Increase the time window to at least 15 minutes (900 seconds) for security-critical operations. Consider using block.number with average block time for more predictable timing.',
|
|
390
|
+
references: [
|
|
391
|
+
'https://swcregistry.io/docs/SWC-116'
|
|
392
|
+
]
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
reportShortTimeWindow(node, analysis) {
|
|
397
|
+
this.addFinding({
|
|
398
|
+
title: 'Short Timestamp-Based Time Window',
|
|
399
|
+
description: `${analysis.reason}. While not critical, this window is short enough that timestamp manipulation could have measurable impact.`,
|
|
400
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
401
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
402
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
403
|
+
code: this.getCodeSnippet(node.loc),
|
|
404
|
+
severity: 'MEDIUM',
|
|
405
|
+
confidence: 'MEDIUM',
|
|
406
|
+
exploitable: true,
|
|
407
|
+
exploitabilityScore: 40,
|
|
408
|
+
recommendation: `Consider increasing the time window if this controls high-value operations. Current window: ${this.formatDuration(analysis.windowSize)}.`,
|
|
409
|
+
references: [
|
|
410
|
+
'https://swcregistry.io/docs/SWC-116'
|
|
411
|
+
]
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
reportTimestampFinancial(node, financialRisk) {
|
|
416
|
+
this.addFinding({
|
|
417
|
+
title: 'Timestamp in Short-Term Financial Calculation',
|
|
418
|
+
description: `${financialRisk.impactReason}. Long-term calculations (APY over months/years) are generally safe as 15s manipulation is negligible.`,
|
|
419
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
420
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
421
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
422
|
+
code: this.getCodeSnippet(node.loc),
|
|
423
|
+
severity: 'MEDIUM',
|
|
424
|
+
confidence: financialRisk.confidence,
|
|
425
|
+
exploitable: true,
|
|
426
|
+
exploitabilityScore: 35,
|
|
427
|
+
recommendation: 'For short-term reward calculations, consider using block numbers or adding minimum time thresholds. Long-term interest/APY calculations using timestamps are acceptable.',
|
|
428
|
+
references: [
|
|
429
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/'
|
|
430
|
+
]
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
reportTimestampAccessControl(node, accessRisk) {
|
|
435
|
+
this.addFinding({
|
|
436
|
+
title: 'Timestamp-Based Access Control',
|
|
437
|
+
description: `${accessRisk.reason}. This could allow early access or bypass of time-restricted functionality.`,
|
|
438
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
439
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
440
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
441
|
+
code: this.getCodeSnippet(node.loc),
|
|
442
|
+
severity: accessRisk.severity,
|
|
443
|
+
confidence: 'MEDIUM',
|
|
444
|
+
exploitable: true,
|
|
445
|
+
exploitabilityScore: accessRisk.severity === 'HIGH' ? 60 : 25,
|
|
446
|
+
recommendation: 'Use timelocks of at least 1 day for critical operations. Add buffer time (e.g., 1 hour) to account for timestamp manipulation.',
|
|
447
|
+
references: [
|
|
448
|
+
'https://swcregistry.io/docs/SWC-116'
|
|
449
|
+
]
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
generateRandomnessPoC() {
|
|
454
|
+
return `// SPDX-License-Identifier: MIT
|
|
455
|
+
pragma solidity ^0.8.0;
|
|
456
|
+
|
|
457
|
+
import "forge-std/Test.sol";
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Proof of Concept: Timestamp-Based Randomness Manipulation
|
|
461
|
+
* Demonstrates how miners can manipulate block.timestamp for favorable outcomes
|
|
462
|
+
*/
|
|
463
|
+
contract TimestampRandomnessExploit is Test {
|
|
464
|
+
// Target contract using timestamp for randomness
|
|
465
|
+
address constant TARGET = address(0);
|
|
466
|
+
|
|
467
|
+
function testExploit() public {
|
|
468
|
+
// Simulate miner ability to set timestamp within valid range
|
|
469
|
+
// Miners can adjust timestamp up to ~15 seconds from previous block
|
|
470
|
+
|
|
471
|
+
uint256 desiredOutcome = 1; // The outcome we want
|
|
472
|
+
uint256 baseTimestamp = block.timestamp;
|
|
473
|
+
|
|
474
|
+
// Try different timestamps within manipulation window
|
|
475
|
+
for (uint256 delta = 0; delta <= 15; delta++) {
|
|
476
|
+
vm.warp(baseTimestamp + delta);
|
|
477
|
+
|
|
478
|
+
// Calculate what the "random" result would be
|
|
479
|
+
// uint256 result = uint256(keccak256(abi.encodePacked(block.timestamp))) % N;
|
|
480
|
+
|
|
481
|
+
// if (result == desiredOutcome) {
|
|
482
|
+
// // Found favorable timestamp, proceed with attack
|
|
483
|
+
// break;
|
|
484
|
+
// }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Call target contract at manipulated timestamp
|
|
488
|
+
// TARGET.randomFunction();
|
|
489
|
+
}
|
|
490
|
+
}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
module.exports = TimestampDependenceDetector;
|