recon-generate 0.0.17 → 0.0.19

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.
@@ -0,0 +1,1295 @@
1
+ "use strict";
2
+ /**
3
+ * CFG Builder - Transforms Solidity AST into Control Flow Graph
4
+ *
5
+ * Key features:
6
+ * - Enumerates ALL paths (for "n total, m covered" tracking)
7
+ * - Each branch creates a CoverageEdge
8
+ * - External calls marked for oracle integration
9
+ * - Supports forward/backward/mid-point solving
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CFGBuilder = void 0;
13
+ const solc_typed_ast_1 = require("solc-typed-ast");
14
+ const types_1 = require("./types");
15
+ const utils_1 = require("../utils");
16
+ const ethers_1 = require("ethers");
17
+ // ============================================================================
18
+ // Main Builder Class
19
+ // ============================================================================
20
+ class CFGBuilder {
21
+ constructor() {
22
+ this.ctx = {
23
+ counter: new Map(),
24
+ types: new Map(),
25
+ blockCounter: 0,
26
+ pathCounter: 0,
27
+ edgeCounter: 0
28
+ };
29
+ this.blocks = new Map();
30
+ this.edges = [];
31
+ this.paths = [];
32
+ this.externalCalls = [];
33
+ this.ternaryEdges = [];
34
+ this.loops = [];
35
+ this.hasKeccak = false;
36
+ this.inUnchecked = false;
37
+ this.currentFunction = '';
38
+ this.contract = null;
39
+ this.ternaryCounter = 0;
40
+ this.loopCounter = 0;
41
+ }
42
+ /**
43
+ * Build CFG for a contract from CallTreeData
44
+ */
45
+ buildContract(contract, callTrees) {
46
+ this.contract = contract;
47
+ const functions = [];
48
+ for (const ctData of callTrees) {
49
+ if (ctData.contract !== contract.name)
50
+ continue;
51
+ const funcCFG = this.buildFunction(ctData.function, ctData.callTree);
52
+ functions.push(funcCFG);
53
+ }
54
+ // Extract state variables with storage slot computation
55
+ // IMPORTANT: Solidity storage layout follows C3 linearization order
56
+ // Parent contract state vars come FIRST, then child contract vars
57
+ // We use forwardLinearization which gives us [child, parent1, parent2, ...]
58
+ // So we process in REVERSE order to get correct slot assignments
59
+ //
60
+ // MUTABILITY:
61
+ // - constant: Compile-time constant, embedded in bytecode, no slot
62
+ // - immutable: Set in constructor, stored in bytecode, no slot
63
+ // - mutable: Regular storage variable, has slot
64
+ const stateVars = [];
65
+ const mappings = [];
66
+ const constants = [];
67
+ const immutables = [];
68
+ let currentSlot = 0;
69
+ // Get all contracts in linearization order and reverse to process parents first
70
+ const linearized = (0, utils_1.forwardLinearization)(contract);
71
+ const contractsInStorageOrder = [...linearized].reverse();
72
+ // Track seen variable names to avoid duplicates from overrides
73
+ const seenVars = new Set();
74
+ for (const c of contractsInStorageOrder) {
75
+ for (const v of c.vStateVariables) {
76
+ // Skip if we've already seen this var name (override)
77
+ if (seenVars.has(v.name))
78
+ continue;
79
+ seenVars.add(v.name);
80
+ const typeStr = v.typeString || 'unknown';
81
+ const contractName = c.name;
82
+ // Handle constant variables - no storage slot, value known at compile time
83
+ if (v.mutability === solc_typed_ast_1.Mutability.Constant) {
84
+ let constValue;
85
+ if (v.vValue) {
86
+ // Try to extract the constant value
87
+ constValue = this.tryExtractConstantValue(v.vValue);
88
+ }
89
+ constants.push({
90
+ name: v.name,
91
+ type: typeStr,
92
+ value: constValue,
93
+ contract: contractName
94
+ });
95
+ continue; // No storage slot for constants
96
+ }
97
+ // Handle immutable variables - no storage slot, set in constructor
98
+ if (v.mutability === solc_typed_ast_1.Mutability.Immutable) {
99
+ immutables.push({
100
+ name: v.name,
101
+ type: typeStr,
102
+ contract: contractName
103
+ });
104
+ continue; // No storage slot for immutables
105
+ }
106
+ // Regular mutable state variables - have storage slots
107
+ if (typeStr.startsWith('mapping')) {
108
+ // Parse mapping(K => V)
109
+ const match = typeStr.match(/mapping\((\w+)\s*=>\s*(.+)\)/);
110
+ if (match) {
111
+ mappings.push({
112
+ name: v.name,
113
+ keyType: match[1],
114
+ valueType: match[2].trim(),
115
+ slot: currentSlot,
116
+ contract: contractName
117
+ });
118
+ }
119
+ currentSlot++; // Mappings use one slot for base
120
+ }
121
+ else if (typeStr.includes('[]')) {
122
+ // Dynamic array - uses one slot for length
123
+ stateVars.push({ name: v.name, type: typeStr, slot: currentSlot, contract: contractName });
124
+ currentSlot++;
125
+ }
126
+ else {
127
+ // Simple type - compute slot based on type size
128
+ const typeSize = this.getTypeSlots(typeStr);
129
+ stateVars.push({ name: v.name, type: typeStr, slot: currentSlot, contract: contractName });
130
+ currentSlot += typeSize;
131
+ }
132
+ }
133
+ }
134
+ // Build variable roles map
135
+ const varRoles = this.buildVarRoles(contract, functions, stateVars, mappings);
136
+ const totalPaths = functions.reduce((sum, f) => sum + f.paths.length, 0);
137
+ const totalEdges = functions.reduce((sum, f) => sum + f.edges.length, 0);
138
+ return {
139
+ name: contract.name,
140
+ solidityVersion: '0.8.x', // TODO: extract from pragma
141
+ stateVars,
142
+ mappings,
143
+ functions,
144
+ totalPaths,
145
+ totalEdges,
146
+ oracle: [],
147
+ varRoles,
148
+ constants,
149
+ immutables
150
+ };
151
+ }
152
+ /**
153
+ * Try to extract a constant value from an expression
154
+ */
155
+ tryExtractConstantValue(expr) {
156
+ if (expr instanceof solc_typed_ast_1.Literal) {
157
+ return expr.value;
158
+ }
159
+ // For more complex constant expressions, we could evaluate them
160
+ // For now, just return the source representation
161
+ try {
162
+ return (0, utils_1.toSource)(expr);
163
+ }
164
+ catch {
165
+ return undefined;
166
+ }
167
+ }
168
+ /**
169
+ * Get number of storage slots a type uses
170
+ */
171
+ getTypeSlots(typeStr) {
172
+ // Most types fit in one slot (32 bytes)
173
+ // Structs and fixed arrays may use more
174
+ if (typeStr.includes('struct')) {
175
+ // Simplified: assume 1 slot per struct (would need ABI for accurate count)
176
+ return 1;
177
+ }
178
+ if (typeStr.match(/\[\d+\]/)) {
179
+ // Fixed array - extract size
180
+ const match = typeStr.match(/\[(\d+)\]/);
181
+ if (match) {
182
+ return parseInt(match[1]);
183
+ }
184
+ }
185
+ return 1;
186
+ }
187
+ /**
188
+ * Build variable roles map from functions and state variables
189
+ */
190
+ buildVarRoles(contract, functions, stateVars, mappings) {
191
+ const roles = new Map();
192
+ // State variables
193
+ for (const sv of stateVars) {
194
+ roles.set(sv.name, {
195
+ name: sv.name,
196
+ baseName: sv.name,
197
+ role: 'state',
198
+ type: sv.type,
199
+ slot: sv.slot
200
+ });
201
+ }
202
+ // Mappings
203
+ for (const m of mappings) {
204
+ roles.set(m.name, {
205
+ name: m.name,
206
+ baseName: m.name,
207
+ role: 'state',
208
+ type: `mapping(${m.keyType} => ${m.valueType})`,
209
+ slot: m.slot,
210
+ isMapping: true,
211
+ keyTypes: [m.keyType]
212
+ });
213
+ }
214
+ // Global variables
215
+ const globals = [
216
+ { name: 'msg_sender', type: 'address' },
217
+ { name: 'msg_value', type: 'uint256' },
218
+ { name: 'msg_data', type: 'bytes' },
219
+ { name: 'block_timestamp', type: 'uint256' },
220
+ { name: 'block_number', type: 'uint256' },
221
+ { name: 'block_coinbase', type: 'address' },
222
+ { name: 'block_difficulty', type: 'uint256' },
223
+ { name: 'block_gaslimit', type: 'uint256' },
224
+ { name: 'block_basefee', type: 'uint256' },
225
+ { name: 'tx_origin', type: 'address' },
226
+ { name: 'tx_gasprice', type: 'uint256' },
227
+ ];
228
+ for (const g of globals) {
229
+ roles.set(g.name, {
230
+ name: g.name,
231
+ baseName: g.name,
232
+ role: 'global',
233
+ type: g.type
234
+ });
235
+ }
236
+ // Function parameters and locals (from functions)
237
+ for (const func of functions) {
238
+ // Parameters are inputs
239
+ for (const p of func.params) {
240
+ // SSA version will be param_1, param_2, etc.
241
+ for (let v = 1; v <= 10; v++) {
242
+ const ssaName = `${p.name}_${v}`;
243
+ roles.set(ssaName, {
244
+ name: ssaName,
245
+ baseName: p.name,
246
+ role: 'input',
247
+ type: p.type
248
+ });
249
+ }
250
+ }
251
+ // Local variables defined in let statements
252
+ for (const block of func.blocks.values()) {
253
+ for (const stmt of block.statements) {
254
+ if (stmt.kind === 'let') {
255
+ const baseName = stmt.name.replace(/_\d+$/, '');
256
+ // Check if it's a state var read or a local
257
+ const isStateVar = stateVars.some(sv => sv.name === baseName) ||
258
+ mappings.some(m => m.name === baseName);
259
+ if (!isStateVar) {
260
+ roles.set(stmt.name, {
261
+ name: stmt.name,
262
+ baseName,
263
+ role: 'local',
264
+ type: stmt.type
265
+ });
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ return roles;
272
+ }
273
+ /**
274
+ * Build CFG for a single function
275
+ */
276
+ buildFunction(func, callTree) {
277
+ // Reset state
278
+ this.ctx = {
279
+ counter: new Map(),
280
+ types: new Map(),
281
+ blockCounter: 0,
282
+ pathCounter: 0,
283
+ edgeCounter: 0
284
+ };
285
+ this.blocks = new Map();
286
+ this.edges = [];
287
+ this.paths = [];
288
+ this.externalCalls = [];
289
+ this.ternaryEdges = [];
290
+ this.loops = [];
291
+ this.ternaryCounter = 0;
292
+ this.loopCounter = 0;
293
+ this.hasKeccak = false;
294
+ this.currentFunction = func.name;
295
+ // Initialize parameters as SSA variables
296
+ const params = [];
297
+ for (const p of func.vParameters.vParameters) {
298
+ const type = p.typeString || 'uint256';
299
+ (0, types_1.freshSSA)(this.ctx, p.name, type);
300
+ params.push({ name: p.name, type });
301
+ }
302
+ // Returns
303
+ const returns = [];
304
+ for (const r of func.vReturnParameters.vParameters) {
305
+ returns.push({ name: r.name || undefined, type: r.typeString || 'uint256' });
306
+ }
307
+ // Build CFG from function body
308
+ const entryBlock = (0, types_1.newBlock)(this.ctx);
309
+ if (func.vBody) {
310
+ const exitBlock = this.processBlock(func.vBody, entryBlock, null);
311
+ // Ensure the exit block has a proper terminator (implicit return if missing)
312
+ const exitBlockData = this.blocks.get(exitBlock);
313
+ if (exitBlockData && exitBlockData.terminator.kind === 'jump' && exitBlockData.terminator.target === '') {
314
+ // Implicit return at end of function
315
+ exitBlockData.terminator = { kind: 'return', values: [] };
316
+ }
317
+ }
318
+ else {
319
+ // Abstract function - single empty block
320
+ this.blocks.set(entryBlock, {
321
+ id: entryBlock,
322
+ statements: [],
323
+ terminator: { kind: 'return', values: [] },
324
+ astIds: [func.id]
325
+ });
326
+ }
327
+ // Enumerate all complete paths
328
+ this.enumeratePaths(entryBlock);
329
+ // Extract internal calls from CallTree
330
+ const internalCalls = this.extractInternalCalls(callTree);
331
+ // Get selector
332
+ const selector = this.computeSelector(func);
333
+ return {
334
+ contract: this.contract.name,
335
+ name: func.name,
336
+ selector,
337
+ visibility: func.visibility,
338
+ params,
339
+ returns,
340
+ blocks: this.blocks,
341
+ entryBlock,
342
+ edges: [...this.edges, ...this.ternaryEdges], // Include ternary edges in main edges
343
+ paths: this.paths,
344
+ externalCalls: this.externalCalls,
345
+ hasKeccak: this.hasKeccak,
346
+ internalCalls,
347
+ modifiers: func.vModifiers.map(m => m.vModifierName.name),
348
+ ternaryEdges: this.ternaryEdges,
349
+ loops: this.loops
350
+ };
351
+ }
352
+ // ==========================================================================
353
+ // Statement Processing
354
+ // ==========================================================================
355
+ /**
356
+ * Process a block of statements, returns exit block ID
357
+ */
358
+ processBlock(block, currentBlockId, exitTarget) {
359
+ const wasUnchecked = this.inUnchecked;
360
+ if (block instanceof solc_typed_ast_1.UncheckedBlock) {
361
+ this.inUnchecked = true;
362
+ }
363
+ let blockId = currentBlockId;
364
+ // Ensure block exists
365
+ if (!this.blocks.has(blockId)) {
366
+ this.blocks.set(blockId, {
367
+ id: blockId,
368
+ statements: [],
369
+ terminator: { kind: 'jump', target: '' },
370
+ astIds: []
371
+ });
372
+ }
373
+ for (const stmt of block.vStatements) {
374
+ blockId = this.processStatement(stmt, blockId);
375
+ }
376
+ // If we have an exit target and the current block hasn't terminated
377
+ const currentBlock = this.blocks.get(blockId);
378
+ if (exitTarget && currentBlock.terminator.kind === 'jump' && currentBlock.terminator.target === '') {
379
+ currentBlock.terminator = { kind: 'jump', target: exitTarget };
380
+ this.addEdge(blockId, exitTarget, { kind: 'bool', value: true }, 'J');
381
+ }
382
+ this.inUnchecked = wasUnchecked;
383
+ return blockId;
384
+ }
385
+ /**
386
+ * Process a single statement, returns the block ID to continue from
387
+ */
388
+ processStatement(stmt, blockId) {
389
+ const block = this.blocks.get(blockId);
390
+ block.astIds.push(stmt.id);
391
+ if (stmt instanceof solc_typed_ast_1.IfStatement) {
392
+ return this.processIf(stmt, blockId);
393
+ }
394
+ if (stmt instanceof solc_typed_ast_1.WhileStatement) {
395
+ return this.processWhile(stmt, blockId);
396
+ }
397
+ if (stmt instanceof solc_typed_ast_1.ForStatement) {
398
+ return this.processFor(stmt, blockId);
399
+ }
400
+ if (stmt instanceof solc_typed_ast_1.Return) {
401
+ return this.processReturn(stmt, blockId);
402
+ }
403
+ if (stmt instanceof solc_typed_ast_1.RevertStatement) {
404
+ block.terminator = { kind: 'revert', reason: this.extractRevertReason(stmt) };
405
+ return blockId;
406
+ }
407
+ if (stmt instanceof solc_typed_ast_1.VariableDeclarationStatement) {
408
+ this.processVarDecl(stmt, block);
409
+ return blockId;
410
+ }
411
+ if (stmt instanceof solc_typed_ast_1.ExpressionStatement) {
412
+ this.processExprStmt(stmt, block);
413
+ return blockId;
414
+ }
415
+ if (stmt instanceof solc_typed_ast_1.Block) {
416
+ return this.processBlock(stmt, blockId, null);
417
+ }
418
+ if (stmt instanceof solc_typed_ast_1.UncheckedBlock) {
419
+ return this.processBlock(stmt, blockId, null);
420
+ }
421
+ if (stmt instanceof solc_typed_ast_1.PlaceholderStatement) {
422
+ block.terminator = { kind: 'placeholder' };
423
+ // Create continuation block
424
+ const nextBlock = (0, types_1.newBlock)(this.ctx);
425
+ this.blocks.set(nextBlock, {
426
+ id: nextBlock,
427
+ statements: [],
428
+ terminator: { kind: 'jump', target: '' },
429
+ astIds: []
430
+ });
431
+ return nextBlock;
432
+ }
433
+ // Unknown statement type - add as comment/skip
434
+ return blockId;
435
+ }
436
+ // ==========================================================================
437
+ // Control Flow
438
+ // ==========================================================================
439
+ processIf(stmt, blockId) {
440
+ const block = this.blocks.get(blockId);
441
+ // Convert condition to symbolic expression
442
+ const cond = this.exprToSym(stmt.vCondition);
443
+ // Create blocks for true/false branches
444
+ const trueBlockId = (0, types_1.newBlock)(this.ctx);
445
+ const falseBlockId = (0, types_1.newBlock)(this.ctx);
446
+ const mergeBlockId = (0, types_1.newBlock)(this.ctx);
447
+ // Initialize blocks
448
+ this.blocks.set(trueBlockId, {
449
+ id: trueBlockId,
450
+ statements: [],
451
+ terminator: { kind: 'jump', target: mergeBlockId },
452
+ astIds: []
453
+ });
454
+ this.blocks.set(falseBlockId, {
455
+ id: falseBlockId,
456
+ statements: [],
457
+ terminator: { kind: 'jump', target: mergeBlockId },
458
+ astIds: []
459
+ });
460
+ this.blocks.set(mergeBlockId, {
461
+ id: mergeBlockId,
462
+ statements: [],
463
+ terminator: { kind: 'jump', target: '' },
464
+ astIds: []
465
+ });
466
+ // Set branch terminator
467
+ block.terminator = { kind: 'branch', cond, trueBlock: trueBlockId, falseBlock: falseBlockId };
468
+ // Add coverage edges
469
+ this.addEdge(blockId, trueBlockId, cond, 'T');
470
+ this.addEdge(blockId, falseBlockId, { kind: 'unop', op: 'not', arg: cond }, 'F');
471
+ // Process true branch - handle both Block and single Statement
472
+ if (stmt.vTrueBody instanceof solc_typed_ast_1.Block || stmt.vTrueBody instanceof solc_typed_ast_1.UncheckedBlock) {
473
+ this.processBlock(stmt.vTrueBody, trueBlockId, mergeBlockId);
474
+ }
475
+ else {
476
+ // Single statement - process directly
477
+ this.processStatement(stmt.vTrueBody, trueBlockId);
478
+ const tb = this.blocks.get(trueBlockId);
479
+ if (tb.terminator.kind === 'jump' && tb.terminator.target === '') {
480
+ tb.terminator = { kind: 'jump', target: mergeBlockId };
481
+ }
482
+ }
483
+ // Process false branch if exists
484
+ if (stmt.vFalseBody) {
485
+ if (stmt.vFalseBody instanceof solc_typed_ast_1.Block || stmt.vFalseBody instanceof solc_typed_ast_1.UncheckedBlock) {
486
+ this.processBlock(stmt.vFalseBody, falseBlockId, mergeBlockId);
487
+ }
488
+ else {
489
+ // Single statement
490
+ this.processStatement(stmt.vFalseBody, falseBlockId);
491
+ const fb = this.blocks.get(falseBlockId);
492
+ if (fb.terminator.kind === 'jump' && fb.terminator.target === '') {
493
+ fb.terminator = { kind: 'jump', target: mergeBlockId };
494
+ }
495
+ }
496
+ }
497
+ else {
498
+ // No else - false block jumps directly to merge
499
+ const fb = this.blocks.get(falseBlockId);
500
+ fb.terminator = { kind: 'jump', target: mergeBlockId };
501
+ }
502
+ return mergeBlockId;
503
+ }
504
+ processWhile(stmt, blockId) {
505
+ const block = this.blocks.get(blockId);
506
+ // Create blocks: header (condition check), body, exit
507
+ const headerBlockId = (0, types_1.newBlock)(this.ctx);
508
+ const bodyBlockId = (0, types_1.newBlock)(this.ctx);
509
+ const exitBlockId = (0, types_1.newBlock)(this.ctx);
510
+ // Jump to header
511
+ block.terminator = { kind: 'jump', target: headerBlockId };
512
+ this.addEdge(blockId, headerBlockId, { kind: 'bool', value: true }, 'J');
513
+ // Header block - condition check
514
+ const cond = this.exprToSym(stmt.vCondition);
515
+ this.blocks.set(headerBlockId, {
516
+ id: headerBlockId,
517
+ statements: [],
518
+ terminator: { kind: 'branch', cond, trueBlock: bodyBlockId, falseBlock: exitBlockId },
519
+ astIds: [stmt.vCondition.id]
520
+ });
521
+ this.addEdge(headerBlockId, bodyBlockId, cond, 'T');
522
+ this.addEdge(headerBlockId, exitBlockId, { kind: 'unop', op: 'not', arg: cond }, 'F');
523
+ // Body block
524
+ this.blocks.set(bodyBlockId, {
525
+ id: bodyBlockId,
526
+ statements: [],
527
+ terminator: { kind: 'jump', target: headerBlockId }, // Loop back
528
+ astIds: []
529
+ });
530
+ // Process body - handle both Block and single Statement
531
+ if (stmt.vBody instanceof solc_typed_ast_1.Block || stmt.vBody instanceof solc_typed_ast_1.UncheckedBlock) {
532
+ this.processBlock(stmt.vBody, bodyBlockId, headerBlockId);
533
+ }
534
+ else {
535
+ this.processStatement(stmt.vBody, bodyBlockId);
536
+ const bb = this.blocks.get(bodyBlockId);
537
+ if (bb.terminator.kind === 'jump' && bb.terminator.target === '') {
538
+ bb.terminator = { kind: 'jump', target: headerBlockId };
539
+ }
540
+ }
541
+ // Exit block
542
+ this.blocks.set(exitBlockId, {
543
+ id: exitBlockId,
544
+ statements: [],
545
+ terminator: { kind: 'jump', target: '' },
546
+ astIds: []
547
+ });
548
+ // Record loop info for symbolic reasoning
549
+ const loopId = `L${this.loopCounter++}`;
550
+ this.loops.push({
551
+ id: loopId,
552
+ headerBlock: headerBlockId,
553
+ exitBlock: exitBlockId,
554
+ condition: cond,
555
+ bodyBlocks: [bodyBlockId]
556
+ });
557
+ return exitBlockId;
558
+ }
559
+ processFor(stmt, blockId) {
560
+ const block = this.blocks.get(blockId);
561
+ // For loop: init; cond; post { body }
562
+ // Try to extract induction variable from init
563
+ let inductionVar;
564
+ let startValue;
565
+ // Process init in current block
566
+ if (stmt.vInitializationExpression) {
567
+ if (stmt.vInitializationExpression instanceof solc_typed_ast_1.VariableDeclarationStatement) {
568
+ const decls = stmt.vInitializationExpression.vDeclarations;
569
+ if (decls.length === 1) {
570
+ inductionVar = (0, types_1.currentSSA)(this.ctx, decls[0].name);
571
+ if (stmt.vInitializationExpression.vInitialValue) {
572
+ startValue = this.exprToSym(stmt.vInitializationExpression.vInitialValue);
573
+ }
574
+ }
575
+ this.processVarDecl(stmt.vInitializationExpression, block);
576
+ // Update inductionVar to the SSA version after processing
577
+ if (decls.length === 1) {
578
+ inductionVar = (0, types_1.currentSSA)(this.ctx, decls[0].name);
579
+ }
580
+ }
581
+ else if (stmt.vInitializationExpression instanceof solc_typed_ast_1.ExpressionStatement) {
582
+ this.processExprStmt(stmt.vInitializationExpression, block);
583
+ }
584
+ }
585
+ // Create blocks
586
+ const headerBlockId = (0, types_1.newBlock)(this.ctx);
587
+ const bodyBlockId = (0, types_1.newBlock)(this.ctx);
588
+ const postBlockId = (0, types_1.newBlock)(this.ctx);
589
+ const exitBlockId = (0, types_1.newBlock)(this.ctx);
590
+ // Jump to header
591
+ block.terminator = { kind: 'jump', target: headerBlockId };
592
+ this.addEdge(blockId, headerBlockId, { kind: 'bool', value: true }, 'J');
593
+ // Header - condition check
594
+ const cond = stmt.vCondition
595
+ ? this.exprToSym(stmt.vCondition)
596
+ : { kind: 'bool', value: true };
597
+ // Try to extract bound from condition (e.g., i < n)
598
+ let bound;
599
+ if (cond.kind === 'binop' && (cond.op === 'lt' || cond.op === 'le')) {
600
+ bound = cond.right;
601
+ }
602
+ this.blocks.set(headerBlockId, {
603
+ id: headerBlockId,
604
+ statements: [],
605
+ terminator: { kind: 'branch', cond, trueBlock: bodyBlockId, falseBlock: exitBlockId },
606
+ astIds: stmt.vCondition ? [stmt.vCondition.id] : []
607
+ });
608
+ this.addEdge(headerBlockId, bodyBlockId, cond, 'T');
609
+ this.addEdge(headerBlockId, exitBlockId, { kind: 'unop', op: 'not', arg: cond }, 'F');
610
+ // Body
611
+ this.blocks.set(bodyBlockId, {
612
+ id: bodyBlockId,
613
+ statements: [],
614
+ terminator: { kind: 'jump', target: postBlockId },
615
+ astIds: []
616
+ });
617
+ // Handle both Block and single statement bodies
618
+ if (stmt.vBody instanceof solc_typed_ast_1.Block || stmt.vBody instanceof solc_typed_ast_1.UncheckedBlock) {
619
+ this.processBlock(stmt.vBody, bodyBlockId, postBlockId);
620
+ }
621
+ else {
622
+ // Single statement body (not a block)
623
+ this.processStatement(stmt.vBody, bodyBlockId);
624
+ // Fix terminator if still empty
625
+ const bb = this.blocks.get(bodyBlockId);
626
+ if (bb.terminator.kind === 'jump' && bb.terminator.target === postBlockId) {
627
+ // Already correct
628
+ }
629
+ else if (bb.terminator.kind === 'jump' && bb.terminator.target === '') {
630
+ bb.terminator = { kind: 'jump', target: postBlockId };
631
+ }
632
+ }
633
+ // Post (loop increment)
634
+ this.blocks.set(postBlockId, {
635
+ id: postBlockId,
636
+ statements: [],
637
+ terminator: { kind: 'jump', target: headerBlockId },
638
+ astIds: []
639
+ });
640
+ if (stmt.vLoopExpression) {
641
+ this.processExprStmt(stmt.vLoopExpression, this.blocks.get(postBlockId));
642
+ }
643
+ // Exit
644
+ this.blocks.set(exitBlockId, {
645
+ id: exitBlockId,
646
+ statements: [],
647
+ terminator: { kind: 'jump', target: '' },
648
+ astIds: []
649
+ });
650
+ // Record loop info for symbolic reasoning
651
+ const loopId = `L${this.loopCounter++}`;
652
+ this.loops.push({
653
+ id: loopId,
654
+ headerBlock: headerBlockId,
655
+ exitBlock: exitBlockId,
656
+ condition: cond,
657
+ inductionVar,
658
+ bound,
659
+ bodyBlocks: [bodyBlockId, postBlockId]
660
+ });
661
+ return exitBlockId;
662
+ }
663
+ processReturn(stmt, blockId) {
664
+ const block = this.blocks.get(blockId);
665
+ const values = [];
666
+ if (stmt.vExpression) {
667
+ if (stmt.vExpression instanceof solc_typed_ast_1.TupleExpression) {
668
+ for (const comp of stmt.vExpression.vComponents) {
669
+ if (comp)
670
+ values.push(this.exprToSym(comp));
671
+ }
672
+ }
673
+ else {
674
+ values.push(this.exprToSym(stmt.vExpression));
675
+ }
676
+ }
677
+ block.terminator = { kind: 'return', values };
678
+ return blockId;
679
+ }
680
+ // ==========================================================================
681
+ // Variable/Expression Processing
682
+ // ==========================================================================
683
+ processVarDecl(stmt, block) {
684
+ const decls = stmt.vDeclarations;
685
+ const init = stmt.vInitialValue;
686
+ if (decls.length === 1 && init) {
687
+ const decl = decls[0];
688
+ const type = decl.typeString || 'uint256';
689
+ const name = (0, types_1.freshSSA)(this.ctx, decl.name, type);
690
+ const expr = this.exprToSym(init);
691
+ block.statements.push({ kind: 'let', name, type, expr });
692
+ }
693
+ else if (decls.length > 1 && init) {
694
+ // Tuple assignment
695
+ for (let i = 0; i < decls.length; i++) {
696
+ const decl = decls[i];
697
+ if (decl) {
698
+ const type = decl.typeString || 'uint256';
699
+ const name = (0, types_1.freshSSA)(this.ctx, decl.name, type);
700
+ // Extract i-th component
701
+ const expr = {
702
+ kind: 'field',
703
+ base: this.exprToSym(init),
704
+ field: `_${i}`
705
+ };
706
+ block.statements.push({ kind: 'let', name, type, expr });
707
+ }
708
+ }
709
+ }
710
+ }
711
+ processExprStmt(stmt, block) {
712
+ const expr = stmt.vExpression;
713
+ if (expr instanceof solc_typed_ast_1.Assignment) {
714
+ this.processAssignment(expr, block);
715
+ }
716
+ else if (expr instanceof solc_typed_ast_1.FunctionCall) {
717
+ this.processFunctionCall(expr, block);
718
+ }
719
+ else if (expr instanceof solc_typed_ast_1.UnaryOperation) {
720
+ // Could be ++/-- which modify state
721
+ this.processUnaryOp(expr, block);
722
+ }
723
+ }
724
+ processAssignment(expr, block) {
725
+ const lhs = expr.vLeftHandSide;
726
+ const rhs = this.exprToSym(expr.vRightHandSide);
727
+ if (lhs instanceof solc_typed_ast_1.Identifier) {
728
+ // Simple variable assignment
729
+ const type = lhs.typeString || 'uint256';
730
+ // For compound assignments (+=, -=, etc.), get the OLD SSA name BEFORE incrementing
731
+ const oldName = (0, types_1.currentSSA)(this.ctx, lhs.name);
732
+ // Now create the new SSA version
733
+ const name = (0, types_1.freshSSA)(this.ctx, lhs.name, type);
734
+ // Handle compound assignments (+=, -=, etc.)
735
+ let value = rhs;
736
+ if (expr.operator !== '=') {
737
+ const op = this.compoundOpToBinOp(expr.operator);
738
+ if (op) {
739
+ value = {
740
+ kind: 'binop',
741
+ op,
742
+ left: { kind: 'var', name: oldName, type }, // Use the OLD version
743
+ right: rhs,
744
+ checked: !this.inUnchecked,
745
+ signed: this.isSigned(type)
746
+ };
747
+ }
748
+ }
749
+ block.statements.push({ kind: 'let', name, type, expr: value });
750
+ }
751
+ else if (lhs instanceof solc_typed_ast_1.IndexAccess) {
752
+ // Array/mapping assignment: arr[idx] = value or arr[idx] += value
753
+ const array = this.exprToSym(lhs.vBaseExpression);
754
+ const index = this.exprToSym(lhs.vIndexExpression);
755
+ // Handle compound assignments (+=, -=, etc.)
756
+ let value = rhs;
757
+ if (expr.operator !== '=') {
758
+ const op = this.compoundOpToBinOp(expr.operator);
759
+ if (op) {
760
+ // For compound assignment, need to read the old value first
761
+ const oldValue = { kind: 'select', array, index };
762
+ value = {
763
+ kind: 'binop',
764
+ op,
765
+ left: oldValue,
766
+ right: rhs,
767
+ checked: !this.inUnchecked,
768
+ signed: this.isSigned(lhs.typeString || 'uint256')
769
+ };
770
+ }
771
+ }
772
+ block.statements.push({ kind: 'store', target: array, index, value });
773
+ }
774
+ else if (lhs instanceof solc_typed_ast_1.MemberAccess) {
775
+ // Struct field or state var via this
776
+ const base = this.exprToSym(lhs.vExpression);
777
+ const index = { kind: 'lit', value: lhs.memberName, type: 'string' };
778
+ // Handle compound assignment (+=, -=, etc.) for member access
779
+ let value = rhs;
780
+ if (expr.operator !== '=') {
781
+ const op = this.compoundOpToBinOp(expr.operator);
782
+ if (op) {
783
+ const oldValue = { kind: 'select', array: base, index };
784
+ value = {
785
+ kind: 'binop',
786
+ op,
787
+ left: oldValue,
788
+ right: rhs,
789
+ checked: !this.inUnchecked,
790
+ signed: this.isSigned(lhs.typeString || 'uint256')
791
+ };
792
+ }
793
+ }
794
+ // Model as store with field name as "index"
795
+ block.statements.push({
796
+ kind: 'store',
797
+ target: base,
798
+ index,
799
+ value
800
+ });
801
+ }
802
+ }
803
+ processFunctionCall(call, block) {
804
+ // Check if it's require/assert
805
+ if (call.vExpression instanceof solc_typed_ast_1.Identifier) {
806
+ const name = call.vExpression.name;
807
+ if (name === 'require' || name === 'assert') {
808
+ const cond = this.exprToSym(call.vArguments[0]);
809
+ const reason = call.vArguments.length > 1
810
+ ? this.extractStringLiteral(call.vArguments[1])
811
+ : undefined;
812
+ // Generate a revert edge ID for tracking
813
+ const revertEdgeId = `${this.currentFunction}_${name}${this.ctx.edgeCounter++}_revert`;
814
+ block.statements.push({ kind: 'assume', cond, reason, revertEdge: revertEdgeId });
815
+ // Add coverage edges for require/assert conditions
816
+ // These allow the solver to target specific require failures
817
+ // T edge: condition is true (continue execution)
818
+ // F edge: condition is false (revert) - this is the revert edge
819
+ this.edges.push({
820
+ id: `${this.currentFunction}_B${block.id.slice(1)}_${name}${this.ctx.edgeCounter - 1}_T`,
821
+ from: block.id,
822
+ to: block.id, // Same block - continue execution
823
+ condition: cond,
824
+ functionName: this.currentFunction,
825
+ astId: call.id
826
+ });
827
+ this.edges.push({
828
+ id: revertEdgeId.replace('_revert', '_F'), // Use consistent naming
829
+ from: block.id,
830
+ to: 'REVERT', // Special marker for revert
831
+ condition: { kind: 'unop', op: 'not', arg: cond },
832
+ functionName: this.currentFunction,
833
+ astId: call.id
834
+ });
835
+ return;
836
+ }
837
+ }
838
+ // Regular function call
839
+ const sym = this.exprToSym(call);
840
+ if (sym.kind === 'call' || sym.kind === 'external_return') {
841
+ block.statements.push({ kind: 'call', expr: sym });
842
+ }
843
+ }
844
+ processUnaryOp(expr, block) {
845
+ // Handle ++/--
846
+ if (expr.operator === '++' || expr.operator === '--') {
847
+ const operand = expr.vSubExpression;
848
+ if (operand instanceof solc_typed_ast_1.Identifier) {
849
+ const type = operand.typeString || 'uint256';
850
+ const oldName = (0, types_1.currentSSA)(this.ctx, operand.name);
851
+ const newName = (0, types_1.freshSSA)(this.ctx, operand.name, type);
852
+ const op = expr.operator === '++' ? 'add' : 'sub';
853
+ block.statements.push({
854
+ kind: 'let',
855
+ name: newName,
856
+ type,
857
+ expr: {
858
+ kind: 'binop',
859
+ op,
860
+ left: { kind: 'var', name: oldName, type },
861
+ right: { kind: 'lit', value: '1', type },
862
+ checked: !this.inUnchecked,
863
+ signed: this.isSigned(type)
864
+ }
865
+ });
866
+ }
867
+ }
868
+ }
869
+ // ==========================================================================
870
+ // Expression to Symbolic
871
+ // ==========================================================================
872
+ exprToSym(expr) {
873
+ if (expr instanceof solc_typed_ast_1.Identifier) {
874
+ const type = expr.typeString || 'uint256';
875
+ return { kind: 'var', name: (0, types_1.currentSSA)(this.ctx, expr.name), type };
876
+ }
877
+ if (expr instanceof solc_typed_ast_1.Literal) {
878
+ const type = expr.typeString || 'uint256';
879
+ const value = expr.value || '0';
880
+ if (type === 'bool') {
881
+ return { kind: 'bool', value: value === 'true' };
882
+ }
883
+ // Convert to hex if numeric
884
+ if (/^\d+$/.test(value)) {
885
+ return { kind: 'lit', value: '0x' + BigInt(value).toString(16), type };
886
+ }
887
+ return { kind: 'lit', value, type };
888
+ }
889
+ if (expr instanceof solc_typed_ast_1.BinaryOperation) {
890
+ const op = this.solOpToBinOp(expr.operator);
891
+ if (!op) {
892
+ return { kind: 'lit', value: '0', type: 'uint256' }; // Unsupported
893
+ }
894
+ return {
895
+ kind: 'binop',
896
+ op,
897
+ left: this.exprToSym(expr.vLeftExpression),
898
+ right: this.exprToSym(expr.vRightExpression),
899
+ checked: !this.inUnchecked,
900
+ signed: this.isSigned(expr.typeString || '')
901
+ };
902
+ }
903
+ if (expr instanceof solc_typed_ast_1.UnaryOperation) {
904
+ const op = this.solOpToUnOp(expr.operator);
905
+ if (op) {
906
+ return { kind: 'unop', op, arg: this.exprToSym(expr.vSubExpression) };
907
+ }
908
+ }
909
+ if (expr instanceof solc_typed_ast_1.IndexAccess) {
910
+ return {
911
+ kind: 'select',
912
+ array: this.exprToSym(expr.vBaseExpression),
913
+ index: this.exprToSym(expr.vIndexExpression)
914
+ };
915
+ }
916
+ if (expr instanceof solc_typed_ast_1.MemberAccess) {
917
+ // Check for msg.sender, block.timestamp, etc.
918
+ if (expr.vExpression instanceof solc_typed_ast_1.Identifier) {
919
+ const base = expr.vExpression.name;
920
+ if (base === 'msg' || base === 'block' || base === 'tx') {
921
+ return { kind: 'var', name: `${base}_${expr.memberName}`, type: this.getEnvType(base, expr.memberName) };
922
+ }
923
+ }
924
+ return {
925
+ kind: 'field',
926
+ base: this.exprToSym(expr.vExpression),
927
+ field: expr.memberName
928
+ };
929
+ }
930
+ if (expr instanceof solc_typed_ast_1.Conditional) {
931
+ // Generate coverage edges for ternary expressions
932
+ const cond = this.exprToSym(expr.vCondition);
933
+ const ternaryId = this.ternaryCounter++;
934
+ const trueEdgeId = `${this.currentFunction}_ternary${ternaryId}_T`;
935
+ const falseEdgeId = `${this.currentFunction}_ternary${ternaryId}_F`;
936
+ // Add ternary edges for coverage tracking
937
+ this.ternaryEdges.push({
938
+ id: trueEdgeId,
939
+ from: `ternary${ternaryId}`,
940
+ to: 'then',
941
+ condition: cond,
942
+ functionName: this.currentFunction,
943
+ astId: expr.id
944
+ });
945
+ this.ternaryEdges.push({
946
+ id: falseEdgeId,
947
+ from: `ternary${ternaryId}`,
948
+ to: 'else',
949
+ condition: { kind: 'unop', op: 'not', arg: cond },
950
+ functionName: this.currentFunction,
951
+ astId: expr.id
952
+ });
953
+ return {
954
+ kind: 'ite',
955
+ cond,
956
+ then: this.exprToSym(expr.vTrueExpression),
957
+ else: this.exprToSym(expr.vFalseExpression)
958
+ };
959
+ }
960
+ if (expr instanceof solc_typed_ast_1.FunctionCall) {
961
+ return this.functionCallToSym(expr);
962
+ }
963
+ if (expr instanceof solc_typed_ast_1.TupleExpression) {
964
+ // Single element tuple - unwrap
965
+ const comps = expr.vComponents.filter(c => c !== null);
966
+ if (comps.length === 1) {
967
+ return this.exprToSym(comps[0]);
968
+ }
969
+ }
970
+ // Fallback
971
+ return { kind: 'lit', value: '0', type: 'uint256' };
972
+ }
973
+ functionCallToSym(call) {
974
+ // Check for keccak256
975
+ if (call.vExpression instanceof solc_typed_ast_1.Identifier && call.vExpression.name === 'keccak256') {
976
+ this.hasKeccak = true;
977
+ return { kind: 'keccak', arg: this.exprToSym(call.vArguments[0]) };
978
+ }
979
+ // Check for type conversion
980
+ if (call.kind === solc_typed_ast_1.FunctionCallKind.TypeConversion) {
981
+ const toType = call.typeString || 'uint256';
982
+ return { kind: 'cast', toType, arg: this.exprToSym(call.vArguments[0]) };
983
+ }
984
+ // Check if external call
985
+ if (this.isExternalCall(call)) {
986
+ const { target, method } = this.extractCallTarget(call);
987
+ const args = call.vArguments.map(a => this.exprToSym(a));
988
+ const ssaId = (0, types_1.freshSSA)(this.ctx, `ext_${target}_${method.split('(')[0]}`, call.typeString || 'uint256');
989
+ this.externalCalls.push({ target, method, ssaVar: ssaId });
990
+ return {
991
+ kind: 'external_return',
992
+ target,
993
+ method,
994
+ args,
995
+ ssaId
996
+ };
997
+ }
998
+ // Internal/library call
999
+ const { target, method } = this.extractCallTarget(call);
1000
+ const args = call.vArguments.map(a => this.exprToSym(a));
1001
+ return {
1002
+ kind: 'call',
1003
+ target,
1004
+ method,
1005
+ args,
1006
+ callType: this.getCallType(call)
1007
+ };
1008
+ }
1009
+ // ==========================================================================
1010
+ // Path Enumeration
1011
+ // ==========================================================================
1012
+ /**
1013
+ * Enumerate all paths from entry to any terminal block (return/revert)
1014
+ */
1015
+ enumeratePaths(entryBlock) {
1016
+ const worklist = [{
1017
+ currentBlock: entryBlock,
1018
+ edges: [],
1019
+ condition: { kind: 'bool', value: true }
1020
+ }];
1021
+ const visited = new Set();
1022
+ const maxPaths = 100; // Keep paths small - edges are the key metric
1023
+ const maxDepth = 50; // Limit path depth to avoid loop explosion
1024
+ while (worklist.length > 0 && this.paths.length < maxPaths) {
1025
+ const state = worklist.pop();
1026
+ const block = this.blocks.get(state.currentBlock);
1027
+ if (!block)
1028
+ continue;
1029
+ // Skip if path is too deep (loop protection)
1030
+ if (state.edges.length > maxDepth)
1031
+ continue;
1032
+ // Check for loops - simple cycle detection
1033
+ const pathKey = state.edges.join(',') + ':' + state.currentBlock;
1034
+ if (visited.has(pathKey))
1035
+ continue;
1036
+ visited.add(pathKey);
1037
+ const term = block.terminator;
1038
+ if (term.kind === 'return') {
1039
+ // Complete path - success
1040
+ this.paths.push({
1041
+ id: (0, types_1.newPath)(this.ctx),
1042
+ edges: state.edges,
1043
+ entryBlock,
1044
+ exitBlock: state.currentBlock,
1045
+ pathCondition: state.condition,
1046
+ outcome: 'success',
1047
+ solvable: !this.hasKeccak
1048
+ });
1049
+ }
1050
+ else if (term.kind === 'revert') {
1051
+ // Complete path - revert
1052
+ this.paths.push({
1053
+ id: (0, types_1.newPath)(this.ctx),
1054
+ edges: state.edges,
1055
+ entryBlock,
1056
+ exitBlock: state.currentBlock,
1057
+ pathCondition: state.condition,
1058
+ outcome: 'revert',
1059
+ solvable: !this.hasKeccak
1060
+ });
1061
+ }
1062
+ else if (term.kind === 'branch') {
1063
+ // Fork into two paths
1064
+ const trueEdgeId = `${this.currentFunction}_${state.currentBlock}_T`;
1065
+ const falseEdgeId = `${this.currentFunction}_${state.currentBlock}_F`;
1066
+ // True branch
1067
+ worklist.push({
1068
+ currentBlock: term.trueBlock,
1069
+ edges: [...state.edges, trueEdgeId],
1070
+ condition: this.andConditions(state.condition, term.cond)
1071
+ });
1072
+ // False branch
1073
+ worklist.push({
1074
+ currentBlock: term.falseBlock,
1075
+ edges: [...state.edges, falseEdgeId],
1076
+ condition: this.andConditions(state.condition, { kind: 'unop', op: 'not', arg: term.cond })
1077
+ });
1078
+ }
1079
+ else if (term.kind === 'jump') {
1080
+ // Follow jump
1081
+ if (term.target) {
1082
+ worklist.push({
1083
+ currentBlock: term.target,
1084
+ edges: state.edges,
1085
+ condition: state.condition
1086
+ });
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+ andConditions(a, b) {
1092
+ if (a.kind === 'bool' && a.value === true)
1093
+ return b;
1094
+ if (b.kind === 'bool' && b.value === true)
1095
+ return a;
1096
+ return { kind: 'binop', op: 'land', left: a, right: b, checked: false, signed: false };
1097
+ }
1098
+ // ==========================================================================
1099
+ // Helper Methods
1100
+ // ==========================================================================
1101
+ addEdge(from, to, condition, dir) {
1102
+ var _a;
1103
+ const id = (0, types_1.newEdge)(this.ctx, this.currentFunction, from, dir);
1104
+ this.edges.push({
1105
+ id,
1106
+ from,
1107
+ to,
1108
+ condition,
1109
+ functionName: this.currentFunction,
1110
+ astId: (_a = this.blocks.get(from)) === null || _a === void 0 ? void 0 : _a.astIds[0]
1111
+ });
1112
+ }
1113
+ isExternalCall(call) {
1114
+ // Check for .call, .delegatecall, or cross-contract calls
1115
+ if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
1116
+ const memberName = call.vExpression.memberName;
1117
+ if (memberName === 'call' || memberName === 'delegatecall' || memberName === 'staticcall') {
1118
+ return true;
1119
+ }
1120
+ // Check if calling a different contract
1121
+ const baseType = call.vExpression.vExpression.typeString || '';
1122
+ if (baseType.startsWith('contract ') || baseType.startsWith('interface ')) {
1123
+ return true;
1124
+ }
1125
+ }
1126
+ return false;
1127
+ }
1128
+ extractCallTarget(call) {
1129
+ if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
1130
+ const base = call.vExpression.vExpression;
1131
+ const method = call.vExpression.memberName;
1132
+ let target = 'unknown';
1133
+ if (base instanceof solc_typed_ast_1.Identifier) {
1134
+ target = base.name;
1135
+ // Resolve 'super' to actual contract in linearization
1136
+ if (target === 'super' && this.contract) {
1137
+ target = this.resolveSuperTarget(method);
1138
+ }
1139
+ }
1140
+ else {
1141
+ try {
1142
+ target = (0, utils_1.toSource)(base).slice(0, 32);
1143
+ }
1144
+ catch { }
1145
+ }
1146
+ return { target, method };
1147
+ }
1148
+ if (call.vExpression instanceof solc_typed_ast_1.Identifier) {
1149
+ return { target: 'this', method: call.vExpression.name };
1150
+ }
1151
+ return { target: 'unknown', method: 'unknown' };
1152
+ }
1153
+ /**
1154
+ * Resolve super.method() to actual contract name using C3 linearization
1155
+ */
1156
+ resolveSuperTarget(methodName) {
1157
+ if (!this.contract)
1158
+ return 'super';
1159
+ // Get linearized base contracts (skip self which is first)
1160
+ const linearized = this.contract.vLinearizedBaseContracts;
1161
+ for (let i = 1; i < linearized.length; i++) {
1162
+ const baseContract = linearized[i];
1163
+ // Check if this base contract has the method
1164
+ for (const fn of baseContract.vFunctions) {
1165
+ if (fn.name === methodName) {
1166
+ return baseContract.name;
1167
+ }
1168
+ }
1169
+ }
1170
+ return 'super'; // Fallback if not found
1171
+ }
1172
+ getCallType(call) {
1173
+ if (this.isExternalCall(call))
1174
+ return 'external';
1175
+ // Could add more sophisticated detection
1176
+ return 'internal';
1177
+ }
1178
+ extractInternalCalls(callTree) {
1179
+ const calls = [];
1180
+ const visit = (node, parentContract) => {
1181
+ var _a;
1182
+ let callType = 'internal';
1183
+ let contractName = this.contract.name;
1184
+ if (node.definition instanceof solc_typed_ast_1.FunctionDefinition) {
1185
+ const funcDef = node.definition;
1186
+ const target = (0, utils_1.getSignature)(funcDef);
1187
+ // Determine the contract this function belongs to
1188
+ if (funcDef.vScope && funcDef.vScope instanceof solc_typed_ast_1.ContractDefinition) {
1189
+ contractName = funcDef.vScope.name;
1190
+ }
1191
+ // Detect call type
1192
+ if (funcDef.isConstructor) {
1193
+ callType = 'constructor';
1194
+ }
1195
+ else if (funcDef.vScope && funcDef.vScope instanceof solc_typed_ast_1.ContractDefinition) {
1196
+ const funcContract = funcDef.vScope;
1197
+ // Check if it's a library call
1198
+ if (funcContract.kind === 'library') {
1199
+ callType = 'library';
1200
+ }
1201
+ // Check if it's a super call (function from parent contract)
1202
+ else if (funcContract.name !== this.contract.name &&
1203
+ ((_a = this.contract.vLinearizedBaseContracts) === null || _a === void 0 ? void 0 : _a.some(c => c && c.id === funcContract.id))) {
1204
+ callType = 'super';
1205
+ }
1206
+ }
1207
+ // Check if this is via ModifierInvocation (super constructor or base call)
1208
+ if (node.fnCall && 'vModifierName' in node.fnCall) {
1209
+ if (funcDef.isConstructor) {
1210
+ callType = 'constructor';
1211
+ }
1212
+ else {
1213
+ callType = 'super';
1214
+ }
1215
+ }
1216
+ const args = node.callArgs.map(a => this.exprToSym(a));
1217
+ calls.push({ target, contract: contractName, callType, args });
1218
+ }
1219
+ else if (node.definition instanceof solc_typed_ast_1.ModifierDefinition) {
1220
+ const modDef = node.definition;
1221
+ const target = modDef.name;
1222
+ // Get contract for modifier
1223
+ if (modDef.vScope && modDef.vScope instanceof solc_typed_ast_1.ContractDefinition) {
1224
+ contractName = modDef.vScope.name;
1225
+ }
1226
+ const args = node.callArgs.map(a => this.exprToSym(a));
1227
+ calls.push({ target, contract: contractName, callType: 'modifier', args });
1228
+ }
1229
+ for (const child of node.children) {
1230
+ visit(child, contractName);
1231
+ }
1232
+ };
1233
+ for (const child of callTree.children) {
1234
+ visit(child);
1235
+ }
1236
+ return calls;
1237
+ }
1238
+ computeSelector(func) {
1239
+ // Compute proper Solidity function selector using keccak256
1240
+ const sig = (0, utils_1.getSignature)(func);
1241
+ const hash = (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)(sig));
1242
+ return hash.slice(0, 10); // First 4 bytes = 8 hex chars + '0x'
1243
+ }
1244
+ solOpToBinOp(op) {
1245
+ const map = {
1246
+ '+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'mod', '**': 'exp',
1247
+ '==': 'eq', '!=': 'ne', '<': 'lt', '<=': 'le', '>': 'gt', '>=': 'ge',
1248
+ '&': 'and', '|': 'or', '^': 'xor', '<<': 'shl', '>>': 'shr',
1249
+ '&&': 'land', '||': 'lor'
1250
+ };
1251
+ return map[op] || null;
1252
+ }
1253
+ compoundOpToBinOp(op) {
1254
+ const map = {
1255
+ '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'mod',
1256
+ '&=': 'and', '|=': 'or', '^=': 'xor', '<<=': 'shl', '>>=': 'shr'
1257
+ };
1258
+ return map[op] || null;
1259
+ }
1260
+ solOpToUnOp(op) {
1261
+ const map = {
1262
+ '!': 'not', '-': 'neg', '~': 'bnot'
1263
+ };
1264
+ return map[op] || null;
1265
+ }
1266
+ isSigned(type) {
1267
+ return /^int\d*$/.test(type.trim());
1268
+ }
1269
+ getEnvType(base, member) {
1270
+ var _a;
1271
+ const types = {
1272
+ msg: { sender: 'address', value: 'uint256', data: 'bytes', sig: 'bytes4' },
1273
+ block: { timestamp: 'uint256', number: 'uint256', basefee: 'uint256', chainid: 'uint256', coinbase: 'address' },
1274
+ tx: { origin: 'address', gasprice: 'uint256' }
1275
+ };
1276
+ return ((_a = types[base]) === null || _a === void 0 ? void 0 : _a[member]) || 'uint256';
1277
+ }
1278
+ extractRevertReason(stmt) {
1279
+ var _a;
1280
+ try {
1281
+ if (((_a = stmt.errorCall) === null || _a === void 0 ? void 0 : _a.vArguments.length) > 0) {
1282
+ return this.extractStringLiteral(stmt.errorCall.vArguments[0]);
1283
+ }
1284
+ }
1285
+ catch { }
1286
+ return undefined;
1287
+ }
1288
+ extractStringLiteral(expr) {
1289
+ if (expr instanceof solc_typed_ast_1.Literal && expr.kind === 'string') {
1290
+ return expr.value;
1291
+ }
1292
+ return undefined;
1293
+ }
1294
+ }
1295
+ exports.CFGBuilder = CFGBuilder;