Spaces:
Runtime error
Runtime error
Uanderson Silva commited on
Commit Β·
0b3cd21
1
Parent(s): 4c144c5
refactor tools and rank files in define scope
Browse files- biome.json +51 -53
- src/agents/auditor/agent.ts +37 -19
- src/agents/auditor/prompts.ts +34 -14
- src/agents/auditor/state.ts +7 -0
- src/agents/auditor/tools/{repo-tree-tool.ts β repo-tree/tool.ts} +9 -12
- src/agents/auditor/tools/solidity-analyzer-tool.ts +0 -567
- src/agents/auditor/tools/solidity-analyzer/tool.ts +172 -0
- src/agents/auditor/tools/solidity-analyzer/utils.ts +522 -0
biome.json
CHANGED
|
@@ -1,55 +1,53 @@
|
|
| 1 |
{
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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 {
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 115 |
-
for (const {
|
| 116 |
-
parts.push(
|
| 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
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 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 |
-
|
| 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 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
## 5. Premissas de Design
|
| 18 |
-
O que o protocolo assume sobre
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 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 |
-
|
| 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 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 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 |
+
};
|