Uanderson Silva commited on
Commit
2fbd79d
·
1 Parent(s): 4c8dfec

add solidity analyzer tool

Browse files
package-lock.json CHANGED
@@ -13,6 +13,7 @@
13
  "@langchain/core": "^1.1.45",
14
  "@langchain/langgraph": "^1.3.0",
15
  "@langchain/openrouter": "^0.2.4",
 
16
  "dotenv": "^17.4.2",
17
  "langchain": "^1.4.0",
18
  "zod": "^4.4.3"
@@ -755,6 +756,12 @@
755
  "dev": true,
756
  "license": "MIT"
757
  },
 
 
 
 
 
 
758
  "node_modules/@standard-schema/spec": {
759
  "version": "1.1.0",
760
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
 
13
  "@langchain/core": "^1.1.45",
14
  "@langchain/langgraph": "^1.3.0",
15
  "@langchain/openrouter": "^0.2.4",
16
+ "@solidity-parser/parser": "^0.20.2",
17
  "dotenv": "^17.4.2",
18
  "langchain": "^1.4.0",
19
  "zod": "^4.4.3"
 
756
  "dev": true,
757
  "license": "MIT"
758
  },
759
+ "node_modules/@solidity-parser/parser": {
760
+ "version": "0.20.2",
761
+ "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz",
762
+ "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==",
763
+ "license": "MIT"
764
+ },
765
  "node_modules/@standard-schema/spec": {
766
  "version": "1.1.0",
767
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
package.json CHANGED
@@ -31,6 +31,7 @@
31
  "@langchain/core": "^1.1.45",
32
  "@langchain/langgraph": "^1.3.0",
33
  "@langchain/openrouter": "^0.2.4",
 
34
  "dotenv": "^17.4.2",
35
  "langchain": "^1.4.0",
36
  "zod": "^4.4.3"
 
31
  "@langchain/core": "^1.1.45",
32
  "@langchain/langgraph": "^1.3.0",
33
  "@langchain/openrouter": "^0.2.4",
34
+ "@solidity-parser/parser": "^0.20.2",
35
  "dotenv": "^17.4.2",
36
  "langchain": "^1.4.0",
37
  "zod": "^4.4.3"
src/agents/auditor/tools/solidity-analyzer-tool.ts ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parse, visit } from "@solidity-parser/parser";
2
+ import { tool } from "langchain";
3
+ import { z } from "zod";
4
+
5
+ const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
6
+ const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
7
+
8
+ interface StateVar {
9
+ name: string;
10
+ type: string;
11
+ visibility: string;
12
+ constant: boolean;
13
+ immutable: boolean;
14
+ }
15
+
16
+ interface EventDef {
17
+ name: string;
18
+ params: string[];
19
+ anonymous: boolean;
20
+ }
21
+
22
+ interface ModifierDef {
23
+ name: string;
24
+ params: string[];
25
+ }
26
+
27
+ interface FunctionDef {
28
+ name: string;
29
+ isConstructor: boolean;
30
+ isReceive: boolean;
31
+ isFallback: boolean;
32
+ visibility: string;
33
+ mutability: string;
34
+ params: string[];
35
+ returns: string[];
36
+ modifiers: string[];
37
+ internalCalls: string[];
38
+ externalCalls: string[];
39
+ stateReads: string[];
40
+ stateWrites: string[];
41
+ }
42
+
43
+ interface ContractAnalysis {
44
+ name: string;
45
+ kind: string;
46
+ baseContracts: string[];
47
+ usingFor: string[];
48
+ stateVars: StateVar[];
49
+ events: EventDef[];
50
+ modifiers: ModifierDef[];
51
+ functions: FunctionDef[];
52
+ }
53
+
54
+ const typeToString = (node: any): string => {
55
+ if (!node) return "unknown";
56
+
57
+ switch (node.type) {
58
+ case "ElementaryTypeName":
59
+ return node.name as string;
60
+ case "UserDefinedTypeName":
61
+ return (node.namePath ?? node.name) as string;
62
+ case "ArrayTypeName":
63
+ return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
64
+ case "Mapping":
65
+ return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
66
+ case "FunctionTypeName":
67
+ return "function";
68
+ default:
69
+ return "unknown";
70
+ }
71
+ };
72
+
73
+ const paramToString = (p: any) => {
74
+ if (!p) return "?";
75
+ const type = typeToString(p.typeName);
76
+ return p.name ? `${type} ${p.name}` : type;
77
+ };
78
+
79
+ const collectLHSRoots = (node: any, targets: Set<string>) => {
80
+ if (!node) return;
81
+ switch (node.type) {
82
+ case "Identifier":
83
+ targets.add(node.name as string);
84
+ break;
85
+ case "MemberAccess":
86
+ collectLHSRoots(node.expression, targets);
87
+ break;
88
+ case "IndexAccess":
89
+ collectLHSRoots(node.base, targets);
90
+ break;
91
+ case "TupleExpression":
92
+ for (const c of node.components ?? []) collectLHSRoots(c, targets);
93
+ break;
94
+ }
95
+ };
96
+
97
+ const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
98
+ const internalCalls = new Set<string>();
99
+ const externalCalls = new Set<string>();
100
+ const writeTargets = new Set<string>();
101
+ const allStateAccesses = new Set<string>();
102
+ const localVars = new Set<string>();
103
+
104
+ if (!funcNode.body) {
105
+ return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
106
+ }
107
+
108
+ // Collect function params and return params as locals so they don't shadow state vars
109
+ for (const p of funcNode.parameters ?? []) {
110
+ if (p?.name) localVars.add(p.name as string);
111
+ }
112
+ for (const p of funcNode.returnParameters ?? []) {
113
+ if (p?.name) localVars.add(p.name as string);
114
+ }
115
+
116
+ // Collect local variable declarations
117
+ visit(funcNode.body, {
118
+ VariableDeclarationStatement: (node: any) => {
119
+ for (const v of node.variables ?? []) {
120
+ if (v?.name) localVars.add(v.name as string);
121
+ }
122
+ },
123
+ });
124
+
125
+ const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
126
+
127
+ // Collect write targets from assignment LHS, unary mutations, and delete
128
+ visit(funcNode.body, {
129
+ ExpressionStatement: (node: any) => {
130
+ const expr = node.expression;
131
+ if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
132
+ collectLHSRoots(expr.left, writeTargets);
133
+ }
134
+ // Handle ++, --, and delete — all work on any lvalue (arr[i]++, delete s.field, etc.)
135
+ if (
136
+ expr?.type === "UnaryOperation" &&
137
+ (expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
138
+ ) {
139
+ collectLHSRoots(expr.subExpression, writeTargets);
140
+ }
141
+ },
142
+ });
143
+
144
+ // Collect calls and state-var identifier accesses
145
+ visit(funcNode.body, {
146
+ FunctionCall: (node: any) => {
147
+ const expr = node.expression;
148
+ if (expr?.type === "Identifier") {
149
+ internalCalls.add(expr.name as string);
150
+ } else if (expr?.type === "MemberAccess") {
151
+ const base = expr.expression;
152
+ if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
153
+ internalCalls.add(expr.memberName as string);
154
+ } else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
155
+ // abi.encode, block.xxx, msg.xxx, etc. — not external calls
156
+ } else {
157
+ const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
158
+ externalCalls.add(`${baseStr}.${expr.memberName as string}`);
159
+ }
160
+ }
161
+ },
162
+ Identifier: (node: any) => {
163
+ if (effectiveStateVars.has(node.name as string)) {
164
+ allStateAccesses.add(node.name as string);
165
+ }
166
+ },
167
+ });
168
+
169
+ const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
170
+ // A var can be in both — e.g. x = x + 1 is both a read and a write.
171
+ const stateReads = [...allStateAccesses];
172
+
173
+ return {
174
+ internalCalls: [...internalCalls],
175
+ externalCalls: [...externalCalls],
176
+ stateReads,
177
+ stateWrites,
178
+ };
179
+ };
180
+
181
+ const hasCycle = (start: string, current: string, callMap: Map<string, string[]>, visited: Set<string>) => {
182
+ for (const callee of callMap.get(current) ?? []) {
183
+ if (callee === start) return true;
184
+ if (!visited.has(callee)) {
185
+ visited.add(callee);
186
+ if (hasCycle(start, callee, callMap, visited)) return true;
187
+ }
188
+ }
189
+
190
+ return false;
191
+ };
192
+
193
+ const fnLabel = (fn: FunctionDef) => {
194
+ if (fn.isConstructor) return "constructor";
195
+ if (fn.isReceive) return "receive";
196
+ if (fn.isFallback) return "fallback";
197
+ return fn.name;
198
+ };
199
+
200
+ const generateShortMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
201
+ const lines: string[] = [];
202
+ lines.push("# Solidity Analysis\n");
203
+
204
+ if (imports.length > 0) {
205
+ lines.push(`**Imports:** ${imports.map((i) => `\`${i}\``).join(", ")}\n`);
206
+ }
207
+
208
+ for (const contract of contracts) {
209
+ const inheritance =
210
+ contract.baseContracts.length > 0 ? ` : ${contract.baseContracts.map((b) => `\`${b}\``).join(", ")}` : "";
211
+ lines.push(`---\n\n## \`${contract.name}\` (${contract.kind})${inheritance}\n`);
212
+
213
+ // State variables — one line, name:type
214
+ if (contract.stateVars.length > 0) {
215
+ const vars = contract.stateVars.map((v) => {
216
+ const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean);
217
+ const suffix = flags.length > 0 ? `, ${flags.join(", ")}` : "";
218
+ return `\`${v.name}: ${v.type}\` (${v.visibility}${suffix})`;
219
+ });
220
+ lines.push(`**State:** ${vars.join(", ")}\n`);
221
+ }
222
+
223
+ // Modifiers — names only
224
+ if (contract.modifiers.length > 0) {
225
+ const mods = contract.modifiers.map(
226
+ (m) => `\`${m.name}${m.params.length > 0 ? `(${m.params.join(", ")})` : ""}\``,
227
+ );
228
+ lines.push(`**Modifiers:** ${mods.join(", ")}\n`);
229
+ }
230
+
231
+ // Events — name + params, one line each
232
+ if (contract.events.length > 0) {
233
+ const evts = contract.events.map((e) => `\`${e.name}(${e.params.join(", ")})\``);
234
+ lines.push(`**Events:** ${evts.join(", ")}\n`);
235
+ }
236
+
237
+ // Function list — compact, one line per function
238
+ if (contract.functions.length > 0) {
239
+ lines.push("**Functions:**");
240
+ for (const fn of contract.functions) {
241
+ const label = fnLabel(fn);
242
+ const params = fn.params.join(", ");
243
+ const ret = fn.returns.length > 0 ? ` → ${fn.returns.join(", ")}` : "";
244
+ const mods = fn.modifiers.length > 0 ? ` [${fn.modifiers.join(", ")}]` : "";
245
+ lines.push(`- \`${label}(${params})${ret}\` — ${fn.visibility} ${fn.mutability}${mods}`);
246
+ }
247
+ lines.push("");
248
+ }
249
+
250
+ // External calls — only functions that make them
251
+ const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
252
+ if (externalFuncs.length > 0) {
253
+ lines.push("**External Calls:**");
254
+ for (const fn of externalFuncs) {
255
+ lines.push(`- \`${fnLabel(fn)}\`: ${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}`);
256
+ }
257
+ lines.push("");
258
+ }
259
+ }
260
+
261
+ return lines.join("\n");
262
+ };
263
+
264
+ const generateMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
265
+ const lines: string[] = [];
266
+ lines.push("# Solidity Contract Analysis\n");
267
+
268
+ // Imports
269
+ lines.push("## Imports\n");
270
+ if (imports.length === 0) {
271
+ lines.push("_No imports._\n");
272
+ } else {
273
+ for (const imp of imports) lines.push(`- \`${imp}\``);
274
+ lines.push("");
275
+ }
276
+
277
+ for (const contract of contracts) {
278
+ const kindLabel = contract.kind.charAt(0).toUpperCase() + contract.kind.slice(1);
279
+ lines.push(`---\n\n## ${kindLabel}: \`${contract.name}\`\n`);
280
+
281
+ // Inheritance
282
+ lines.push("### Inheritance\n");
283
+ if (contract.baseContracts.length === 0) {
284
+ lines.push("_None._\n");
285
+ } else {
286
+ for (const base of contract.baseContracts) lines.push(`- \`${base}\``);
287
+ lines.push("");
288
+ }
289
+
290
+ // Using For
291
+ if (contract.usingFor.length > 0) {
292
+ lines.push("### Using For\n");
293
+ for (const u of contract.usingFor) lines.push(`- ${u}`);
294
+ lines.push("");
295
+ }
296
+
297
+ // Storage layout
298
+ lines.push("### Storage Layout (State Variables)\n");
299
+ if (contract.stateVars.length === 0) {
300
+ lines.push("_No state variables._\n");
301
+ } else {
302
+ lines.push("| Slot | Name | Type | Visibility | Flags |");
303
+ lines.push("|------|------|------|------------|-------|");
304
+ contract.stateVars.forEach((v, i) => {
305
+ const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ");
306
+ lines.push(`| ${i} | \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} |`);
307
+ });
308
+ lines.push("");
309
+ }
310
+
311
+ // Events
312
+ lines.push("### Events\n");
313
+ if (contract.events.length === 0) {
314
+ lines.push("_No events._\n");
315
+ } else {
316
+ for (const evt of contract.events) {
317
+ const params = evt.params.join(", ");
318
+ lines.push(`- **\`${evt.name}\`**\`(${params})\`${evt.anonymous ? " _(anonymous)_" : ""}`);
319
+ }
320
+ lines.push("");
321
+ }
322
+
323
+ // Modifiers
324
+ lines.push("### Modifiers\n");
325
+ if (contract.modifiers.length === 0) {
326
+ lines.push("_No modifiers._\n");
327
+ } else {
328
+ for (const mod of contract.modifiers) {
329
+ lines.push(`- **\`${mod.name}\`**\`(${mod.params.join(", ")})\``);
330
+ }
331
+ lines.push("");
332
+ }
333
+
334
+ // Function list
335
+ lines.push("### Function List\n");
336
+ if (contract.functions.length === 0) {
337
+ lines.push("_No functions._\n");
338
+ } else {
339
+ lines.push("| Name | Visibility | Mutability | Parameters | Returns | Modifiers |");
340
+ lines.push("|------|------------|------------|------------|---------|-----------|");
341
+ for (const fn of contract.functions) {
342
+ lines.push(
343
+ `| \`${fnLabel(fn)}\` | ${fn.visibility} | ${fn.mutability} | \`${fn.params.join(", ")}\` | \`${fn.returns.join(", ")}\` | ${fn.modifiers.join(", ")} |`,
344
+ );
345
+ }
346
+ lines.push("");
347
+ }
348
+
349
+ // Call graph
350
+ lines.push("### Call Graph\n");
351
+ const hasCalls = contract.functions.some((f) => f.internalCalls.length > 0 || f.externalCalls.length > 0);
352
+ if (!hasCalls) {
353
+ lines.push("_No function calls detected._\n");
354
+ } else {
355
+ for (const fn of contract.functions) {
356
+ if (fn.internalCalls.length === 0 && fn.externalCalls.length === 0) continue;
357
+ lines.push(`**\`${fnLabel(fn)}\`**`);
358
+ for (const call of fn.internalCalls) lines.push(` - → \`${call}\` _(internal)_`);
359
+ for (const call of fn.externalCalls) lines.push(` - → \`${call}\` _(external)_`);
360
+ }
361
+ lines.push("");
362
+ }
363
+
364
+ // External calls
365
+ lines.push("### External Calls\n");
366
+ const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
367
+ if (externalFuncs.length === 0) {
368
+ lines.push("_No external calls detected._\n");
369
+ } else {
370
+ for (const fn of externalFuncs) {
371
+ lines.push(`**\`${fnLabel(fn)}\`**`);
372
+ for (const call of fn.externalCalls) lines.push(` - \`${call}\``);
373
+ }
374
+ lines.push("");
375
+ }
376
+
377
+ // Internal recursion
378
+ lines.push("### Internal Recursion\n");
379
+ const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
380
+ const recursiveFns = contract.functions.filter((fn) => hasCycle(fnLabel(fn), fnLabel(fn), callMap, new Set()));
381
+ if (recursiveFns.length === 0) {
382
+ lines.push("_No recursive functions detected._\n");
383
+ } else {
384
+ for (const fn of recursiveFns) lines.push(`- **\`${fnLabel(fn)}\`** is recursive`);
385
+ lines.push("");
386
+ }
387
+
388
+ // State variable touchpoints
389
+ lines.push("### State Variable Touchpoints\n");
390
+ const touchedFns = contract.functions.filter((f) => f.stateReads.length > 0 || f.stateWrites.length > 0);
391
+ if (touchedFns.length === 0) {
392
+ lines.push("_No state variable accesses detected._\n");
393
+ } else {
394
+ lines.push("| Function | Reads | Writes |");
395
+ lines.push("|----------|-------|--------|");
396
+ for (const fn of touchedFns) {
397
+ const reads = fn.stateReads.map((r) => `\`${r}\``).join(", ");
398
+ const writes = fn.stateWrites.map((w) => `\`${w}\``).join(", ");
399
+ lines.push(`| \`${fnLabel(fn)}\` | ${reads} | ${writes} |`);
400
+ }
401
+ lines.push("");
402
+ }
403
+ }
404
+
405
+ // External dependencies summary
406
+ lines.push("---\n\n## External Dependencies\n");
407
+
408
+ lines.push("### Import Paths\n");
409
+ if (imports.length === 0) {
410
+ lines.push("_No imports._\n");
411
+ } else {
412
+ for (const imp of imports) lines.push(`- \`${imp}\``);
413
+ lines.push("");
414
+ }
415
+
416
+ const externalTargets = new Set<string>();
417
+ for (const contract of contracts) {
418
+ for (const fn of contract.functions) {
419
+ for (const call of fn.externalCalls) {
420
+ const target = call.split(".")[0];
421
+ if (target && target !== "<expr>") externalTargets.add(target);
422
+ }
423
+ }
424
+ }
425
+
426
+ lines.push("### External Contract Interactions\n");
427
+ if (externalTargets.size === 0) {
428
+ lines.push("_No external contract interactions detected._\n");
429
+ } else {
430
+ for (const dep of externalTargets) lines.push(`- \`${dep}\``);
431
+ lines.push("");
432
+ }
433
+
434
+ return lines.join("\n");
435
+ };
436
+
437
+ const analyzeSolidityFile = async (soliditySource: string, mode: "full" | "short") => {
438
+ let ast: any;
439
+
440
+ try {
441
+ ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
442
+ } catch (e: any) {
443
+ return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
444
+ }
445
+
446
+ const imports: string[] = [];
447
+ const contracts: ContractAnalysis[] = [];
448
+
449
+ for (const node of ast.children ?? []) {
450
+ if (node.type === "ImportDirective") {
451
+ imports.push(node.path as string);
452
+ }
453
+ }
454
+
455
+ for (const node of ast.children ?? []) {
456
+ if (node.type !== "ContractDefinition") continue;
457
+
458
+ const contract: ContractAnalysis = {
459
+ name: node.name as string,
460
+ kind: (node.kind as string) ?? "contract",
461
+ baseContracts: (node.baseContracts ?? []).map(
462
+ (bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
463
+ ),
464
+ usingFor: [],
465
+ stateVars: [],
466
+ events: [],
467
+ modifiers: [],
468
+ functions: [],
469
+ };
470
+
471
+ const stateVarNames = new Set<string>();
472
+
473
+ for (const member of node.subNodes ?? []) {
474
+ switch (member.type) {
475
+ case "StateVariableDeclaration":
476
+ for (const v of member.variables ?? []) {
477
+ stateVarNames.add(v.name as string);
478
+ contract.stateVars.push({
479
+ name: v.name as string,
480
+ type: typeToString(v.typeName),
481
+ visibility: (v.visibility as string) ?? "internal",
482
+ constant: (v.isDeclaredConst as boolean) ?? false,
483
+ immutable: (v.isImmutable as boolean) ?? false,
484
+ });
485
+ }
486
+ break;
487
+
488
+ case "EventDefinition": {
489
+ const params = (member.parameters ?? []).map((p: any) => {
490
+ const indexed = p.isIndexed ? "indexed " : "";
491
+ const name = p.name ? ` ${p.name as string}` : "";
492
+ return `${indexed}${typeToString(p.typeName)}${name}`;
493
+ });
494
+ contract.events.push({
495
+ name: member.name as string,
496
+ params,
497
+ anonymous: (member.isAnonymous as boolean) ?? false,
498
+ });
499
+ break;
500
+ }
501
+
502
+ case "ModifierDefinition":
503
+ contract.modifiers.push({
504
+ name: member.name as string,
505
+ params: (member.parameters ?? []).map(paramToString),
506
+ });
507
+ break;
508
+
509
+ case "FunctionDefinition": {
510
+ const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
511
+ contract.functions.push({
512
+ name: (member.name as string) ?? "",
513
+ isConstructor: (member.isConstructor as boolean) ?? false,
514
+ isReceive: (member.isReceiveEther as boolean) ?? false,
515
+ isFallback: (member.isFallback as boolean) ?? false,
516
+ visibility: (member.visibility as string) ?? "internal",
517
+ mutability: (member.stateMutability as string) ?? "nonpayable",
518
+ params: (member.parameters ?? []).map(paramToString),
519
+ returns: (member.returnParameters ?? []).map(paramToString),
520
+ modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
521
+ internalCalls,
522
+ externalCalls,
523
+ stateReads,
524
+ stateWrites,
525
+ });
526
+ break;
527
+ }
528
+
529
+ case "UsingForDeclaration": {
530
+ const forType = member.typeName ? typeToString(member.typeName) : "*";
531
+ if (member.libraryName) {
532
+ contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
533
+ } else {
534
+ // New-style: using {fn1, fn2, ...} for T
535
+ const fns = (member.functions ?? [])
536
+ .map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
537
+ .join(", ");
538
+ contract.usingFor.push(`{${fns}} for \`${forType}\``);
539
+ }
540
+ break;
541
+ }
542
+ }
543
+ }
544
+
545
+ contracts.push(contract);
546
+ }
547
+
548
+ return mode === "short" ? generateShortMarkdown(imports, contracts) : generateMarkdown(imports, contracts);
549
+ };
550
+
551
+ export const solidityAnalyzerTool = tool(
552
+ async ({ solidityFile, mode }) => {
553
+ return analyzeSolidityFile(solidityFile, mode);
554
+ },
555
+ {
556
+ name: "solidity_analyzer",
557
+ description:
558
+ "Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact token-efficient summary (imports, state, modifiers, events, function signatures, external calls). Use mode='full' for the complete report including storage layout table, call graph, recursion detection, state variable touchpoints, and external dependencies.",
559
+ schema: z.object({
560
+ solidityFile: z.string().describe("The full Solidity source code to analyze."),
561
+ mode: z
562
+ .enum(["full", "short"])
563
+ .default("full")
564
+ .describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
565
+ }),
566
+ },
567
+ );