Tales-Cunha commited on
Commit
52550fd
·
1 Parent(s): 59372df

feat: update the agent structure

Browse files
data/benchmark_summary.json CHANGED
@@ -1,18 +1,30 @@
1
  {
2
  "summary": {
3
- "timestamp": "2026-06-09T17:52:42.551Z",
4
- "total_processed": 14,
5
- "reproducibility_rate": 42.857142857142854,
6
- "specificity_rate": 16.666666666666664,
7
- "overall_ground_truth_rate": 7.142857142857142,
8
- "average_iterations": 7.857142857142857
9
  },
10
  "details": [
 
 
 
 
 
 
 
 
 
 
 
 
11
  {
12
  "id": "008",
13
- "reproducible": true,
14
  "specific": false,
15
- "iterations": 3
16
  },
17
  {
18
  "id": "009",
@@ -45,43 +57,55 @@
45
  "iterations": 10
46
  },
47
  {
48
- "id": "039",
49
- "reproducible": true,
50
  "specific": false,
51
- "iterations": 8
52
  },
53
  {
54
- "id": "041",
55
- "reproducible": true,
56
  "specific": false,
57
- "iterations": 6
58
  },
59
  {
60
- "id": "042",
 
 
 
 
 
 
 
 
 
 
 
 
61
  "reproducible": false,
62
  "specific": false,
63
  "iterations": 10
64
  },
65
  {
66
- "id": "046",
67
  "reproducible": true,
68
  "specific": false,
69
  "iterations": 7
70
  },
71
  {
72
- "id": "048",
73
  "reproducible": false,
74
  "specific": false,
75
  "iterations": 10
76
  },
77
  {
78
- "id": "049",
79
  "reproducible": false,
80
  "specific": false,
81
  "iterations": 10
82
  },
83
  {
84
- "id": "070",
85
  "reproducible": false,
86
  "specific": false,
87
  "iterations": 10
@@ -89,8 +113,8 @@
89
  {
90
  "id": "098",
91
  "reproducible": true,
92
- "specific": true,
93
- "iterations": 2
94
  }
95
  ]
96
  }
 
1
  {
2
  "summary": {
3
+ "timestamp": "2026-06-11T17:27:55.677Z",
4
+ "total_processed": 18,
5
+ "reproducibility_rate": 16.666666666666664,
6
+ "specificity_rate": 0,
7
+ "overall_ground_truth_rate": 0,
8
+ "average_iterations": 9.38888888888889
9
  },
10
  "details": [
11
+ {
12
+ "id": "001",
13
+ "reproducible": false,
14
+ "specific": false,
15
+ "iterations": 10
16
+ },
17
+ {
18
+ "id": "003",
19
+ "reproducible": false,
20
+ "specific": false,
21
+ "iterations": 10
22
+ },
23
  {
24
  "id": "008",
25
+ "reproducible": false,
26
  "specific": false,
27
+ "iterations": 10
28
  },
29
  {
30
  "id": "009",
 
57
  "iterations": 10
58
  },
59
  {
60
+ "id": "042",
61
+ "reproducible": false,
62
  "specific": false,
63
+ "iterations": 10
64
  },
65
  {
66
+ "id": "048",
67
+ "reproducible": false,
68
  "specific": false,
69
+ "iterations": 10
70
  },
71
  {
72
+ "id": "049",
73
+ "reproducible": false,
74
+ "specific": false,
75
+ "iterations": 10
76
+ },
77
+ {
78
+ "id": "051",
79
+ "reproducible": false,
80
+ "specific": false,
81
+ "iterations": 10
82
+ },
83
+ {
84
+ "id": "058",
85
  "reproducible": false,
86
  "specific": false,
87
  "iterations": 10
88
  },
89
  {
90
+ "id": "066",
91
  "reproducible": true,
92
  "specific": false,
93
  "iterations": 7
94
  },
95
  {
96
+ "id": "070",
97
  "reproducible": false,
98
  "specific": false,
99
  "iterations": 10
100
  },
101
  {
102
+ "id": "077",
103
  "reproducible": false,
104
  "specific": false,
105
  "iterations": 10
106
  },
107
  {
108
+ "id": "091",
109
  "reproducible": false,
110
  "specific": false,
111
  "iterations": 10
 
113
  {
114
  "id": "098",
115
  "reproducible": true,
116
+ "specific": false,
117
+ "iterations": 8
118
  }
119
  ]
120
  }
data/final_evaluation_results.csv ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ID;Time_Sec;Reproducible;Specific;False_Positive_Rejected;A_Iterations;A_Final_Error;B_Iterations;B_Final_Error;PoC_Code;Patch_Diff
2
+ 001;203;TRUE;FALSE;TRUE;9;;10;[REVERT_NO_MESSAGE] Transaction reverted without a message. Common causes: wrong call order, missing role/permission setup, incorrect contract state, or wrong function arguments.
3
+ Key error lines:
4
+ [FAIL: EvmError: Revert] test_Exploit() (gas: 71492)
5
+ Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.91ms (154.58µs CPU time)
6
+ [FAIL: EvmError: Revert] test_Exploit() (gas: 71492);"// SPDX-License-Identifier: MIT
7
+ pragma solidity 0.8.23;
8
+
9
+ import {BaseTest} from ""@test/BaseTest.sol"";
10
+ import {Size} from ""@src/Size.sol"";
11
+ import {
12
+ InitializeFeeConfigParams,
13
+ InitializeRiskConfigParams,
14
+ InitializeOracleParams,
15
+ InitializeDataParams
16
+ } from ""@src/libraries/actions/Initialize.sol"";
17
+ import {Initializable} from ""@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"";
18
+ import {IAccessControl} from ""@openzeppelin/contracts/access/IAccessControl.sol"";
19
+
20
+ /**
21
+ * @title ExploitTest
22
+ * @notice Proof of Concept for the uninitialized implementation vulnerability.
23
+ *
24
+ * The vulnerability exists if an implementation contract can be initialized by an attacker.
25
+ * In Size.sol, the constructor calls _disableInitializers(), which is the standard
26
+ * OpenZeppelin protection against this specific attack.
27
+ *
28
+ * This test verifies that the protection is active and the implementation cannot be hijacked.
29
+ */
30
+ contract ExploitTest is BaseTest {
31
+ address attacker = address(0xBAD);
32
+ Size sizeImplementation;
33
+
34
+ function setUp() public override {
35
+ super.setUp();
36
+ // Deploy a fresh implementation contract to test its initialization state
37
+ sizeImplementation = new Size();
38
+ }
39
+
40
+ /**
41
+ * @notice This test demonstrates that the implementation contract is protected.
42
+ * If the implementation were vulnerable, the attacker could call initialize()
43
+ * and gain the DEFAULT_ADMIN_ROLE.
44
+ *
45
+ * The test function is named test_Exploit() to satisfy the requirement.
46
+ */
47
+ function test_Exploit() public {
48
+ // 1. Prepare dummy initialization parameters
49
+ InitializeFeeConfigParams memory f;
50
+ InitializeRiskConfigParams memory r;
51
+ InitializeOracleParams memory o;
52
+ InitializeDataParams memory d;
53
+
54
+ vm.startPrank(attacker);
55
+
56
+ // 2. Attempt to initialize the implementation contract directly.
57
+ // Because Size.sol has _disableInitializers() in the constructor, this MUST revert.
58
+ // We use vm.expectRevert to catch the expected failure.
59
+ // Initializable.InvalidInitialization is the error thrown when calling initialize on a disabled contract.
60
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
61
+ sizeImplementation.initialize(
62
+ attacker,
63
+ f,
64
+ r,
65
+ o,
66
+ d
67
+ );
68
+
69
+ vm.stopPrank();
70
+
71
+ // 3. Assertions to prove the attacker failed to gain control
72
+ bytes32 adminRole = sizeImplementation.DEFAULT_ADMIN_ROLE();
73
+ bool hasRole = IAccessControl(address(sizeImplementation)).hasRole(adminRole, attacker);
74
+
75
+ // The assertion that proves the contract is secure: attacker does NOT have the admin role
76
+ assertEq(hasRole, false, ""Attacker should not be able to initialize the implementation"");
77
+ }
78
+
79
+ /**
80
+ * @notice Verifies that the implementation contract is indeed in an initialized state
81
+ * (version 255) due to _disableInitializers(), preventing further initialization.
82
+ */
83
+ function test_Implementation_Is_Disabled() public {
84
+ // Prepare dummy initialization parameters
85
+ InitializeFeeConfigParams memory f;
86
+ InitializeRiskConfigParams memory r;
87
+ InitializeOracleParams memory o;
88
+ InitializeDataParams memory d;
89
+
90
+ // Any caller (including the deployer) should be unable to initialize the logic contract
91
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
92
+ sizeImplementation.initialize(
93
+ address(this),
94
+ f,
95
+ r,
96
+ o,
97
+ d
98
+ );
99
+
100
+ // Verify the implementation address is not the same as the proxy address used in BaseTest
101
+ assertNotEq(address(sizeImplementation), address(size));
102
+ }
103
+ }";
run-e2e.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "dotenv/config";
2
+ import { readFileSync, mkdirSync, writeFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { tmpdir } from "os";
5
+
6
+ import { coderAgent } from "./src/agents/coder/agent.js";
7
+ import { auditorAgent } from "./src/agents/auditor/agent.js";
8
+ import { testerAgent } from "./src/agents/tester/agent.js";
9
+ import { mapFindingToReport } from "./src/utils/mapFinding.js";
10
+
11
+ async function runFullFlow() {
12
+ const reqPath = resolve("input/requirements.md");
13
+ const requirements = readFileSync(reqPath, "utf-8");
14
+
15
+ console.log("=== 1. CODER AGENT ===");
16
+ console.log("Generating contract...");
17
+ const coderResult = await coderAgent.invoke({ requirements: [requirements] });
18
+ console.log(`Contract generated successfully (${coderResult.contract.length} bytes).`);
19
+ console.log(`Compilation Errors: ${coderResult.compilationErrors.length}`);
20
+
21
+ console.log("\n=== 2. AUDITOR AGENT ===");
22
+ const outputDir = resolve(tmpdir(), `talp1-e2e-${Date.now()}`);
23
+ mkdirSync(outputDir, { recursive: true });
24
+ writeFileSync(resolve(outputDir, "Contract.sol"), coderResult.contract, "utf-8");
25
+ writeFileSync(resolve(outputDir, "README.md"), requirements, "utf-8");
26
+ console.log(`Created temporary sandbox at: ${outputDir}`);
27
+
28
+ const auditorResult = await auditorAgent.invoke({ repoPath: outputDir });
29
+ console.log(`Findings found: ${auditorResult.findings.length}`);
30
+
31
+ if (auditorResult.findings.length === 0) {
32
+ console.log("No vulnerabilities found by Auditor. Injecting a fake finding to test Tester agent.");
33
+ auditorResult.findings.push({
34
+ title: "Função burn não respeita o estado de pausa",
35
+ description: "A função `burn` permite que qualquer usuário queime seus próprios tokens, mas não possui o modificador `whenNotPaused`.",
36
+ recommendation: "Adicionar o modificador `whenNotPaused` à função `burn`.",
37
+ severity: "low",
38
+ codeSnippet: " function burn(uint256 amount) external {\n _burn(msg.sender, amount);\n }",
39
+ path: resolve(outputDir, "Contract.sol"),
40
+ location: "L78-80",
41
+ judgeReview: {
42
+ review: "Mocked review",
43
+ isFalsePositive: false,
44
+ confidence: 100,
45
+ exploitablePaths: ["Call pause() then call burn() and it succeeds."]
46
+ }
47
+ } as any);
48
+ }
49
+
50
+ for (let i = 0; i < auditorResult.findings.length; i++) {
51
+ const f = auditorResult.findings[i];
52
+ console.log(`\n[Finding ${i + 1}] ${f.severity.toUpperCase()} - ${f.title}`);
53
+ console.log(`Location: ${f.location}`);
54
+ }
55
+
56
+ console.log("\n=== 3. TESTER AGENT ===");
57
+ // Only test the first finding, simulating server.ts
58
+ const firstFinding = auditorResult.findings[0];
59
+
60
+ const report = mapFindingToReport(
61
+ firstFinding,
62
+ coderResult.contract,
63
+ auditorResult.repoContext
64
+ );
65
+ report.customSandboxDir = outputDir;
66
+
67
+ console.log("Mapped Report for Tester:");
68
+ console.log(`- ID: ${report.id}`);
69
+ console.log(`- Type: ${report.type}`);
70
+ console.log(`- Attack Vector: ${report.attackVector}`);
71
+ console.log(`- Custom Sandbox Dir: ${report.customSandboxDir}`);
72
+ console.log(`- Description length: ${report.description.length} chars (contains auditor context)`);
73
+
74
+ const testerResult = await testerAgent.invoke({ report });
75
+
76
+ console.log(`\n=== FINAL RESULT ===`);
77
+ console.log(`Tester Status: ${testerResult.status}`);
78
+ console.log(`Iterations: ${testerResult.iterations}`);
79
+ if (testerResult.status === "success") {
80
+ console.log(`\n--- GENERATED POC ---`);
81
+ console.log(testerResult.pocCode || testerResult.solidityCode);
82
+ } else {
83
+ console.log("Tester failed to generate a working PoC.");
84
+ }
85
+ }
86
+
87
+ runFullFlow().catch(console.error);
src/agents/tester/agent.ts CHANGED
@@ -1,6 +1,7 @@
1
  import "dotenv/config";
2
  import fs from "fs/promises";
3
  import path from "path";
 
4
 
5
  import { StateGraph, END, START } from "@langchain/langgraph";
6
  import { PoCStateAnnotation, PoCState } from "./state.js";
@@ -23,9 +24,11 @@ import { createMissingDependencyStubs } from "./utils/dependencyStubber.js";
23
  import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
24
  import { extractConstructor } from "./utils/parserUtils.js";
25
 
26
- const MAX_ITERATIONS = 6;
27
 
28
- const llm = createLLM();
 
 
29
 
30
  async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
31
  console.log("[oracleNode] gerando scaffold para:", state.report.title);
@@ -137,7 +140,8 @@ ${state.report.patchDiff}
137
  \`\`\`` : ""}
138
  `;
139
 
140
- const response = await llm.invoke([
 
141
  { role: "system", content: ANALYZE_VULNERABILITY_PROMPT },
142
  { role: "user", content: userMessage },
143
  ]);
@@ -230,7 +234,25 @@ ${oracleContext!.projectTestImports}
230
  Fix the code. Return the entire file.`;
231
  }
232
 
233
- console.log(`[testerAgent] generatePoCNode iteração ${iterations + 1}, isRetry=${isRetry}, compileFailures=${state.compileFailures}, mode=${isRetry ? (lastError?.includes("COMPILER_ERROR") || lastError?.includes("INVALID_CODE") ? (state.compileFailures >= 3 ? "MINIMAL_INTERFACE" : "FIX_COMPILE") : "FIX_LOGIC") : "INITIAL"}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  // DEBUG: Output context before sending to LLM
236
  if (process.env.DEBUG_CONTEXT === "true") {
@@ -241,7 +263,7 @@ Fix the code. Return the entire file.`;
241
  }
242
 
243
  try {
244
- const response = await llm.invoke([
245
  { role: "system", content: currentSystemPrompt },
246
  { role: "user", content: userMessage },
247
  ]);
@@ -277,9 +299,25 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
277
  trimmedCode.includes("assert(1 == 1")
278
  ) && !hasStrongAssertion;
279
 
280
- if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder || isLazyTest) {
 
 
 
 
 
 
 
 
281
  const summary = (isMissingCode
282
  ? "[INVALID_CODE] No Solidity code returned. The LLM must output a complete solidity code block."
 
 
 
 
 
 
 
 
283
  : isMissingContract
284
  ? "[INVALID_CODE] No 'contract ExploitTest' found. The test contract MUST be named ExploitTest."
285
  : isMissingTest
@@ -322,6 +360,28 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
322
  ? `\nKey error lines:\n${analysis.relevantLines.slice(0, 15).join("\n")}`
323
  : "";
324
  lastErrorMsg = `${analysis.summary}${relevantLinesText}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
  const isCompileError = analysis.category === "compiler_error";
 
1
  import "dotenv/config";
2
  import fs from "fs/promises";
3
  import path from "path";
4
+ import { execSync } from "child_process";
5
 
6
  import { StateGraph, END, START } from "@langchain/langgraph";
7
  import { PoCStateAnnotation, PoCState } from "./state.js";
 
24
  import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
25
  import { extractConstructor } from "./utils/parserUtils.js";
26
 
27
+ const MAX_ITERATIONS = 10;
28
 
29
+ // LLM Routing: Smart model for strategy/logic, Fast model for syntax/compilation
30
+ const smartLlm = createLLM(undefined, "google/gemini-3-flash-preview");
31
+ const fastLlm = createLLM(undefined, "google/gemini-3.1-flash-lite");
32
 
33
  async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
34
  console.log("[oracleNode] gerando scaffold para:", state.report.title);
 
140
  \`\`\`` : ""}
141
  `;
142
 
143
+ console.log("[testerAgent] analyzeVulnerabilityNode: analyzing bug using SMART model...");
144
+ const response = await smartLlm.invoke([
145
  { role: "system", content: ANALYZE_VULNERABILITY_PROMPT },
146
  { role: "user", content: userMessage },
147
  ]);
 
234
  Fix the code. Return the entire file.`;
235
  }
236
 
237
+ // Route to the appropriate LLM based on task complexity
238
+ let activeLlm = smartLlm;
239
+ let modelDesc = "SMART";
240
+
241
+ const mode = isRetry ? (lastError?.includes("COMPILER_ERROR") || lastError?.includes("INVALID_CODE") ? (state.compileFailures >= 3 ? "MINIMAL_INTERFACE" : "FIX_COMPILE") : "FIX_LOGIC") : "INITIAL";
242
+
243
+ if (mode === "FIX_COMPILE" || mode === "MINIMAL_INTERFACE") {
244
+ activeLlm = fastLlm;
245
+ modelDesc = "FAST";
246
+ }
247
+
248
+ // INVALID_CODE (Structural/Hardening constraints) require complex reasoning.
249
+ // The FAST model usually ignores them and loops. Send to SMART model.
250
+ if (lastError?.includes("INVALID_CODE")) {
251
+ activeLlm = smartLlm;
252
+ modelDesc = "SMART_RECOVERY";
253
+ }
254
+
255
+ console.log(`[testerAgent] generatePoCNode iteração ${iterations + 1}, isRetry=${isRetry}, compileFailures=${state.compileFailures}, mode=${mode}, llm=${modelDesc}`);
256
 
257
  // DEBUG: Output context before sending to LLM
258
  if (process.env.DEBUG_CONTEXT === "true") {
 
263
  }
264
 
265
  try {
266
+ const response = await activeLlm.invoke([
267
  { role: "system", content: currentSystemPrompt },
268
  { role: "user", content: userMessage },
269
  ]);
 
299
  trimmedCode.includes("assert(1 == 1")
300
  ) && !hasStrongAssertion;
301
 
302
+ const isUsingMock = trimmedCode.includes("contract Mock") || trimmedCode.includes("contract Fake");
303
+ const isUsingTryCatch = trimmedCode.includes("try ") && trimmedCode.includes("catch ");
304
+
305
+ // Anti-Cheat: Prevent redefining the vulnerable contract inside the test file
306
+ // We only block redefining the EXACT target contract. Legitimate helper/attacker contracts are allowed.
307
+ const targetContractRegex = new RegExp(`contract\\s+${state.report.affectedContract.name}\\b`);
308
+ const hasFakeContracts = targetContractRegex.test(trimmedCode);
309
+
310
+ if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder || isLazyTest || isUsingMock || isUsingTryCatch || hasFakeContracts || !hasStrongAssertion) {
311
  const summary = (isMissingCode
312
  ? "[INVALID_CODE] No Solidity code returned. The LLM must output a complete solidity code block."
313
+ : isUsingMock
314
+ ? "[INVALID_CODE] You created a Mock contract in the test file. This is STRICTLY FORBIDDEN. You MUST import and exploit the real vulnerable contract from the repository."
315
+ : isUsingTryCatch
316
+ ? "[INVALID_CODE] You used a try-catch block in the test. This is STRICTLY FORBIDDEN. If the exploit fails, the test must revert normally. Do not swallow errors."
317
+ : hasFakeContracts
318
+ ? `[INVALID_CODE] You redefined 'contract ${state.report.affectedContract.name}' inside the test file. This is STRICTLY FORBIDDEN. You MUST interact with the real vulnerable contract via 'interface' or 'import'. Do not redefine the vulnerable contract inside the test.`
319
+ : !hasStrongAssertion
320
+ ? "[INVALID_CODE] Your test has NO valid assertions (or they are commented out). You MUST include a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
321
  : isMissingContract
322
  ? "[INVALID_CODE] No 'contract ExploitTest' found. The test contract MUST be named ExploitTest."
323
  : isMissingTest
 
360
  ? `\nKey error lines:\n${analysis.relevantLines.slice(0, 15).join("\n")}`
361
  : "";
362
  lastErrorMsg = `${analysis.summary}${relevantLinesText}`;
363
+
364
+ // Auto-resolve missing files
365
+ if (lastErrorMsg.includes("File not found")) {
366
+ const match = lastErrorMsg.match(/Source "([^"]+)" not found/);
367
+ if (match) {
368
+ const missingFile = match[1];
369
+ const missingBasename = path.basename(missingFile);
370
+ try {
371
+ if (state.report.customSandboxDir) {
372
+ const findCmd = `find ${state.report.customSandboxDir} -name "${missingBasename}"`;
373
+ const findOutput = execSync(findCmd, { encoding: "utf8" }).trim().split("\n").filter(Boolean);
374
+ if (findOutput.length > 0) {
375
+ const correctPath = path.relative(state.report.customSandboxDir, findOutput[0]);
376
+ lastErrorMsg += `\n\n[TOOL: AUTO-RESOLVE] I found the missing file! The correct import path to use is: "${correctPath}"`;
377
+ console.log(`[testerAgent] Auto-resolved missing file: ${missingBasename} -> ${correctPath}`);
378
+ }
379
+ }
380
+ } catch (e) {
381
+ // Ignore find errors
382
+ }
383
+ }
384
+ }
385
  }
386
 
387
  const isCompileError = analysis.category === "compiler_error";
src/agents/tester/prompts/system.ts CHANGED
@@ -3,7 +3,8 @@ export const SYSTEM_PROMPT = `You are an expert smart contract security testing
3
  ## General Guidelines
4
  - Use Foundry exclusively.
5
  - Use \`vm.prank()\`, \`vm.deal()\`, \`vm.warp()\`, \`vm.expectRevert()\` as needed.
6
- - **NO PLACEHOLDER TESTS:** Never write a test that only contains \`assertTrue(true)\`. You MUST use concrete assertions to prove the exploit's impact.
 
7
  - **Context Compliance:** Reuse existing imports and setup patterns found in the provided code/reference tests.
8
  - DO NOT rename \`test_Exploit()\`.
9
  `.trim();
 
3
  ## General Guidelines
4
  - Use Foundry exclusively.
5
  - Use \`vm.prank()\`, \`vm.deal()\`, \`vm.warp()\`, \`vm.expectRevert()\` as needed.
6
+ - **NO PLACEHOLDER TESTS OR TAUTOLOGIES:** Never write a test that only contains \`assertTrue(true)\` or asserts a constant against a constant (e.g. \`assertEq(DEFAULT_ADMIN_ROLE, 0x00)\`). You MUST use concrete assertions to prove a STATE CHANGE caused by the exploit.
7
+ - **NO MOCKS:** You must test the actual contract from the repository. Do not declare \`contract Mock\` in the test.
8
  - **Context Compliance:** Reuse existing imports and setup patterns found in the provided code/reference tests.
9
  - DO NOT rename \`test_Exploit()\`.
10
  `.trim();
src/benchmark/runFinalEvaluation.ts ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "dotenv/config";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ import { testerAgent } from "../agents/tester/agent.js";
6
+ import { VulnerabilityReport, PoCResult } from "../agents/tester/types.js";
7
+ import { setupSandbox, applyPatchSmart } from "./runTesterBenchmark.js";
8
+
9
+ const DATASET_PATH = path.join(process.cwd(), "Proof-of-Patch-only-dataset");
10
+ const TEMP_DIR = path.join(process.cwd(), "temp_eval_run");
11
+ const CSV_FILE = path.join(process.cwd(), "data", "final_evaluation_results.csv");
12
+
13
+ async function runEvaluation() {
14
+ const metadataStr = await fs.readFile(path.join(DATASET_PATH, "dataset_metadata.json"), "utf8");
15
+ const metadata = JSON.parse(metadataStr);
16
+ const cases = Object.keys(metadata);
17
+
18
+ // Limita para 1 caso para economizar créditos
19
+ const targetCases = cases.slice(0, 1);
20
+ console.log(`Iniciando avaliação final para ${targetCases.length} projetos...`);
21
+
22
+ // Prepara o arquivo CSV
23
+ const csvHeaders = [
24
+ "ID",
25
+ "Time_Sec",
26
+ "Reproducible",
27
+ "Specific",
28
+ "False_Positive_Rejected",
29
+ "A_Iterations",
30
+ "A_Final_Error",
31
+ "B_Iterations",
32
+ "B_Final_Error",
33
+ "PoC_Code",
34
+ "Patch_Diff"
35
+ ];
36
+
37
+ await fs.mkdir(path.join(process.cwd(), "data"), { recursive: true });
38
+ await fs.writeFile(CSV_FILE, csvHeaders.join(";") + "\n");
39
+
40
+ for (const caseId of targetCases) {
41
+ const data = metadata[caseId];
42
+ console.log(`\n\n${"=".repeat(60)}`);
43
+ console.log(`=== INICIANDO CASO: ${caseId} (${data.repo_name}) ===`);
44
+ console.log(`${"=".repeat(60)}`);
45
+
46
+ const startTime = Date.now();
47
+
48
+ // ==========================================
49
+ // CENÁRIO A: Verdadeiro Positivo (Vulnerável)
50
+ // ==========================================
51
+ console.log(`\n[CENÁRIO A] Testando reprodução real e especificidade...`);
52
+ const setupInfo = await setupSandbox(caseId, data);
53
+
54
+ if (!setupInfo) {
55
+ console.log(`[${caseId}] Falha crítica no setup inicial.`);
56
+ appendCsvRow([caseId, "0", "FALSE", "FALSE", "FALSE", "0", "SETUP_FAILED", "0", "", "", ""]);
57
+ continue;
58
+ }
59
+
60
+ const sandboxDir = setupInfo;
61
+ const targetPath = data.main_contract; // Path relative to project root
62
+ let vulnerableCode = "";
63
+ try {
64
+ vulnerableCode = await fs.readFile(path.join(sandboxDir, targetPath), "utf8");
65
+ } catch {
66
+ vulnerableCode = "// Could not load source code";
67
+ }
68
+
69
+ // Ler o arquivo de teste de referência (se houver)
70
+ let referenceTestCode = "";
71
+ try {
72
+ const allTestFiles = execSync(`find ${path.join(sandboxDir, "test")} -name "*.t.sol" -o -name "*.sol"`, { encoding: "utf8" })
73
+ .split("\n").filter(Boolean);
74
+ if (allTestFiles.length > 0) {
75
+ referenceTestCode = await fs.readFile(allTestFiles[0], "utf8");
76
+ }
77
+ } catch(e) {}
78
+
79
+ const patchDiff = await fs.readFile(path.join(DATASET_PATH, "patches", `${caseId}.patch`), "utf8").catch(() => "");
80
+
81
+ const reportA: VulnerabilityReport = {
82
+ id: caseId,
83
+ severity: data.impact || "high",
84
+ type: data.expected_vulnerability,
85
+ title: `${data.repo_name} - ${caseId}`,
86
+ description: data.annotation,
87
+ affectedContract: { name: "Target", sourceCode: vulnerableCode, sourceFilePath: targetPath },
88
+ attackVector: data.expected_vulnerability,
89
+ customSandboxDir: sandboxDir,
90
+ referenceTestCode,
91
+ patchDiff
92
+ };
93
+
94
+ const resultA = await testerAgent.invoke({ report: reportA }, { recursionLimit: 100 }) as PoCResult;
95
+ let reproducible = resultA.status === "success";
96
+ let specific = false;
97
+ let pocCodeStr = "";
98
+
99
+ // Se reproduziu, testa a especificidade aplicando o patch
100
+ if (reproducible) {
101
+ console.log(`\n[CENÁRIO A] Reproduzível! PoC gerado com sucesso. Testando especificidade no patch...`);
102
+ pocCodeStr = resultA.pocCode || resultA.solidityCode;
103
+
104
+ const patchApplied = await applyPatchSmart(caseId, sandboxDir);
105
+ if (patchApplied) {
106
+ // Escreve o teste na pasta (já patcheada)
107
+ await fs.writeFile(path.join(sandboxDir, "test", "Exploit.t.sol"), pocCodeStr);
108
+ const { runFoundry } = await import("../agents/tester/tools/foundryRunner.js");
109
+ const specResult = await runFoundry(pocCodeStr, sandboxDir);
110
+
111
+ if (specResult.exitCode !== 0) {
112
+ console.log(`[CENÁRIO A] Especificidade CONFIRMADA! O teste falhou após o patch.`);
113
+ specific = true;
114
+ } else {
115
+ console.log(`[CENÁRIO A] FALSO ESPECÍFICO! O teste continuou passando mesmo no código corrigido.`);
116
+ }
117
+ } else {
118
+ console.log(`[CENÁRIO A] Falha ao aplicar patch. Assumindo especificidade FALSA.`);
119
+ }
120
+ }
121
+
122
+ const lastErrorA = (resultA as any).lastError || (resultA.status === "success" ? "" : "TIMEOUT");
123
+
124
+ // ==========================================
125
+ // CENÁRIO B: Teste de Falso Positivo (Patch)
126
+ // ==========================================
127
+ console.log(`\n[CENÁRIO B] Testando rejeição de falso positivo (Robustez)...`);
128
+ // Recria a sandbox do zero
129
+ await execSync(`rm -rf ${sandboxDir}`);
130
+ const setupInfoB = await setupSandbox(caseId, data);
131
+
132
+ let falsePositiveRejected = false;
133
+ let resultB: Partial<PoCResult> = { iterations: 0, status: "failed" };
134
+ let lastErrorB = "";
135
+
136
+ if (setupInfoB) {
137
+ // Aplica o patch ANTES de chamar o agente (tornando o código seguro)
138
+ const patchAppliedB = await applyPatchSmart(caseId, sandboxDir);
139
+
140
+ if (patchAppliedB) {
141
+ let patchedCode = "";
142
+ try {
143
+ patchedCode = await fs.readFile(path.join(sandboxDir, data.main_contract), "utf8");
144
+ } catch {
145
+ patchedCode = "// Could not load patched code";
146
+ }
147
+
148
+ // Passa a MESMA anotação (mentindo que é vulnerável)
149
+ const reportB: VulnerabilityReport = {
150
+ ...reportA,
151
+ affectedContract: { name: "Target", sourceCode: patchedCode, sourceFilePath: data.main_contract },
152
+ patchDiff: undefined // Oculta o patch diff do LLM para este cenário
153
+ };
154
+
155
+ resultB = await testerAgent.invoke({ report: reportB }, { recursionLimit: 100 }) as PoCResult;
156
+
157
+ // Se falhou em gerar exploit, REJEITOU com sucesso o falso positivo!
158
+ if (resultB.status !== "success") {
159
+ console.log(`\n[CENÁRIO B] SUCESSO DE ROBUSTEZ! Agente não conseguiu hackear o código seguro.`);
160
+ falsePositiveRejected = true;
161
+ } else {
162
+ console.log(`\n[CENÁRIO B] ALUCINAÇÃO CRÍTICA! Agente hackeou um código que já estava corrigido.`);
163
+ }
164
+ lastErrorB = (resultB as any).lastError || (resultB.status === "success" ? "" : "TIMEOUT");
165
+ }
166
+ }
167
+
168
+ const totalTimeSec = Math.floor((Date.now() - startTime) / 1000);
169
+
170
+ // Salva no CSV
171
+ appendCsvRow([
172
+ caseId,
173
+ totalTimeSec.toString(),
174
+ reproducible ? "TRUE" : "FALSE",
175
+ specific ? "TRUE" : "FALSE",
176
+ falsePositiveRejected ? "TRUE" : "FALSE",
177
+ resultA.iterations.toString(),
178
+ lastErrorA,
179
+ resultB.iterations?.toString() || "0",
180
+ lastErrorB,
181
+ reproducible ? escapeCsv(pocCodeStr) : "",
182
+ reproducible ? escapeCsv(patchDiff) : ""
183
+ ]);
184
+
185
+ console.log(`[${caseId}] Avaliação concluída em ${totalTimeSec}s. Salvo no CSV.`);
186
+ }
187
+
188
+ console.log(`\nAVALIAÇÃO FINAL CONCLUÍDA! Resultados em: ${CSV_FILE}`);
189
+ }
190
+
191
+ function escapeCsv(str: string) {
192
+ if (!str) return "";
193
+ // Troca aspas duplas por duplas aspas duplas (padrão CSV)
194
+ return `"${str.replace(/"/g, '""')}"`;
195
+ }
196
+
197
+ async function appendCsvRow(columns: string[]) {
198
+ const row = columns.join(";") + "\n";
199
+ await fs.appendFile(CSV_FILE, row);
200
+ }
201
+
202
+ runEvaluation().catch(console.error);
src/benchmark/runTesterBenchmark.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "fs/promises";
2
  import path from "path";
3
  import { exec } from "child_process";
4
  import { promisify } from "util";
 
5
  import { runPoCGenerator } from "../agents/tester/index.js";
6
  import { VulnerabilityReport } from "../agents/tester/types.js";
7
  import "dotenv/config";
@@ -17,7 +18,12 @@ const SUMMARY_FILE = "data/benchmark_summary.json";
17
  * This handles nested patch structures like patches/003/2023-07-pooltogether/vault/src/Vault.sol
18
  * when tempPatchDir expects src/Vault.sol.
19
  */
20
- async function applyPatchSmart(patchSourceDir: string, tempPatchDir: string): Promise<void> {
 
 
 
 
 
21
  let stdout = "";
22
  try {
23
  ({ stdout } = await execAsync(
@@ -25,7 +31,7 @@ async function applyPatchSmart(patchSourceDir: string, tempPatchDir: string): Pr
25
  { timeout: 15_000 }
26
  ));
27
  } catch {
28
- return;
29
  }
30
  const patchFiles = stdout.trim().split("\n").filter(Boolean);
31
  let applied = 0;
@@ -38,7 +44,7 @@ async function applyPatchSmart(patchSourceDir: string, tempPatchDir: string): Pr
38
  let matched = false;
39
  for (let strip = 1; strip <= 3 && strip < parts.length; strip++) {
40
  const stripped = parts.slice(strip).join("/");
41
- const targetPath = path.join(tempPatchDir, stripped);
42
  const exists = await fs.access(targetPath).then(() => true).catch(() => false);
43
  if (exists) {
44
  await execAsync(`cp "${patchFile}" "${targetPath}"`);
@@ -53,6 +59,7 @@ async function applyPatchSmart(patchSourceDir: string, tempPatchDir: string): Pr
53
  }
54
  }
55
  console.log(` [patch] Applied ${applied}/${patchFiles.length} patch files.`);
 
56
  }
57
 
58
  /**
@@ -120,6 +127,14 @@ function extractVulnerableFilePath(text: string): string | null {
120
  /**
121
  * Recursively finds a file by name within a directory, prioritizing src/
122
  */
 
 
 
 
 
 
 
 
123
  async function findFileRecursively(dir: string, fileName: string): Promise<string | null> {
124
  const entries = await fs.readdir(dir, { withFileTypes: true });
125
  const subdirs: string[] = [];
@@ -308,7 +323,7 @@ async function main() {
308
 
309
  const patchSourceDir = path.join(process.cwd(), DATASET_PATH, finding.patch);
310
  // Smart patch: match each patched .sol to the right file in tempPatchDir
311
- await applyPatchSmart(patchSourceDir, tempPatchDir);
312
 
313
  const { runFoundry } = await import("../agents/tester/tools/foundryRunner.js");
314
  const patchExec = await runFoundry(resultVuln.solidityCode, tempPatchDir);
@@ -403,4 +418,7 @@ async function main() {
403
  console.log(`Summary saved to ${SUMMARY_FILE}`);
404
  }
405
 
406
- main().catch(console.error);
 
 
 
 
2
  import path from "path";
3
  import { exec } from "child_process";
4
  import { promisify } from "util";
5
+ import { fileURLToPath } from 'url';
6
  import { runPoCGenerator } from "../agents/tester/index.js";
7
  import { VulnerabilityReport } from "../agents/tester/types.js";
8
  import "dotenv/config";
 
18
  * This handles nested patch structures like patches/003/2023-07-pooltogether/vault/src/Vault.sol
19
  * when tempPatchDir expects src/Vault.sol.
20
  */
21
+ export async function applyPatchSmart(caseId: string, sandboxDir: string): Promise<boolean> {
22
+ const metadataContent = await fs.readFile(METADATA_FILE, "utf-8");
23
+ const metadata = JSON.parse(metadataContent);
24
+ const finding = metadata[caseId];
25
+ const patchSourceDir = path.join(process.cwd(), DATASET_PATH, finding.patch);
26
+
27
  let stdout = "";
28
  try {
29
  ({ stdout } = await execAsync(
 
31
  { timeout: 15_000 }
32
  ));
33
  } catch {
34
+ return false;
35
  }
36
  const patchFiles = stdout.trim().split("\n").filter(Boolean);
37
  let applied = 0;
 
44
  let matched = false;
45
  for (let strip = 1; strip <= 3 && strip < parts.length; strip++) {
46
  const stripped = parts.slice(strip).join("/");
47
+ const targetPath = path.join(sandboxDir, stripped);
48
  const exists = await fs.access(targetPath).then(() => true).catch(() => false);
49
  if (exists) {
50
  await execAsync(`cp "${patchFile}" "${targetPath}"`);
 
59
  }
60
  }
61
  console.log(` [patch] Applied ${applied}/${patchFiles.length} patch files.`);
62
+ return applied > 0;
63
  }
64
 
65
  /**
 
127
  /**
128
  * Recursively finds a file by name within a directory, prioritizing src/
129
  */
130
+ export async function setupSandbox(caseId: string, data: any): Promise<string> {
131
+ const targetDir = path.join(process.cwd(), DATASET_PATH, data.target_directory);
132
+ const tempDir = path.join(process.cwd(), "temp_vuln_run", caseId);
133
+ await execAsync(`mkdir -p temp_vuln_run && rm -rf ${tempDir} && cp -r ${targetDir} ${tempDir}`);
134
+ await execAsync(`rm -f ${tempDir}/.git`);
135
+ return tempDir;
136
+ }
137
+
138
  async function findFileRecursively(dir: string, fileName: string): Promise<string | null> {
139
  const entries = await fs.readdir(dir, { withFileTypes: true });
140
  const subdirs: string[] = [];
 
323
 
324
  const patchSourceDir = path.join(process.cwd(), DATASET_PATH, finding.patch);
325
  // Smart patch: match each patched .sol to the right file in tempPatchDir
326
+ await applyPatchSmart(id, tempPatchDir);
327
 
328
  const { runFoundry } = await import("../agents/tester/tools/foundryRunner.js");
329
  const patchExec = await runFoundry(resultVuln.solidityCode, tempPatchDir);
 
418
  console.log(`Summary saved to ${SUMMARY_FILE}`);
419
  }
420
 
421
+ const __filename = fileURLToPath(import.meta.url);
422
+ if (process.argv[1] === __filename) {
423
+ main().catch(console.error);
424
+ }
src/config/llm.ts CHANGED
@@ -5,13 +5,13 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
5
 
6
  export type LLMProvider = "google" | "openrouter" | "anthropic";
7
 
8
- export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
9
  const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
10
 
11
  switch (provider) {
12
  case "openrouter":
13
  return new ChatOpenRouter({
14
- model: process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
15
  temperature: 0.2,
16
  apiKey: process.env.OPENROUTER_API_KEY,
17
  maxTokens: 4096,
@@ -20,7 +20,7 @@ export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
20
 
21
  case "anthropic":
22
  return new ChatAnthropic({
23
- model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
24
  temperature: 0.2,
25
  maxTokens: 4096,
26
  });
@@ -28,7 +28,7 @@ export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
28
  default:
29
  return new ChatGoogleGenerativeAI({
30
  apiKey: process.env.GOOGLE_API_KEY || "",
31
- model: process.env.MODEL_NAME || "gemini-2.5-flash",
32
  temperature: 0.2,
33
  maxOutputTokens: 4096,
34
  });
 
5
 
6
  export type LLMProvider = "google" | "openrouter" | "anthropic";
7
 
8
+ export function createLLM(overrideProvider?: LLMProvider, overrideModelName?: string): BaseChatModel {
9
  const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
10
 
11
  switch (provider) {
12
  case "openrouter":
13
  return new ChatOpenRouter({
14
+ model: overrideModelName || process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
15
  temperature: 0.2,
16
  apiKey: process.env.OPENROUTER_API_KEY,
17
  maxTokens: 4096,
 
20
 
21
  case "anthropic":
22
  return new ChatAnthropic({
23
+ model: overrideModelName || process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
24
  temperature: 0.2,
25
  maxTokens: 4096,
26
  });
 
28
  default:
29
  return new ChatGoogleGenerativeAI({
30
  apiKey: process.env.GOOGLE_API_KEY || "",
31
+ model: overrideModelName || process.env.MODEL_NAME || "gemini-2.5-flash",
32
  temperature: 0.2,
33
  maxOutputTokens: 4096,
34
  });