Uanderson Silva commited on
Commit
0b3cd21
Β·
1 Parent(s): 4c144c5

refactor tools and rank files in define scope

Browse files
biome.json CHANGED
@@ -1,55 +1,53 @@
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "includes": [
10
- "**",
11
- "!!**/dist"
12
- ]
13
- },
14
- "formatter": {
15
- "enabled": true,
16
- "lineWidth": 120,
17
- "indentStyle": "space"
18
- },
19
- "linter": {
20
- "enabled": true,
21
- "rules": {
22
- "recommended": true,
23
- "suspicious": {
24
- "noUnknownAtRules": "off",
25
- "noArrayIndexKey": "off",
26
- "noExplicitAny": "off"
27
- },
28
- "complexity": {
29
- "noStaticOnlyClass": "off"
30
- },
31
- "correctness": {
32
- "useExhaustiveDependencies": "off",
33
- "useParseIntRadix": "off"
34
- },
35
- "a11y": "off",
36
- "style": {
37
- "noNonNullAssertion": "off",
38
- "useTemplate": "off"
39
- }
40
- }
41
- },
42
- "javascript": {
43
- "formatter": {
44
- "quoteStyle": "double"
45
- }
46
- },
47
- "assist": {
48
- "enabled": true,
49
- "actions": {
50
- "source": {
51
- "organizeImports": "on"
52
- }
53
- }
54
- }
55
  }
 
1
  {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "includes": ["**", "!!**/dist"]
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "lineWidth": 120,
14
+ "indentStyle": "space"
15
+ },
16
+ "linter": {
17
+ "enabled": true,
18
+ "rules": {
19
+ "recommended": true,
20
+ "suspicious": {
21
+ "noUnknownAtRules": "off",
22
+ "noArrayIndexKey": "off",
23
+ "noExplicitAny": "off",
24
+ "noAssignInExpressions": "off"
25
+ },
26
+ "complexity": {
27
+ "noStaticOnlyClass": "off"
28
+ },
29
+ "correctness": {
30
+ "useExhaustiveDependencies": "off",
31
+ "useParseIntRadix": "off"
32
+ },
33
+ "a11y": "off",
34
+ "style": {
35
+ "noNonNullAssertion": "off",
36
+ "useTemplate": "off"
37
+ }
38
+ }
39
+ },
40
+ "javascript": {
41
+ "formatter": {
42
+ "quoteStyle": "double"
43
+ }
44
+ },
45
+ "assist": {
46
+ "enabled": true,
47
+ "actions": {
48
+ "source": {
49
+ "organizeImports": "on"
50
+ }
51
+ }
52
+ }
 
 
53
  }
src/agents/auditor/agent.ts CHANGED
@@ -5,12 +5,8 @@ import { HumanMessage, SystemMessage } from "@langchain/core/messages";
5
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
6
  import { z } from "zod";
7
 
8
- import { logger } from "../../logger.ts";
9
  import { createLLM } from "../../config/llm.ts";
10
- import { JUDGE_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
11
- import { AuditorState, JudgeReviewSchema, CandidateFindingSchema } from "./state.ts";
12
- import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
13
- import { buildRepoTree } from "./tools/repo-tree-tool.ts";
14
  import {
15
  DOC_BASENAMES,
16
  DOC_EXTS,
@@ -22,6 +18,15 @@ import {
22
  SOL_EXT,
23
  SOL_TEST_SUFFIXES,
24
  } from "./config.ts";
 
 
 
 
 
 
 
 
 
25
  import { matchLines } from "./utils.ts";
26
 
27
  const llm = createLLM();
@@ -71,7 +76,24 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
71
  logger.debug(`defineScope: doc files: ${JSON.stringify(docFiles)}`);
72
  logger.debug(`defineScope: file tree:\n${fileTree}`);
73
 
74
- return { scope: solFiles, docs: docFiles, fileTree };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  };
76
 
77
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
@@ -85,23 +107,23 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
85
  }
86
  };
87
 
88
- // Read and analyze each Solidity file
89
  const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
90
  for (const filePath of state.scope) {
91
  const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
92
  if (!source) continue;
93
- const analysis = await analyzeSolidityFile(source, "full");
 
 
 
94
  solidityEntries.push({ filePath, source, analysis });
95
  }
96
 
97
- // Read documentation files
98
  const docEntries: { filePath: string; content: string }[] = [];
99
  for (const filePath of state.docs) {
100
  const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
101
  if (content) docEntries.push({ filePath, content });
102
  }
103
 
104
- // Build the LLM input
105
  const parts: string[] = [];
106
 
107
  if (docEntries.length > 0) {
@@ -111,21 +133,17 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
111
  }
112
  }
113
 
114
- parts.push("## Structural Analysis (auto-generated)\n");
115
- for (const { filePath, analysis } of solidityEntries) {
116
- parts.push(`### ${filePath}\n${analysis}`);
117
- }
118
-
119
- parts.push("## Contract Source Code\n");
120
- for (const { filePath, source } of solidityEntries) {
121
- parts.push(`### ${filePath}\n\`\`\`solidity\n${source}\n\`\`\``);
122
  }
123
 
124
  const model = llm.withStructuredOutput(z.object({ context: z.string() }));
125
  const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
126
 
127
- logger.info(`gatherContext: context built (${parts.join("\n\n").length} chars)`);
128
  logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
 
 
129
 
130
  return { repoContext: result.context };
131
  };
 
5
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
6
  import { z } from "zod";
7
 
 
8
  import { createLLM } from "../../config/llm.ts";
9
+ import { logger } from "../../logger.ts";
 
 
 
10
  import {
11
  DOC_BASENAMES,
12
  DOC_EXTS,
 
18
  SOL_EXT,
19
  SOL_TEST_SUFFIXES,
20
  } from "./config.ts";
21
+ import {
22
+ FIND_VULNERABILITIES_PROMPT,
23
+ GATHER_CONTEXT_PROMPT,
24
+ JUDGE_FINDINGS_PROMPT,
25
+ RANK_FILES_PROMPT,
26
+ } from "./prompts.ts";
27
+ import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
28
+ import { buildRepoTree } from "./tools/repo-tree/tool.ts";
29
+ import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
30
  import { matchLines } from "./utils.ts";
31
 
32
  const llm = createLLM();
 
76
  logger.debug(`defineScope: doc files: ${JSON.stringify(docFiles)}`);
77
  logger.debug(`defineScope: file tree:\n${fileTree}`);
78
 
79
+ logger.info("defineScope: ranking files by importance");
80
+
81
+ const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
82
+ const rankingModel = llm.withStructuredOutput(RankFilesSchema);
83
+
84
+ const { rankings } = await rankingModel.invoke([
85
+ new SystemMessage(RANK_FILES_PROMPT),
86
+ new HumanMessage(
87
+ `File tree:\n\`\`\`\n${fileTree}\n\`\`\`\n\nSolidity files to rank:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
88
+ ),
89
+ ]);
90
+
91
+ const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
92
+ logger.info(
93
+ `defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} β€” ${r.reasoning}`).join("\n")}`,
94
+ );
95
+
96
+ return { scope: solFiles, docs: docFiles, fileTree, fileRankings: sorted };
97
  };
98
 
99
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
 
107
  }
108
  };
109
 
 
110
  const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
111
  for (const filePath of state.scope) {
112
  const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
113
  if (!source) continue;
114
+
115
+ const ranking = state.fileRankings.find((r) => r.filePath === filePath);
116
+ const mode = ranking && ranking.importance >= 4 ? "full" : "short";
117
+ const analysis = await analyzeSolidityFile(source, mode, filePath, ranking?.importance);
118
  solidityEntries.push({ filePath, source, analysis });
119
  }
120
 
 
121
  const docEntries: { filePath: string; content: string }[] = [];
122
  for (const filePath of state.docs) {
123
  const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
124
  if (content) docEntries.push({ filePath, content });
125
  }
126
 
 
127
  const parts: string[] = [];
128
 
129
  if (docEntries.length > 0) {
 
133
  }
134
  }
135
 
136
+ parts.push("## Structural Analysis\n");
137
+ for (const { analysis } of solidityEntries) {
138
+ parts.push(analysis);
 
 
 
 
 
139
  }
140
 
141
  const model = llm.withStructuredOutput(z.object({ context: z.string() }));
142
  const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
143
 
 
144
  logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
145
+ logger.info(`gatherContext: context built (${result.context.length} chars)`);
146
+ logger.debug(`gatherContext: compact context:\n${result.context}`);
147
 
148
  return { repoContext: result.context };
149
  };
src/agents/auditor/prompts.ts CHANGED
@@ -1,26 +1,46 @@
1
- export const GATHER_CONTEXT_PROMPT = `VocΓͺ Γ© um especialista em seguranΓ§a de smart contracts. VocΓͺ receberΓ‘ documentaΓ§Γ£o, uma anΓ‘lise estrutural e o cΓ³digo-fonte completo de todos os contratos Solidity em escopo. Produza um contexto detalhado do protocolo que guiarΓ‘ a descoberta de vulnerabilidades.
2
 
3
- Estruture sua resposta nas seguintes seΓ§Γ΅es:
 
 
4
 
5
- ## 1. VisΓ£o Geral dos Contratos
6
- Para cada contrato: seu propΓ³sito, tipo (contract/interface/library/abstract), cadeia de heranΓ§a e principais dependΓͺncias de outros contratos em escopo ou protocolos externos.
 
 
 
7
 
8
- ## 2. Mapa de Estado e Armazenamento
9
- Liste todas as variΓ‘veis de estado relevantes entre os contratos, o que representam e quais funΓ§Γ΅es as leem ou escrevem. Sinalize armazenamento compartilhado ou herdado.
10
 
11
- ## 3. Fluxos Principais
12
- Trace os principais caminhos de execuΓ§Γ£o e transiΓ§Γ΅es de estado de ponta a ponta entre contratos (ex.: depΓ³sito β†’ cunhar shares β†’ atualizar recompensas; saque β†’ queimar shares β†’ transferir ETH). Inclua chamadas entre contratos.
13
 
14
- ## 4. Invariantes
15
- CondiΓ§Γ΅es que devem sempre ser verdadeiras (ex.: "o supply total deve ser igual Γ  soma de todos os saldos", "o saldo de ETH do contrato β‰₯ soma de todos os depΓ³sitos dos usuΓ‘rios"). Derive-as tanto do cΓ³digo-fonte quanto da documentaΓ§Γ£o.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  ## 5. Premissas de Design
18
- O que o protocolo assume sobre chamadores, contratos externos, orΓ‘culos, chaves de administrador e comportamento de tokens (ex.: "tokens sΓ£o compatΓ­veis com ERC-20", "o admin Γ© confiΓ‘vel", "sem tokens com taxa de transferΓͺncia").
 
 
 
19
 
20
- ## 6. Regras de NegΓ³cio
21
- Controles de acesso, estruturas de taxas, timelocks, limites, mecanismos de pausa, padrΓ΅es de atualizaΓ§Γ£o e quaisquer outras restriΓ§Γ΅es de domΓ­nio.
22
 
23
- Seja preciso e exaustivo β€” quanto mais rico o contexto, com mais precisΓ£o as vulnerabilidades podem ser identificadas e validadas.`;
24
 
25
  export const FIND_VULNERABILITIES_PROMPT = `VocΓͺ Γ© um auditor especialista em seguranΓ§a de smart contracts com foco em Solidity. Analise sistematicamente o cΓ³digo-fonte do contrato e o contexto do protocolo para identificar vulnerabilidades de seguranΓ§a.
26
 
 
1
+ export const RANK_FILES_PROMPT = `VocΓͺ Γ© um especialista em seguranΓ§a de smart contracts. Dado o arquivo tree de um repositΓ³rio e uma lista de contratos Solidity, classifique cada arquivo pela sua importΓ’ncia para a descoberta de vulnerabilidades de seguranΓ§a.
2
 
3
+ Para cada arquivo atribua:
4
+ - importance: inteiro de 1 (menos importante) a 5 (mais importante)
5
+ - reasoning: uma frase concisa justificando a classificaΓ§Γ£o
6
 
7
+ ImportΓ’ncia 5: lΓ³gica central do protocolo, vaults de tokens, contratos de custΓ³dia, mecanismos de upgrade/proxy, cΓ‘lculos financeiros, controle de acesso.
8
+ ImportΓ’ncia 4: fluxos significativos de valor, contratos que interagem diretamente com os de importΓ’ncia 5, mΓ‘quinas de estado complexas, distribuiΓ§Γ£o de taxas/recompensas.
9
+ ImportΓ’ncia 3: helpers perifΓ©ricos, bibliotecas, funcionalidades secundΓ‘rias, governanΓ§a com timelocks.
10
+ ImportΓ’ncia 2: interfaces simples, wrappers triviais, contratos utilitΓ‘rios menores.
11
+ ImportΓ’ncia 1: views somente-leitura, configuraΓ§Γ£o pura, arquivos apenas com constantes.
12
 
13
+ Retorne a classificaΓ§Γ£o de TODOS os arquivos fornecidos.`;
 
14
 
15
+ export const GATHER_CONTEXT_PROMPT = `VocΓͺ Γ© um especialista em seguranΓ§a de smart contracts. VocΓͺ receberΓ‘ documentaΓ§Γ£o, uma anΓ‘lise estrutural e o cΓ³digo-fonte completo de todos os contratos Solidity em escopo.
 
16
 
17
+ Produza um contexto conciso e denso do protocolo β€” ele serΓ‘ antecedido por cΓ³digo e anΓ‘lises, entΓ£o seja econΓ΄mico: sem introduΓ§Γ΅es, sem padding, sem repetiΓ§Γ΅es. MΓ‘ximo de **800 palavras no total**.
18
+
19
+ ---
20
+
21
+ ## 1. Contratos (3–5 linhas por contrato)
22
+ Para cada contrato: propΓ³sito em uma frase, tipo (contract/interface/library/abstract), heranΓ§a relevante e dependΓͺncias externas crΓ­ticas (orΓ‘culos, tokens, protocolos).
23
+
24
+ ## 2. Estado CrΓ­tico (bullet por variΓ‘vel relevante)
25
+ VariΓ‘veis de estado que afetam lΓ³gica de negΓ³cio, seguranΓ§a ou contabilidade interna. Formato: \`nomeVar β€” o que representa β€” quem lΓͺ/escreve\`. Omita getters triviais e variΓ‘veis puramente administrativas sem impacto em seguranΓ§a.
26
+
27
+ ## 3. Fluxos Principais (mΓ‘x. 4 fluxos, 3–5 passos cada)
28
+ Somente os caminhos crΓ­ticos de ponta a ponta. Formato: \`aΓ§Γ£o β†’ efeito β†’ estado alterado\`. Inclua chamadas cross-contract apenas quando materiais para entender riscos.
29
+
30
+ ## 4. Invariantes e Propriedades
31
+ Liste em bullets as condiΓ§Γ΅es que **sempre** devem ser verdadeiras. Separe em dois grupos:
32
+ - **ContΓ‘beis**: balanΓ§os, totais, proporΓ§Γ΅es (ex.: \`totalSupply == Ξ£ balances\`)
33
+ - **De controle**: acesso, sequΓͺncia de operaΓ§Γ΅es, estados permitidos
34
 
35
  ## 5. Premissas de Design
36
+ O que o protocolo assume sobre o mundo externo β€” em bullets curtos: confianΓ§a em admin/owner, comportamento esperado de tokens (sem fee-on-transfer, sem rebase), confiabilidade de orΓ‘culos, atomicidade de operaΓ§Γ΅es.
37
+
38
+ ## 6. Regras de NegΓ³cio e RestriΓ§Γ΅es
39
+ Em bullets: controles de acesso (roles/modifiers), limites numΓ©ricos (caps, mΓ­nimos, mΓ‘ximos), taxas e destinatΓ‘rios, timelocks, pausabilidade e condiΓ§Γ΅es de upgrade. Inclua apenas regras com impacto direto em vetores de ataque.
40
 
41
+ ---
 
42
 
43
+ **Formato obrigatΓ³rio**: bullets e frases curtas. Sem prosa explicativa. Dados concretos (nomes de funΓ§Γ΅es, variΓ‘veis, valores) sempre que disponΓ­veis.`;
44
 
45
  export const FIND_VULNERABILITIES_PROMPT = `VocΓͺ Γ© um auditor especialista em seguranΓ§a de smart contracts com foco em Solidity. Analise sistematicamente o cΓ³digo-fonte do contrato e o contexto do protocolo para identificar vulnerabilidades de seguranΓ§a.
46
 
src/agents/auditor/state.ts CHANGED
@@ -1,6 +1,12 @@
1
  import { StateSchema } from "@langchain/langgraph";
2
  import { z } from "zod";
3
 
 
 
 
 
 
 
4
  export const CandidateFindingSchema = z.object({
5
  title: z.string(),
6
  description: z.string(),
@@ -34,6 +40,7 @@ export const AuditorState = new StateSchema({
34
  scope: z.array(z.string()).default([]),
35
  docs: z.array(z.string()).default([]),
36
  fileTree: z.string().default(""),
 
37
  repoContext: z.string().default(""),
38
  candidateFindings: z.array(LocatedFindingSchema).default([]),
39
  judgeReviews: z.array(JudgeReviewSchema).default([]),
 
1
  import { StateSchema } from "@langchain/langgraph";
2
  import { z } from "zod";
3
 
4
+ export const FileRankingSchema = z.object({
5
+ filePath: z.string(),
6
+ importance: z.number().int().min(1).max(5),
7
+ reasoning: z.string(),
8
+ });
9
+
10
  export const CandidateFindingSchema = z.object({
11
  title: z.string(),
12
  description: z.string(),
 
40
  scope: z.array(z.string()).default([]),
41
  docs: z.array(z.string()).default([]),
42
  fileTree: z.string().default(""),
43
+ fileRankings: z.array(FileRankingSchema).default([]),
44
  repoContext: z.string().default(""),
45
  candidateFindings: z.array(LocatedFindingSchema).default([]),
46
  judgeReviews: z.array(JudgeReviewSchema).default([]),
src/agents/auditor/tools/{repo-tree-tool.ts β†’ repo-tree/tool.ts} RENAMED
@@ -4,7 +4,7 @@ import path from "node:path";
4
  import { tool } from "langchain";
5
  import { z } from "zod";
6
 
7
- import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../config.ts";
8
 
9
  const CONFIG_FILES = new Set([
10
  "foundry.toml",
@@ -90,14 +90,11 @@ export const buildRepoTree = (repoPath: string): string => {
90
  return `${repoName}/\n${renderTree(nodes, "")}`;
91
  };
92
 
93
- export const repoTreeTool = tool(
94
- async ({ repoPath }) => buildRepoTree(repoPath),
95
- {
96
- name: "repo_tree",
97
- description:
98
- "Walk a repository and return a file-system tree of relevant files tagged by kind: [sol] for auditable Solidity contracts, [test] for Solidity test files, [doc] for documentation, and [config] for project config files. Use this during Define Scope to understand repository layout before selecting which files to audit.",
99
- schema: z.object({
100
- repoPath: z.string().describe("Absolute path to the repository root."),
101
- }),
102
- },
103
- );
 
4
  import { tool } from "langchain";
5
  import { z } from "zod";
6
 
7
+ import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../../config.ts";
8
 
9
  const CONFIG_FILES = new Set([
10
  "foundry.toml",
 
90
  return `${repoName}/\n${renderTree(nodes, "")}`;
91
  };
92
 
93
+ export const repoTreeTool = tool(async ({ repoPath }) => buildRepoTree(repoPath), {
94
+ name: "repo_tree",
95
+ description:
96
+ "Walk a repository and return a file-system tree of relevant files tagged by kind: [sol] for auditable Solidity contracts, [test] for Solidity test files, [doc] for documentation, and [config] for project config files. Use this during Define Scope to understand repository layout before selecting which files to audit.",
97
+ schema: z.object({
98
+ repoPath: z.string().describe("Absolute path to the repository root."),
99
+ }),
100
+ });
 
 
 
src/agents/auditor/tools/solidity-analyzer-tool.ts DELETED
@@ -1,567 +0,0 @@
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
- export 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
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/auditor/tools/solidity-analyzer/tool.ts ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parse } from "@solidity-parser/parser";
2
+ import { tool } from "langchain";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ analyzeFunction,
7
+ buildCommentBlocks,
8
+ type ContractAnalysis,
9
+ extractSolcVersion,
10
+ findCommentFor,
11
+ generateBriefMarkdown,
12
+ generateFullMarkdown,
13
+ paramToString,
14
+ type RenderOptions,
15
+ typeToString,
16
+ } from "./utils.ts";
17
+
18
+ export const analyzeSolidityFile = async (
19
+ soliditySource: string,
20
+ mode: "full" | "short",
21
+ filePath?: string,
22
+ importance?: number,
23
+ ): Promise<string> => {
24
+ let ast: any;
25
+ try {
26
+ ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
27
+ } catch (e: any) {
28
+ return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
29
+ }
30
+
31
+ const comments = buildCommentBlocks(soliditySource);
32
+ const imports: string[] = [];
33
+ const contracts: ContractAnalysis[] = [];
34
+
35
+ for (const node of ast.children ?? []) {
36
+ if (node.type === "ImportDirective") imports.push(node.path as string);
37
+ }
38
+
39
+ for (const node of ast.children ?? []) {
40
+ if (node.type !== "ContractDefinition") continue;
41
+
42
+ const contractComment = node.loc ? findCommentFor(node.loc.start.line, comments) : undefined;
43
+
44
+ const contract: ContractAnalysis = {
45
+ name: node.name as string,
46
+ kind: (node.kind as string) ?? "contract",
47
+ baseContracts: (node.baseContracts ?? []).map(
48
+ (bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
49
+ ),
50
+ usingFor: [],
51
+ stateVars: [],
52
+ events: [],
53
+ errors: [],
54
+ modifiers: [],
55
+ functions: [],
56
+ natspec: contractComment?.natspec,
57
+ };
58
+
59
+ const stateVarNames = new Set<string>();
60
+
61
+ for (const member of node.subNodes ?? []) {
62
+ const memberComment = member.loc ? findCommentFor(member.loc.start.line, comments) : undefined;
63
+
64
+ switch (member.type) {
65
+ case "StateVariableDeclaration":
66
+ for (const v of member.variables ?? []) {
67
+ stateVarNames.add(v.name as string);
68
+ contract.stateVars.push({
69
+ name: v.name as string,
70
+ type: typeToString(v.typeName),
71
+ visibility: (v.visibility as string) ?? "internal",
72
+ constant: (v.isDeclaredConst as boolean) ?? false,
73
+ immutable: (v.isImmutable as boolean) ?? false,
74
+ natspec: memberComment?.natspec,
75
+ });
76
+ }
77
+ break;
78
+
79
+ case "EventDefinition": {
80
+ const params = (member.parameters ?? []).map((p: any) => {
81
+ const indexed = p.isIndexed ? "indexed " : "";
82
+ const name = p.name ? ` ${p.name as string}` : "";
83
+ return `${indexed}${typeToString(p.typeName)}${name}`;
84
+ });
85
+ contract.events.push({
86
+ name: member.name as string,
87
+ params,
88
+ anonymous: (member.isAnonymous as boolean) ?? false,
89
+ natspec: memberComment?.natspec,
90
+ });
91
+ break;
92
+ }
93
+
94
+ case "CustomErrorDefinition":
95
+ contract.errors.push({
96
+ name: member.name as string,
97
+ params: (member.parameters ?? []).map((p: any) => paramToString(p)),
98
+ natspec: memberComment?.natspec,
99
+ });
100
+ break;
101
+
102
+ case "ModifierDefinition":
103
+ contract.modifiers.push({
104
+ name: member.name as string,
105
+ params: (member.parameters ?? []).map(paramToString),
106
+ natspec: memberComment?.natspec,
107
+ });
108
+ break;
109
+
110
+ case "FunctionDefinition": {
111
+ const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
112
+ contract.functions.push({
113
+ name: (member.name as string) ?? "",
114
+ isConstructor: (member.isConstructor as boolean) ?? false,
115
+ isReceive: (member.isReceiveEther as boolean) ?? false,
116
+ isFallback: (member.isFallback as boolean) ?? false,
117
+ visibility: (member.visibility as string) ?? "internal",
118
+ mutability: (member.stateMutability as string) ?? "nonpayable",
119
+ params: (member.parameters ?? []).map(paramToString),
120
+ returns: (member.returnParameters ?? []).map(paramToString),
121
+ modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
122
+ internalCalls,
123
+ externalCalls,
124
+ stateReads,
125
+ stateWrites,
126
+ natspec: memberComment?.natspec,
127
+ });
128
+ break;
129
+ }
130
+
131
+ case "UsingForDeclaration": {
132
+ const forType = member.typeName ? typeToString(member.typeName) : "*";
133
+ if (member.libraryName) {
134
+ contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
135
+ } else {
136
+ const fns = (member.functions ?? [])
137
+ .map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
138
+ .join(", ");
139
+ contract.usingFor.push(`{${fns}} for \`${forType}\``);
140
+ }
141
+ break;
142
+ }
143
+ }
144
+ }
145
+
146
+ contracts.push(contract);
147
+ }
148
+
149
+ const opts: RenderOptions = {
150
+ filePath,
151
+ importance,
152
+ lineCount: soliditySource.split("\n").length,
153
+ solcVersion: extractSolcVersion(soliditySource),
154
+ };
155
+
156
+ return mode === "short"
157
+ ? generateBriefMarkdown(imports, contracts, opts)
158
+ : generateFullMarkdown(imports, contracts, opts);
159
+ };
160
+
161
+ export const solidityAnalyzerTool = tool(async ({ solidityFile, mode }) => analyzeSolidityFile(solidityFile, mode), {
162
+ name: "solidity_analyzer",
163
+ description:
164
+ "Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact summary (meta, external calls, function table). Use mode='full' for the complete report including storage, events, errors, per-function call graph, recursion detection, and state variable touchpoints.",
165
+ schema: z.object({
166
+ solidityFile: z.string().describe("The full Solidity source code to analyze."),
167
+ mode: z
168
+ .enum(["full", "short"])
169
+ .default("full")
170
+ .describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
171
+ }),
172
+ });
src/agents/auditor/tools/solidity-analyzer/utils.ts ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { visit } from "@solidity-parser/parser";
2
+
3
+ const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
4
+ const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
5
+
6
+ export interface NatSpec {
7
+ title?: string;
8
+ author?: string;
9
+ notice?: string;
10
+ dev?: string;
11
+ params: Record<string, string>;
12
+ returns: string[];
13
+ inheritdoc?: string;
14
+ custom: Record<string, string>;
15
+ }
16
+
17
+ export interface ParsedComment {
18
+ text: string;
19
+ startLine: number;
20
+ endLine: number;
21
+ isNatSpec: boolean;
22
+ natspec?: NatSpec;
23
+ }
24
+
25
+ export interface StateVar {
26
+ name: string;
27
+ type: string;
28
+ visibility: string;
29
+ constant: boolean;
30
+ immutable: boolean;
31
+ natspec?: NatSpec;
32
+ }
33
+
34
+ export interface EventDef {
35
+ name: string;
36
+ params: string[];
37
+ anonymous: boolean;
38
+ natspec?: NatSpec;
39
+ }
40
+
41
+ export interface ErrorDef {
42
+ name: string;
43
+ params: string[];
44
+ natspec?: NatSpec;
45
+ }
46
+
47
+ export interface ModifierDef {
48
+ name: string;
49
+ params: string[];
50
+ natspec?: NatSpec;
51
+ }
52
+
53
+ export interface FunctionDef {
54
+ name: string;
55
+ isConstructor: boolean;
56
+ isReceive: boolean;
57
+ isFallback: boolean;
58
+ visibility: string;
59
+ mutability: string;
60
+ params: string[];
61
+ returns: string[];
62
+ modifiers: string[];
63
+ internalCalls: string[];
64
+ externalCalls: string[];
65
+ stateReads: string[];
66
+ stateWrites: string[];
67
+ natspec?: NatSpec;
68
+ }
69
+
70
+ export interface ContractAnalysis {
71
+ name: string;
72
+ kind: string;
73
+ baseContracts: string[];
74
+ usingFor: string[];
75
+ stateVars: StateVar[];
76
+ events: EventDef[];
77
+ errors: ErrorDef[];
78
+ modifiers: ModifierDef[];
79
+ functions: FunctionDef[];
80
+ natspec?: NatSpec;
81
+ }
82
+
83
+ export interface RenderOptions {
84
+ filePath?: string;
85
+ importance?: number;
86
+ lineCount: number;
87
+ solcVersion: string;
88
+ }
89
+
90
+ export const extractSolcVersion = (source: string): string => {
91
+ const match = source.match(/pragma\s+solidity\s+([^;]+);/);
92
+ return match ? match[1].trim() : "β€”";
93
+ };
94
+
95
+ const parseNatSpecTags = (text: string): NatSpec => {
96
+ const result: NatSpec = { params: {}, returns: [], custom: {} };
97
+
98
+ const firstTag = text.search(/@(?:title|author|notice|dev|param|return|inheritdoc|custom:)/);
99
+ if (firstTag > 0) {
100
+ const implicit = text.slice(0, firstTag).trim();
101
+ if (implicit) result.notice = implicit.replace(/\n\s*/g, " ");
102
+ } else if (firstTag === -1 && text.trim()) {
103
+ result.notice = text.trim().replace(/\n\s*/g, " ");
104
+ }
105
+
106
+ const tagRe = /@(custom:\S+|\w+)([^@]*)/g;
107
+ let m: RegExpExecArray | null;
108
+ while ((m = tagRe.exec(text)) !== null) {
109
+ const tag = m[1];
110
+ const value = m[2].trim().replace(/\n\s*/g, " ");
111
+ if (tag === "title") result.title = value;
112
+ else if (tag === "author") result.author = value;
113
+ else if (tag === "notice") result.notice = value;
114
+ else if (tag === "dev") result.dev = value;
115
+ else if (tag === "inheritdoc") result.inheritdoc = value;
116
+ else if (tag === "param") {
117
+ const sp = value.indexOf(" ");
118
+ if (sp > 0) result.params[value.slice(0, sp)] = value.slice(sp + 1);
119
+ else if (value) result.params[value] = "";
120
+ } else if (tag === "return") result.returns.push(value);
121
+ else if (tag.startsWith("custom:")) result.custom[tag.slice(7)] = value;
122
+ }
123
+
124
+ return result;
125
+ };
126
+
127
+ export const buildCommentBlocks = (source: string): ParsedComment[] => {
128
+ const blocks: ParsedComment[] = [];
129
+ const lines = source.split("\n");
130
+ let i = 0;
131
+
132
+ while (i < lines.length) {
133
+ const raw = lines[i];
134
+ const trimmed = raw.trimStart();
135
+
136
+ if (trimmed.startsWith("///")) {
137
+ const startLine = i + 1;
138
+ const texts: string[] = [];
139
+ while (i < lines.length && lines[i].trimStart().startsWith("///")) {
140
+ texts.push(lines[i].trimStart().slice(3).replace(/^ /, ""));
141
+ i++;
142
+ }
143
+ const text = texts.join("\n");
144
+ blocks.push({ text, startLine, endLine: i, isNatSpec: true, natspec: parseNatSpecTags(text) });
145
+ continue;
146
+ }
147
+
148
+ const mlStart = raw.indexOf("/*");
149
+ if (mlStart !== -1) {
150
+ const isNatSpec = raw[mlStart + 2] === "*" && raw[mlStart + 3] !== "/";
151
+ const startLine = i + 1;
152
+ const closeOnSame = raw.indexOf("*/", mlStart + 2);
153
+
154
+ if (closeOnSame !== -1) {
155
+ const inner = raw.slice(mlStart + (isNatSpec ? 3 : 2), closeOnSame).trim();
156
+ blocks.push({
157
+ text: inner,
158
+ startLine,
159
+ endLine: startLine,
160
+ isNatSpec,
161
+ natspec: isNatSpec ? parseNatSpecTags(inner) : undefined,
162
+ });
163
+ i++;
164
+ continue;
165
+ }
166
+
167
+ const rawLines: string[] = [raw.slice(mlStart + (isNatSpec ? 3 : 2))];
168
+ i++;
169
+ while (i < lines.length) {
170
+ const closeIdx = lines[i].indexOf("*/");
171
+ if (closeIdx !== -1) {
172
+ rawLines.push(lines[i].slice(0, closeIdx));
173
+ i++;
174
+ break;
175
+ }
176
+ rawLines.push(lines[i]);
177
+ i++;
178
+ }
179
+ const text = rawLines
180
+ .map((l) => l.replace(/^\s*\*\s?/, ""))
181
+ .join("\n")
182
+ .trim();
183
+ blocks.push({ text, startLine, endLine: i, isNatSpec, natspec: isNatSpec ? parseNatSpecTags(text) : undefined });
184
+ continue;
185
+ }
186
+
187
+ if (trimmed.startsWith("//")) {
188
+ blocks.push({ text: trimmed.slice(2).trim(), startLine: i + 1, endLine: i + 1, isNatSpec: false });
189
+ }
190
+
191
+ i++;
192
+ }
193
+
194
+ return blocks;
195
+ };
196
+
197
+ export const findCommentFor = (line: number, comments: ParsedComment[]): ParsedComment | undefined =>
198
+ comments.find((c) => c.endLine === line - 1) ?? comments.find((c) => c.startLine === line && !c.isNatSpec);
199
+
200
+ export const typeToString = (node: any): string => {
201
+ if (!node) return "unknown";
202
+ switch (node.type) {
203
+ case "ElementaryTypeName":
204
+ return node.name as string;
205
+ case "UserDefinedTypeName":
206
+ return (node.namePath ?? node.name) as string;
207
+ case "ArrayTypeName":
208
+ return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
209
+ case "Mapping":
210
+ return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
211
+ case "FunctionTypeName":
212
+ return "function";
213
+ default:
214
+ return "unknown";
215
+ }
216
+ };
217
+
218
+ export const paramToString = (p: any): string => {
219
+ if (!p) return "?";
220
+ const type = typeToString(p.typeName);
221
+ return p.name ? `${type} ${p.name}` : type;
222
+ };
223
+
224
+ const collectLHSRoots = (node: any, targets: Set<string>) => {
225
+ if (!node) return;
226
+ switch (node.type) {
227
+ case "Identifier":
228
+ targets.add(node.name as string);
229
+ break;
230
+ case "MemberAccess":
231
+ collectLHSRoots(node.expression, targets);
232
+ break;
233
+ case "IndexAccess":
234
+ collectLHSRoots(node.base, targets);
235
+ break;
236
+ case "TupleExpression":
237
+ for (const c of node.components ?? []) collectLHSRoots(c, targets);
238
+ break;
239
+ }
240
+ };
241
+
242
+ export const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
243
+ const internalCalls = new Set<string>();
244
+ const externalCalls = new Set<string>();
245
+ const writeTargets = new Set<string>();
246
+ const allStateAccesses = new Set<string>();
247
+ const localVars = new Set<string>();
248
+
249
+ if (!funcNode.body) {
250
+ return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
251
+ }
252
+
253
+ for (const p of funcNode.parameters ?? []) {
254
+ if (p?.name) localVars.add(p.name as string);
255
+ }
256
+ for (const p of funcNode.returnParameters ?? []) {
257
+ if (p?.name) localVars.add(p.name as string);
258
+ }
259
+
260
+ visit(funcNode.body, {
261
+ VariableDeclarationStatement: (node: any) => {
262
+ for (const v of node.variables ?? []) {
263
+ if (v?.name) localVars.add(v.name as string);
264
+ }
265
+ },
266
+ });
267
+
268
+ const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
269
+
270
+ visit(funcNode.body, {
271
+ ExpressionStatement: (node: any) => {
272
+ const expr = node.expression;
273
+ if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
274
+ collectLHSRoots(expr.left, writeTargets);
275
+ }
276
+ if (
277
+ expr?.type === "UnaryOperation" &&
278
+ (expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
279
+ ) {
280
+ collectLHSRoots(expr.subExpression, writeTargets);
281
+ }
282
+ },
283
+ });
284
+
285
+ visit(funcNode.body, {
286
+ FunctionCall: (node: any) => {
287
+ const expr = node.expression;
288
+ if (expr?.type === "Identifier") {
289
+ internalCalls.add(expr.name as string);
290
+ } else if (expr?.type === "MemberAccess") {
291
+ const base = expr.expression;
292
+ if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
293
+ internalCalls.add(expr.memberName as string);
294
+ } else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
295
+ // builtin namespace β€” skip
296
+ } else {
297
+ const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
298
+ externalCalls.add(`${baseStr}.${expr.memberName as string}`);
299
+ }
300
+ }
301
+ },
302
+ Identifier: (node: any) => {
303
+ if (effectiveStateVars.has(node.name as string)) {
304
+ allStateAccesses.add(node.name as string);
305
+ }
306
+ },
307
+ });
308
+
309
+ const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
310
+
311
+ return {
312
+ internalCalls: [...internalCalls],
313
+ externalCalls: [...externalCalls],
314
+ stateReads: [...allStateAccesses],
315
+ stateWrites,
316
+ };
317
+ };
318
+
319
+ export const hasCycle = (
320
+ start: string,
321
+ current: string,
322
+ callMap: Map<string, string[]>,
323
+ visited: Set<string>,
324
+ ): boolean => {
325
+ for (const callee of callMap.get(current) ?? []) {
326
+ if (callee === start) return true;
327
+ if (!visited.has(callee)) {
328
+ visited.add(callee);
329
+ if (hasCycle(start, callee, callMap, visited)) return true;
330
+ }
331
+ }
332
+ return false;
333
+ };
334
+
335
+ export const fnLabel = (fn: FunctionDef): string => {
336
+ if (fn.isConstructor) return "constructor";
337
+ if (fn.isReceive) return "receive";
338
+ if (fn.isFallback) return "fallback";
339
+ return fn.name;
340
+ };
341
+
342
+ const renderNatSpec = (ns: NatSpec | undefined): string => {
343
+ if (!ns) return "β€”";
344
+ const parts: string[] = [];
345
+ if (ns.title) parts.push(`@title "${ns.title}"`);
346
+ if (ns.author) parts.push(`@author "${ns.author}"`);
347
+ if (ns.notice) parts.push(`@notice "${ns.notice}"`);
348
+ if (ns.dev) parts.push(`@dev "${ns.dev}"`);
349
+ for (const [k, v] of Object.entries(ns.params)) {
350
+ parts.push(v ? `@param ${k}: "${v}"` : `@param ${k}`);
351
+ }
352
+ for (const r of ns.returns) {
353
+ if (r) parts.push(`@return "${r}"`);
354
+ }
355
+ return parts.length > 0 ? parts.join(" Β· ") : "β€”";
356
+ };
357
+
358
+ const renderContractFull = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
359
+ lines.push("## Meta");
360
+ const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "β€”";
361
+ lines.push(`- kind: ${contract.kind} Β· inherits: ${inherits}`);
362
+ if (contract.usingFor.length > 0) lines.push(`- uses: [${contract.usingFor.join(", ")}]`);
363
+ lines.push(`- imports: ${imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "β€”"}`);
364
+ lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
365
+ lines.push("");
366
+
367
+ if (contract.stateVars.length > 0) {
368
+ lines.push("## Storage");
369
+ lines.push("| name | type | vis | flags | desc |");
370
+ lines.push("|------|------|-----|-------|------|");
371
+ for (const v of contract.stateVars) {
372
+ const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ") || "β€”";
373
+ const desc = v.natspec?.notice ?? v.natspec?.dev ?? "β€”";
374
+ lines.push(`| \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} | ${desc} |`);
375
+ }
376
+ lines.push("");
377
+ }
378
+
379
+ if (contract.events.length > 0) {
380
+ lines.push("## Events");
381
+ for (const e of contract.events) {
382
+ const notice = e.natspec?.notice ? ` β€” ${e.natspec.notice}` : "";
383
+ lines.push(`- \`${e.name}(${e.params.join(", ")})\`${e.anonymous ? " _(anon)_" : ""}${notice}`);
384
+ }
385
+ lines.push("");
386
+ }
387
+
388
+ lines.push("## Errors");
389
+ if (contract.errors.length === 0) {
390
+ lines.push("- None");
391
+ } else {
392
+ for (const e of contract.errors) lines.push(`- \`${e.name}(${e.params.join(", ")})\``);
393
+ }
394
+ lines.push("");
395
+
396
+ if (contract.modifiers.length > 0) {
397
+ lines.push("## Modifiers");
398
+ for (const m of contract.modifiers) {
399
+ const notice = m.natspec?.notice ? ` β€” ${m.natspec.notice}` : "";
400
+ lines.push(`- \`${m.name}(${m.params.join(", ")})\`${notice}`);
401
+ }
402
+ lines.push("");
403
+ }
404
+
405
+ const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
406
+ if (allExternalCalls.size > 0) {
407
+ lines.push("## External Calls");
408
+ for (const call of allExternalCalls) lines.push(`- \`${call}\``);
409
+ lines.push("");
410
+ }
411
+
412
+ if (contract.functions.length > 0) {
413
+ lines.push("## Functions");
414
+ lines.push("");
415
+
416
+ const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
417
+
418
+ for (const fn of contract.functions) {
419
+ const label = fnLabel(fn);
420
+ lines.push(`### ${label}`);
421
+
422
+ const modsStr = fn.modifiers.length > 0 ? ` Β· modifiers: [${fn.modifiers.join(", ")}]` : "";
423
+ lines.push(`- visibility: ${fn.visibility} Β· mutability: ${fn.mutability}${modsStr}`);
424
+
425
+ const paramsStr = fn.params.length > 0 ? fn.params.join(", ") : "β€”";
426
+ const returnsStr = fn.returns.length > 0 ? `\`${fn.returns.join(", ")}\`` : "β€”";
427
+ lines.push(`- parameters: \`(${paramsStr})\` Β· returns: ${returnsStr}`);
428
+
429
+ if (fn.externalCalls.length > 0) {
430
+ lines.push(`- calls: [${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}]`);
431
+ }
432
+ if (fn.internalCalls.length > 0) {
433
+ lines.push(`- graph: \`${fn.internalCalls.map((c) => `${label} β†’ ${c}`).join(", ")}\``);
434
+ }
435
+
436
+ lines.push(`- recurse: ${hasCycle(label, label, callMap, new Set()) ? "yes ⚠" : "no"}`);
437
+
438
+ if (fn.stateReads.length > 0 || fn.stateWrites.length > 0) {
439
+ const reads = fn.stateReads.length > 0 ? fn.stateReads.map((r) => `\`${r}\``).join(", ") : "β€”";
440
+ const writes = fn.stateWrites.length > 0 ? fn.stateWrites.map((w) => `\`${w}\``).join(", ") : "β€”";
441
+ lines.push(`- state: reads [${reads}] Β· writes [${writes}]`);
442
+ }
443
+
444
+ lines.push(`- docs: ${renderNatSpec(fn.natspec)}`);
445
+ lines.push("");
446
+ }
447
+ }
448
+ };
449
+
450
+ const renderContractBrief = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
451
+ lines.push("## Meta");
452
+ const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "β€”";
453
+ const importsList = imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "β€”";
454
+ lines.push(`- kind: ${contract.kind} Β· inherits: ${inherits}`);
455
+ lines.push(`- imports: ${importsList}`);
456
+ lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
457
+ lines.push("");
458
+
459
+ const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
460
+ if (allExternalCalls.size > 0) {
461
+ lines.push("## External Calls");
462
+ lines.push([...allExternalCalls].map((c) => `\`${c}\``).join(" Β· "));
463
+ lines.push("");
464
+ }
465
+
466
+ if (contract.functions.length > 0) {
467
+ lines.push("## Functions");
468
+ lines.push("| function | visibility | mutability | parameters | returns | modifiers |");
469
+ lines.push("|----------|------------|------------|------------|---------|-----------|");
470
+ for (const fn of contract.functions) {
471
+ const label = fnLabel(fn);
472
+ const params = fn.params.length > 0 ? fn.params.join(", ") : "β€”";
473
+ const returns = fn.returns.length > 0 ? fn.returns.join(", ") : "β€”";
474
+ const mods = fn.modifiers.length > 0 ? fn.modifiers.join(", ") : "β€”";
475
+ lines.push(`| \`${label}\` | ${fn.visibility} | ${fn.mutability} | ${params} | ${returns} | ${mods} |`);
476
+ }
477
+ lines.push("");
478
+ }
479
+ };
480
+
481
+ const fileHeader = (contracts: ContractAnalysis[], mode: "full" | "short", opts: RenderOptions): string[] => {
482
+ const names = contracts.map((c) => c.name).join(", ");
483
+ const label = mode === "full" ? "FULL" : "BRIEF";
484
+ const rankStr = opts.importance !== undefined ? ` | importance: ${opts.importance}/5` : "";
485
+ return [
486
+ `# ${names} Β· ${label}`,
487
+ `> path: \`${opts.filePath ?? "β€”"}\` | lines: ${opts.lineCount} | solc: ${opts.solcVersion}${rankStr}`,
488
+ "",
489
+ ];
490
+ };
491
+
492
+ export const generateFullMarkdown = (imports: string[], contracts: ContractAnalysis[], opts: RenderOptions): string => {
493
+ const lines: string[] = fileHeader(contracts, "full", opts);
494
+
495
+ for (let i = 0; i < contracts.length; i++) {
496
+ if (contracts.length > 1) {
497
+ if (i > 0) lines.push("---", "");
498
+ lines.push(`## β—† ${contracts[i].name}`, "");
499
+ }
500
+ renderContractFull(contracts[i], imports, lines);
501
+ }
502
+
503
+ return lines.join("\n");
504
+ };
505
+
506
+ export const generateBriefMarkdown = (
507
+ imports: string[],
508
+ contracts: ContractAnalysis[],
509
+ opts: RenderOptions,
510
+ ): string => {
511
+ const lines: string[] = fileHeader(contracts, "short", opts);
512
+
513
+ for (let i = 0; i < contracts.length; i++) {
514
+ if (contracts.length > 1) {
515
+ if (i > 0) lines.push("---", "");
516
+ lines.push(`## β—† ${contracts[i].name}`, "");
517
+ }
518
+ renderContractBrief(contracts[i], imports, lines);
519
+ }
520
+
521
+ return lines.join("\n");
522
+ };