Uanderson Silva commited on
Commit
184b6e6
·
1 Parent(s): 2c8beb8

small changes to prompt and caching

Browse files
package-lock.json CHANGED
@@ -11,7 +11,7 @@
11
  "license": "ISC",
12
  "dependencies": {
13
  "@hono/node-server": "^2.0.3",
14
- "@langchain/anthropic": "^1.3.29",
15
  "@langchain/core": "^1.1.45",
16
  "@langchain/google-genai": "^2.1.31",
17
  "@langchain/langgraph": "^1.3.2",
@@ -36,9 +36,9 @@
36
  }
37
  },
38
  "node_modules/@anthropic-ai/sdk": {
39
- "version": "0.95.2",
40
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.2.tgz",
41
- "integrity": "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==",
42
  "license": "MIT",
43
  "dependencies": {
44
  "json-schema-to-ts": "^3.1.1",
@@ -199,23 +199,25 @@
199
  }
200
  },
201
  "node_modules/@langchain/anthropic": {
202
- "version": "1.4.0",
203
- "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.0.tgz",
204
- "integrity": "sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==",
205
  "license": "MIT",
206
  "dependencies": {
207
- "@anthropic-ai/sdk": "^0.95.1",
208
  "zod": "^3.25.76 || ^4"
209
  },
210
  "engines": {
211
  "node": ">=20"
212
  },
213
  "peerDependencies": {
214
- "@langchain/core": "^1.1.47"
215
  }
216
  },
217
  "node_modules/@langchain/core": {
218
- "version": "1.1.47",
 
 
219
  "license": "MIT",
220
  "dependencies": {
221
  "@cfworker/json-schema": "^4.0.2",
 
11
  "license": "ISC",
12
  "dependencies": {
13
  "@hono/node-server": "^2.0.3",
14
+ "@langchain/anthropic": "^1.4.1",
15
  "@langchain/core": "^1.1.45",
16
  "@langchain/google-genai": "^2.1.31",
17
  "@langchain/langgraph": "^1.3.2",
 
36
  }
37
  },
38
  "node_modules/@anthropic-ai/sdk": {
39
+ "version": "0.103.0",
40
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.103.0.tgz",
41
+ "integrity": "sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==",
42
  "license": "MIT",
43
  "dependencies": {
44
  "json-schema-to-ts": "^3.1.1",
 
199
  }
200
  },
201
  "node_modules/@langchain/anthropic": {
202
+ "version": "1.4.1",
203
+ "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.1.tgz",
204
+ "integrity": "sha512-h3b6hxThcfh0WdmpuWr+qBi74MN+0BpNI/4H681vwXxbD3hLr2qMYN6ghqcPQhCxGJjg8ufs85qu2/ldSWonYQ==",
205
  "license": "MIT",
206
  "dependencies": {
207
+ "@anthropic-ai/sdk": "^0.103.0",
208
  "zod": "^3.25.76 || ^4"
209
  },
210
  "engines": {
211
  "node": ">=20"
212
  },
213
  "peerDependencies": {
214
+ "@langchain/core": "^1.1.49"
215
  }
216
  },
217
  "node_modules/@langchain/core": {
218
+ "version": "1.1.49",
219
+ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.49.tgz",
220
+ "integrity": "sha512-7wkN3Qv/qZqsY0p3h48CNu6E6y5GMYatYxj+JrX4uVNBiqIVQm1Z528QrmayJWVW9SQTQicqRNoyTCzl+K9F8Q==",
221
  "license": "MIT",
222
  "dependencies": {
223
  "@cfworker/json-schema": "^4.0.2",
package.json CHANGED
@@ -33,7 +33,7 @@
33
  },
34
  "dependencies": {
35
  "@hono/node-server": "^2.0.3",
36
- "@langchain/anthropic": "^1.3.29",
37
  "@langchain/core": "^1.1.45",
38
  "@langchain/google-genai": "^2.1.31",
39
  "@langchain/langgraph": "^1.3.2",
 
33
  },
34
  "dependencies": {
35
  "@hono/node-server": "^2.0.3",
36
+ "@langchain/anthropic": "^1.4.1",
37
  "@langchain/core": "^1.1.45",
38
  "@langchain/google-genai": "^2.1.31",
39
  "@langchain/langgraph": "^1.3.2",
src/agents/auditor/agent.ts CHANGED
@@ -1,5 +1,4 @@
1
  import fs from "node:fs";
2
- import path from "node:path";
3
 
4
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
5
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
@@ -7,18 +6,7 @@ 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,
13
- MAX_DEPTH,
14
- MAX_DOC_CHARS,
15
- MAX_REFLECTIONS,
16
- MAX_SOL_CHARS,
17
- MIN_FILE_IMPORTANCE,
18
- SKIP_DIRS,
19
- SOL_EXT,
20
- SOL_TEST_SUFFIXES,
21
- } from "./config.ts";
22
  import {
23
  FIND_VULNERABILITIES_PROMPT,
24
  GATHER_CONTEXT_PROMPT,
@@ -29,42 +17,12 @@ import {
29
  import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
30
  import { buildRepoTree } from "./tools/repo-tree/tool.ts";
31
  import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
32
- import { buildReviewBlocks, matchLines } from "./utils.ts";
33
 
34
  const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
35
  const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
36
  const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
37
 
38
- const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
39
- if (depth > MAX_DEPTH) return;
40
-
41
- let entries: fs.Dirent[];
42
- try {
43
- entries = fs.readdirSync(dir, { withFileTypes: true });
44
- } catch {
45
- return;
46
- }
47
-
48
- for (const entry of entries) {
49
- if (entry.isDirectory()) {
50
- if (!SKIP_DIRS.has(entry.name)) {
51
- walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
52
- }
53
- } else if (entry.isFile()) {
54
- const fullPath = path.join(dir, entry.name);
55
- const ext = path.extname(entry.name).toLowerCase();
56
- const base = path.basename(entry.name, ext).toLowerCase();
57
-
58
- if (ext === SOL_EXT) {
59
- const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
60
- if (!isTest) solFiles.push(fullPath);
61
- } else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
62
- docFiles.push(fullPath);
63
- }
64
- }
65
- }
66
- };
67
-
68
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
69
  logger.info(`defineScope: walking repo at ${state.repoPath}`);
70
 
@@ -152,7 +110,9 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
152
 
153
  const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
154
  const result = await model.invoke([
155
- new SystemMessage({ content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }] }),
 
 
156
  new HumanMessage(parts.join("\n\n")),
157
  ]);
158
 
@@ -176,46 +136,52 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
176
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
177
  );
178
 
179
- const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
180
-
181
- const allFindings = await Promise.all(
182
- state.scope.map(async (filePath) => {
183
- let source: string;
184
- try {
185
- source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
186
- } catch {
187
- return [];
188
- }
189
- if (!source) return [];
190
-
191
- const fileEntries = isReflection
192
- ? state.candidateFindings
193
- .map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
194
- .filter(({ finding }) => finding.path === filePath)
195
- : [];
196
-
197
- const isRefinement = fileEntries.length > 0;
198
- const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
199
- const contractText = isRefinement
200
- ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
201
- : `Contrato (${filePath}):\n\n${source}`;
202
-
203
- logger.debug(`findVulnerabilities: processing ${filePath}`);
204
-
205
- const result = await model.invoke([
206
- new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
207
- new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
208
- ]);
209
-
210
- return result.findings.map((finding: any) => ({
211
- ...finding,
212
- path: filePath,
213
- location: matchLines(source, finding.codeSnippet) ?? "",
214
- }));
215
- }),
216
- );
 
 
 
 
 
 
 
217
 
218
- const candidateFindings = allFindings.flat();
219
  logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
220
  logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
221
 
@@ -236,31 +202,40 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
236
 
237
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
238
 
239
- const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
240
-
241
- const reviews = await Promise.all(
242
- state.candidateFindings.map(async (finding, i) => {
243
- let source: string;
244
- try {
245
- source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
246
- } catch {
247
- source = "";
248
- }
249
-
250
- const findingText = `[Achado ${i + 1}] ${finding.title}\nSeveridade: ${finding.severity}\nDescrição: ${finding.description}\nLocalização: ${finding.path} linhas ${finding.location}\nCódigo:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
251
-
252
- logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
253
- return model.invoke([
254
- new SystemMessage({ content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }] }),
255
- new HumanMessage({
256
- content: [
257
- cachedContext,
258
- { type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
259
- ],
260
- }),
261
- ]);
262
- }),
263
- );
 
 
 
 
 
 
 
 
 
264
 
265
  const confirmedEntries = state.candidateFindings
266
  .map((finding, i) => ({ finding, review: reviews[i] }))
 
1
  import fs from "node:fs";
 
2
 
3
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
4
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
 
6
 
7
  import { createLLM } from "../../config/llm.ts";
8
  import { logger } from "../../logger.ts";
9
+ import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
 
 
 
 
 
 
 
 
 
 
 
10
  import {
11
  FIND_VULNERABILITIES_PROMPT,
12
  GATHER_CONTEXT_PROMPT,
 
17
  import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
18
  import { buildRepoTree } from "./tools/repo-tree/tool.ts";
19
  import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
20
+ import { buildReviewBlocks, matchLines, walkDirectory } from "./utils.ts";
21
 
22
  const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
23
  const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
24
  const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
27
  logger.info(`defineScope: walking repo at ${state.repoPath}`);
28
 
 
110
 
111
  const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
112
  const result = await model.invoke([
113
+ new SystemMessage({
114
+ content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }],
115
+ }),
116
  new HumanMessage(parts.join("\n\n")),
117
  ]);
118
 
 
136
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
137
  );
138
 
139
+ const cachedContext = {
140
+ type: "text" as const,
141
+ text: `Contexto do Protocolo:\n${state.repoContext}`,
142
+ cache_control: { type: "ephemeral" as const },
143
+ };
144
+
145
+ const processFile = async (filePath: string) => {
146
+ let source: string;
147
+ try {
148
+ source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
149
+ } catch {
150
+ return [];
151
+ }
152
+ if (!source) return [];
153
+
154
+ const fileEntries = isReflection
155
+ ? state.candidateFindings
156
+ .map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
157
+ .filter(({ finding }) => finding.path === filePath)
158
+ : [];
159
+
160
+ const isRefinement = fileEntries.length > 0;
161
+ const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
162
+ const contractText = isRefinement
163
+ ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
164
+ : `Contrato (${filePath}):\n\n${source}`;
165
+
166
+ logger.debug(`findVulnerabilities: processing ${filePath}`);
167
+
168
+ const result = await model.invoke([
169
+ new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
170
+ new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
171
+ ]);
172
+
173
+ return result.findings.map((finding: any) => ({
174
+ ...finding,
175
+ path: filePath,
176
+ location: matchLines(source, finding.codeSnippet) ?? "",
177
+ }));
178
+ };
179
+
180
+ const [firstFile, ...restFiles] = state.scope;
181
+ const firstFindings = firstFile ? await processFile(firstFile) : [];
182
+ const restFindings = await Promise.all(restFiles.map(processFile));
183
+ const candidateFindings = [firstFindings, ...restFindings].flat();
184
 
 
185
  logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
186
  logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
187
 
 
202
 
203
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
204
 
205
+ const cachedContext = {
206
+ type: "text" as const,
207
+ text: `Contexto do Protocolo:\n${state.repoContext}`,
208
+ cache_control: { type: "ephemeral" as const },
209
+ };
210
+
211
+ const reviewFinding = async (finding: (typeof state.candidateFindings)[number], i: number) => {
212
+ let source: string;
213
+ try {
214
+ source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
215
+ } catch {
216
+ source = "";
217
+ }
218
+
219
+ const findingText = `[Achado ${i + 1}] ${finding.title}\nSeveridade: ${finding.severity}\nDescrição: ${finding.description}\nLocalização: ${finding.path} linhas ${finding.location}\nCódigo:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
220
+
221
+ logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
222
+ return model.invoke([
223
+ new SystemMessage({
224
+ content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
225
+ }),
226
+ new HumanMessage({
227
+ content: [
228
+ cachedContext,
229
+ { type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
230
+ ],
231
+ }),
232
+ ]);
233
+ };
234
+
235
+ const [firstFinding, ...restFindings] = state.candidateFindings;
236
+ const firstReview = await reviewFinding(firstFinding, 0);
237
+ const restReviews = await Promise.all(restFindings.map((f, i) => reviewFinding(f, i + 1)));
238
+ const reviews = [firstReview, ...restReviews];
239
 
240
  const confirmedEntries = state.candidateFindings
241
  .map((finding, i) => ({ finding, review: reviews[i] }))
src/agents/auditor/prompts.ts CHANGED
@@ -34,13 +34,13 @@ Apenas os caminhos críticos ponta a ponta que movem valor ou alteram estado rel
34
  Formato por passo: \`ação (função) → efeito colateral → variável/estado alterado\`.
35
  Inclua chamadas cross-contract quando materiais para entender superfície de ataque.
36
 
37
- ## Invariantes e Propriedades de Segurança
38
  Condições que devem ser verdadeiras para o protocolo operar corretamente, derivadas APENAS de \`require\`/\`assert\`/\`revert\` explícitos no código, NatSpec \`@dev\`, ou comentários que as declarem literalmente. Separe em dois grupos:
39
- - **Contábeis**: balanços, totais, proporções (ex.: \`totalDebt == Σ userDebt[i]\`, \`reservas >= totalSupply * exchangeRate\`)
40
  - **De controle**: acesso, sequência de operações, transições de estado permitidas
41
 
42
  ## Trust Assumptions
43
- **SOMENTE o que estiver EXPLICITAMENTE declarado** em código-fonte (require, NatSpec, comentários inline) ou na documentação. **NÃO inferir. NÃO supor. NÃO extrapolar.** Se não há declaração explícita sobre confiança em um componente externo ou comportamento esperado, ele NÃO entra nesta seção — mesmo que pareça óbvio.
44
  Bullets curtos com referência à fonte (ex.: "owner pode pausar o contrato — \`onlyOwner\` em \`pause()\`").
45
 
46
  ## Regras de Negócio e Restrições de Segurança
@@ -64,7 +64,7 @@ Além das categorias técnicas listadas abaixo, sua análise deve cobrir:
64
 
65
  ## Categorias técnicas a verificar sistematicamente
66
 
67
- Reentrância (simples, cross-function, cross-contract, read-only), controle de acesso (funções privilegiadas desprotegidas, erros em herança de roles), overflow/underflow (Solidity <0.8 ou uso de \`unchecked\`), manipulação de oráculo (TWAP curto, preço spot, valor de reserves), ataques de flash loan (price impact, liquidações artificiais), front-running e MEV (sandwich, race condition em aprovações), replay de assinatura (nonce ausente, falta de chainId), colisões de storage (proxies, delegatecall), proxies não inicializados (initializer sem proteção), delegatecall inseguro (destino controlável pelo usuário), griefing de gas (loops ilimitados, arrays crescentes), negação de serviço (push payments, dependência de chamada externa), perda de precisão (divisão antes de multiplicação, truncamento acumulativo), lógica de negócio (violação de invariantes, casos de borda em math financeira, race conditions de estado), eficiência de gas, boas práticas.
68
 
69
  ## Formato de saída
70
 
@@ -114,14 +114,14 @@ Para cada achado, forneça OBRIGATORIAMENTE todos os campos abaixo:
114
  - **review**: Análise técnica detalhada (3 a 6 frases) explicando o veredicto. Referencie: (a) o código específico envolvido, (b) invariantes ou premissas do protocolo que confirmam ou bloqueiam o exploit, (c) pré-condições necessárias para exploração, (d) controles mitigadores existentes que o auditor pode ter ignorado. Seja preciso — cite nomes de funções, variáveis e valores.
115
  - **isFalsePositive**: \`true\` se o achado NÃO for explorável na prática; \`false\` se for uma vulnerabilidade real.
116
  - **confidence**: Inteiro de 0 a 100 refletindo sua certeza no veredicto. Use < 60 apenas quando existir ambiguidade genuína no código.
117
- - **exploitablePaths**: Array de strings.
118
  - Se verdadeiro positivo (\`isFalsePositive: false\`): forneça 1 a 3 caminhos concretos de exploit, cada um com passos numerados, entradas realistas e estado do contrato antes/depois. Ex.: ["1. Atacante chama flashLoan(500k USDC). 2. No callback, chama deposit() inflando reserves. 3. Chama withdraw() com preço manipulado. 4. Lucra 50k USDC. Estado: reserves inflado temporariamente, totalShares inalterado."].
119
  - Se falso positivo (\`isFalsePositive: true\`): forneça o raciocínio exato que bloqueia cada caminho de exploit tentado pelo auditor.
120
 
121
  ## Critérios para falso positivo (aplique com rigor — não seja permissivo)
122
 
123
  1. O caminho de exploit é bloqueado por controle de acesso verificável no código.
124
- 2. A vulnerabilidade já é totalmente mitigada por outro mecanismo no código (ex.: nonReentrant, require com validação suficiente).
125
  3. A condição necessária para o exploit é impossível ou economicamente inviável dado o modelo do protocolo (ex.: requer ser o próprio contrato, ou lucro < custo de gas em qualquer cenário realista).
126
  4. O comportamento é explicitamente documentado como intencional nas premissas de design do protocolo.
127
 
 
34
  Formato por passo: \`ação (função) → efeito colateral → variável/estado alterado\`.
35
  Inclua chamadas cross-contract quando materiais para entender superfície de ataque.
36
 
37
+ ## Invariantes e Propriedades
38
  Condições que devem ser verdadeiras para o protocolo operar corretamente, derivadas APENAS de \`require\`/\`assert\`/\`revert\` explícitos no código, NatSpec \`@dev\`, ou comentários que as declarem literalmente. Separe em dois grupos:
39
+ - **Econômicas**: balanços, totais, proporções (ex.: \`totalDebt == Σ userDebt[i]\`, \`reservas >= totalSupply * exchangeRate\`)
40
  - **De controle**: acesso, sequência de operações, transições de estado permitidas
41
 
42
  ## Trust Assumptions
43
+ **SOMENTE o que estiver EXPLICITAMENTE declarado** em código-fonte (require, NatSpec, comentários inline) ou na documentação. **NÃO inferir. NÃO supor. NÃO extrapolar.** Se não há declaração explícita sobre confiança em um componente externo ou comportamento esperado, ele NÃO entra nesta seção — mesmo que pareça óbvio.
44
  Bullets curtos com referência à fonte (ex.: "owner pode pausar o contrato — \`onlyOwner\` em \`pause()\`").
45
 
46
  ## Regras de Negócio e Restrições de Segurança
 
64
 
65
  ## Categorias técnicas a verificar sistematicamente
66
 
67
+ Reentrância (simples, cross-function, cross-contract, read-only), controle de acesso (funções privilegiadas desprotegidas, erros em herança de roles), overflow/underflow (Solidity <0.8 ou uso de \`unchecked\`), manipulação de oráculo (TWAP curto, preço spot, valor de reserves), ataques de flash loan (price impact, liquidações artificiais), front-running e MEV (sandwich, race condition em aprovações), replay de assinatura (nonce ausente, falta de chainId), colisões de storage (proxies, delegatecall), proxies não inicializados (initializer sem proteção), delegatecall inseguro (destino controlável pelo usuário), griefing de gas (loops ilimitados, arrays crescentes), negação de serviço (push payments, dependência de chamada externa), perda de precisão (divisão antes de multiplicação, truncamento acumulativo), lógica de negócio (violação de invariantes, casos de borda em math financeira, race conditions de estado).
68
 
69
  ## Formato de saída
70
 
 
114
  - **review**: Análise técnica detalhada (3 a 6 frases) explicando o veredicto. Referencie: (a) o código específico envolvido, (b) invariantes ou premissas do protocolo que confirmam ou bloqueiam o exploit, (c) pré-condições necessárias para exploração, (d) controles mitigadores existentes que o auditor pode ter ignorado. Seja preciso — cite nomes de funções, variáveis e valores.
115
  - **isFalsePositive**: \`true\` se o achado NÃO for explorável na prática; \`false\` se for uma vulnerabilidade real.
116
  - **confidence**: Inteiro de 0 a 100 refletindo sua certeza no veredicto. Use < 60 apenas quando existir ambiguidade genuína no código.
117
+ - **exploitablePaths**: Retorne uma array de strings.
118
  - Se verdadeiro positivo (\`isFalsePositive: false\`): forneça 1 a 3 caminhos concretos de exploit, cada um com passos numerados, entradas realistas e estado do contrato antes/depois. Ex.: ["1. Atacante chama flashLoan(500k USDC). 2. No callback, chama deposit() inflando reserves. 3. Chama withdraw() com preço manipulado. 4. Lucra 50k USDC. Estado: reserves inflado temporariamente, totalShares inalterado."].
119
  - Se falso positivo (\`isFalsePositive: true\`): forneça o raciocínio exato que bloqueia cada caminho de exploit tentado pelo auditor.
120
 
121
  ## Critérios para falso positivo (aplique com rigor — não seja permissivo)
122
 
123
  1. O caminho de exploit é bloqueado por controle de acesso verificável no código.
124
+ 2. A vulnerabilidade já é totalmente mitigada por outro mecanismo no código (ex.: nonReentrant, onlyOwner, require com validação suficiente).
125
  3. A condição necessária para o exploit é impossível ou economicamente inviável dado o modelo do protocolo (ex.: requer ser o próprio contrato, ou lucro < custo de gas em qualquer cenário realista).
126
  4. O comportamento é explicitamente documentado como intencional nas premissas de design do protocolo.
127
 
src/agents/auditor/tools/solidity-analyzer/utils.ts CHANGED
@@ -385,13 +385,11 @@ const renderContractFull = (contract: ContractAnalysis, imports: string[], lines
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");
 
385
  lines.push("");
386
  }
387
 
388
+ if (contract.errors.length > 0) {
389
+ lines.push("## Errors");
 
 
390
  for (const e of contract.errors) lines.push(`- \`${e.name}(${e.params.join(", ")})\``);
391
+ lines.push("");
392
  }
 
393
 
394
  if (contract.modifiers.length > 0) {
395
  lines.push("## Modifiers");
src/agents/auditor/utils.ts CHANGED
@@ -1,3 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
2
  const fileLines = fileContent.split("\n");
3
  const snippetLines = codeSnippet.split("\n").map((line) => line.trim());
 
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "./config.ts";
5
+
6
+ export const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
7
+ if (depth > MAX_DEPTH) return;
8
+
9
+ let entries: fs.Dirent[];
10
+ try {
11
+ entries = fs.readdirSync(dir, { withFileTypes: true });
12
+ } catch {
13
+ return;
14
+ }
15
+
16
+ for (const entry of entries) {
17
+ if (entry.isDirectory()) {
18
+ if (!SKIP_DIRS.has(entry.name)) {
19
+ walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
20
+ }
21
+ } else if (entry.isFile()) {
22
+ const fullPath = path.join(dir, entry.name);
23
+ const ext = path.extname(entry.name).toLowerCase();
24
+ const base = path.basename(entry.name, ext).toLowerCase();
25
+
26
+ if (ext === SOL_EXT) {
27
+ const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
28
+ if (!isTest) solFiles.push(fullPath);
29
+ } else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
30
+ docFiles.push(fullPath);
31
+ }
32
+ }
33
+ }
34
+ };
35
+
36
  export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
37
  const fileLines = fileContent.split("\n");
38
  const snippetLines = codeSnippet.split("\n").map((line) => line.trim());