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

Fix: change the agent

Browse files
.gitignore CHANGED
@@ -142,4 +142,6 @@ vite.config.js.timestamp-*
142
  vite.config.ts.timestamp-*
143
  .vite/
144
 
145
-
 
 
 
142
  vite.config.ts.timestamp-*
143
  .vite/
144
 
145
+ temp_*/
146
+ Proof*/
147
+ PoCo*/
Proof-of-Patch-only-dataset ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit eca2a566326d7636665c45c670698e05ea12a3ac
data/final_evaluation_results.csv CHANGED
@@ -1,103 +1,2 @@
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
- }";
 
1
+ ID;Time_Sec;Reproducible;Specific;False_Positive_Rejected;A_Infra_Iters;A_Exploit_Iters;A_Final_Error;B_Infra_Iters;B_Exploit_Iters;B_Final_Error;PoC_Code;Patch_Diff
2
+ 054;140;FALSE;FALSE;TRUE;1;31;[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.;2;31;[INVALID_CODE] You added a comment in the code. This is STRICTLY FORBIDDEN. You must write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX and INJECT_HACK).;;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/agent.ts CHANGED
@@ -23,13 +23,17 @@ import { extractProjectContext } from "./utils/projectContextExtractor.js";
23
  import { createMissingDependencyStubs } from "./utils/dependencyStubber.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);
35
 
@@ -149,132 +153,8 @@ ${state.report.patchDiff}
149
  return { vulnerabilityAnalysis: response.content as string };
150
  }
151
 
152
- /**
153
- * Multi-Pass Node 2 - Code Generation (Initial & Fixes)
154
- */
155
- async function generatePoCNode(state: PoCState): Promise<Partial<PoCState>> {
156
- const { report, oracleContext, executionLogs, pocCode, iterations, lastError, vulnerabilityAnalysis } = state;
157
- const isRetry = iterations > 0;
158
-
159
- let currentSystemPrompt = SYSTEM_PROMPT;
160
- let userMessage = "";
161
-
162
- if (!isRetry) {
163
- // PASS 2: INITIAL GENERATION
164
- currentSystemPrompt = POC_INITIAL_PROMPT;
165
- userMessage = `Vulnerability Analysis Plan:
166
- ${vulnerabilityAnalysis}
167
-
168
- ### Contract Source:
169
- \`\`\`solidity
170
- ${report.affectedContract.sourceCode}
171
- \`\`\`
172
-
173
- ### API Reference:
174
- ${oracleContext!.targetContractAPI}
175
-
176
- ${oracleContext!.referenceTestHelpers ? `### Test Helpers:
177
- ${oracleContext!.referenceTestHelpers}` : ""}
178
-
179
- ${oracleContext!.projectRemappings ? `### Project Remappings (use these for import paths):
180
- \`\`\`
181
- ${oracleContext!.projectRemappings}
182
- \`\`\`` : ""}
183
-
184
- ${oracleContext!.projectTestImports ? `### Import Pattern from Existing Test (${oracleContext!.projectTestFilePath}):
185
- \`\`\`solidity
186
- ${oracleContext!.projectTestImports}
187
- \`\`\`` : ""}
188
-
189
- ### Scaffold:
190
- \`\`\`solidity
191
- ${oracleContext!.solidityScaffold}
192
- \`\`\`
193
- `;
194
- } else {
195
- // PASS 3+: FIXING ERRORS (BRANCHING)
196
- const isCompilerError = lastError?.includes("[COMPILER_ERROR]") || lastError?.includes("[INVALID_CODE]");
197
- const useMinimalStrategy = state.compileFailures >= 3;
198
-
199
- if (useMinimalStrategy && isCompilerError) {
200
- // ESCAPE HATCH: After 3 compile failures, switch to zero-import minimal interface strategy
201
- currentSystemPrompt = POC_MINIMAL_INTERFACE_PROMPT;
202
- console.log("[testerAgent] Switching to MINIMAL_INTERFACE strategy after", state.compileFailures, "compile failures");
203
- } else {
204
- currentSystemPrompt = isCompilerError ? POC_COMPILE_FIX_PROMPT : POC_TEST_FIX_PROMPT;
205
- }
206
-
207
- userMessage = `The previous PoC failed.
208
-
209
- Error Category: ${isCompilerError ? "Compilation Failure" : "Execution/Logic Failure"}
210
- Error Details:
211
- ${lastError ?? ""}
212
-
213
- Forge Output (last attempt):
214
- ${executionLogs[executionLogs.length - 1]?.slice(0, 3500) ?? "sem logs"}
215
-
216
- Previous Code:
217
- \`\`\`solidity
218
- ${pocCode}
219
- \`\`\`
220
-
221
- Analysis of the bug:
222
- ${vulnerabilityAnalysis}
223
-
224
- ${!useMinimalStrategy && oracleContext!.projectRemappings ? `Project Remappings (use these for import paths):
225
- \`\`\`
226
- ${oracleContext!.projectRemappings}
227
- \`\`\`` : ""}
228
-
229
- ${!useMinimalStrategy && oracleContext!.projectTestImports ? `Import Pattern from Existing Test (${oracleContext!.projectTestFilePath}):
230
- \`\`\`solidity
231
- ${oracleContext!.projectTestImports}
232
- \`\`\`` : ""}
233
-
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") {
259
- console.log("\n" + "=".repeat(20) + " LLM CONTEXT START " + "=".repeat(20));
260
- console.log("System Prompt:", currentSystemPrompt);
261
- console.log("User Message:", userMessage);
262
- console.log("=".repeat(20) + " LLM CONTEXT END " + "=".repeat(20) + "\n");
263
- }
264
 
265
- try {
266
- const response = await activeLlm.invoke([
267
- { role: "system", content: currentSystemPrompt },
268
- { role: "user", content: userMessage },
269
- ]);
270
- const solidityCode = extractSolidity(response.content as string);
271
- console.log("[testerAgent] Solidity extraído, tamanho:", solidityCode.length);
272
- return { pocCode: solidityCode, iterations: 1 };
273
- } catch (err) {
274
- console.error("[testerAgent] falha na geração:", (err as Error).message);
275
- return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
276
- }
277
- }
278
 
279
  async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
280
  console.log("[testerAgent] Executando runFoundryNode...");
@@ -284,6 +164,10 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
284
  const isMissingContract = !trimmedCode.includes("contract ExploitTest");
285
  const isMissingTest = !trimmedCode.includes("function test_Exploit()");
286
  const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
 
 
 
 
287
  const hasStrongAssertion = (
288
  trimmedCode.includes("assertEq") ||
289
  trimmedCode.includes("assertGt") ||
@@ -307,16 +191,26 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
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."
@@ -326,7 +220,10 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
326
  ? "[INVALID_CODE] Exploit has TODO placeholder. You must implement the actual exploit logic."
327
  : "[WEAK_ASSERTION] Only assertTrue(true) found — this never proves the vulnerability. Add a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
328
  );
329
- const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
 
 
 
330
  return {
331
  executionLogs: [summary],
332
  lastError: summary,
@@ -338,15 +235,16 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
338
  const analysis = analyzeFoundryLog(result);
339
  const noTestsFound = result.combined.includes("No tests found");
340
  const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
341
- const isLastAttempt = state.iterations >= MAX_ITERATIONS;
 
 
342
 
343
- const status = passed
344
- ? "success"
345
- : result.timedOut
346
- ? "timeout"
347
- : isLastAttempt
348
- ? "failed"
349
- : "running";
350
 
351
  console.log(`[testerAgent] Resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
352
  if (!passed) {
@@ -401,27 +299,59 @@ async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
401
  return {};
402
  }
403
 
404
- function routeAfterFoundry(state: PoCState): "reflectNode" | typeof END {
405
- if (state.status === "success") return END;
406
- if (state.status === "timeout") return END;
407
- if (state.iterations >= MAX_ITERATIONS) return END;
408
- return "reflectNode";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
 
411
  const graph = new StateGraph(PoCStateAnnotation)
412
  .addNode("oracleNode", oracleNode)
413
  .addNode("analyzeVulnerabilityNode", analyzeVulnerabilityNode)
414
- .addNode("generatePoCNode", generatePoCNode)
 
415
  .addNode("runFoundryNode", runFoundryNode)
416
  .addNode("reflectNode", reflectNode)
417
  .addEdge(START, "oracleNode")
418
  .addEdge("oracleNode", "analyzeVulnerabilityNode")
419
- .addEdge("analyzeVulnerabilityNode", "generatePoCNode")
420
- .addEdge("generatePoCNode", "runFoundryNode")
 
421
  .addConditionalEdges("runFoundryNode", routeAfterFoundry, {
422
  reflectNode: "reflectNode",
 
423
  [END]: END,
424
  })
425
- .addEdge("reflectNode", "generatePoCNode");
 
 
 
426
 
427
  export const testerAgent = graph.compile();
 
23
  import { createMissingDependencyStubs } from "./utils/dependencyStubber.js";
24
  import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
25
  import { extractConstructor } from "./utils/parserUtils.js";
26
+ import { generateInfrastructureNode, generateExploitNode } from "./nodes.js";
27
+ import { oracleNode } from "./nodes/oracle.js";
28
+ import { analyzeVulnerabilityNode } from "./nodes/analyzeVulnerability.js";
29
+ import { generateInfrastructureNode, generateExploitNode } from "./nodes.js";
30
 
31
+ const MAX_INFRA_ITERATIONS = 30;
32
+ const MAX_EXPLOIT_ITERATIONS = 30;
33
 
34
  // LLM Routing: Smart model for strategy/logic, Fast model for syntax/compilation
35
  const smartLlm = createLLM(undefined, "google/gemini-3-flash-preview");
36
  const fastLlm = createLLM(undefined, "google/gemini-3.1-flash-lite");
 
37
  async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
38
  console.log("[oracleNode] gerando scaffold para:", state.report.title);
39
 
 
153
  return { vulnerabilityAnalysis: response.content as string };
154
  }
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
 
 
 
 
 
 
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
160
  console.log("[testerAgent] Executando runFoundryNode...");
 
164
  const isMissingContract = !trimmedCode.includes("contract ExploitTest");
165
  const isMissingTest = !trimmedCode.includes("function test_Exploit()");
166
  const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
167
+ const isTargetNotDeployed = trimmedCode.includes("// target = new") ||
168
+ trimmedCode.includes("//Target target = new") ||
169
+ trimmedCode.includes("// target = address(new") ||
170
+ trimmedCode.match(/\/\/\s*([a-zA-Z0-9_]+)\s*=\s*(address\()?new\s+[a-zA-Z0-9_]+/);
171
  const hasStrongAssertion = (
172
  trimmedCode.includes("assertEq") ||
173
  trimmedCode.includes("assertGt") ||
 
191
  const targetContractRegex = new RegExp(`contract\\s+${state.report.affectedContract.name}\\b`);
192
  const hasFakeContracts = targetContractRegex.test(trimmedCode);
193
 
194
+ const hasIllegalComments = trimmedCode.split('\n').some(line => {
195
+ const isComment = line.includes('//') || line.includes('/*');
196
+ const isAllowed = line.includes('SPDX-License-Identifier') || line.includes('INJECT_HACK');
197
+ return isComment && !isAllowed;
198
+ });
199
+
200
+ if (isMissingCode || isMissingContract || isUsingMock || isUsingTryCatch || hasFakeContracts || isTargetNotDeployed || hasIllegalComments || (!state.infrastructurePhase && (isMissingTest || isPlaceholder || isLazyTest || !hasStrongAssertion))) {
201
  const summary = (isMissingCode
202
  ? "[INVALID_CODE] No Solidity code returned. The LLM must output a complete solidity code block."
203
+ : isTargetNotDeployed
204
+ ? "[INVALID_CODE] You left the target contract instantiation commented out. You MUST instantiate the real target contract in setUp() (e.g. `target = new Target()`). Exploiting address(0) is a cheat and will fail."
205
+ : hasIllegalComments
206
+ ? "[INVALID_CODE] You added a comment in the code. This is STRICTLY FORBIDDEN. You must write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX and INJECT_HACK)."
207
  : isUsingMock
208
  ? "[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."
209
  : isUsingTryCatch
210
  ? "[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."
211
  : hasFakeContracts
212
  ? `[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.`
213
+ : (!state.infrastructurePhase && !hasStrongAssertion)
214
  ? "[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)."
215
  : isMissingContract
216
  ? "[INVALID_CODE] No 'contract ExploitTest' found. The test contract MUST be named ExploitTest."
 
220
  ? "[INVALID_CODE] Exploit has TODO placeholder. You must implement the actual exploit logic."
221
  : "[WEAK_ASSERTION] Only assertTrue(true) found — this never proves the vulnerability. Add a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
222
  );
223
+ const isLastAttempt = state.infrastructurePhase
224
+ ? state.infraIterations >= MAX_INFRA_ITERATIONS
225
+ : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
226
+ const status = isLastAttempt ? "failed" : "running";
227
  return {
228
  executionLogs: [summary],
229
  lastError: summary,
 
235
  const analysis = analyzeFoundryLog(result);
236
  const noTestsFound = result.combined.includes("No tests found");
237
  const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
238
+ const isLastAttempt = state.infrastructurePhase
239
+ ? state.infraIterations >= MAX_INFRA_ITERATIONS
240
+ : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
241
 
242
+ let status: "running" | "success" | "failed" | "timeout" = "running";
243
+ if (state.infrastructurePhase) {
244
+ status = result.timedOut ? "timeout" : isLastAttempt && !passed ? "failed" : "running";
245
+ } else {
246
+ status = passed ? "success" : result.timedOut ? "timeout" : isLastAttempt ? "failed" : "running";
247
+ }
 
248
 
249
  console.log(`[testerAgent] Resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
250
  if (!passed) {
 
299
  return {};
300
  }
301
 
302
+ function routeAfterFoundry(state: PoCState): "reflectNode" | "generateExploitNode" | typeof END {
303
+ const isLastAttempt = state.infrastructurePhase
304
+ ? state.infraIterations >= MAX_INFRA_ITERATIONS
305
+ : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
306
+
307
+ if (state.status === "timeout" || (isLastAttempt && state.status === "failed")) {
308
+ return END;
309
+ }
310
+
311
+ if (!state.infrastructurePhase && state.status === "success") {
312
+ return END;
313
+ }
314
+
315
+ const isCompileError = state.lastError?.includes("[COMPILER_ERROR]") || state.lastError?.includes("[INVALID_CODE]");
316
+
317
+ if (state.infrastructurePhase) {
318
+ // We are in the Infra Loop
319
+ if (isCompileError) {
320
+ return "reflectNode"; // Go back to infra fix
321
+ } else {
322
+ // Compiled successfully! Move to Exploit Loop
323
+ return "generateExploitNode";
324
+ }
325
+ } else {
326
+ // We are in the Exploit Loop
327
+ return "reflectNode"; // Go back to exploit fix
328
+ }
329
+ }
330
+
331
+ function routeReflection(state: PoCState): "generateInfrastructureNode" | "generateExploitNode" {
332
+ return state.infrastructurePhase ? "generateInfrastructureNode" : "generateExploitNode";
333
  }
334
 
335
  const graph = new StateGraph(PoCStateAnnotation)
336
  .addNode("oracleNode", oracleNode)
337
  .addNode("analyzeVulnerabilityNode", analyzeVulnerabilityNode)
338
+ .addNode("generateInfrastructureNode", generateInfrastructureNode)
339
+ .addNode("generateExploitNode", generateExploitNode)
340
  .addNode("runFoundryNode", runFoundryNode)
341
  .addNode("reflectNode", reflectNode)
342
  .addEdge(START, "oracleNode")
343
  .addEdge("oracleNode", "analyzeVulnerabilityNode")
344
+ .addEdge("analyzeVulnerabilityNode", "generateInfrastructureNode")
345
+ .addEdge("generateInfrastructureNode", "runFoundryNode")
346
+ .addEdge("generateExploitNode", "runFoundryNode")
347
  .addConditionalEdges("runFoundryNode", routeAfterFoundry, {
348
  reflectNode: "reflectNode",
349
+ generateExploitNode: "generateExploitNode",
350
  [END]: END,
351
  })
352
+ .addConditionalEdges("reflectNode", routeReflection, {
353
+ generateInfrastructureNode: "generateInfrastructureNode",
354
+ generateExploitNode: "generateExploitNode"
355
+ });
356
 
357
  export const testerAgent = graph.compile();
src/agents/tester/nodes.ts ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SystemMessage, HumanMessage } from "@langchain/core/messages";
2
+ import { PoCState } from "./state.js";
3
+ import {
4
+ INFRASTRUCTURE_PROMPT,
5
+ INFRA_FIX_PROMPT,
6
+ EXPLOIT_INJECTION_PROMPT,
7
+ EXPLOIT_FIX_PROMPT
8
+ } from "./prompts/system.js";
9
+ import { extractSolidity } from "./utils/extractSolidity.js";
10
+ import { createLLM } from "../../config/llm.ts";
11
+ import { createSearchCodebaseTool, createReadFileTool } from "./tools/codebaseTools.js";
12
+ import { AIMessage, ToolMessage } from "@langchain/core/messages";
13
+
14
+ // Models
15
+ const smartLlm = createLLM(undefined, "google/gemini-3-flash-preview");
16
+ const fastLlm = createLLM(undefined, "google/gemini-3.1-flash-lite");
17
+
18
+ // --- INFRASTRUCTURE LOOP ---
19
+
20
+ export async function generateInfrastructureNode(state: PoCState): Promise<Partial<PoCState>> {
21
+ const { report, oracleContext, executionLogs, iterations, lastError } = state;
22
+ const isRetry = !!lastError;
23
+
24
+ let systemPrompt = INFRASTRUCTURE_PROMPT.replace("{TARGET_NAME}", report.affectedContract.name);
25
+ let userMessage = "";
26
+
27
+ if (!isRetry) {
28
+ userMessage = `Please generate the Template.
29
+
30
+ ### Scaffold:
31
+ \`\`\`solidity
32
+ ${oracleContext!.solidityScaffold}
33
+ \`\`\`
34
+
35
+ ### Remappings:
36
+ \`\`\`
37
+ ${oracleContext!.projectRemappings}
38
+ \`\`\`
39
+
40
+ ### Import Example:
41
+ \`\`\`solidity
42
+ ${oracleContext!.projectTestImports}
43
+ \`\`\``;
44
+ } else {
45
+ systemPrompt = INFRA_FIX_PROMPT.replace("{ERROR_DETAILS}", lastError ?? "");
46
+ userMessage = `The template failed to compile or execute.
47
+
48
+ ### Previous Template:
49
+ \`\`\`solidity
50
+ ${state.templateCode}
51
+ \`\`\`
52
+
53
+ Fix the issues and return the updated template. Ensure the target is actually instantiated!`;
54
+ }
55
+
56
+ console.log(`[infraNode] Generating template... Retry: ${isRetry}`);
57
+
58
+ const sandboxDir = state.report.customSandboxDir || "./sandbox";
59
+ const searchTool = createSearchCodebaseTool(sandboxDir);
60
+ const readTool = createReadFileTool(sandboxDir);
61
+ const tools = [searchTool, readTool];
62
+ const llmWithTools = fastLlm.bindTools(tools);
63
+
64
+ const messages: any[] = [
65
+ new SystemMessage(systemPrompt),
66
+ new HumanMessage(userMessage)
67
+ ];
68
+
69
+ let iterationsInLoop = 0;
70
+ const maxIterations = 5;
71
+ let rawContent = "";
72
+
73
+ while (iterationsInLoop < maxIterations) {
74
+ console.log(`[infraNode] ReAct loop iteration ${iterationsInLoop + 1}`);
75
+ const response = await llmWithTools.invoke(messages);
76
+ messages.push(response);
77
+
78
+ if (response.tool_calls && response.tool_calls.length > 0) {
79
+ for (const toolCall of response.tool_calls) {
80
+ let toolResult = "";
81
+ try {
82
+ if (toolCall.name === "searchCodebase") {
83
+ toolResult = await searchTool.invoke(toolCall.args);
84
+ } else if (toolCall.name === "readFile") {
85
+ toolResult = await readTool.invoke(toolCall.args);
86
+ } else {
87
+ toolResult = "Unknown tool.";
88
+ }
89
+ } catch (e: any) {
90
+ toolResult = `Error executing tool: ${e.message}`;
91
+ }
92
+
93
+ messages.push(new ToolMessage({
94
+ tool_call_id: toolCall.id!,
95
+ name: toolCall.name,
96
+ content: toolResult,
97
+ }));
98
+ }
99
+ } else {
100
+ rawContent = response.content as string;
101
+ break;
102
+ }
103
+ iterationsInLoop++;
104
+ }
105
+
106
+ if (!rawContent) {
107
+ const lastMsg = messages[messages.length - 1];
108
+ rawContent = (lastMsg instanceof AIMessage && typeof lastMsg.content === 'string')
109
+ ? lastMsg.content
110
+ : `// [LLM_ERROR] Reached max tool iterations without returning final code.`;
111
+ }
112
+ let templateCode = "";
113
+ try {
114
+ templateCode = extractSolidity(rawContent);
115
+ } catch (e: any) {
116
+ // If extraction fails (e.g., safety refusal), return a mock invalid file
117
+ templateCode = `// [LLM_REFUSAL_OR_ERROR] ${e.message}\n// Raw Output: ${rawContent.slice(0, 200)}`;
118
+ }
119
+
120
+ return {
121
+ templateCode,
122
+ pocCode: templateCode, // Temporarily treat template as poc to run compiler
123
+ iterations: 1, // Overall
124
+ infraIterations: 1
125
+ };
126
+ }
127
+
128
+ // --- EXPLOIT LOOP ---
129
+
130
+ export async function generateExploitNode(state: PoCState): Promise<Partial<PoCState>> {
131
+ const { vulnerabilityAnalysis, templateCode, exploitBody, executionLogs, lastError, iterations } = state;
132
+
133
+ // We transition from infra to exploit loop
134
+ const isRetry = state.infrastructurePhase === false;
135
+
136
+ let systemPrompt = EXPLOIT_INJECTION_PROMPT.replace("{TEMPLATE_CODE}", templateCode);
137
+ let userMessage = "";
138
+
139
+ if (!isRetry) {
140
+ userMessage = `Please inject the exploit logic based on this analysis:
141
+
142
+ ${vulnerabilityAnalysis}`;
143
+ } else {
144
+ systemPrompt = EXPLOIT_FIX_PROMPT
145
+ .replace("{ERROR_DETAILS}", lastError ?? "")
146
+ .replace("{EXPLOIT_BODY}", exploitBody);
147
+ userMessage = `The exploit failed. Please rewrite the body of test_Exploit().`;
148
+ }
149
+
150
+ console.log(`[exploitNode] Generating hack... Retry: ${isRetry}`);
151
+
152
+ const sandboxDir = state.report.customSandboxDir || "./sandbox";
153
+ const searchTool = createSearchCodebaseTool(sandboxDir);
154
+ const readTool = createReadFileTool(sandboxDir);
155
+ const tools = [searchTool, readTool];
156
+ const llmWithTools = smartLlm.bindTools(tools);
157
+
158
+ const messages: any[] = [
159
+ new SystemMessage(systemPrompt),
160
+ new HumanMessage(userMessage)
161
+ ];
162
+
163
+ let iterationsInLoop = 0;
164
+ const maxIterations = 5;
165
+ let rawContent = "";
166
+
167
+ while (iterationsInLoop < maxIterations) {
168
+ console.log(`[exploitNode] ReAct loop iteration ${iterationsInLoop + 1}`);
169
+ const response = await llmWithTools.invoke(messages);
170
+ messages.push(response);
171
+
172
+ if (response.tool_calls && response.tool_calls.length > 0) {
173
+ for (const toolCall of response.tool_calls) {
174
+ let toolResult = "";
175
+ try {
176
+ if (toolCall.name === "searchCodebase") {
177
+ toolResult = await searchTool.invoke(toolCall.args);
178
+ } else if (toolCall.name === "readFile") {
179
+ toolResult = await readTool.invoke(toolCall.args);
180
+ } else {
181
+ toolResult = "Unknown tool.";
182
+ }
183
+ } catch (e: any) {
184
+ toolResult = `Error executing tool: ${e.message}`;
185
+ }
186
+
187
+ messages.push(new ToolMessage({
188
+ tool_call_id: toolCall.id!,
189
+ name: toolCall.name,
190
+ content: toolResult,
191
+ }));
192
+ }
193
+ } else {
194
+ rawContent = response.content as string;
195
+ break;
196
+ }
197
+ iterationsInLoop++;
198
+ }
199
+
200
+ if (!rawContent) {
201
+ const lastMsg = messages[messages.length - 1];
202
+ rawContent = (lastMsg instanceof AIMessage && typeof lastMsg.content === 'string')
203
+ ? lastMsg.content
204
+ : `// [LLM_ERROR] Reached max tool iterations without returning final code.`;
205
+ }
206
+
207
+ // --- GUARDRAIL: Catch comments immediately before Foundry ---
208
+ // The system prompts forbid // and /*. If the LLM still generates them (except SPDX/INJECT),
209
+ // we catch it here to save a slow Foundry roundtrip.
210
+ let newExploitBody = "";
211
+ try {
212
+ newExploitBody = extractSolidity(rawContent);
213
+ const codeLines = newExploitBody.split('\n');
214
+ const hasIllegalComments = codeLines.some(line => {
215
+ const t = line.trim();
216
+ if (t.startsWith("// SPDX-License-Identifier:") || t.includes("// INJECT_HACK")) return false;
217
+ return t.includes("//") || t.includes("/*");
218
+ });
219
+
220
+ if (hasIllegalComments) {
221
+ throw new Error("[GUARDRAIL_ERROR] You added a comment in the code. This is STRICTLY FORBIDDEN. Write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX).");
222
+ }
223
+ } catch (e: any) {
224
+ // Return the generated code + error so the reflection loop catches it
225
+ return {
226
+ pocCode: rawContent, // pass raw so reflect node can see the mistake
227
+ lastError: e.message,
228
+ exploitIterations: state.exploitIterations + 1
229
+ };
230
+ }
231
+
232
+ // The Exploit LLM now outputs the FULL file
233
+ const pocCode = newExploitBody;
234
+
235
+ return {
236
+ exploitBody: newExploitBody,
237
+ pocCode: pocCode, // This is the final runnable file
238
+ infrastructurePhase: false, // Transition permanently to exploit loop
239
+ iterations: state.iterations + 1,
240
+ exploitIterations: state.exploitIterations + 1
241
+ };
242
+ }
src/agents/tester/prompts/system.ts CHANGED
@@ -180,3 +180,118 @@ contract ExploitTest is Test {
180
 
181
  Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Use ONLY forge-std imports and inline interfaces.
182
  `.trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Use ONLY forge-std imports and inline interfaces.
182
  `.trim();
183
+
184
+ // ==========================================
185
+ // NEW TEMPLATE INJECTION ARCHITECTURE PROMPTS
186
+ // ==========================================
187
+
188
+ export const INFRASTRUCTURE_PROMPT = `You are a Smart Contract Testing Infrastructure Engineer. Your ONLY job is to create a compiling Foundry test template.
189
+ DO NOT WRITE THE EXPLOIT.
190
+
191
+ ## Rules
192
+ 1. Create a contract named \`ExploitTest\` inheriting from \`Test\` (or the project's base test).
193
+ 2. Write all necessary \`import\` statements using the provided Project Remappings and existing test examples. If you are unsure where a required struct or contract is defined, **USE YOUR TOOLS (searchCodebase, readFile)** to find it. DO NOT GUESS import paths!
194
+ 3. Write ONLY the \`setUp()\` function. It must DEPLOY the target contract, fund the attacker, and prepare the environment.
195
+ - CRITICAL: You MUST actually instantiate the target contract (e.g. \`target = new TargetContract(...)\`).
196
+ - MANDATORY: The instance variable MUST be named \`target\`, and the very last line of \`setUp()\` MUST be: \`require(address(target) != address(0), "Target must be deployed");\`
197
+ 4. Declare an EMPTY function named \`test_Exploit()\`. Leave the body exactly as: \`// INJECT_HACK\`
198
+ 5. Output ONLY a single \`\`\`solidity ... \`\`\` block.
199
+ 6. STRICT RULE: DO NOT WRITE ANY COMMENTS (like // or /*) EXCEPT for the SPDX identifier and the // INJECT_HACK marker. Writing explanatory comments will cause compilation to fail!
200
+
201
+ Target Contract Name: {TARGET_NAME}
202
+ `.trim();
203
+
204
+ export const INFRA_FIX_PROMPT = `The infrastructure template FAILED TO COMPILE OR EXECUTE.
205
+ Your job is to fix the issues so it compiles and executes successfully.
206
+
207
+ ## Errors / Logs:
208
+ {ERROR_DETAILS}
209
+
210
+ ## Rules
211
+ - Fix missing files by adjusting import paths using the Remappings.
212
+ - If an external dependency cannot be imported, declare a minimal interface for it in the same file.
213
+ - If the target contract requires specific parameters, interfaces, or structs in its constructor or setup, **USE YOUR TOOLS (searchCodebase, readFile)** to find where those are defined in the project, and add the correct \`import\` statements. DO NOT GUESS import paths.
214
+ - Keep the \`test_Exploit()\` function empty with exactly: \`// INJECT_HACK\`
215
+ - The \`setUp()\` function MUST instantiate the target and end with: \`require(address(target) != address(0), "Target must be deployed");\`
216
+ - Return the full corrected Solidity file in a \`\`\`solidity\`\`\` block.
217
+ `.trim();
218
+
219
+ export const EXPLOIT_INJECTION_PROMPT = `You are an expert Smart Contract Security Auditor.
220
+ We have already prepared a perfectly compiling Foundry test environment (the Template) that deploys the contract.
221
+
222
+
223
+ ## The Environment (DO NOT MODIFY OR RE-DECLARE)
224
+ \`\`\`solidity
225
+ {TEMPLATE_CODE}
226
+ \`\`\`
227
+
228
+ ## Rules
229
+ 1. You MUST output the ENTIRE Solidity file, from the SPDX license to the end of the contract.
230
+ 2. You MUST keep the \`setUp()\` function exactly as it is in the Template (including the target deployment and \`require\` checks).
231
+ 3. If you need external structs or interfaces (e.g. for function parameters), you MUST **USE YOUR TOOLS (searchCodebase, readFile)** to find their exact file paths and add \`import\` statements at the top. DO NOT GUESS import paths! Alternatively, you can use low-level \`.call(abi.encodeWithSignature(...))\` to bypass struct definitions entirely.
232
+ 4. Write your PoC logic INSIDE \`function test_Exploit() { ... }\`. Use the variables already declared in the Template.
233
+ 5. The PoC MUST conclude with a strict Foundry assertion (assertEq, assertGt, etc.) that proves the vulnerability exists.
234
+ 6. NEVER redefine the target contract inside the test file or use \`try/catch\`.
235
+ 7. STRICT RULE: DO NOT WRITE ANY COMMENTS (like // or /*) ANYWHERE in the code. Writing explanatory or placeholder comments is strictly forbidden and will be rejected!
236
+ 8. CHAIN OF THOUGHT: Before writing the code, you MUST write your step-by-step reasoning inside \`<thinking>...\</thinking>\` tags. Think about how to trigger the vulnerability without using Mocks and without writing comments.
237
+ 9. Output ONLY your full test file enclosed in a \`\`\`solidity ... \`\`\` block immediately after the thinking tags.
238
+
239
+ ## Few-Shot Example (Perfect Exploit Formatting)
240
+ <thinking>
241
+ I need to exploit a reentrancy. I cannot use comments. I will create a MaliciousReceiver contract inside the same file but OUTSIDE the ExploitTest contract. I will not use Mock contracts.
242
+ </thinking>
243
+ \`\`\`solidity
244
+ // SPDX-License-Identifier: UNLICENSED
245
+ pragma solidity ^0.8.0;
246
+
247
+ import "forge-std/Test.sol";
248
+ import "../src/Target.sol";
249
+
250
+ contract MaliciousReceiver {
251
+ Target target;
252
+ constructor(Target _target) { target = _target; }
253
+ receive() external payable {
254
+ if (address(target).balance > 0) {
255
+ target.withdraw(1 ether);
256
+ }
257
+ }
258
+ }
259
+
260
+ contract ExploitTest is Test {
261
+ Target target;
262
+ address constant ATTACKER = address(0xBEEF);
263
+
264
+ function setUp() public {
265
+ target = new Target();
266
+ vm.deal(ATTACKER, 100 ether);
267
+ require(address(target) != address(0), "Target must be deployed");
268
+ }
269
+
270
+ function test_Exploit() public {
271
+ vm.startPrank(ATTACKER);
272
+ MaliciousReceiver receiver = new MaliciousReceiver(target);
273
+ target.deposit{value: 1 ether}();
274
+ target.withdraw(1 ether);
275
+ vm.stopPrank();
276
+ assertGt(ATTACKER.balance, 100 ether);
277
+ }
278
+ }
279
+ \`\`\`
280
+ `.trim();
281
+
282
+ export const EXPLOIT_FIX_PROMPT = `The injected PoC FAILED during execution or validation.
283
+
284
+ ## Execution Error / Logs:
285
+ {ERROR_DETAILS}
286
+
287
+ ## Previous PoC Body:
288
+ \`\`\`solidity
289
+ {EXPLOIT_BODY}
290
+ \`\`\`
291
+
292
+ ## Rules
293
+ - Analyze the execution failure and rewrite the full PoC file.
294
+ - If you had "Identifier not found" or "Source not found" errors, **USE YOUR TOOLS (searchCodebase, readFile)** to find the exact file and add the correct new imports, or use low-level calls. DO NOT GUESS import paths!
295
+ - CHAIN OF THOUGHT: Write your reasoning inside \`<thinking>...\</thinking>\` tags BEFORE the code.
296
+ - Output the FULL Solidity file enclosed in a \`\`\`solidity\`\`\` block.
297
+ `.trim();
src/agents/tester/state.ts CHANGED
@@ -11,7 +11,22 @@ export const PoCStateAnnotation = Annotation.Root({
11
 
12
  pocCode: Annotation<string>({
13
  default: () => "",
14
- reducer: (_, y) => y, // overwrite — always latest version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }),
16
 
17
  vulnerabilityAnalysis: Annotation<string>({
@@ -34,6 +49,16 @@ export const PoCStateAnnotation = Annotation.Root({
34
  reducer: (x, y) => x + y, // additive — incremented by +1 per call
35
  }),
36
 
 
 
 
 
 
 
 
 
 
 
37
  compileFailures: Annotation<number>({
38
  default: () => 0,
39
  reducer: (x, y) => x + y, // additive — incremented on each compile failure
 
11
 
12
  pocCode: Annotation<string>({
13
  default: () => "",
14
+ reducer: (_, y) => y, // overwrite — full combined file
15
+ }),
16
+
17
+ templateCode: Annotation<string>({
18
+ default: () => "",
19
+ reducer: (_, y) => y, // overwrite — only imports and setUp
20
+ }),
21
+
22
+ exploitBody: Annotation<string>({
23
+ default: () => "",
24
+ reducer: (_, y) => y, // overwrite — only the hack logic
25
+ }),
26
+
27
+ infrastructurePhase: Annotation<boolean>({
28
+ default: () => true,
29
+ reducer: (_, y) => y, // overwrite — true while fixing imports
30
  }),
31
 
32
  vulnerabilityAnalysis: Annotation<string>({
 
49
  reducer: (x, y) => x + y, // additive — incremented by +1 per call
50
  }),
51
 
52
+ infraIterations: Annotation<number>({
53
+ default: () => 0,
54
+ reducer: (x, y) => x + y, // additive
55
+ }),
56
+
57
+ exploitIterations: Annotation<number>({
58
+ default: () => 0,
59
+ reducer: (x, y) => x + y, // additive
60
+ }),
61
+
62
  compileFailures: Annotation<number>({
63
  default: () => 0,
64
  reducer: (x, y) => x + y, // additive — incremented on each compile failure
src/agents/tester/tools/codebaseTools.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { tool } from "@langchain/core/tools";
2
+ import { z } from "zod";
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ export const createSearchCodebaseTool = (sandboxDir: string) => {
11
+ return tool(
12
+ async ({ query }) => {
13
+ try {
14
+ // Find .sol files containing the query in the sandboxDir
15
+ const { stdout } = await execAsync(`grep -rn --include="*.sol" "${query}" .`, { cwd: sandboxDir });
16
+ const lines = stdout.split("\n").filter(l => l.trim() !== "");
17
+ if (lines.length === 0) return "No results found.";
18
+
19
+ const preview = lines.slice(0, 30);
20
+ const truncatedMsg = lines.length > 30 ? `\n...and ${lines.length - 30} more results.` : "";
21
+ return `Found ${lines.length} results. Showing first 30:\n${preview.join("\n")}${truncatedMsg}`;
22
+ } catch (e: any) {
23
+ if (e.code === 1) return "No results found."; // grep exit code 1 means no match
24
+ return `Error searching codebase: ${e.message}`;
25
+ }
26
+ },
27
+ {
28
+ name: "searchCodebase",
29
+ description: "Searches the codebase for a specific string (like a struct, contract name, or interface) and returns the file paths and matching lines.",
30
+ schema: z.object({
31
+ query: z.string().describe("The exact string to search for. Keep it simple, e.g. 'LiquidateWithReplacementParams' or 'SizeFactory'"),
32
+ }),
33
+ }
34
+ );
35
+ };
36
+
37
+ export const createReadFileTool = (sandboxDir: string) => {
38
+ return tool(
39
+ async ({ filePath }) => {
40
+ try {
41
+ const fullPath = path.resolve(sandboxDir, filePath);
42
+ // Security check to avoid path traversal
43
+ if (!fullPath.startsWith(path.resolve(sandboxDir))) {
44
+ return "Error: Cannot read files outside the sandbox directory.";
45
+ }
46
+ const content = await fs.readFile(fullPath, "utf-8");
47
+
48
+ // Truncate if extremely large to save context window, though Solidity files are usually small enough
49
+ if (content.length > 20000) {
50
+ return content.slice(0, 20000) + "\n\n... [TRUNCATED] File too large.";
51
+ }
52
+ return content;
53
+ } catch (e: any) {
54
+ return `Error reading file: ${e.message}`;
55
+ }
56
+ },
57
+ {
58
+ name: "readFile",
59
+ description: "Reads the content of a specific file. Pass the relative file path returned by searchCodebase.",
60
+ schema: z.object({
61
+ filePath: z.string().describe("The relative path of the file to read (e.g. 'src/Size.sol')"),
62
+ }),
63
+ }
64
+ );
65
+ };
src/agents/tester/utils/logAnalyzer.ts CHANGED
@@ -21,25 +21,31 @@ export interface LogAnalysis {
21
  function extractCompilerErrors(combined: string): string[] {
22
  const lines = combined.split("\n");
23
  const errorLines: string[] = [];
 
24
 
25
  for (let i = 0; i < lines.length; i++) {
26
  const line = lines[i];
27
- // Match actual error lines with file location arrows
28
- if (line.includes("Error") || line.includes("error[") || line.includes("--> ")) {
 
 
 
 
 
 
 
 
 
 
 
 
29
  errorLines.push(line);
30
- // Include next line (context after the arrow) if it exists
31
- if (lines[i + 1] && (lines[i + 1].includes("|") || lines[i + 1].includes("^"))) {
32
- errorLines.push(lines[i + 1]);
33
- if (lines[i + 2] && lines[i + 2].includes("|")) {
34
- errorLines.push(lines[i + 2]);
35
- }
36
- }
37
  }
38
- if (errorLines.length >= 30) break;
 
39
  }
40
 
41
- // Filter out Warning-only lines
42
- return errorLines.filter(l => !l.trim().startsWith("Warning"));
43
  }
44
 
45
  export function analyzeFoundryLog(result: FoundryResult): LogAnalysis {
 
21
  function extractCompilerErrors(combined: string): string[] {
22
  const lines = combined.split("\n");
23
  const errorLines: string[] = [];
24
+ let inErrorBlock = false;
25
 
26
  for (let i = 0; i < lines.length; i++) {
27
  const line = lines[i];
28
+ // Start of an error block
29
+ if (line.trim().startsWith("Error") || line.trim().startsWith("error[")) {
30
+ inErrorBlock = true;
31
+ }
32
+ // Start of a warning block
33
+ else if (line.trim().startsWith("Warning") || line.trim().startsWith("warning[")) {
34
+ inErrorBlock = false;
35
+ }
36
+ // End of compilation output
37
+ else if (line.includes("Compilation failed")) {
38
+ inErrorBlock = false;
39
+ }
40
+
41
+ if (inErrorBlock && line.trim() !== "") {
42
  errorLines.push(line);
 
 
 
 
 
 
 
43
  }
44
+
45
+ if (errorLines.length >= 40) break;
46
  }
47
 
48
+ return errorLines;
 
49
  }
50
 
51
  export function analyzeFoundryLog(result: FoundryResult): LogAnalysis {
src/benchmark/runFinalEvaluation.ts CHANGED
@@ -4,7 +4,7 @@ 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");
@@ -15,8 +15,8 @@ async function runEvaluation() {
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
@@ -26,9 +26,11 @@ async function runEvaluation() {
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"
@@ -76,7 +78,12 @@ async function runEvaluation() {
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,
@@ -84,7 +91,7 @@ async function runEvaluation() {
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,
@@ -148,7 +155,7 @@ async function runEvaluation() {
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
 
@@ -174,9 +181,11 @@ async function runEvaluation() {
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) : ""
 
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, computePatchDiff } 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");
 
15
  const metadata = JSON.parse(metadataStr);
16
  const cases = Object.keys(metadata);
17
 
18
+ // Limitado a 1 projeto (054 - Cally) para observação empírica de simplicidade
19
+ const targetCases = ["054"];
20
  console.log(`Iniciando avaliação final para ${targetCases.length} projetos...`);
21
 
22
  // Prepara o arquivo CSV
 
26
  "Reproducible",
27
  "Specific",
28
  "False_Positive_Rejected",
29
+ "A_Infra_Iters",
30
+ "A_Exploit_Iters",
31
  "A_Final_Error",
32
+ "B_Infra_Iters",
33
+ "B_Exploit_Iters",
34
  "B_Final_Error",
35
  "PoC_Code",
36
  "Patch_Diff"
 
78
  }
79
  } catch(e) {}
80
 
81
+ let patchDiff = "";
82
+ try {
83
+ const patchSourceDir = path.join(process.cwd(), DATASET_PATH, data.patch);
84
+ const targetDir = path.join(process.cwd(), DATASET_PATH, data.target_directory);
85
+ patchDiff = await computePatchDiff(patchSourceDir, path.join(sandboxDir, targetPath), targetDir, targetPath);
86
+ } catch {}
87
 
88
  const reportA: VulnerabilityReport = {
89
  id: caseId,
 
91
  type: data.expected_vulnerability,
92
  title: `${data.repo_name} - ${caseId}`,
93
  description: data.annotation,
94
+ affectedContract: { name: targetPath.split("/").pop()!.replace(".sol", ""), sourceCode: vulnerableCode, sourceFilePath: targetPath },
95
  attackVector: data.expected_vulnerability,
96
  customSandboxDir: sandboxDir,
97
  referenceTestCode,
 
155
  // Passa a MESMA anotação (mentindo que é vulnerável)
156
  const reportB: VulnerabilityReport = {
157
  ...reportA,
158
+ affectedContract: { name: targetPath.split("/").pop()!.replace(".sol", ""), sourceCode: patchedCode, sourceFilePath: targetPath },
159
  patchDiff: undefined // Oculta o patch diff do LLM para este cenário
160
  };
161
 
 
181
  reproducible ? "TRUE" : "FALSE",
182
  specific ? "TRUE" : "FALSE",
183
  falsePositiveRejected ? "TRUE" : "FALSE",
184
+ (resultA as any).infraIterations || 0,
185
+ (resultA as any).exploitIterations || 0,
186
  lastErrorA,
187
+ (resultB as any).infraIterations || 0,
188
+ (resultB as any).exploitIterations || 0,
189
  lastErrorB,
190
  reproducible ? escapeCsv(pocCodeStr) : "",
191
  reproducible ? escapeCsv(patchDiff) : ""
src/benchmark/runTesterBenchmark.ts CHANGED
@@ -66,7 +66,7 @@ export async function applyPatchSmart(caseId: string, sandboxDir: string): Promi
66
  * Computes a unified diff of the main contract between vulnerable and patched versions.
67
  * Uses the same strip-depth matching as applyPatchSmart.
68
  */
69
- async function computePatchDiff(
70
  patchSourceDir: string,
71
  mainContractPath: string,
72
  targetDir: string,
 
66
  * Computes a unified diff of the main contract between vulnerable and patched versions.
67
  * Uses the same strip-depth matching as applyPatchSmart.
68
  */
69
+ export async function computePatchDiff(
70
  patchSourceDir: string,
71
  mainContractPath: string,
72
  targetDir: string,
testRegex.cjs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const trimmedCode = fs.readFileSync('/home/tales/Mestrado/IA/projeto-talp1/temp_vuln_run/001/test/Exploit.t.sol', 'utf8');
3
+ const hasIllegalComments = trimmedCode.split('\n').some(line => {
4
+ const isComment = line.includes('//') || line.includes('/*');
5
+ const isAllowed = line.includes('SPDX-License-Identifier') || line.includes('INJECT_HACK');
6
+ if (isComment && !isAllowed) {
7
+ console.log("ILLEGAL COMMENT LINE: ", line);
8
+ }
9
+ return isComment && !isAllowed;
10
+ });
11
+ console.log("hasIllegalComments:", hasIllegalComments);
test_regex.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const code = `
2
+ function setUp() public virtual {
3
+ // target = address(new Target());
4
+ vm.startPrank(ATTACKER);
5
+ vm.deal(ATTACKER, 100 ether);
6
+ }
7
+ `;
8
+ const isTargetNotDeployed = code.includes("// target = new") ||
9
+ code.includes("//Target target = new") ||
10
+ code.match(/\/\/\s*([a-zA-Z0-9_]+)\s*=\s*new\s+[a-zA-Z0-9_]+/);
11
+ console.log(!!isTargetNotDeployed);
test_regex2.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const code = `
2
+ function setUp() public virtual {
3
+ // target = address(new Target());
4
+ vm.startPrank(ATTACKER);
5
+ vm.deal(ATTACKER, 100 ether);
6
+ }
7
+ `;
8
+ const isTargetNotDeployed = code.includes("// target = new") ||
9
+ code.includes("//Target target = new") ||
10
+ code.includes("// target = address(new") ||
11
+ code.match(/\/\/\s*([a-zA-Z0-9_]+)\s*=\s*(address\()?new\s+[a-zA-Z0-9_]+/);
12
+ console.log(!!isTargetNotDeployed);