Spaces:
Runtime error
feat: smart patch, dep stubs, progress log, docker fix
Browse filesBenchmark improvements (Run 3: 27.3% -> 54.5% reproducibility):
- applyPatchSmart: fix critical patch application bug (specificity was 0%)
Previously cp -rv copied wrong directory level, patch was never applied
Now strips 1-3 path prefix levels to match files correctly
- computePatchDiff: correct diff using same strip-depth matching logic
- dependencyStubber.ts: pre-forge probe creates stubs for missing deps
Targets 7 failing cases using lib/caviar or node_modules imports
- projectContextExtractor.ts: BFS test file selection + remappings context
- compileFailures counter + MINIMAL_INTERFACE escape at 3 failures
- Patch diff shown in analyzeVulnerabilityNode for specific assertions
- Improved prompts: reentrancy receive() pattern, unchecked return pattern
- Error lines surfaced in fix prompt for faster compiler error resolution
Docker / integration:
- setup-sandbox.sh: source ~/.foundry/env for non-interactive shells
Fixes case 046 (forge not in PATH in Docker)
Fallback: checks ~/.foundry/bin/forge directly before failing
Docs:
- AGENT_PROGRESS.md: full timeline with all runs, per-case results,
root cause analysis, architecture diagram, and next steps
- AGENT_PROGRESS.md +238 -0
- data/benchmark_summary.json +20 -20
- scripts/setup-sandbox.sh +14 -2
- src/agents/tester/agent.ts +110 -19
- src/agents/tester/prompts/system.ts +155 -28
- src/agents/tester/state.ts +5 -0
- src/agents/tester/types.ts +4 -0
- src/agents/tester/utils/dependencyStubber.ts +82 -0
- src/agents/tester/utils/logAnalyzer.ts +81 -14
- src/agents/tester/utils/projectContextExtractor.ts +156 -0
- src/benchmark/runTesterBenchmark.ts +106 -2
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PoC Tester Agent — Progress Log
|
| 2 |
+
|
| 3 |
+
> **Repository:** `uandersonricardo/projeto-talp1`
|
| 4 |
+
> **Branch:** `teste_agent`
|
| 5 |
+
> **Model used:** `google/gemini-3.1-flash-lite` (via OpenRouter)
|
| 6 |
+
> **Framework:** LangGraph + Foundry
|
| 7 |
+
> **Dataset:** [ASSERT-KTH/Proof-of-Patch](https://github.com/ASSERT-KTH/Proof-of-Patch) — 22 real-world smart contract vulnerabilities with verified patches
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Metric Definitions
|
| 12 |
+
|
| 13 |
+
| Metric | Formula | Meaning |
|
| 14 |
+
|---|---|---|
|
| 15 |
+
| **Reproducibility Rate** | `PoCs passing on vulnerable version / 22` | Agent generates a working exploit |
|
| 16 |
+
| **Specificity Rate** | `PoCs failing on patched version / reproducible PoCs` | Exploit is logically correct (patch stops it) |
|
| 17 |
+
| **Overall Ground Truth** | `reproducible AND specific / 22` | Both conditions satisfied |
|
| 18 |
+
|
| 19 |
+
> **Reference:** The PoCo paper (Andersson et al., arXiv:2511.02780) evaluated on the same dataset using GPT-4o.
|
| 20 |
+
> Cases #003 and #015 have "inconclusive patches" per §5.3.1 — the patch fixes the bug but the PoC still passes because it tests side-effects unaffected by the fix. This is a known dataset limitation.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Benchmark History
|
| 25 |
+
|
| 26 |
+
### Run 0 — Baseline (before any improvements)
|
| 27 |
+
**Date:** 2026-06-07 (pre-session)
|
| 28 |
+
**Commit:** `d849e68`
|
| 29 |
+
|
| 30 |
+
| Metric | Value |
|
| 31 |
+
|---|---|
|
| 32 |
+
| Reproducibility | **27.3%** (6/22) |
|
| 33 |
+
| Specificity | **0.0%** (0/6) |
|
| 34 |
+
| Overall Ground Truth | **0.0%** |
|
| 35 |
+
| Avg Iterations | 8.18 |
|
| 36 |
+
|
| 37 |
+
**Reproducible:** 001, 003, 008, 051, 054, 091
|
| 38 |
+
**Main failure:** ~63% were COMPILER_ERROR — agent guessing wrong import paths with no context.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
### Run 1 — Quick validation (5 cases, first improvements)
|
| 43 |
+
**Date:** 2026-06-08
|
| 44 |
+
|
| 45 |
+
| Metric | Value |
|
| 46 |
+
|---|---|
|
| 47 |
+
| Reproducibility | **20.0%** (1/5) |
|
| 48 |
+
| Specificity | **0.0%** |
|
| 49 |
+
| Avg Iterations | 8.40 |
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
### Run 2 — Quick validation (5 cases, BFS + minimal interface)
|
| 54 |
+
**Date:** 2026-06-08
|
| 55 |
+
**Key changes:** BFS test file selection, compileFailures counter, MINIMAL_INTERFACE escape hatch
|
| 56 |
+
|
| 57 |
+
| Metric | Value |
|
| 58 |
+
|---|---|
|
| 59 |
+
| Reproducibility | **80.0%** (4/5) |
|
| 60 |
+
| Specificity | **0.0%** |
|
| 61 |
+
| Avg Iterations | 7.00 |
|
| 62 |
+
|
| 63 |
+
**Notable:** Case 015: 10 iters → failed became 6 iters → success thanks to MINIMAL_INTERFACE.
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
### Run 3 — Full benchmark (22 cases)
|
| 68 |
+
**Date:** 2026-06-08
|
| 69 |
+
**Commit:** `1929f1b`
|
| 70 |
+
|
| 71 |
+
| Metric | Value |
|
| 72 |
+
|---|---|
|
| 73 |
+
| Reproducibility | **54.5%** (12/22) |
|
| 74 |
+
| Specificity | **0.0%** (0/12) |
|
| 75 |
+
| Overall Ground Truth | **0.0%** |
|
| 76 |
+
| Avg Iterations | 7.55 |
|
| 77 |
+
|
| 78 |
+
**Reproducible (12):** 001, 003, 008, 015, 032, 051, 054, 058, 066, 077, 091, 098
|
| 79 |
+
**Failed (10):** 009, 018, 020, 033, 039, 041, 042, 048, 049, 070
|
| 80 |
+
**Error (1):** 046 (forge not in PATH during setup)
|
| 81 |
+
|
| 82 |
+
**Root cause of specificity=0%:** Patch application was broken — `cp -rv patches/ID/*` copied
|
| 83 |
+
a repo-name subdirectory INTO tempPatchDir instead of overwriting the actual source files.
|
| 84 |
+
The "patched" version was still running the vulnerable code.
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Per-Case Results (Run 3)
|
| 89 |
+
|
| 90 |
+
| ID | Vuln Type | Project | Repro | Iter | Failure Root Cause |
|
| 91 |
+
|---|---|---|---|---|---|
|
| 92 |
+
| 001 | multicall | 2024-06-size | ✅ | 9 | Patch not applied (dir bug) |
|
| 93 |
+
| 003 | access control | 2023-07-pooltogether | ✅ | 5 | Inconclusive patch (paper §5.3.1) |
|
| 94 |
+
| 008 | logic error | 2023-09-centrifuge | ✅ | 2 | Patch not applied (dir bug) |
|
| 95 |
+
| 009 | logic error | 2023-10-caviar | ❌ | 10 | `lib/caviar/lib/oracle` missing |
|
| 96 |
+
| 015 | access control | 2023-07-pooltogether | ✅ | 5 | Inconclusive patch (paper §5.3.1) |
|
| 97 |
+
| 018 | flash loan | 2023-10-caviar | ❌ | 10 | `lib/caviar/lib/oracle` missing |
|
| 98 |
+
| 020 | denial of service | 2023-10-dopex | ❌ | 10 | `node_modules/@openzeppelin` missing |
|
| 99 |
+
| 032 | access control | 2022-06-putty | ✅ | 4 | Patch not applied (dir bug) |
|
| 100 |
+
| 033 | logic error | 2023-10-caviar | ❌ | 10 | `lib/caviar/lib/oracle` missing |
|
| 101 |
+
| 039 | unchecked calls | 2024-03-axis-finance | ❌ | 10 | Compiled OK but logic reverted |
|
| 102 |
+
| 041 | reentrancy | 2024-03-axis-finance | ❌ | 10 | Compiled OK but logic reverted |
|
| 103 |
+
| 042 | access control | 2023-10-cap | ❌ | 10 | `node_modules/@openzeppelin-upgradeable` missing |
|
| 104 |
+
| 046 | n/a | n/a | ❌ | — | forge not in PATH (setup script error) |
|
| 105 |
+
| 048 | reentrancy | 2023-10-caviar | ❌ | 10 | `lib/caviar/lib/oracle` missing |
|
| 106 |
+
| 049 | access control | 2024-01-salty | ❌ | 10 | `test/lib/UserFactory.sol` missing |
|
| 107 |
+
| 051 | logic error | 2023-11-panoptic | ✅ | 2 | Patch not applied (dir bug) |
|
| 108 |
+
| 054 | logic error | 2024-02-wise-lending | ✅ | 7 | Patch not applied (dir bug) |
|
| 109 |
+
| 058 | logic error | 2024-04-renzo | ✅ | 5 | Patch not applied (dir bug) |
|
| 110 |
+
| 066 | unchecked calls | 2024-05-munchables | ✅ | 8 | Patch not applied (dir bug) |
|
| 111 |
+
| 070 | reentrancy | 2024-08-ph | ❌ | 10 | `node_modules/@prb/test` missing |
|
| 112 |
+
| 077 | reentrancy | 2024-07-templegold | ✅ | 8 | Patch not applied (dir bug) |
|
| 113 |
+
| 091 | logic error | 2024-08-basin | ✅ | 9 | Patch not applied (dir bug) |
|
| 114 |
+
| 098 | reentrancy | 2022-05-cally | ✅ | 2 | Patch not applied (dir bug) |
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Improvements Implemented
|
| 119 |
+
|
| 120 |
+
### 1. `projectContextExtractor.ts` (NEW)
|
| 121 |
+
**Problem:** LLM was guessing import paths → 63% COMPILER_ERROR failures.
|
| 122 |
+
**Fix:** Before generating PoC, reads `remappings.txt`, `foundry.toml`, and the most import-rich
|
| 123 |
+
`.t.sol` test file (BFS across all subdirs, picks file with most import lines).
|
| 124 |
+
|
| 125 |
+
### 2. `compileFailures` Counter + MINIMAL_INTERFACE Escape Hatch
|
| 126 |
+
**Problem:** After 10 failed compile attempts, LLM stuck in loop on wrong imports.
|
| 127 |
+
**Fix:** Counter increments per compile failure. After 3 consecutive → switches to
|
| 128 |
+
`POC_MINIMAL_INTERFACE_PROMPT` which forbids all external imports and uses inline interfaces.
|
| 129 |
+
|
| 130 |
+
### 3. Error Context in Fix Prompt
|
| 131 |
+
**Problem:** LLM only saw "Import path is WRONG" — not which file.
|
| 132 |
+
**Fix:** `analyzeFoundryLog` extracts actual error lines (file path + line number) and surfaces
|
| 133 |
+
them at the top of the fix prompt.
|
| 134 |
+
|
| 135 |
+
### 4. Patch Diff in Analysis Prompt
|
| 136 |
+
**Problem:** LLM generating generic assertions passing on both vulnerable and patched versions.
|
| 137 |
+
**Fix:** Patch diff (unified diff of vulnerable vs patched contract) included in
|
| 138 |
+
`analyzeVulnerabilityNode` with instruction: "assertion must PASS on vulnerable, FAIL on patched."
|
| 139 |
+
|
| 140 |
+
### 5. `dependencyStubber.ts` (NEW)
|
| 141 |
+
**Problem:** 7 cases fail because `lib/caviar/lib/oracle/...` or `node_modules/@openzeppelin/...`
|
| 142 |
+
are absent from the sandbox.
|
| 143 |
+
**Fix:** Runs `forge build` probe before generation, detects all "Source not found" errors,
|
| 144 |
+
creates minimal stub contracts at those exact paths.
|
| 145 |
+
|
| 146 |
+
### 6. `applyPatchSmart` — Smart Patch Application (CRITICAL for specificity)
|
| 147 |
+
**Problem:** `cp -rv patches/ID/*` was copying a repo-name subdirectory INTO `tempPatchDir`.
|
| 148 |
+
The "patched" test was running the vulnerable code. This is why specificity was always 0%.
|
| 149 |
+
**Fix:** For each `.sol` in the patch dir, strips 1, 2, then 3 path prefix levels to find
|
| 150 |
+
matching file in `tempPatchDir` and copies it correctly.
|
| 151 |
+
|
| 152 |
+
```
|
| 153 |
+
Patch file: patches/003/2023-07-pooltogether/vault/src/Vault.sol
|
| 154 |
+
tempPatchDir: copy of findings/003/2023-07-pooltogether/vault/
|
| 155 |
+
|
| 156 |
+
strip=1: vault/src/Vault.sol → NOT in tempPatchDir
|
| 157 |
+
strip=2: src/Vault.sol → EXISTS ✅ → copy applied
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### 7. `computePatchDiff` — Correct Diff Calculation
|
| 161 |
+
**Problem:** Previous diff command tried a hardcoded path that didn't match the nested structure.
|
| 162 |
+
**Fix:** Uses same strip-depth logic as `applyPatchSmart` to find the right file pair.
|
| 163 |
+
|
| 164 |
+
### 8. Improved Prompts
|
| 165 |
+
- `ANALYZE_VULNERABILITY_PROMPT`: asks for specific assertion that fails after patching
|
| 166 |
+
- `POC_COMPILE_FIX_PROMPT`: import resolution hierarchy (remappings → existing test → inline)
|
| 167 |
+
- `POC_TEST_FIX_PROMPT`: added reentrancy `receive()` callback pattern + unchecked return pattern
|
| 168 |
+
- `POC_MINIMAL_INTERFACE_PROMPT` (NEW): full template for zero-external-import strategy
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## Agent Architecture
|
| 173 |
+
|
| 174 |
+
```
|
| 175 |
+
VulnerabilityReport
|
| 176 |
+
│
|
| 177 |
+
▼
|
| 178 |
+
oracleNode
|
| 179 |
+
├── generateLocalScaffold()
|
| 180 |
+
├── analyzeSolidityFile() ← contract API extraction
|
| 181 |
+
├── extractProjectContext() ← remappings + best .t.sol (BFS)
|
| 182 |
+
└── createMissingDependencyStubs() ← stubs missing lib/node_modules
|
| 183 |
+
│
|
| 184 |
+
▼
|
| 185 |
+
analyzeVulnerabilityNode ← LLM: root cause + specific assertion
|
| 186 |
+
[ANALYZE_VULNERABILITY_PROMPT + patch diff]
|
| 187 |
+
│
|
| 188 |
+
▼
|
| 189 |
+
generatePoCNode ←──────────────────────────┐
|
| 190 |
+
├── INITIAL: [POC_INITIAL_PROMPT] │
|
| 191 |
+
├── FIX_COMPILE (failures < 3): │
|
| 192 |
+
│ [POC_COMPILE_FIX_PROMPT] │
|
| 193 |
+
├── MINIMAL_INTERFACE (failures >= 3): │
|
| 194 |
+
│ [POC_MINIMAL_INTERFACE_PROMPT] │
|
| 195 |
+
└── FIX_LOGIC: [POC_TEST_FIX_PROMPT] │
|
| 196 |
+
│ │
|
| 197 |
+
▼ │
|
| 198 |
+
runFoundryNode → analyzeFoundryLog │
|
| 199 |
+
│ │
|
| 200 |
+
├── success ──────────────────── END │
|
| 201 |
+
│ │
|
| 202 |
+
└── failed + iters < 10 → reflectNode ─┘
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## Next Steps
|
| 208 |
+
|
| 209 |
+
### P0 — Run benchmark with patch fix + dep stubs (Run 4)
|
| 210 |
+
Expected: Reproducibility ~68%, Specificity ~30%, Ground Truth ~20%
|
| 211 |
+
|
| 212 |
+
### P1 — Add `refineSpecificityNode`
|
| 213 |
+
If PoC passes on both versions, run a refinement step:
|
| 214 |
+
- Show: current PoC + patch diff
|
| 215 |
+
- Ask: "Make the assertion target exactly what the patch changes"
|
| 216 |
+
|
| 217 |
+
### P2 — Docker update
|
| 218 |
+
The Dockerfile builds TypeScript, so new files are included automatically.
|
| 219 |
+
Fix needed: `setup-sandbox.sh` may not find `forge` in Docker because foundryup
|
| 220 |
+
sets PATH in `~/.bashrc` (not sourced in non-interactive shells). Fix: add
|
| 221 |
+
`source ~/.foundry/env` or `export PATH="$HOME/.foundry/bin:$PATH"` explicitly.
|
| 222 |
+
|
| 223 |
+
### P3 — Integration check
|
| 224 |
+
The tester agent's public API (`VulnerabilityReport → { status, solidityCode }`) is unchanged.
|
| 225 |
+
The server.ts route calling `runPoCGenerator()` still works.
|
| 226 |
+
Docker image needs rebuild after code changes.
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## Expected Run 4 Results
|
| 231 |
+
|
| 232 |
+
| Metric | Run 3 | Target Run 4 |
|
| 233 |
+
|---|---|---|
|
| 234 |
+
| Reproducibility | 54.5% | ~68% |
|
| 235 |
+
| Specificity | 0.0% | ~30% |
|
| 236 |
+
| Overall Ground Truth | 0.0% | ~20% |
|
| 237 |
+
| Avg Iterations | 7.55 | <7.0 |
|
| 238 |
+
|
|
@@ -1,18 +1,18 @@
|
|
| 1 |
{
|
| 2 |
"summary": {
|
| 3 |
-
"timestamp": "2026-06-
|
| 4 |
"total_processed": 22,
|
| 5 |
-
"reproducibility_rate":
|
| 6 |
"specificity_rate": 0,
|
| 7 |
"overall_ground_truth_rate": 0,
|
| 8 |
-
"average_iterations":
|
| 9 |
},
|
| 10 |
"details": [
|
| 11 |
{
|
| 12 |
"id": "001",
|
| 13 |
-
"reproducible":
|
| 14 |
"specific": false,
|
| 15 |
-
"iterations":
|
| 16 |
},
|
| 17 |
{
|
| 18 |
"id": "003",
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
"id": "008",
|
| 25 |
"reproducible": true,
|
| 26 |
"specific": false,
|
| 27 |
-
"iterations":
|
| 28 |
},
|
| 29 |
{
|
| 30 |
"id": "009",
|
|
@@ -34,9 +34,9 @@
|
|
| 34 |
},
|
| 35 |
{
|
| 36 |
"id": "015",
|
| 37 |
-
"reproducible":
|
| 38 |
"specific": false,
|
| 39 |
-
"iterations":
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"id": "018",
|
|
@@ -52,9 +52,9 @@
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"id": "032",
|
| 55 |
-
"reproducible":
|
| 56 |
"specific": false,
|
| 57 |
-
"iterations":
|
| 58 |
},
|
| 59 |
{
|
| 60 |
"id": "033",
|
|
@@ -96,25 +96,25 @@
|
|
| 96 |
"id": "051",
|
| 97 |
"reproducible": true,
|
| 98 |
"specific": false,
|
| 99 |
-
"iterations":
|
| 100 |
},
|
| 101 |
{
|
| 102 |
"id": "054",
|
| 103 |
"reproducible": true,
|
| 104 |
"specific": false,
|
| 105 |
-
"iterations":
|
| 106 |
},
|
| 107 |
{
|
| 108 |
"id": "058",
|
| 109 |
-
"reproducible":
|
| 110 |
"specific": false,
|
| 111 |
-
"iterations":
|
| 112 |
},
|
| 113 |
{
|
| 114 |
"id": "066",
|
| 115 |
-
"reproducible":
|
| 116 |
"specific": false,
|
| 117 |
-
"iterations":
|
| 118 |
},
|
| 119 |
{
|
| 120 |
"id": "070",
|
|
@@ -124,21 +124,21 @@
|
|
| 124 |
},
|
| 125 |
{
|
| 126 |
"id": "077",
|
| 127 |
-
"reproducible":
|
| 128 |
"specific": false,
|
| 129 |
-
"iterations":
|
| 130 |
},
|
| 131 |
{
|
| 132 |
"id": "091",
|
| 133 |
"reproducible": true,
|
| 134 |
"specific": false,
|
| 135 |
-
"iterations":
|
| 136 |
},
|
| 137 |
{
|
| 138 |
"id": "098",
|
| 139 |
"reproducible": true,
|
| 140 |
"specific": false,
|
| 141 |
-
"iterations":
|
| 142 |
}
|
| 143 |
]
|
| 144 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"summary": {
|
| 3 |
+
"timestamp": "2026-06-08T14:13:38.811Z",
|
| 4 |
"total_processed": 22,
|
| 5 |
+
"reproducibility_rate": 54.54545454545454,
|
| 6 |
"specificity_rate": 0,
|
| 7 |
"overall_ground_truth_rate": 0,
|
| 8 |
+
"average_iterations": 7.545454545454546
|
| 9 |
},
|
| 10 |
"details": [
|
| 11 |
{
|
| 12 |
"id": "001",
|
| 13 |
+
"reproducible": true,
|
| 14 |
"specific": false,
|
| 15 |
+
"iterations": 9
|
| 16 |
},
|
| 17 |
{
|
| 18 |
"id": "003",
|
|
|
|
| 24 |
"id": "008",
|
| 25 |
"reproducible": true,
|
| 26 |
"specific": false,
|
| 27 |
+
"iterations": 2
|
| 28 |
},
|
| 29 |
{
|
| 30 |
"id": "009",
|
|
|
|
| 34 |
},
|
| 35 |
{
|
| 36 |
"id": "015",
|
| 37 |
+
"reproducible": true,
|
| 38 |
"specific": false,
|
| 39 |
+
"iterations": 5
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"id": "018",
|
|
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"id": "032",
|
| 55 |
+
"reproducible": true,
|
| 56 |
"specific": false,
|
| 57 |
+
"iterations": 4
|
| 58 |
},
|
| 59 |
{
|
| 60 |
"id": "033",
|
|
|
|
| 96 |
"id": "051",
|
| 97 |
"reproducible": true,
|
| 98 |
"specific": false,
|
| 99 |
+
"iterations": 2
|
| 100 |
},
|
| 101 |
{
|
| 102 |
"id": "054",
|
| 103 |
"reproducible": true,
|
| 104 |
"specific": false,
|
| 105 |
+
"iterations": 7
|
| 106 |
},
|
| 107 |
{
|
| 108 |
"id": "058",
|
| 109 |
+
"reproducible": true,
|
| 110 |
"specific": false,
|
| 111 |
+
"iterations": 5
|
| 112 |
},
|
| 113 |
{
|
| 114 |
"id": "066",
|
| 115 |
+
"reproducible": true,
|
| 116 |
"specific": false,
|
| 117 |
+
"iterations": 8
|
| 118 |
},
|
| 119 |
{
|
| 120 |
"id": "070",
|
|
|
|
| 124 |
},
|
| 125 |
{
|
| 126 |
"id": "077",
|
| 127 |
+
"reproducible": true,
|
| 128 |
"specific": false,
|
| 129 |
+
"iterations": 8
|
| 130 |
},
|
| 131 |
{
|
| 132 |
"id": "091",
|
| 133 |
"reproducible": true,
|
| 134 |
"specific": false,
|
| 135 |
+
"iterations": 9
|
| 136 |
},
|
| 137 |
{
|
| 138 |
"id": "098",
|
| 139 |
"reproducible": true,
|
| 140 |
"specific": false,
|
| 141 |
+
"iterations": 2
|
| 142 |
}
|
| 143 |
]
|
| 144 |
}
|
|
@@ -3,9 +3,21 @@ set -e
|
|
| 3 |
|
| 4 |
SANDBOX="${SANDBOX_DIR:-/tmp/poc-sandbox}"
|
| 5 |
|
| 6 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
if [ -z "$FORGE_BIN" ] || [ ! -f "$FORGE_BIN" ]; then
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
fi
|
| 10 |
|
| 11 |
echo "Inicializando sandbox Foundry em $SANDBOX usando $FORGE_BIN..."
|
|
|
|
| 3 |
|
| 4 |
SANDBOX="${SANDBOX_DIR:-/tmp/poc-sandbox}"
|
| 5 |
|
| 6 |
+
# Source Foundry environment (sets PATH in non-interactive shells like Docker)
|
| 7 |
+
if [ -f "$HOME/.foundry/env" ]; then
|
| 8 |
+
source "$HOME/.foundry/env"
|
| 9 |
+
fi
|
| 10 |
+
|
| 11 |
+
# Resolve forge binary: env var > PATH > common install locations
|
| 12 |
if [ -z "$FORGE_BIN" ] || [ ! -f "$FORGE_BIN" ]; then
|
| 13 |
+
if command -v forge &>/dev/null; then
|
| 14 |
+
FORGE_BIN=$(command -v forge)
|
| 15 |
+
elif [ -f "$HOME/.foundry/bin/forge" ]; then
|
| 16 |
+
FORGE_BIN="$HOME/.foundry/bin/forge"
|
| 17 |
+
else
|
| 18 |
+
echo "ERROR: forge not found. Install Foundry: curl -L https://foundry.paradigm.xyz | bash"
|
| 19 |
+
exit 1
|
| 20 |
+
fi
|
| 21 |
fi
|
| 22 |
|
| 23 |
echo "Inicializando sandbox Foundry em $SANDBOX usando $FORGE_BIN..."
|
|
@@ -9,13 +9,16 @@ import {
|
|
| 9 |
ANALYZE_VULNERABILITY_PROMPT,
|
| 10 |
POC_INITIAL_PROMPT,
|
| 11 |
POC_COMPILE_FIX_PROMPT,
|
| 12 |
-
POC_TEST_FIX_PROMPT
|
|
|
|
| 13 |
} from "./prompts/system.js";
|
| 14 |
import { extractSolidity } from "./utils/extractSolidity.js";
|
| 15 |
import { runFoundry } from "./tools/foundryRunner.js";
|
| 16 |
import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
|
| 17 |
-
import {
|
|
|
|
| 18 |
import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
|
|
|
|
| 19 |
|
| 20 |
const MAX_ITERATIONS = 10;
|
| 21 |
|
|
@@ -38,11 +41,40 @@ async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 38 |
referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
|
| 39 |
}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
const oracleContext: OracleContext = {
|
| 42 |
solidityScaffold,
|
| 43 |
constructorInfo: constructorInfo?.parameters,
|
| 44 |
targetContractAPI,
|
| 45 |
-
referenceTestHelpers
|
|
|
|
|
|
|
|
|
|
| 46 |
};
|
| 47 |
|
| 48 |
console.log("[oracleNode] scaffold gerado, context built.");
|
|
@@ -70,6 +102,16 @@ ${state.oracleContext!.targetContractAPI}
|
|
| 70 |
|
| 71 |
${state.oracleContext!.referenceTestHelpers ? `### Environment Helpers:
|
| 72 |
${state.oracleContext!.referenceTestHelpers}` : ""}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
`;
|
| 74 |
|
| 75 |
const response = await llm.invoke([
|
|
@@ -107,6 +149,16 @@ ${oracleContext!.targetContractAPI}
|
|
| 107 |
${oracleContext!.referenceTestHelpers ? `### Test Helpers:
|
| 108 |
${oracleContext!.referenceTestHelpers}` : ""}
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
### Scaffold:
|
| 111 |
\`\`\`solidity
|
| 112 |
${oracleContext!.solidityScaffold}
|
|
@@ -114,14 +166,25 @@ ${oracleContext!.solidityScaffold}
|
|
| 114 |
`;
|
| 115 |
} else {
|
| 116 |
// PASS 3+: FIXING ERRORS (BRANCHING)
|
| 117 |
-
const isCompilerError = lastError?.includes("[COMPILER_ERROR]");
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
userMessage = `The previous PoC failed.
|
| 121 |
|
| 122 |
Error Category: ${isCompilerError ? "Compilation Failure" : "Execution/Logic Failure"}
|
| 123 |
-
|
| 124 |
-
${
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
Previous Code:
|
| 127 |
\`\`\`solidity
|
|
@@ -131,10 +194,20 @@ ${pocCode}
|
|
| 131 |
Analysis of the bug:
|
| 132 |
${vulnerabilityAnalysis}
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
Fix the code. Return the entire file.`;
|
| 135 |
}
|
| 136 |
|
| 137 |
-
console.log(`[testerAgent] generatePoCNode iteração ${iterations + 1}, isRetry=${isRetry}, mode=${isRetry ? (lastError?.includes("
|
| 138 |
|
| 139 |
// DEBUG: Output context before sending to LLM
|
| 140 |
if (process.env.DEBUG_CONTEXT === "true") {
|
|
@@ -166,22 +239,31 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 166 |
const isMissingContract = !trimmedCode.includes("contract ExploitTest");
|
| 167 |
const isMissingTest = !trimmedCode.includes("function test_Exploit()");
|
| 168 |
const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
const isLazyTest = (
|
| 170 |
trimmedCode.includes("assertTrue(true") ||
|
| 171 |
trimmedCode.includes("assert(true") ||
|
| 172 |
trimmedCode.includes("assert(1 == 1")
|
| 173 |
-
) && !
|
| 174 |
|
| 175 |
if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder || isLazyTest) {
|
| 176 |
-
const summary =
|
| 177 |
-
? "
|
| 178 |
: isMissingContract
|
| 179 |
-
? "
|
| 180 |
: isMissingTest
|
| 181 |
-
? "
|
| 182 |
: isPlaceholder
|
| 183 |
-
? "Exploit
|
| 184 |
-
: "
|
| 185 |
);
|
| 186 |
const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
|
| 187 |
return {
|
|
@@ -194,9 +276,6 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 194 |
const result = await runFoundry(state.pocCode, state.report.customSandboxDir);
|
| 195 |
const analysis = analyzeFoundryLog(result);
|
| 196 |
const noTestsFound = result.combined.includes("No tests found");
|
| 197 |
-
const summary = noTestsFound
|
| 198 |
-
? "Forge não encontrou nenhum teste. Verifique se o contrato se chama ExploitTest e se existe test_Exploit()."
|
| 199 |
-
: analysis.summary;
|
| 200 |
const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
|
| 201 |
const isLastAttempt = state.iterations >= MAX_ITERATIONS;
|
| 202 |
|
|
@@ -213,10 +292,22 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 213 |
console.log(`[testerAgent] Falha detectada: ${analysis.summary}`);
|
| 214 |
}
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
return {
|
| 217 |
executionLogs: [result.combined], // reducer append
|
| 218 |
-
lastError:
|
| 219 |
status,
|
|
|
|
| 220 |
};
|
| 221 |
}
|
| 222 |
|
|
|
|
| 9 |
ANALYZE_VULNERABILITY_PROMPT,
|
| 10 |
POC_INITIAL_PROMPT,
|
| 11 |
POC_COMPILE_FIX_PROMPT,
|
| 12 |
+
POC_TEST_FIX_PROMPT,
|
| 13 |
+
POC_MINIMAL_INTERFACE_PROMPT
|
| 14 |
} from "./prompts/system.js";
|
| 15 |
import { extractSolidity } from "./utils/extractSolidity.js";
|
| 16 |
import { runFoundry } from "./tools/foundryRunner.js";
|
| 17 |
import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
|
| 18 |
+
import { extractProjectContext } from "./utils/projectContextExtractor.js";
|
| 19 |
+
import { createMissingDependencyStubs } from "./utils/dependencyStubber.js";
|
| 20 |
import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
|
| 21 |
+
import { extractConstructor } from "./utils/parserUtils.js";
|
| 22 |
|
| 23 |
const MAX_ITERATIONS = 10;
|
| 24 |
|
|
|
|
| 41 |
referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
|
| 42 |
}
|
| 43 |
|
| 44 |
+
// STEP 4: Extract project-level context (remappings, existing test imports)
|
| 45 |
+
let projectRemappings = "";
|
| 46 |
+
let projectTestImports = "";
|
| 47 |
+
let projectTestFilePath: string | null = null;
|
| 48 |
+
if (state.report.customSandboxDir) {
|
| 49 |
+
console.log("[oracleNode] extracting project context (remappings, test imports)...");
|
| 50 |
+
try {
|
| 51 |
+
const projectCtx = await extractProjectContext(state.report.customSandboxDir);
|
| 52 |
+
projectRemappings = projectCtx.remappings;
|
| 53 |
+
projectTestImports = projectCtx.existingTestImports;
|
| 54 |
+
projectTestFilePath = projectCtx.existingTestFilePath;
|
| 55 |
+
if (projectRemappings) console.log("[oracleNode] found remappings:", projectRemappings.split("\n").length, "entries");
|
| 56 |
+
if (projectTestImports) console.log("[oracleNode] found existing test imports from:", projectTestFilePath);
|
| 57 |
+
} catch (e) {
|
| 58 |
+
console.warn("[oracleNode] could not extract project context:", (e as Error).message);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// STEP 5: Pre-flight dependency stub creation
|
| 62 |
+
// Run a quick forge build probe to detect missing dependencies, then stub them
|
| 63 |
+
try {
|
| 64 |
+
await createMissingDependencyStubs(state.report.customSandboxDir);
|
| 65 |
+
} catch (e) {
|
| 66 |
+
console.warn("[oracleNode] stub creation failed (non-fatal):", (e as Error).message);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
const oracleContext: OracleContext = {
|
| 71 |
solidityScaffold,
|
| 72 |
constructorInfo: constructorInfo?.parameters,
|
| 73 |
targetContractAPI,
|
| 74 |
+
referenceTestHelpers,
|
| 75 |
+
projectRemappings,
|
| 76 |
+
projectTestImports,
|
| 77 |
+
projectTestFilePath,
|
| 78 |
};
|
| 79 |
|
| 80 |
console.log("[oracleNode] scaffold gerado, context built.");
|
|
|
|
| 102 |
|
| 103 |
${state.oracleContext!.referenceTestHelpers ? `### Environment Helpers:
|
| 104 |
${state.oracleContext!.referenceTestHelpers}` : ""}
|
| 105 |
+
|
| 106 |
+
${state.report.patchDiff ? `### PATCH DIFF (what the fix changes — use this to write a SPECIFIC assertion):
|
| 107 |
+
The following diff shows exactly what changed between the vulnerable and patched version.
|
| 108 |
+
Your generated PoC MUST produce an assertion that:
|
| 109 |
+
- PASSES on the vulnerable version (bug exists)
|
| 110 |
+
- FAILS on the patched version (bug is fixed)
|
| 111 |
+
|
| 112 |
+
\`\`\`diff
|
| 113 |
+
${state.report.patchDiff}
|
| 114 |
+
\`\`\`` : ""}
|
| 115 |
`;
|
| 116 |
|
| 117 |
const response = await llm.invoke([
|
|
|
|
| 149 |
${oracleContext!.referenceTestHelpers ? `### Test Helpers:
|
| 150 |
${oracleContext!.referenceTestHelpers}` : ""}
|
| 151 |
|
| 152 |
+
${oracleContext!.projectRemappings ? `### Project Remappings (use these for import paths):
|
| 153 |
+
\`\`\`
|
| 154 |
+
${oracleContext!.projectRemappings}
|
| 155 |
+
\`\`\`` : ""}
|
| 156 |
+
|
| 157 |
+
${oracleContext!.projectTestImports ? `### Import Pattern from Existing Test (${oracleContext!.projectTestFilePath}):
|
| 158 |
+
\`\`\`solidity
|
| 159 |
+
${oracleContext!.projectTestImports}
|
| 160 |
+
\`\`\`` : ""}
|
| 161 |
+
|
| 162 |
### Scaffold:
|
| 163 |
\`\`\`solidity
|
| 164 |
${oracleContext!.solidityScaffold}
|
|
|
|
| 166 |
`;
|
| 167 |
} else {
|
| 168 |
// PASS 3+: FIXING ERRORS (BRANCHING)
|
| 169 |
+
const isCompilerError = lastError?.includes("[COMPILER_ERROR]") || lastError?.includes("[INVALID_CODE]");
|
| 170 |
+
const useMinimalStrategy = state.compileFailures >= 3;
|
| 171 |
+
|
| 172 |
+
if (useMinimalStrategy && isCompilerError) {
|
| 173 |
+
// ESCAPE HATCH: After 3 compile failures, switch to zero-import minimal interface strategy
|
| 174 |
+
currentSystemPrompt = POC_MINIMAL_INTERFACE_PROMPT;
|
| 175 |
+
console.log("[testerAgent] Switching to MINIMAL_INTERFACE strategy after", state.compileFailures, "compile failures");
|
| 176 |
+
} else {
|
| 177 |
+
currentSystemPrompt = isCompilerError ? POC_COMPILE_FIX_PROMPT : POC_TEST_FIX_PROMPT;
|
| 178 |
+
}
|
| 179 |
|
| 180 |
userMessage = `The previous PoC failed.
|
| 181 |
|
| 182 |
Error Category: ${isCompilerError ? "Compilation Failure" : "Execution/Logic Failure"}
|
| 183 |
+
Error Details:
|
| 184 |
+
${lastError ?? ""}
|
| 185 |
+
|
| 186 |
+
Forge Output (last attempt):
|
| 187 |
+
${executionLogs[executionLogs.length - 1]?.slice(0, 3500) ?? "sem logs"}
|
| 188 |
|
| 189 |
Previous Code:
|
| 190 |
\`\`\`solidity
|
|
|
|
| 194 |
Analysis of the bug:
|
| 195 |
${vulnerabilityAnalysis}
|
| 196 |
|
| 197 |
+
${!useMinimalStrategy && oracleContext!.projectRemappings ? `Project Remappings (use these for import paths):
|
| 198 |
+
\`\`\`
|
| 199 |
+
${oracleContext!.projectRemappings}
|
| 200 |
+
\`\`\`` : ""}
|
| 201 |
+
|
| 202 |
+
${!useMinimalStrategy && oracleContext!.projectTestImports ? `Import Pattern from Existing Test (${oracleContext!.projectTestFilePath}):
|
| 203 |
+
\`\`\`solidity
|
| 204 |
+
${oracleContext!.projectTestImports}
|
| 205 |
+
\`\`\`` : ""}
|
| 206 |
+
|
| 207 |
Fix the code. Return the entire file.`;
|
| 208 |
}
|
| 209 |
|
| 210 |
+
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"}`);
|
| 211 |
|
| 212 |
// DEBUG: Output context before sending to LLM
|
| 213 |
if (process.env.DEBUG_CONTEXT === "true") {
|
|
|
|
| 239 |
const isMissingContract = !trimmedCode.includes("contract ExploitTest");
|
| 240 |
const isMissingTest = !trimmedCode.includes("function test_Exploit()");
|
| 241 |
const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
|
| 242 |
+
const hasStrongAssertion = (
|
| 243 |
+
trimmedCode.includes("assertEq") ||
|
| 244 |
+
trimmedCode.includes("assertGt") ||
|
| 245 |
+
trimmedCode.includes("assertLt") ||
|
| 246 |
+
trimmedCode.includes("assertLe") ||
|
| 247 |
+
trimmedCode.includes("assertGe") ||
|
| 248 |
+
trimmedCode.includes("assertNotEq") ||
|
| 249 |
+
trimmedCode.includes("assertApproxEq")
|
| 250 |
+
);
|
| 251 |
const isLazyTest = (
|
| 252 |
trimmedCode.includes("assertTrue(true") ||
|
| 253 |
trimmedCode.includes("assert(true") ||
|
| 254 |
trimmedCode.includes("assert(1 == 1")
|
| 255 |
+
) && !hasStrongAssertion;
|
| 256 |
|
| 257 |
if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder || isLazyTest) {
|
| 258 |
+
const summary = (isMissingCode
|
| 259 |
+
? "[INVALID_CODE] No Solidity code returned. The LLM must output a complete solidity code block."
|
| 260 |
: isMissingContract
|
| 261 |
+
? "[INVALID_CODE] No 'contract ExploitTest' found. The test contract MUST be named ExploitTest."
|
| 262 |
: isMissingTest
|
| 263 |
+
? "[INVALID_CODE] No 'function test_Exploit()' found. The test function MUST be named test_Exploit()."
|
| 264 |
: isPlaceholder
|
| 265 |
+
? "[INVALID_CODE] Exploit has TODO placeholder. You must implement the actual exploit logic."
|
| 266 |
+
: "[WEAK_ASSERTION] Only assertTrue(true) found — this never proves the vulnerability. Add a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
|
| 267 |
);
|
| 268 |
const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
|
| 269 |
return {
|
|
|
|
| 276 |
const result = await runFoundry(state.pocCode, state.report.customSandboxDir);
|
| 277 |
const analysis = analyzeFoundryLog(result);
|
| 278 |
const noTestsFound = result.combined.includes("No tests found");
|
|
|
|
|
|
|
|
|
|
| 279 |
const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
|
| 280 |
const isLastAttempt = state.iterations >= MAX_ITERATIONS;
|
| 281 |
|
|
|
|
| 292 |
console.log(`[testerAgent] Falha detectada: ${analysis.summary}`);
|
| 293 |
}
|
| 294 |
|
| 295 |
+
// Build lastError: include relevant error lines prominently so the LLM sees them at the top of the fix prompt
|
| 296 |
+
let lastErrorMsg: string | null = null;
|
| 297 |
+
if (!passed) {
|
| 298 |
+
const relevantLinesText = analysis.relevantLines.length > 0
|
| 299 |
+
? `\nKey error lines:\n${analysis.relevantLines.slice(0, 15).join("\n")}`
|
| 300 |
+
: "";
|
| 301 |
+
lastErrorMsg = `${analysis.summary}${relevantLinesText}`;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
const isCompileError = analysis.category === "compiler_error";
|
| 305 |
+
|
| 306 |
return {
|
| 307 |
executionLogs: [result.combined], // reducer append
|
| 308 |
+
lastError: lastErrorMsg,
|
| 309 |
status,
|
| 310 |
+
compileFailures: isCompileError && !passed ? 1 : 0, // additive reducer counts each compile failure
|
| 311 |
};
|
| 312 |
}
|
| 313 |
|
|
@@ -8,47 +8,174 @@ export const SYSTEM_PROMPT = `You are an expert smart contract security testing
|
|
| 8 |
- DO NOT rename \`test_Exploit()\`.
|
| 9 |
`.trim();
|
| 10 |
|
| 11 |
-
export const ANALYZE_VULNERABILITY_PROMPT = `
|
| 12 |
|
| 13 |
-
Focus on understanding the root cause and mechanism
|
| 14 |
|
| 15 |
-
Please provide:
|
| 16 |
-
1. **
|
| 17 |
-
2. **
|
| 18 |
-
3. **
|
| 19 |
-
4. **
|
| 20 |
|
| 21 |
-
Be
|
| 22 |
`.trim();
|
| 23 |
|
| 24 |
-
export const POC_INITIAL_PROMPT = `Based on
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
##
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
`.trim();
|
| 33 |
|
| 34 |
-
export const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
`.trim();
|
| 44 |
|
| 45 |
-
export const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block.
|
| 54 |
`.trim();
|
|
|
|
| 8 |
- DO NOT rename \`test_Exploit()\`.
|
| 9 |
`.trim();
|
| 10 |
|
| 11 |
+
export const ANALYZE_VULNERABILITY_PROMPT = `You are an expert smart contract security analyst. Analyze the vulnerability in the following Solidity contract.
|
| 12 |
|
| 13 |
+
Focus on understanding the root cause and the precise mechanism needed to trigger it in a Foundry test.
|
| 14 |
|
| 15 |
+
Please provide a concise technical plan covering:
|
| 16 |
+
1. **Root cause**: What exactly is the bug at the code level?
|
| 17 |
+
2. **Trigger conditions**: What state must the contract be in? What parameters or roles are needed?
|
| 18 |
+
3. **Exploit steps**: Numbered, concrete step-by-step actions to trigger the vulnerability.
|
| 19 |
+
4. **Assertion**: What specific \`assertEq\` / \`assertGt\` / \`assertLt\` statement will prove the vulnerability exists AND would fail if the vulnerability were patched? (e.g., "assertGt(attacker.balance, initialBalance)" or "assertEq(owner, attacker)")
|
| 20 |
|
| 21 |
+
Be precise and actionable. Your plan will be directly used to write Foundry test code.
|
| 22 |
`.trim();
|
| 23 |
|
| 24 |
+
export const POC_INITIAL_PROMPT = `You are an expert smart contract security tester. Based on the vulnerability analysis plan provided, generate a complete Foundry Proof of Concept (PoC) test file.
|
| 25 |
|
| 26 |
+
## CRITICAL IMPORT RULES
|
| 27 |
+
- You MUST use the project's own import paths (see "Project Remappings" and "Import Pattern from Existing Test" if provided).
|
| 28 |
+
- If remappings are provided (e.g., \`@openzeppelin/=lib/openzeppelin-contracts/\`), use them exactly as listed.
|
| 29 |
+
- If an existing test shows \`import {Foo} from "project/Foo.sol"\`, follow that exact pattern.
|
| 30 |
+
- If you are NOT sure about the import path for an external dependency, AVOID importing it. Use a minimal interface or mock instead.
|
| 31 |
+
- The contract under test is already in the sandbox — use a relative import like \`import { ContractName } from "../src/ContractName.sol"\` unless remappings say otherwise.
|
| 32 |
|
| 33 |
+
## QUALITY RULES
|
| 34 |
+
- The final assertion MUST prove the vulnerability. It should use assertEq, assertGt, assertLt, assertGe, assertLe, or assertNotEq with meaningful values.
|
| 35 |
+
- NEVER write \`assertTrue(true)\` or \`assert(true)\`. This is an automatic failure.
|
| 36 |
+
- The assertion must be specific enough that it would FAIL on a patched version of the contract.
|
| 37 |
+
- Output ONLY a single \`\`\`solidity ... \`\`\` block with the complete file.
|
| 38 |
+
|
| 39 |
+
## FALLBACK STRATEGY
|
| 40 |
+
If the contract has complex dependencies that are hard to mock, use this minimal approach:
|
| 41 |
+
1. Declare a minimal interface for the contract with only the functions you need.
|
| 42 |
+
2. Deploy the real contract by importing it directly (relative path).
|
| 43 |
+
3. Call the vulnerable function directly without complex setup.
|
| 44 |
+
`.trim();
|
| 45 |
+
|
| 46 |
+
export const POC_COMPILE_FIX_PROMPT = `The previous PoC test FAILED TO COMPILE. You must fix ALL compilation errors and return the complete corrected file.
|
| 47 |
+
|
| 48 |
+
## IMPORT ERROR STRATEGY (most common fix)
|
| 49 |
+
If the error is about a missing source file or identifier not found:
|
| 50 |
+
1. Check the "Project Remappings" provided — use those exact paths.
|
| 51 |
+
2. Check the "Import Pattern from Existing Test" — copy those import statements exactly.
|
| 52 |
+
3. If you cannot find the right import path, REMOVE that import and replace it with a minimal interface:
|
| 53 |
+
\`\`\`solidity
|
| 54 |
+
interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); }
|
| 55 |
+
\`\`\`
|
| 56 |
+
4. NEVER guess an import path. Only use paths you can verify from the remappings or existing test.
|
| 57 |
+
|
| 58 |
+
## OTHER COMPILATION FIXES
|
| 59 |
+
- Missing type members: declare a minimal struct/interface instead of importing the full library.
|
| 60 |
+
- Visibility errors: check that you're calling public/external functions only.
|
| 61 |
+
- Type mismatches: cast explicitly (e.g., \`uint256(value)\`, \`address(contract)\`).
|
| 62 |
+
- ABI encoding errors: use \`abi.encodeWithSelector(Contract.func.selector, args)\`.
|
| 63 |
+
|
| 64 |
+
## STRICT RULE
|
| 65 |
+
Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Fix ALL errors in one pass.
|
| 66 |
`.trim();
|
| 67 |
|
| 68 |
+
export const POC_TEST_FIX_PROMPT = `The PoC compiled successfully but FAILED DURING EXECUTION (revert or assertion failure). Fix the test logic.
|
| 69 |
+
|
| 70 |
+
## REVERT DIAGNOSIS
|
| 71 |
+
If the test reverted without a message:
|
| 72 |
+
1. The call order may be wrong — check what preconditions the contract requires.
|
| 73 |
+
2. A role/permission may be missing — use \`vm.prank(owner)\` to set up roles first.
|
| 74 |
+
3. The contract may need funding — use \`vm.deal(address(contract), amount)\`.
|
| 75 |
+
4. A previous transaction may have changed state — check ordering carefully.
|
| 76 |
+
5. **"call to non-contract address"**: The contract was not deployed yet — you must deploy it in setUp() first.
|
| 77 |
+
|
| 78 |
+
## ASSERTION FAILURE DIAGNOSIS
|
| 79 |
+
If the assertion failed (values didn't match expected):
|
| 80 |
+
1. The exploit logic is incorrect — re-read the vulnerability description carefully.
|
| 81 |
+
2. The vulnerable code path may not be reached — trace with intermediate assertions.
|
| 82 |
+
3. The assertion values may be wrong — recalculate what the expected outcome should be.
|
| 83 |
+
|
| 84 |
+
## REENTRANCY PATTERN
|
| 85 |
+
If testing a reentrancy vulnerability, add a callback to ExploitTest:
|
| 86 |
+
\`\`\`solidity
|
| 87 |
+
uint256 public reentrancyCount;
|
| 88 |
+
uint256 public stolenAmount;
|
| 89 |
+
|
| 90 |
+
receive() external payable {
|
| 91 |
+
if (reentrancyCount < 3 && address(target).balance > 0) {
|
| 92 |
+
reentrancyCount++;
|
| 93 |
+
target.withdraw(/* same amount */);
|
| 94 |
+
}
|
| 95 |
+
stolenAmount += msg.value;
|
| 96 |
+
}
|
| 97 |
+
\`\`\`
|
| 98 |
+
Then assert: \`assertGt(stolenAmount, initialDeposit, "Reentrancy drained more than deposited")\`
|
| 99 |
|
| 100 |
+
## UNCHECKED RETURN VALUE PATTERN
|
| 101 |
+
If testing unchecked external call return values:
|
| 102 |
+
\`\`\`solidity
|
| 103 |
+
// The contract ignores the return value of an external call
|
| 104 |
+
// You can demonstrate by causing the call to fail while the contract still proceeds
|
| 105 |
+
bool callSucceeded = target.doExternalCall(params);
|
| 106 |
+
// If the vulnerability is that a false return is ignored:
|
| 107 |
+
assertFalse(callSucceeded, "External call returned false but was ignored");
|
| 108 |
+
// Or show the state changed incorrectly:
|
| 109 |
+
assertEq(target.state(), wrongValue, "State updated despite failed external call");
|
| 110 |
+
\`\`\`
|
| 111 |
|
| 112 |
+
## MINIMAL VIABLE EXPLOIT RULE
|
| 113 |
+
If after 2+ failed attempts you cannot get the full exploit to work:
|
| 114 |
+
- Simplify to the most minimal version that demonstrates the bug.
|
| 115 |
+
- A partial demonstration (e.g., wrong state, unauthorized access) is better than nothing.
|
| 116 |
+
- Focus on the ASSERTION — it must prove the vulnerability exists.
|
| 117 |
+
|
| 118 |
+
## STRICT RULE
|
| 119 |
+
Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Fix ALL errors in one pass.
|
| 120 |
`.trim();
|
| 121 |
|
| 122 |
+
export const POC_MINIMAL_INTERFACE_PROMPT = `The PoC has failed to compile multiple times due to import errors. You MUST now use the MINIMAL INTERFACE STRATEGY.
|
| 123 |
+
|
| 124 |
+
## MANDATORY RULES — READ CAREFULLY
|
| 125 |
+
1. **REMOVE ALL EXTERNAL IMPORTS** — Do NOT import any library or contract except \`forge-std/Test.sol\`.
|
| 126 |
+
2. **DECLARE EVERYTHING INLINE** — Declare minimal interfaces for every external type you need:
|
| 127 |
+
|
| 128 |
+
\`\`\`solidity
|
| 129 |
+
// Example minimal interfaces — adapt to your contract
|
| 130 |
+
interface ITargetContract {
|
| 131 |
+
function vulnerableFunction(uint256 amount) external returns (bool);
|
| 132 |
+
function balanceOf(address account) external view returns (uint256);
|
| 133 |
+
function owner() external view returns (address);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
interface IERC20 {
|
| 137 |
+
function transfer(address to, uint256 amount) external returns (bool);
|
| 138 |
+
function approve(address spender, uint256 amount) external returns (bool);
|
| 139 |
+
function balanceOf(address account) external view returns (uint256);
|
| 140 |
+
}
|
| 141 |
+
\`\`\`
|
| 142 |
+
|
| 143 |
+
3. **USE address() CASTS** — If you need a contract type, cast from address: \`ITargetContract(contractAddress)\`
|
| 144 |
+
4. **DEPLOY WITH low-level calls if needed** — If you cannot import the contract, use \`address(new bytes(code))\` or \`ITargetContract(deployedAddress)\`
|
| 145 |
+
5. **THE CONTRACT UNDER TEST IS AT A RELATIVE PATH** — If you must import it, use ONLY: \`import "../src/ContractName.sol"\` (the only safe import besides forge-std)
|
| 146 |
+
|
| 147 |
+
## TEMPLATE
|
| 148 |
+
\`\`\`solidity
|
| 149 |
+
// SPDX-License-Identifier: UNLICENSED
|
| 150 |
+
pragma solidity ^0.8.0;
|
| 151 |
+
|
| 152 |
+
import "forge-std/Test.sol";
|
| 153 |
+
|
| 154 |
+
// Declare ONLY the functions you actually call:
|
| 155 |
+
interface ITarget {
|
| 156 |
+
function theVulnerableFunction(uint256 x) external;
|
| 157 |
+
function someGetter() external view returns (uint256);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
contract ExploitTest is Test {
|
| 161 |
+
ITarget target;
|
| 162 |
+
address constant ATTACKER = address(0xBEEF);
|
| 163 |
+
|
| 164 |
+
function setUp() public {
|
| 165 |
+
// Import only if absolutely needed — otherwise use the interface
|
| 166 |
+
// target = ITarget(address(new RealContract(constructorArgs)));
|
| 167 |
+
vm.deal(ATTACKER, 100 ether);
|
| 168 |
+
}
|
| 169 |
|
| 170 |
+
function test_Exploit() public {
|
| 171 |
+
uint256 before = target.someGetter();
|
| 172 |
+
vm.prank(ATTACKER);
|
| 173 |
+
target.theVulnerableFunction(/* exploit args */);
|
| 174 |
+
uint256 after_ = target.someGetter();
|
| 175 |
+
assertGt(after_, before, "Vulnerability confirmed: value changed unexpectedly");
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
\`\`\`
|
| 179 |
|
| 180 |
+
Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Use ONLY forge-std imports and inline interfaces.
|
| 181 |
`.trim();
|
|
@@ -34,6 +34,11 @@ export const PoCStateAnnotation = Annotation.Root({
|
|
| 34 |
reducer: (x, y) => x + y, // additive — incremented by +1 per call
|
| 35 |
}),
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
status: Annotation<"running" | "success" | "failed" | "timeout">({
|
| 38 |
default: () => "running",
|
| 39 |
reducer: (_, y) => y, // overwrite
|
|
|
|
| 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
|
| 40 |
+
}),
|
| 41 |
+
|
| 42 |
status: Annotation<"running" | "success" | "failed" | "timeout">({
|
| 43 |
default: () => "running",
|
| 44 |
reducer: (_, y) => y, // overwrite
|
|
@@ -31,6 +31,7 @@ export interface VulnerabilityReport {
|
|
| 31 |
exploitablePaths?: string[];
|
| 32 |
customSandboxDir?: string; // Caminho para execução do Forge (opcional)
|
| 33 |
referenceTestCode?: string; // Código de um teste existente para referência de setup
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
export interface OracleContext {
|
|
@@ -38,6 +39,9 @@ export interface OracleContext {
|
|
| 38 |
constructorInfo?: string; // Assinatura do constructor para ajudar no deploy
|
| 39 |
targetContractAPI?: string; // Resumo dos métodos e variáveis do contrato alvo
|
| 40 |
referenceTestHelpers?: string; // Resumo das funções auxiliares disponíveis no ambiente de teste
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
export interface PoCResult {
|
|
|
|
| 31 |
exploitablePaths?: string[];
|
| 32 |
customSandboxDir?: string; // Caminho para execução do Forge (opcional)
|
| 33 |
referenceTestCode?: string; // Código de um teste existente para referência de setup
|
| 34 |
+
patchDiff?: string; // Unified diff of the patch (vulnerable vs patched) for specificity guidance
|
| 35 |
}
|
| 36 |
|
| 37 |
export interface OracleContext {
|
|
|
|
| 39 |
constructorInfo?: string; // Assinatura do constructor para ajudar no deploy
|
| 40 |
targetContractAPI?: string; // Resumo dos métodos e variáveis do contrato alvo
|
| 41 |
referenceTestHelpers?: string; // Resumo das funções auxiliares disponíveis no ambiente de teste
|
| 42 |
+
projectRemappings?: string; // Content of remappings.txt for correct import paths
|
| 43 |
+
projectTestImports?: string; // Import lines from an existing test file in the project
|
| 44 |
+
projectTestFilePath?: string | null; // Path to the reference test file used
|
| 45 |
}
|
| 46 |
|
| 47 |
export interface PoCResult {
|
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from "fs/promises";
|
| 2 |
+
import path from "path";
|
| 3 |
+
import { exec } from "child_process";
|
| 4 |
+
import { promisify } from "util";
|
| 5 |
+
|
| 6 |
+
const execAsync = promisify(exec);
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Runs a minimal forge build probe to detect missing source files,
|
| 10 |
+
* then creates minimal stub contracts at those exact paths.
|
| 11 |
+
* This unblocks projects that use deep submodule dependencies (e.g. lib/caviar/lib/oracle/...)
|
| 12 |
+
* or node_modules imports that are not present in the sandbox.
|
| 13 |
+
*/
|
| 14 |
+
export async function createMissingDependencyStubs(sandboxDir: string): Promise<void> {
|
| 15 |
+
// Run forge build on just the project (no test files), capture errors
|
| 16 |
+
let combined = "";
|
| 17 |
+
try {
|
| 18 |
+
const { stdout, stderr } = await execAsync(
|
| 19 |
+
`cd "${sandboxDir}" && forge build --no-cache 2>&1 || true`,
|
| 20 |
+
{ timeout: 60_000 }
|
| 21 |
+
);
|
| 22 |
+
combined = stdout + stderr;
|
| 23 |
+
} catch (e: any) {
|
| 24 |
+
combined = e.message || "";
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Extract all "Source X not found" paths
|
| 28 |
+
const missingPaths: string[] = [];
|
| 29 |
+
const sourceNotFoundRegex = /Source "([^"]+)" not found/g;
|
| 30 |
+
let match: RegExpExecArray | null;
|
| 31 |
+
while ((match = sourceNotFoundRegex.exec(combined)) !== null) {
|
| 32 |
+
const missing = match[1];
|
| 33 |
+
if (!missingPaths.includes(missing)) {
|
| 34 |
+
missingPaths.push(missing);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if (missingPaths.length === 0) return;
|
| 39 |
+
|
| 40 |
+
console.log(`[oracleNode] Detected ${missingPaths.length} missing dependencies, creating stubs...`);
|
| 41 |
+
|
| 42 |
+
// Detect Solidity version used in the project (for the stub pragma)
|
| 43 |
+
let pragmaVersion = "^0.8.0";
|
| 44 |
+
try {
|
| 45 |
+
const toml = await fs.readFile(path.join(sandboxDir, "foundry.toml"), "utf-8");
|
| 46 |
+
const vMatch = toml.match(/solc[_-]?version\s*=\s*"([^"]+)"/);
|
| 47 |
+
if (vMatch) pragmaVersion = vMatch[1];
|
| 48 |
+
} catch { /* use default */ }
|
| 49 |
+
|
| 50 |
+
for (const missing of missingPaths) {
|
| 51 |
+
// Build the stub path inside the sandbox
|
| 52 |
+
const stubPath = path.join(sandboxDir, missing);
|
| 53 |
+
|
| 54 |
+
// Skip if file already exists
|
| 55 |
+
try {
|
| 56 |
+
await fs.access(stubPath);
|
| 57 |
+
continue; // already exists
|
| 58 |
+
} catch { /* doesn't exist, create it */ }
|
| 59 |
+
|
| 60 |
+
// Skip forge-std — it should always be available
|
| 61 |
+
if (missing.startsWith("forge-std/") || missing.startsWith("lib/forge-std/")) continue;
|
| 62 |
+
|
| 63 |
+
try {
|
| 64 |
+
await fs.mkdir(path.dirname(stubPath), { recursive: true });
|
| 65 |
+
|
| 66 |
+
// Generate a minimal stub that satisfies the import
|
| 67 |
+
const contractName = path.basename(missing, ".sol");
|
| 68 |
+
const stubContent = `// SPDX-License-Identifier: MIT
|
| 69 |
+
// AUTO-GENERATED STUB — replaces missing dependency: ${missing}
|
| 70 |
+
pragma solidity ${pragmaVersion};
|
| 71 |
+
|
| 72 |
+
// Minimal stub to satisfy missing import
|
| 73 |
+
contract ${contractName} {}
|
| 74 |
+
interface I${contractName} {}
|
| 75 |
+
`;
|
| 76 |
+
await fs.writeFile(stubPath, stubContent);
|
| 77 |
+
console.log(` [stub] Created: ${missing}`);
|
| 78 |
+
} catch (e) {
|
| 79 |
+
console.warn(` [stub] Failed to create ${missing}:`, (e as Error).message);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
|
@@ -14,44 +14,111 @@ export interface LogAnalysis {
|
|
| 14 |
relevantLines: string[]; // máx 10 linhas do log original
|
| 15 |
}
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
export function analyzeFoundryLog(result: FoundryResult): LogAnalysis {
|
| 18 |
if (result.timedOut) return {
|
| 19 |
category: "timeout",
|
| 20 |
-
summary: "Forge
|
| 21 |
relevantLines: [],
|
| 22 |
};
|
| 23 |
|
| 24 |
if (result.combined.includes("Compiler run failed")) {
|
| 25 |
-
const
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
return {
|
| 29 |
category: "compiler_error",
|
| 30 |
-
summary:
|
| 31 |
-
relevantLines:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
};
|
| 33 |
}
|
| 34 |
|
| 35 |
if (result.combined.includes("FAIL")) {
|
| 36 |
const revertReason = result.combined.match(/revert: (.+)/)?.[1];
|
|
|
|
| 37 |
const assertionFail = result.combined.includes("Assertion Failed") || result.combined.includes("assertion failed");
|
|
|
|
| 38 |
|
| 39 |
-
if (assertionFail)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
};
|
| 45 |
|
| 46 |
if (revertReason) return {
|
| 47 |
category: "revert_with_message",
|
| 48 |
-
summary: `
|
| 49 |
relevantLines: [revertReason],
|
| 50 |
};
|
| 51 |
|
| 52 |
return {
|
| 53 |
category: "revert_no_message",
|
| 54 |
-
summary: "
|
| 55 |
relevantLines: result.combined.split("\n")
|
| 56 |
.filter(l => l.includes("revert") || l.includes("FAIL")).slice(0, 5),
|
| 57 |
};
|
|
@@ -59,7 +126,7 @@ export function analyzeFoundryLog(result: FoundryResult): LogAnalysis {
|
|
| 59 |
|
| 60 |
return {
|
| 61 |
category: "unknown",
|
| 62 |
-
summary: "
|
| 63 |
relevantLines: result.combined.split("\n").slice(0, 10),
|
| 64 |
};
|
| 65 |
}
|
|
|
|
| 14 |
relevantLines: string[]; // máx 10 linhas do log original
|
| 15 |
}
|
| 16 |
|
| 17 |
+
/**
|
| 18 |
+
* Extracts the most actionable compiler error lines from forge output.
|
| 19 |
+
* Focuses on the actual error messages and file locations.
|
| 20 |
+
*/
|
| 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 {
|
| 46 |
if (result.timedOut) return {
|
| 47 |
category: "timeout",
|
| 48 |
+
summary: "Forge exceeded 60s. The exploit may have an infinite loop or blocking logic.",
|
| 49 |
relevantLines: [],
|
| 50 |
};
|
| 51 |
|
| 52 |
if (result.combined.includes("Compiler run failed")) {
|
| 53 |
+
const errorLines = extractCompilerErrors(result.combined);
|
| 54 |
+
|
| 55 |
+
// Detect specific compiler error patterns for targeted guidance
|
| 56 |
+
let specificGuidance = "";
|
| 57 |
+
const fullOutput = result.combined;
|
| 58 |
+
|
| 59 |
+
if (fullOutput.includes("File not found") || fullOutput.includes("Source") && fullOutput.includes("not found")) {
|
| 60 |
+
specificGuidance = " Import path is WRONG — check remappings and use the pattern from existing tests.";
|
| 61 |
+
} else if (fullOutput.includes("Identifier not found") || fullOutput.includes("not visible")) {
|
| 62 |
+
specificGuidance = " Identifier/member not found — check function name, visibility, or declare a minimal interface.";
|
| 63 |
+
} else if (fullOutput.includes("type conversion") || fullOutput.includes("Type") && fullOutput.includes("not implicitly convertible")) {
|
| 64 |
+
specificGuidance = " Type mismatch — add explicit cast.";
|
| 65 |
+
} else if (fullOutput.includes("Function") && fullOutput.includes("not found")) {
|
| 66 |
+
specificGuidance = " Function signature is wrong — check the API reference and use the exact signature.";
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
return {
|
| 70 |
category: "compiler_error",
|
| 71 |
+
summary: `[COMPILER_ERROR] Solidity compilation failed.${specificGuidance} Check: wrong import paths, missing members, type mismatches. Use the project remappings and existing test import patterns.`,
|
| 72 |
+
relevantLines: errorLines,
|
| 73 |
+
};
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
if (result.combined.includes("No tests found")) {
|
| 77 |
+
return {
|
| 78 |
+
category: "unknown",
|
| 79 |
+
summary: "[COMPILER_ERROR] No tests found in ExploitTest. Ensure the contract is named exactly 'ExploitTest' and the test function is 'test_Exploit()'.",
|
| 80 |
+
relevantLines: ["No tests found in ExploitTest"],
|
| 81 |
};
|
| 82 |
}
|
| 83 |
|
| 84 |
if (result.combined.includes("FAIL")) {
|
| 85 |
const revertReason = result.combined.match(/revert: (.+)/)?.[1];
|
| 86 |
+
const customError = result.combined.match(/custom error '([^']+)'/)?.[1];
|
| 87 |
const assertionFail = result.combined.includes("Assertion Failed") || result.combined.includes("assertion failed");
|
| 88 |
+
const transferFail = result.combined.includes("TRANSFER_FROM_FAILED") || result.combined.includes("TRANSFER_FAILED");
|
| 89 |
|
| 90 |
+
if (assertionFail) {
|
| 91 |
+
const assertLines = result.combined.split("\n")
|
| 92 |
+
.filter(l => l.includes("assertion") || l.includes("FAIL") || l.includes("Left") || l.includes("Right"))
|
| 93 |
+
.slice(0, 10);
|
| 94 |
+
return {
|
| 95 |
+
category: "assertion_failed",
|
| 96 |
+
summary: "[ASSERTION_FAILED] The exploit ran but the final assertion failed — the attacker did not achieve the expected outcome. Re-check the exploit logic and expected values.",
|
| 97 |
+
relevantLines: assertLines,
|
| 98 |
+
};
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (transferFail) return {
|
| 102 |
+
category: "revert_with_message",
|
| 103 |
+
summary: `[REVERT] Token transfer failed (TRANSFER_FROM_FAILED). The contract does not have enough tokens, or approval is missing. Setup token balances and approvals before the exploit.`,
|
| 104 |
+
relevantLines: [result.combined.split("\n").find(l => l.includes("TRANSFER")) ?? "TRANSFER_FROM_FAILED"],
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
if (customError) return {
|
| 108 |
+
category: "revert_with_message",
|
| 109 |
+
summary: `[REVERT] Contract reverted with custom error: "${customError}". Check what conditions trigger this error in the contract source.`,
|
| 110 |
+
relevantLines: [customError],
|
| 111 |
};
|
| 112 |
|
| 113 |
if (revertReason) return {
|
| 114 |
category: "revert_with_message",
|
| 115 |
+
summary: `[REVERT] Transaction reverted with: "${revertReason}". The contract rejected the operation — check permissions, roles, and call order.`,
|
| 116 |
relevantLines: [revertReason],
|
| 117 |
};
|
| 118 |
|
| 119 |
return {
|
| 120 |
category: "revert_no_message",
|
| 121 |
+
summary: "[REVERT_NO_MESSAGE] Transaction reverted without a message. Common causes: wrong call order, missing role/permission setup, incorrect contract state, or wrong function arguments.",
|
| 122 |
relevantLines: result.combined.split("\n")
|
| 123 |
.filter(l => l.includes("revert") || l.includes("FAIL")).slice(0, 5),
|
| 124 |
};
|
|
|
|
| 126 |
|
| 127 |
return {
|
| 128 |
category: "unknown",
|
| 129 |
+
summary: "[UNKNOWN_ERROR] Unexpected forge output. Review the full output below.",
|
| 130 |
relevantLines: result.combined.split("\n").slice(0, 10),
|
| 131 |
};
|
| 132 |
}
|
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from "fs/promises";
|
| 2 |
+
import path from "path";
|
| 3 |
+
|
| 4 |
+
export interface ProjectContext {
|
| 5 |
+
remappings: string; // Content of remappings.txt or foundry.toml [profile.default.remappings]
|
| 6 |
+
existingTestImports: string; // First few import lines from an existing test file
|
| 7 |
+
foundryTomlProfile: string; // Relevant foundry.toml settings (src, libs)
|
| 8 |
+
existingTestFilePath: string | null; // Relative path to an existing test file for reference
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Extracts project-level context needed for correct import paths in PoC tests.
|
| 13 |
+
* Reads remappings.txt, foundry.toml, and the first existing test file in the project.
|
| 14 |
+
*/
|
| 15 |
+
export async function extractProjectContext(sandboxDir: string): Promise<ProjectContext> {
|
| 16 |
+
let remappings = "";
|
| 17 |
+
let foundryTomlProfile = "";
|
| 18 |
+
let existingTestImports = "";
|
| 19 |
+
let existingTestFilePath: string | null = null;
|
| 20 |
+
|
| 21 |
+
// 1. Read remappings.txt
|
| 22 |
+
try {
|
| 23 |
+
const remappingsPath = path.join(sandboxDir, "remappings.txt");
|
| 24 |
+
remappings = await fs.readFile(remappingsPath, "utf-8");
|
| 25 |
+
} catch {
|
| 26 |
+
// fallback: try to extract from foundry.toml
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 2. Read foundry.toml for additional context
|
| 30 |
+
try {
|
| 31 |
+
const foundryTomlPath = path.join(sandboxDir, "foundry.toml");
|
| 32 |
+
const tomlContent = await fs.readFile(foundryTomlPath, "utf-8");
|
| 33 |
+
// Extract relevant lines (src, libs, remappings)
|
| 34 |
+
const relevantLines = tomlContent
|
| 35 |
+
.split("\n")
|
| 36 |
+
.filter(l =>
|
| 37 |
+
l.includes("src") ||
|
| 38 |
+
l.includes("libs") ||
|
| 39 |
+
l.includes("remapping") ||
|
| 40 |
+
l.includes("[profile")
|
| 41 |
+
)
|
| 42 |
+
.slice(0, 20)
|
| 43 |
+
.join("\n");
|
| 44 |
+
foundryTomlProfile = relevantLines;
|
| 45 |
+
|
| 46 |
+
// If no remappings.txt, try to extract from foundry.toml remappings array
|
| 47 |
+
if (!remappings) {
|
| 48 |
+
const remappingMatch = tomlContent.match(/remappings\s*=\s*\[([\s\S]*?)\]/);
|
| 49 |
+
if (remappingMatch) {
|
| 50 |
+
remappings = remappingMatch[1]
|
| 51 |
+
.split(",")
|
| 52 |
+
.map(s => s.trim().replace(/^["']|["']$/g, ""))
|
| 53 |
+
.filter(Boolean)
|
| 54 |
+
.join("\n");
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
} catch {
|
| 58 |
+
// ignore
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// 3. Find an existing test file to use as import reference
|
| 62 |
+
try {
|
| 63 |
+
const testDir = path.join(sandboxDir, "test");
|
| 64 |
+
const testFile = await findFirstTestFile(testDir);
|
| 65 |
+
if (testFile) {
|
| 66 |
+
existingTestFilePath = path.relative(sandboxDir, testFile);
|
| 67 |
+
const testContent = await fs.readFile(testFile, "utf-8");
|
| 68 |
+
// Extract the first 20 lines which typically contain imports
|
| 69 |
+
existingTestImports = testContent
|
| 70 |
+
.split("\n")
|
| 71 |
+
.slice(0, 25)
|
| 72 |
+
.filter(l => l.startsWith("import") || l.startsWith("pragma") || l.startsWith("//") || l.startsWith("contract") || l.startsWith("abstract"))
|
| 73 |
+
.join("\n");
|
| 74 |
+
}
|
| 75 |
+
} catch {
|
| 76 |
+
// ignore
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return { remappings, existingTestImports, foundryTomlProfile, existingTestFilePath };
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Recursively finds the best reference test file in the test directory.
|
| 84 |
+
* Strategy:
|
| 85 |
+
* 1. Collect ALL .t.sol files (excluding Exploit.t.sol) across all subdirs (BFS)
|
| 86 |
+
* 2. Pick the one with the most import lines (most context-rich)
|
| 87 |
+
* 3. Fallback to .sol files that have "import" statements (e.g. BaseTest.sol, Fixture.sol)
|
| 88 |
+
* 4. Skip pure mock contracts (files in "mock" directories or named *Mock.sol)
|
| 89 |
+
*/
|
| 90 |
+
async function findFirstTestFile(dir: string): Promise<string | null> {
|
| 91 |
+
const tSolFiles: string[] = [];
|
| 92 |
+
const solFiles: string[] = [];
|
| 93 |
+
|
| 94 |
+
// BFS collect all files
|
| 95 |
+
const queue = [dir];
|
| 96 |
+
let depth = 0;
|
| 97 |
+
while (queue.length > 0 && depth < 4) {
|
| 98 |
+
const currentDepth: string[] = [...queue];
|
| 99 |
+
queue.length = 0;
|
| 100 |
+
depth++;
|
| 101 |
+
for (const currentDir of currentDepth) {
|
| 102 |
+
let entries: import('fs').Dirent[];
|
| 103 |
+
try {
|
| 104 |
+
entries = await fs.readdir(currentDir, { withFileTypes: true }) as import('fs').Dirent[];
|
| 105 |
+
} catch {
|
| 106 |
+
continue;
|
| 107 |
+
}
|
| 108 |
+
for (const entry of entries) {
|
| 109 |
+
const fullPath = path.join(currentDir, entry.name);
|
| 110 |
+
const name = entry.name as string;
|
| 111 |
+
if (entry.isFile()) {
|
| 112 |
+
if (name === "Exploit.t.sol") continue; // skip our own file
|
| 113 |
+
if (name.endsWith(".t.sol")) {
|
| 114 |
+
tSolFiles.push(fullPath);
|
| 115 |
+
} else if (name.endsWith(".sol")) {
|
| 116 |
+
// Skip mock contracts
|
| 117 |
+
const isMock = name.toLowerCase().includes("mock") || currentDir.toLowerCase().includes("mock");
|
| 118 |
+
if (!isMock) {
|
| 119 |
+
solFiles.push(fullPath);
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
} else if (entry.isDirectory()) {
|
| 123 |
+
queue.push(fullPath);
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Pick the .t.sol file with the most import lines (richest context)
|
| 130 |
+
if (tSolFiles.length > 0) {
|
| 131 |
+
let bestFile = tSolFiles[0];
|
| 132 |
+
let bestImportCount = 0;
|
| 133 |
+
for (const f of tSolFiles.slice(0, 10)) { // check up to 10
|
| 134 |
+
try {
|
| 135 |
+
const content = await fs.readFile(f, "utf-8");
|
| 136 |
+
const importCount = (content.match(/^import/gm) ?? []).length;
|
| 137 |
+
if (importCount > bestImportCount) {
|
| 138 |
+
bestImportCount = importCount;
|
| 139 |
+
bestFile = f;
|
| 140 |
+
}
|
| 141 |
+
} catch { /* skip */ }
|
| 142 |
+
}
|
| 143 |
+
return bestFile;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Fallback: .sol files that have import statements (like BaseTest.sol, Fixture.sol)
|
| 147 |
+
for (const f of solFiles.slice(0, 10)) {
|
| 148 |
+
try {
|
| 149 |
+
const content = await fs.readFile(f, "utf-8");
|
| 150 |
+
if (content.includes("import ")) return f;
|
| 151 |
+
} catch { /* skip */ }
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return null;
|
| 155 |
+
}
|
| 156 |
+
|
|
@@ -11,6 +11,82 @@ const DATASET_PATH = "Proof-of-Patch-only-dataset";
|
|
| 11 |
const METADATA_FILE = path.join(DATASET_PATH, "dataset_metadata.json");
|
| 12 |
const SUMMARY_FILE = "data/benchmark_summary.json";
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
/**
|
| 15 |
* Extracts the likely vulnerable file path from annotation text.
|
| 16 |
* Looks for paths ending in .sol or github links.
|
|
@@ -180,6 +256,20 @@ async function main() {
|
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const report: VulnerabilityReport = {
|
| 184 |
id: id,
|
| 185 |
title: `${finding.repo_name} - ${id}`,
|
|
@@ -187,6 +277,7 @@ async function main() {
|
|
| 187 |
type: finding.expected_vulnerability || "unknown",
|
| 188 |
description: annotationText,
|
| 189 |
referenceTestCode: referenceTestCode,
|
|
|
|
| 190 |
affectedContract: {
|
| 191 |
name: relativeContractPath.split("/").pop()!.replace(".sol", ""),
|
| 192 |
sourceCode: sourceCode,
|
|
@@ -216,16 +307,29 @@ async function main() {
|
|
| 216 |
await execAsync(`rm -f ${tempPatchDir}/.git`);
|
| 217 |
|
| 218 |
const patchSourceDir = path.join(process.cwd(), DATASET_PATH, finding.patch);
|
| 219 |
-
|
|
|
|
| 220 |
|
| 221 |
const { runFoundry } = await import("../agents/tester/tools/foundryRunner.js");
|
| 222 |
const patchExec = await runFoundry(resultVuln.solidityCode, tempPatchDir);
|
| 223 |
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
statusPatch = passedOnPatch ? "success" : "failed";
|
| 226 |
|
| 227 |
if (statusPatch === "failed") {
|
|
|
|
| 228 |
await execAsync(`rm -rf ${tempPatchDir}`);
|
|
|
|
|
|
|
| 229 |
}
|
| 230 |
} catch (e: any) {
|
| 231 |
console.error(`[${id}] Patch run error:`, e.message);
|
|
|
|
| 11 |
const METADATA_FILE = path.join(DATASET_PATH, "dataset_metadata.json");
|
| 12 |
const SUMMARY_FILE = "data/benchmark_summary.json";
|
| 13 |
|
| 14 |
+
/**
|
| 15 |
+
* Smartly applies a patch by matching each patched .sol file to its
|
| 16 |
+
* counterpart in tempPatchDir by stripping 1-3 directory prefix levels.
|
| 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(
|
| 24 |
+
`find "${patchSourceDir}" -name "*.sol" -not -path "*/lib/*" -not -path "*/node_modules/*" -type f`,
|
| 25 |
+
{ timeout: 15_000 }
|
| 26 |
+
));
|
| 27 |
+
} catch {
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
const patchFiles = stdout.trim().split("\n").filter(Boolean);
|
| 31 |
+
let applied = 0;
|
| 32 |
+
|
| 33 |
+
for (const patchFile of patchFiles) {
|
| 34 |
+
const relFromPatch = path.relative(patchSourceDir, patchFile);
|
| 35 |
+
const parts = relFromPatch.split("/");
|
| 36 |
+
|
| 37 |
+
// Try stripping 1, 2, 3 prefix levels to find matching file in tempPatchDir
|
| 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}"`);
|
| 45 |
+
console.log(` [patch] Applied: ${stripped}`);
|
| 46 |
+
applied++;
|
| 47 |
+
matched = true;
|
| 48 |
+
break;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
if (!matched) {
|
| 52 |
+
console.log(` [patch] No match found for: ${relFromPatch}`);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
console.log(` [patch] Applied ${applied}/${patchFiles.length} patch files.`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Computes a unified diff of the main contract between vulnerable and patched versions.
|
| 60 |
+
* Uses the same strip-depth matching as applyPatchSmart.
|
| 61 |
+
*/
|
| 62 |
+
async function computePatchDiff(
|
| 63 |
+
patchSourceDir: string,
|
| 64 |
+
mainContractPath: string,
|
| 65 |
+
targetDir: string,
|
| 66 |
+
relativeContractPath: string
|
| 67 |
+
): Promise<string> {
|
| 68 |
+
let diffOut = "";
|
| 69 |
+
try {
|
| 70 |
+
let stdout = "";
|
| 71 |
+
try {
|
| 72 |
+
({ stdout } = await execAsync(
|
| 73 |
+
`find "${patchSourceDir}" -name "${path.basename(relativeContractPath)}" -not -path "*/lib/*" -type f`,
|
| 74 |
+
{ timeout: 10_000 }
|
| 75 |
+
));
|
| 76 |
+
} catch { return ""; }
|
| 77 |
+
|
| 78 |
+
const patchedFile = stdout.trim().split("\n")[0];
|
| 79 |
+
if (!patchedFile) return "";
|
| 80 |
+
|
| 81 |
+
const { stdout: diff } = await execAsync(
|
| 82 |
+
`diff -u "${mainContractPath}" "${patchedFile}"`,
|
| 83 |
+
{ timeout: 10_000 }
|
| 84 |
+
).catch(({ stdout: s }: any) => ({ stdout: s as string }));
|
| 85 |
+
diffOut = (diff || "").trim().slice(0, 3000);
|
| 86 |
+
} catch { /* ignore */ }
|
| 87 |
+
return diffOut;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
/**
|
| 91 |
* Extracts the likely vulnerable file path from annotation text.
|
| 92 |
* Looks for paths ending in .sol or github links.
|
|
|
|
| 256 |
}
|
| 257 |
}
|
| 258 |
|
| 259 |
+
// STEP 2: Compute patch diff for specificity guidance
|
| 260 |
+
let patchDiff = "";
|
| 261 |
+
try {
|
| 262 |
+
const patchSourceDir = path.join(process.cwd(), DATASET_PATH, finding.patch);
|
| 263 |
+
patchDiff = await computePatchDiff(patchSourceDir, mainContractPath, targetDir, relativeContractPath);
|
| 264 |
+
if (patchDiff) {
|
| 265 |
+
console.log(`[${id}] Patch diff computed: ${patchDiff.split("\n").length} lines`);
|
| 266 |
+
} else {
|
| 267 |
+
console.log(`[${id}] No patch diff found for main contract`);
|
| 268 |
+
}
|
| 269 |
+
} catch {
|
| 270 |
+
// Patch diff is optional, ignore errors
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
const report: VulnerabilityReport = {
|
| 274 |
id: id,
|
| 275 |
title: `${finding.repo_name} - ${id}`,
|
|
|
|
| 277 |
type: finding.expected_vulnerability || "unknown",
|
| 278 |
description: annotationText,
|
| 279 |
referenceTestCode: referenceTestCode,
|
| 280 |
+
patchDiff: patchDiff || undefined,
|
| 281 |
affectedContract: {
|
| 282 |
name: relativeContractPath.split("/").pop()!.replace(".sol", ""),
|
| 283 |
sourceCode: sourceCode,
|
|
|
|
| 307 |
await execAsync(`rm -f ${tempPatchDir}/.git`);
|
| 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);
|
| 315 |
|
| 316 |
+
// Specific = PoC FAILS on patched version (exploit doesn't work anymore)
|
| 317 |
+
// i.e., exit code != 0, OR stdout doesn't contain "ok", OR test was not found
|
| 318 |
+
const passedOnPatch = (
|
| 319 |
+
patchExec.exitCode === 0 &&
|
| 320 |
+
patchExec.stdout.includes("ok") &&
|
| 321 |
+
!patchExec.stdout.includes("FAIL") &&
|
| 322 |
+
!patchExec.combined.includes("No tests found")
|
| 323 |
+
);
|
| 324 |
+
// statusPatch = "success" means PoC still works on patch (BAD, not specific)
|
| 325 |
+
// statusPatch = "failed" means PoC correctly fails on patch (GOOD, specific)
|
| 326 |
statusPatch = passedOnPatch ? "success" : "failed";
|
| 327 |
|
| 328 |
if (statusPatch === "failed") {
|
| 329 |
+
console.log(`[${id}] PoC correctly fails on PATCHED version — exploit is SPECIFIC.`);
|
| 330 |
await execAsync(`rm -rf ${tempPatchDir}`);
|
| 331 |
+
} else {
|
| 332 |
+
console.log(`[${id}] PoC still passes on PATCHED version — exploit is NOT specific.`);
|
| 333 |
}
|
| 334 |
} catch (e: any) {
|
| 335 |
console.error(`[${id}] Patch run error:`, e.message);
|