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,697 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-Contract Reentrancy Detector
|
|
5
|
+
* Detects complex reentrancy attacks involving multiple contracts
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Reentrancy across multiple contracts in same transaction
|
|
9
|
+
* - State changes in one contract affecting another
|
|
10
|
+
* - External calls that can trigger state changes in related contracts
|
|
11
|
+
* - Missing reentrancy guards in cross-contract interactions
|
|
12
|
+
* - Reentrancy via delegatecall patterns
|
|
13
|
+
* - Reentrancy in multi-step protocols
|
|
14
|
+
*/
|
|
15
|
+
class CrossContractReentrancyDetector extends BaseDetector {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(
|
|
18
|
+
'Cross-Contract Reentrancy',
|
|
19
|
+
'Detects reentrancy attacks involving multiple contracts and complex state interactions',
|
|
20
|
+
'CRITICAL'
|
|
21
|
+
);
|
|
22
|
+
this.currentContract = null;
|
|
23
|
+
this.externalCalls = [];
|
|
24
|
+
this.stateChanges = [];
|
|
25
|
+
this.contractInteractions = new Map(); // contract -> [functions called]
|
|
26
|
+
this.cfg = null;
|
|
27
|
+
this.dataFlow = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
31
|
+
this.findings = [];
|
|
32
|
+
this.ast = ast;
|
|
33
|
+
this.sourceCode = sourceCode;
|
|
34
|
+
this.fileName = fileName;
|
|
35
|
+
this.sourceLines = sourceCode.split('\n');
|
|
36
|
+
this.cfg = cfg;
|
|
37
|
+
this.dataFlow = dataFlow;
|
|
38
|
+
|
|
39
|
+
this.traverse(ast);
|
|
40
|
+
|
|
41
|
+
// Post-traversal analysis
|
|
42
|
+
this.analyzeCrossContractReentrancy();
|
|
43
|
+
|
|
44
|
+
return this.findings;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
visitContractDefinition(node) {
|
|
48
|
+
this.currentContract = node.name;
|
|
49
|
+
this.externalCalls = [];
|
|
50
|
+
this.stateChanges = [];
|
|
51
|
+
this.contractInteractions = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
visitFunctionDefinition(node) {
|
|
55
|
+
const funcName = node.name || '';
|
|
56
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
57
|
+
|
|
58
|
+
// Skip private/internal functions (not directly exploitable)
|
|
59
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reset per-function tracking
|
|
64
|
+
const functionExternalCalls = [];
|
|
65
|
+
const functionStateChanges = [];
|
|
66
|
+
const functionInteractions = new Set();
|
|
67
|
+
|
|
68
|
+
// Analyze function body - use both AST and code analysis
|
|
69
|
+
this.analyzeFunctionBody(node, functionExternalCalls, functionStateChanges, functionInteractions);
|
|
70
|
+
|
|
71
|
+
// Also do direct AST traversal for interface calls
|
|
72
|
+
if (node.body && node.body.statements) {
|
|
73
|
+
this.analyzeFunctionBodyAST(node.body.statements, functionExternalCalls, functionStateChanges, functionInteractions);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Store for cross-contract analysis
|
|
77
|
+
if (functionExternalCalls.length > 0 || functionStateChanges.length > 0) {
|
|
78
|
+
this.externalCalls.push({
|
|
79
|
+
function: funcName,
|
|
80
|
+
calls: functionExternalCalls,
|
|
81
|
+
stateChanges: functionStateChanges,
|
|
82
|
+
interactions: Array.from(functionInteractions),
|
|
83
|
+
node: node
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Direct AST analysis for interface calls
|
|
90
|
+
*/
|
|
91
|
+
analyzeFunctionBodyAST(statements, externalCalls, stateChanges, interactions) {
|
|
92
|
+
for (let i = 0; i < statements.length; i++) {
|
|
93
|
+
const stmt = statements[i];
|
|
94
|
+
|
|
95
|
+
// Check for interface calls: contractA.withdraw() pattern
|
|
96
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression) {
|
|
97
|
+
const expr = stmt.expression;
|
|
98
|
+
if (expr.type === 'FunctionCall' && expr.expression && expr.expression.type === 'MemberAccess') {
|
|
99
|
+
const memberAccess = expr.expression;
|
|
100
|
+
if (memberAccess.expression && memberAccess.expression.type === 'Identifier') {
|
|
101
|
+
const varName = memberAccess.expression.name;
|
|
102
|
+
// Check if it's a contract variable (not a built-in)
|
|
103
|
+
if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(varName.toLowerCase())) {
|
|
104
|
+
// This is an interface call
|
|
105
|
+
if (!externalCalls.some(c => c.index === i)) {
|
|
106
|
+
const stmtCode = this.getCodeSnippet(stmt.loc);
|
|
107
|
+
externalCalls.push({
|
|
108
|
+
type: 'external',
|
|
109
|
+
target: varName,
|
|
110
|
+
statement: stmt,
|
|
111
|
+
code: stmtCode || `${varName}.${memberAccess.memberName}()`,
|
|
112
|
+
index: i
|
|
113
|
+
});
|
|
114
|
+
interactions.add(varName);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for state changes: balances[user] = value
|
|
122
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'Assignment') {
|
|
123
|
+
const assignment = stmt.expression;
|
|
124
|
+
if (assignment.left) {
|
|
125
|
+
// Check if left side is a mapping access or state variable
|
|
126
|
+
const leftCode = this.getCodeSnippet(assignment.left.loc);
|
|
127
|
+
if (leftCode && (leftCode.includes('balances[') || leftCode.includes('['))) {
|
|
128
|
+
if (!stateChanges.some(s => s.index === i)) {
|
|
129
|
+
stateChanges.push({
|
|
130
|
+
type: 'balance',
|
|
131
|
+
statement: stmt,
|
|
132
|
+
code: this.getCodeSnippet(stmt.loc),
|
|
133
|
+
index: i
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Analyze function body for external calls and state changes
|
|
144
|
+
*/
|
|
145
|
+
analyzeFunctionBody(node, externalCalls, stateChanges, interactions) {
|
|
146
|
+
if (!node.body || !node.body.statements) return;
|
|
147
|
+
|
|
148
|
+
const statements = node.body.statements;
|
|
149
|
+
let externalCallBeforeStateUpdate = false;
|
|
150
|
+
let stateUpdateAfterExternalCall = false;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < statements.length; i++) {
|
|
153
|
+
const stmt = statements[i];
|
|
154
|
+
let stmtCode = this.getCodeSnippet(stmt.loc);
|
|
155
|
+
|
|
156
|
+
// If code snippet is too short, try to get more context from surrounding lines
|
|
157
|
+
if (stmtCode.length < 10 && stmt.loc) {
|
|
158
|
+
const lineNum = stmt.loc.start.line;
|
|
159
|
+
if (lineNum > 0 && lineNum <= this.sourceLines.length) {
|
|
160
|
+
stmtCode = this.sourceLines[lineNum - 1] || stmtCode;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const stmtCodeLower = stmtCode.toLowerCase();
|
|
165
|
+
|
|
166
|
+
// Detect external calls
|
|
167
|
+
if (this.isExternalCall(stmt, stmtCode)) {
|
|
168
|
+
const callTarget = this.getCallTarget(stmtCode);
|
|
169
|
+
externalCalls.push({
|
|
170
|
+
type: this.getCallType(stmtCode),
|
|
171
|
+
target: callTarget,
|
|
172
|
+
statement: stmt,
|
|
173
|
+
code: stmtCode,
|
|
174
|
+
index: i
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Track which contract is being called
|
|
178
|
+
if (callTarget) {
|
|
179
|
+
interactions.add(callTarget);
|
|
180
|
+
} else {
|
|
181
|
+
// Even if we can't extract the target, mark as external call
|
|
182
|
+
interactions.add('external contract');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
externalCallBeforeStateUpdate = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Also check AST structure directly for interface calls (e.g., contractA.withdraw())
|
|
189
|
+
// This is a more reliable way to detect interface calls
|
|
190
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression) {
|
|
191
|
+
const expr = stmt.expression;
|
|
192
|
+
if (expr.type === 'FunctionCall' && expr.expression) {
|
|
193
|
+
// Check if it's a member access (variable.function())
|
|
194
|
+
if (expr.expression.type === 'MemberAccess') {
|
|
195
|
+
const memberAccess = expr.expression;
|
|
196
|
+
// Check if the base is an identifier (contract variable)
|
|
197
|
+
if (memberAccess.expression && memberAccess.expression.type === 'Identifier') {
|
|
198
|
+
const varName = memberAccess.expression.name;
|
|
199
|
+
// Check if it's a contract variable (not a built-in)
|
|
200
|
+
if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(varName.toLowerCase())) {
|
|
201
|
+
// This is an interface call - add it if not already added
|
|
202
|
+
if (!externalCalls.some(c => c.index === i)) {
|
|
203
|
+
externalCalls.push({
|
|
204
|
+
type: 'external',
|
|
205
|
+
target: varName,
|
|
206
|
+
statement: stmt,
|
|
207
|
+
code: stmtCode,
|
|
208
|
+
index: i
|
|
209
|
+
});
|
|
210
|
+
interactions.add(varName);
|
|
211
|
+
externalCallBeforeStateUpdate = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Detect state changes
|
|
220
|
+
if (this.isStateChange(stmt, stmtCode)) {
|
|
221
|
+
stateChanges.push({
|
|
222
|
+
type: this.getStateChangeType(stmtCode),
|
|
223
|
+
statement: stmt,
|
|
224
|
+
code: stmtCode,
|
|
225
|
+
index: i
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// If we had an external call before, this is a reentrancy pattern
|
|
229
|
+
if (externalCallBeforeStateUpdate) {
|
|
230
|
+
stateUpdateAfterExternalCall = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check for cross-contract reentrancy pattern
|
|
236
|
+
// Report if there's at least one external call and state change after it
|
|
237
|
+
if (externalCalls.length > 0 && stateChanges.length > 0) {
|
|
238
|
+
// Check if any state change happens after an external call
|
|
239
|
+
const hasStateAfterCall = stateChanges.some(stateChange => {
|
|
240
|
+
return externalCalls.some(call => stateChange.index > call.index);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (hasStateAfterCall) {
|
|
244
|
+
this.checkCrossContractReentrancy(
|
|
245
|
+
node,
|
|
246
|
+
externalCalls,
|
|
247
|
+
stateChanges,
|
|
248
|
+
interactions
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check for cross-contract reentrancy vulnerabilities
|
|
256
|
+
*/
|
|
257
|
+
checkCrossContractReentrancy(node, externalCalls, stateChanges, interactions) {
|
|
258
|
+
const funcName = node.name || '';
|
|
259
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
260
|
+
|
|
261
|
+
// Check if function has reentrancy guard
|
|
262
|
+
const hasReentrancyGuard = this.hasReentrancyGuard(funcCode, node);
|
|
263
|
+
|
|
264
|
+
// Check for cross-contract patterns
|
|
265
|
+
// Report if there's at least one external call and state update after it
|
|
266
|
+
if (externalCalls.length > 0 && stateChanges.length > 0) {
|
|
267
|
+
// Check if state is updated after external call
|
|
268
|
+
const hasStateAfterCall = stateChanges.some(stateChange => {
|
|
269
|
+
return externalCalls.some(call => stateChange.index > call.index);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (hasStateAfterCall && !hasReentrancyGuard) {
|
|
273
|
+
// Check if there are multiple contracts or just one
|
|
274
|
+
const contractList = interactions.size > 0 ? Array.from(interactions).join(', ') : 'external contract(s)';
|
|
275
|
+
const isMultiContract = interactions.size > 1;
|
|
276
|
+
|
|
277
|
+
this.addFinding({
|
|
278
|
+
title: isMultiContract ? 'Cross-Contract Reentrancy Vulnerability' : 'Reentrancy Vulnerability (External Call Before State Update)',
|
|
279
|
+
description: `Function '${funcName}' makes external calls to ${contractList} before updating state. An attacker can exploit this by having the called contract call back into this function while state is still inconsistent.`,
|
|
280
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
281
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
282
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
283
|
+
code: this.getCodeSnippet(node.loc),
|
|
284
|
+
severity: 'CRITICAL',
|
|
285
|
+
confidence: 'HIGH',
|
|
286
|
+
exploitable: true,
|
|
287
|
+
exploitabilityScore: 90,
|
|
288
|
+
attackVector: 'cross-contract-reentrancy',
|
|
289
|
+
recommendation: 'Apply reentrancy guard (nonReentrant modifier) or use Checks-Effects-Interactions pattern. Update all state before making external calls. Consider using internal functions for state updates.',
|
|
290
|
+
references: [
|
|
291
|
+
'https://swcregistry.io/docs/SWC-107',
|
|
292
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/',
|
|
293
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard'
|
|
294
|
+
],
|
|
295
|
+
foundryPoC: this.generateCrossContractReentrancyPoC(this.currentContract, funcName, Array.from(interactions))
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check for delegatecall reentrancy
|
|
301
|
+
const hasDelegatecall = externalCalls.some(call => call.type === 'delegatecall');
|
|
302
|
+
if (hasDelegatecall && !hasReentrancyGuard) {
|
|
303
|
+
this.addFinding({
|
|
304
|
+
title: 'Delegatecall Reentrancy Vulnerability',
|
|
305
|
+
description: `Function '${funcName}' uses delegatecall which can be exploited for reentrancy. The called contract can call back into this contract with elevated privileges.`,
|
|
306
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
307
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
308
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
309
|
+
code: this.getCodeSnippet(node.loc),
|
|
310
|
+
severity: 'CRITICAL',
|
|
311
|
+
confidence: 'HIGH',
|
|
312
|
+
exploitable: true,
|
|
313
|
+
exploitabilityScore: 95,
|
|
314
|
+
attackVector: 'delegatecall-reentrancy',
|
|
315
|
+
recommendation: 'Never use delegatecall with user-controlled addresses. If delegatecall is necessary, apply strict reentrancy guards and validate the target contract.',
|
|
316
|
+
references: [
|
|
317
|
+
'https://swcregistry.io/docs/SWC-112',
|
|
318
|
+
'https://swcregistry.io/docs/SWC-107'
|
|
319
|
+
],
|
|
320
|
+
foundryPoC: this.generateDelegatecallReentrancyPoC(this.currentContract, funcName)
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check for state dependency across contracts
|
|
325
|
+
if (this.hasStateDependency(externalCalls, stateChanges)) {
|
|
326
|
+
if (!hasReentrancyGuard) {
|
|
327
|
+
this.addFinding({
|
|
328
|
+
title: 'State-Dependent Cross-Contract Reentrancy',
|
|
329
|
+
description: `Function '${funcName}' reads state from one contract and writes to another, creating a reentrancy vector. An attacker can manipulate the state between the read and write.`,
|
|
330
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
331
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
332
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
333
|
+
code: this.getCodeSnippet(node.loc),
|
|
334
|
+
severity: 'CRITICAL',
|
|
335
|
+
confidence: 'MEDIUM',
|
|
336
|
+
exploitable: true,
|
|
337
|
+
exploitabilityScore: 85,
|
|
338
|
+
attackVector: 'state-dependent-reentrancy',
|
|
339
|
+
recommendation: 'Cache state values before external calls. Update all state before making external calls. Use internal functions to separate state updates from external interactions.',
|
|
340
|
+
references: [
|
|
341
|
+
'https://swcregistry.io/docs/SWC-107',
|
|
342
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/'
|
|
343
|
+
]
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if statement is an external call
|
|
351
|
+
*/
|
|
352
|
+
isExternalCall(stmt, code) {
|
|
353
|
+
const codeLower = code.toLowerCase();
|
|
354
|
+
|
|
355
|
+
// External call patterns
|
|
356
|
+
const callPatterns = [
|
|
357
|
+
/\.call\s*\(/i,
|
|
358
|
+
/\.delegatecall\s*\(/i,
|
|
359
|
+
/\.send\s*\(/i,
|
|
360
|
+
/\.transfer\s*\(/i,
|
|
361
|
+
/\.callcode\s*\(/i,
|
|
362
|
+
/external\s+contract/i,
|
|
363
|
+
/interface\s+\w+\s*\(/i
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
// Check for call patterns in code first (most reliable)
|
|
367
|
+
if (callPatterns.some(pattern => pattern.test(code))) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check for interface/contract calls (e.g., contractA.withdraw(), contractB.deposit())
|
|
372
|
+
// These are function calls on contract variables
|
|
373
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression) {
|
|
374
|
+
const expr = stmt.expression;
|
|
375
|
+
|
|
376
|
+
// Direct function call on a variable (interface call)
|
|
377
|
+
if (expr.type === 'FunctionCall' && expr.expression) {
|
|
378
|
+
// Check if it's a member access (contract.method())
|
|
379
|
+
if (expr.expression.type === 'MemberAccess') {
|
|
380
|
+
// Check if the base is an identifier (variable name)
|
|
381
|
+
if (expr.expression.expression && expr.expression.expression.type === 'Identifier') {
|
|
382
|
+
const varName = expr.expression.expression.name;
|
|
383
|
+
// Skip built-in variables
|
|
384
|
+
if (!['msg', 'tx', 'block', 'this', 'address', 'abi'].includes(varName.toLowerCase())) {
|
|
385
|
+
// This is likely an external contract call
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Even without identifier check, member access with function call is likely external
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Also check for interface calls by looking at the code directly
|
|
396
|
+
// Pattern: variableName.functionName() where variableName is a contract interface
|
|
397
|
+
// This catches patterns like: contractA.withdraw(), contractB.deposit{value: ...}()
|
|
398
|
+
// Match: contractA.withdraw() or contractB.deposit{value: ...}()
|
|
399
|
+
const interfaceCallPattern = /(\w+)\.(\w+)\s*[\{\(]/;
|
|
400
|
+
if (interfaceCallPattern.test(code)) {
|
|
401
|
+
const match = code.match(interfaceCallPattern);
|
|
402
|
+
if (match && match[1]) {
|
|
403
|
+
const varName = match[1].toLowerCase();
|
|
404
|
+
// Skip built-in variables and common Solidity keywords
|
|
405
|
+
if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string', 'uint', 'int', 'bool', 'mapping', 'array'].includes(varName)) {
|
|
406
|
+
// Check if it looks like a contract variable (camelCase, not a type)
|
|
407
|
+
// Contract variables typically start with lowercase and are not Solidity types
|
|
408
|
+
if (varName[0] === varName[0].toLowerCase() && varName.length > 1) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get type of external call
|
|
420
|
+
*/
|
|
421
|
+
getCallType(code) {
|
|
422
|
+
const codeLower = code.toLowerCase();
|
|
423
|
+
if (codeLower.includes('delegatecall')) return 'delegatecall';
|
|
424
|
+
if (codeLower.includes('.call(')) return 'call';
|
|
425
|
+
if (codeLower.includes('.send(')) return 'send';
|
|
426
|
+
if (codeLower.includes('.transfer(')) return 'transfer';
|
|
427
|
+
return 'external';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Extract call target from code
|
|
432
|
+
*/
|
|
433
|
+
getCallTarget(code) {
|
|
434
|
+
// Try to extract contract/address being called
|
|
435
|
+
// Pattern: contractVariable.function() or address.call()
|
|
436
|
+
const patterns = [
|
|
437
|
+
/(\w+)\.call\s*\(/i,
|
|
438
|
+
/(\w+)\.delegatecall\s*\(/i,
|
|
439
|
+
/(\w+)\.transfer\s*\(/i,
|
|
440
|
+
/(\w+)\.send\s*\(/i,
|
|
441
|
+
/(\w+)\.\w+\s*\{/i, // contractVariable.function{value: ...}() - interface calls with value
|
|
442
|
+
/(\w+)\.\w+\s*\(/i // contractVariable.function() - interface calls
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
// Try patterns in order - more specific first
|
|
446
|
+
for (const pattern of patterns) {
|
|
447
|
+
const match = code.match(pattern);
|
|
448
|
+
if (match && match[1]) {
|
|
449
|
+
const target = match[1];
|
|
450
|
+
const targetLower = target.toLowerCase();
|
|
451
|
+
// Skip common keywords and built-ins
|
|
452
|
+
if (!['msg', 'tx', 'block', 'this', 'address', 'abi', 'bytes', 'string'].includes(targetLower)) {
|
|
453
|
+
// Check if it looks like a contract variable
|
|
454
|
+
// Contract variables are typically camelCase
|
|
455
|
+
if (target[0] === target[0].toLowerCase()) {
|
|
456
|
+
return target;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Check if statement is a state change
|
|
467
|
+
*/
|
|
468
|
+
isStateChange(stmt, code) {
|
|
469
|
+
const codeLower = code.toLowerCase();
|
|
470
|
+
|
|
471
|
+
// Skip variable declarations (local variables)
|
|
472
|
+
if (stmt.type === 'VariableDeclarationStatement') {
|
|
473
|
+
// Only count if it's a state variable assignment
|
|
474
|
+
// State variables are typically declared at contract level, not in functions
|
|
475
|
+
// But we can check if it's modifying a mapping or state variable
|
|
476
|
+
if (codeLower.includes('balances[') || codeLower.includes('allowance[') ||
|
|
477
|
+
codeLower.includes('mapping[') || codeLower.match(/\w+\[.*\]\s*=/)) {
|
|
478
|
+
// This is modifying a state variable through mapping
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false; // Local variable declaration, not a state change
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// State change patterns - assignments to state variables
|
|
485
|
+
const stateChangePatterns = [
|
|
486
|
+
/balances\[.*\]\s*=/i, // balances[user] = ...
|
|
487
|
+
/allowance\[.*\]\s*=/i, // allowance[from][spender] = ...
|
|
488
|
+
/mapping\[.*\]\s*=/i, // mapping assignments
|
|
489
|
+
/\+\+/, // Increment
|
|
490
|
+
/--/, // Decrement
|
|
491
|
+
/\+\s*=/, // Add assign
|
|
492
|
+
/-\s*=/ // Subtract assign
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
// Check for state variable assignments (ExpressionStatement with Assignment)
|
|
496
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression) {
|
|
497
|
+
const expr = stmt.expression;
|
|
498
|
+
if (expr.type === 'Assignment') {
|
|
499
|
+
// Check if left side is a state variable (mapping, storage variable)
|
|
500
|
+
const leftSide = this.getCodeSnippet(expr.left ? expr.left.loc : null);
|
|
501
|
+
if (leftSide && (leftSide.includes('balances') || leftSide.includes('allowance') ||
|
|
502
|
+
leftSide.includes('mapping') || stateChangePatterns.some(p => p.test(leftSide)))) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (expr.type === 'UnaryOperation') {
|
|
507
|
+
// ++ or -- operations
|
|
508
|
+
return /\+\+|--/.test(code);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get type of state change
|
|
517
|
+
*/
|
|
518
|
+
getStateChangeType(code) {
|
|
519
|
+
const codeLower = code.toLowerCase();
|
|
520
|
+
if (codeLower.includes('balance')) return 'balance';
|
|
521
|
+
if (codeLower.includes('mapping')) return 'mapping';
|
|
522
|
+
if (codeLower.includes('array')) return 'array';
|
|
523
|
+
return 'state';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Check if function has reentrancy guard
|
|
528
|
+
*/
|
|
529
|
+
hasReentrancyGuard(code, node) {
|
|
530
|
+
const codeLower = code.toLowerCase();
|
|
531
|
+
|
|
532
|
+
// Check for nonReentrant modifier
|
|
533
|
+
if (node.modifiers) {
|
|
534
|
+
const hasNonReentrant = node.modifiers.some(m =>
|
|
535
|
+
m.name && m.name.toLowerCase().includes('nonreentrant')
|
|
536
|
+
);
|
|
537
|
+
if (hasNonReentrant) return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Check for reentrancy guard patterns
|
|
541
|
+
const guardPatterns = [
|
|
542
|
+
/nonReentrant/i,
|
|
543
|
+
/reentrancyGuard/i,
|
|
544
|
+
/_status\s*==\s*_NOT_ENTERED/i,
|
|
545
|
+
/require\s*\(\s*.*reentrant/i
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
return guardPatterns.some(pattern => pattern.test(codeLower));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Check for state dependency across contracts
|
|
553
|
+
*/
|
|
554
|
+
hasStateDependency(externalCalls, stateChanges) {
|
|
555
|
+
// If we read from one contract and write to another, that's a dependency
|
|
556
|
+
if (externalCalls.length > 0 && stateChanges.length > 0) {
|
|
557
|
+
// Check if external calls read state
|
|
558
|
+
const readsState = externalCalls.some(call => {
|
|
559
|
+
const callCode = call.code.toLowerCase();
|
|
560
|
+
return callCode.includes('balance') ||
|
|
561
|
+
callCode.includes('get') ||
|
|
562
|
+
callCode.includes('view') ||
|
|
563
|
+
callCode.includes('read');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
return readsState;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Post-traversal analysis
|
|
574
|
+
*/
|
|
575
|
+
analyzeCrossContractReentrancy() {
|
|
576
|
+
// Analyze interactions between contracts
|
|
577
|
+
if (this.externalCalls.length === 0) return;
|
|
578
|
+
|
|
579
|
+
// Group by function
|
|
580
|
+
const functionGroups = new Map();
|
|
581
|
+
this.externalCalls.forEach(callInfo => {
|
|
582
|
+
if (!functionGroups.has(callInfo.function)) {
|
|
583
|
+
functionGroups.set(callInfo.function, []);
|
|
584
|
+
}
|
|
585
|
+
functionGroups.get(callInfo.function).push(callInfo);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Check each function for cross-contract patterns
|
|
589
|
+
functionGroups.forEach((callInfos, funcName) => {
|
|
590
|
+
const allInteractions = new Set();
|
|
591
|
+
callInfos.forEach(info => {
|
|
592
|
+
info.interactions.forEach(interaction => allInteractions.add(interaction));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
if (allInteractions.size > 1) {
|
|
596
|
+
// Multiple contracts - potential cross-contract reentrancy
|
|
597
|
+
// Already handled in checkCrossContractReentrancy
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Generate Foundry PoC for cross-contract reentrancy
|
|
604
|
+
*/
|
|
605
|
+
generateCrossContractReentrancyPoC(contractName, funcName, interactions) {
|
|
606
|
+
return `// SPDX-License-Identifier: MIT
|
|
607
|
+
pragma solidity ^0.8.0;
|
|
608
|
+
|
|
609
|
+
import "forge-std/Test.sol";
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Proof of Concept: Cross-Contract Reentrancy Attack
|
|
613
|
+
* Target: ${contractName}.${funcName}()
|
|
614
|
+
* Attack Vector: Reentrancy via multiple contracts
|
|
615
|
+
*/
|
|
616
|
+
contract CrossContractReentrancyExploit is Test {
|
|
617
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
618
|
+
address constant CONTRACT_A = address(0); // ${interactions[0] || 'ContractA'}
|
|
619
|
+
address constant CONTRACT_B = address(0); // ${interactions[1] || 'ContractB'}
|
|
620
|
+
|
|
621
|
+
AttackerContract attacker;
|
|
622
|
+
|
|
623
|
+
function setUp() public {
|
|
624
|
+
attacker = new AttackerContract();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function testExploit() public {
|
|
628
|
+
// 1. Setup: Attacker has funds in both contracts
|
|
629
|
+
// 2. Call vulnerable function which interacts with Contract A
|
|
630
|
+
// ${contractName}(TARGET).${funcName}(...);
|
|
631
|
+
|
|
632
|
+
// 3. Contract A's callback triggers interaction with Contract B
|
|
633
|
+
// 4. Contract B's callback re-enters ${contractName}.${funcName}()
|
|
634
|
+
// 5. State is inconsistent, attacker benefits
|
|
635
|
+
|
|
636
|
+
// Assert exploit succeeded
|
|
637
|
+
// assertGt(attacker.balance, initialBalance);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
contract AttackerContract {
|
|
642
|
+
function onCallback() external {
|
|
643
|
+
// Re-enter target contract
|
|
644
|
+
// Or trigger another contract to re-enter
|
|
645
|
+
}
|
|
646
|
+
}`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Generate Foundry PoC for delegatecall reentrancy
|
|
651
|
+
*/
|
|
652
|
+
generateDelegatecallReentrancyPoC(contractName, funcName) {
|
|
653
|
+
return `// SPDX-License-Identifier: MIT
|
|
654
|
+
pragma solidity ^0.8.0;
|
|
655
|
+
|
|
656
|
+
import "forge-std/Test.sol";
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Proof of Concept: Delegatecall Reentrancy Attack
|
|
660
|
+
* Target: ${contractName}.${funcName}()
|
|
661
|
+
* Attack Vector: Reentrancy via delegatecall
|
|
662
|
+
*/
|
|
663
|
+
contract DelegatecallReentrancyExploit is Test {
|
|
664
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
665
|
+
MaliciousImplementation maliciousImpl;
|
|
666
|
+
|
|
667
|
+
function setUp() public {
|
|
668
|
+
maliciousImpl = new MaliciousImplementation();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function testExploit() public {
|
|
672
|
+
// 1. Deploy malicious implementation
|
|
673
|
+
// 2. Call ${funcName}() which delegatecalls to malicious contract
|
|
674
|
+
// ${contractName}(TARGET).${funcName}(address(maliciousImpl), ...);
|
|
675
|
+
|
|
676
|
+
// 3. Malicious contract executes in target's context
|
|
677
|
+
// 4. Malicious contract calls back into target
|
|
678
|
+
// 5. Reentrancy occurs with elevated privileges
|
|
679
|
+
|
|
680
|
+
// Assert exploit succeeded
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
contract MaliciousImplementation {
|
|
685
|
+
address target;
|
|
686
|
+
|
|
687
|
+
function maliciousFunction() external {
|
|
688
|
+
// Execute in target's storage context
|
|
689
|
+
// Call back into target for reentrancy
|
|
690
|
+
// ${contractName}(target).vulnerableFunction();
|
|
691
|
+
}
|
|
692
|
+
}`;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
module.exports = CrossContractReentrancyDetector;
|
|
697
|
+
|