Spaces:
Runtime error
Runtime error
Uanderson Silva commited on
Commit ·
184b6e6
1
Parent(s): 2c8beb8
small changes to prompt and caching
Browse files- package-lock.json +12 -10
- package.json +1 -1
- src/agents/auditor/agent.ts +84 -109
- src/agents/auditor/prompts.ts +6 -6
- src/agents/auditor/tools/solidity-analyzer/utils.ts +3 -5
- src/agents/auditor/utils.ts +35 -0
package-lock.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
"license": "ISC",
|
| 12 |
"dependencies": {
|
| 13 |
"@hono/node-server": "^2.0.3",
|
| 14 |
-
"@langchain/anthropic": "^1.
|
| 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.
|
| 40 |
-
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.
|
| 41 |
-
"integrity": "sha512-
|
| 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.
|
| 203 |
-
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.
|
| 204 |
-
"integrity": "sha512-
|
| 205 |
"license": "MIT",
|
| 206 |
"dependencies": {
|
| 207 |
-
"@anthropic-ai/sdk": "^0.
|
| 208 |
"zod": "^3.25.76 || ^4"
|
| 209 |
},
|
| 210 |
"engines": {
|
| 211 |
"node": ">=20"
|
| 212 |
},
|
| 213 |
"peerDependencies": {
|
| 214 |
-
"@langchain/core": "^1.1.
|
| 215 |
}
|
| 216 |
},
|
| 217 |
"node_modules/@langchain/core": {
|
| 218 |
-
"version": "1.1.
|
|
|
|
|
|
|
| 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.
|
| 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({
|
|
|
|
|
|
|
| 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 = {
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 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 = {
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 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
|
| 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 |
-
- **
|
| 40 |
- **De controle**: acesso, sequência de operações, transições de estado permitidas
|
| 41 |
|
| 42 |
## Trust Assumptions
|
| 43 |
-
|
| 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)
|
| 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**:
|
| 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 |
-
|
| 389 |
-
|
| 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());
|