Spaces:
Runtime error
Runtime error
Merge branch 'dev' into unify-logs
Browse files- benchmarks/run-formal-eval.ts +153 -0
- benchmarks/samples.jsonl +45 -0
- benchmarks/samples.jsonl_results.jsonl +0 -0
- biome.json +51 -53
- package-lock.json +12 -10
- package.json +2 -1
- src/agents/auditor/agent.ts +161 -131
- src/agents/auditor/config.ts +2 -1
- src/agents/auditor/prompts.ts +113 -32
- src/agents/auditor/state.ts +7 -0
- src/agents/auditor/tools/{repo-tree-tool.ts → repo-tree/tool.ts} +9 -12
- src/agents/auditor/tools/solidity-analyzer-tool.ts +0 -567
- src/agents/auditor/tools/solidity-analyzer/tool.ts +172 -0
- src/agents/auditor/tools/solidity-analyzer/utils.ts +520 -0
- src/agents/auditor/utils.ts +81 -0
- src/config/llm.ts +19 -10
benchmarks/run-formal-eval.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "dotenv/config";
|
| 2 |
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
| 3 |
+
import { resolve } from "node:path";
|
| 4 |
+
import { coderAgent } from "../src/agents/coder/agent.ts";
|
| 5 |
+
|
| 6 |
+
// ─── Config ────────────────────────────────────────────────────────────────────
|
| 7 |
+
const FORMAL_EVAL_PATH =
|
| 8 |
+
process.env.FORMAL_EVAL_PATH ??
|
| 9 |
+
resolve("..", "formal-eval", "data", "FormalEval.jsonl");
|
| 10 |
+
|
| 11 |
+
const OUTPUT_PATH = process.env.BENCHMARK_OUTPUT ?? resolve("benchmarks", "samples.jsonl");
|
| 12 |
+
const SAMPLES_PER_TASK = Number(process.env.SAMPLES_PER_TASK ?? "1");
|
| 13 |
+
const SKIP_REVIEW = process.env.SKIP_REVIEW !== "false"; // skip review by default for speed
|
| 14 |
+
|
| 15 |
+
// ─── Types ─────────────────────────────────────────────────────────────────────
|
| 16 |
+
interface FormalEvalProblem {
|
| 17 |
+
task_id: string;
|
| 18 |
+
complexity: string;
|
| 19 |
+
description: string;
|
| 20 |
+
prompt: string;
|
| 21 |
+
canonical_solution: string;
|
| 22 |
+
test: string;
|
| 23 |
+
entry_point: string;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
interface SampleOutput {
|
| 27 |
+
task_id: string;
|
| 28 |
+
completion: string;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
| 32 |
+
|
| 33 |
+
function readProblems(path: string): FormalEvalProblem[] {
|
| 34 |
+
const content = readFileSync(path, "utf-8");
|
| 35 |
+
return content
|
| 36 |
+
.split("\n")
|
| 37 |
+
.filter((line) => line.trim())
|
| 38 |
+
.map((line) => JSON.parse(line));
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Extrai a "completion" (body do contrato) a partir do contrato completo gerado.
|
| 43 |
+
* O FormalEval espera: prompt + completion = contrato completo.
|
| 44 |
+
* Então precisamos remover tudo que já está no prompt.
|
| 45 |
+
*/
|
| 46 |
+
function extractCompletion(generatedContract: string, prompt: string): string {
|
| 47 |
+
// Estratégia 1: Se o contrato gerado contém o prompt exato, pegar o que vem depois
|
| 48 |
+
const promptTrimmed = prompt.trimEnd();
|
| 49 |
+
const idx = generatedContract.indexOf(promptTrimmed);
|
| 50 |
+
if (idx !== -1) {
|
| 51 |
+
return generatedContract.slice(idx + promptTrimmed.length);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Estratégia 2: Encontrar a declaração "contract <Name> {" e pegar o body
|
| 55 |
+
// O prompt sempre termina com "contract <Name> {\n"
|
| 56 |
+
const contractDeclMatch = prompt.match(/contract\s+(\w+)\s*\{?\s*$/m);
|
| 57 |
+
if (contractDeclMatch) {
|
| 58 |
+
const contractName = contractDeclMatch[1];
|
| 59 |
+
const pattern = new RegExp(`contract\\s+${contractName}\\s*\\{`);
|
| 60 |
+
const match = generatedContract.match(pattern);
|
| 61 |
+
if (match?.index !== undefined) {
|
| 62 |
+
const afterDecl = generatedContract.slice(match.index + match[0].length);
|
| 63 |
+
return afterDecl;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Estratégia 3: Fallback — Tenta remover pragma + imports + declaração até "{"
|
| 68 |
+
const lines = generatedContract.split("\n");
|
| 69 |
+
let bodyStart = 0;
|
| 70 |
+
for (let i = 0; i < lines.length; i++) {
|
| 71 |
+
if (lines[i].match(/contract\s+\w+.*\{/)) {
|
| 72 |
+
bodyStart = i + 1;
|
| 73 |
+
break;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
return lines.slice(bodyStart).join("\n");
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Adapta o prompt do FormalEval para ser interpretado pelo Coder Agent.
|
| 81 |
+
* Inclui instrução explícita para gerar apenas o body.
|
| 82 |
+
*/
|
| 83 |
+
function buildRequirements(problem: FormalEvalProblem): string {
|
| 84 |
+
return `Complete the following Solidity contract. Generate ONLY the contract body (state variables, functions, and closing brace "}").
|
| 85 |
+
Do NOT include the SPDX license, pragma, or contract declaration — they are already provided.
|
| 86 |
+
The code must compile with solc ^0.8.19.
|
| 87 |
+
|
| 88 |
+
Here is the contract declaration with its specification:
|
| 89 |
+
|
| 90 |
+
${problem.prompt}
|
| 91 |
+
|
| 92 |
+
IMPORTANT: Return ONLY the code that goes INSIDE the contract (after the opening brace). Include the closing "}" at the end.`;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
| 96 |
+
|
| 97 |
+
async function main() {
|
| 98 |
+
if (!existsSync(FORMAL_EVAL_PATH)) {
|
| 99 |
+
console.error(`FormalEval dataset not found at: ${FORMAL_EVAL_PATH}`);
|
| 100 |
+
console.error("Set FORMAL_EVAL_PATH env variable to the correct path.");
|
| 101 |
+
process.exit(1);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
const problems = readProblems(FORMAL_EVAL_PATH);
|
| 105 |
+
console.log(`Loaded ${problems.length} problems from FormalEval`);
|
| 106 |
+
console.log(`Generating ${SAMPLES_PER_TASK} sample(s) per task...`);
|
| 107 |
+
console.log(`Output: ${OUTPUT_PATH}\n`);
|
| 108 |
+
|
| 109 |
+
const samples: SampleOutput[] = [];
|
| 110 |
+
let completed = 0;
|
| 111 |
+
|
| 112 |
+
for (const problem of problems) {
|
| 113 |
+
for (let s = 0; s < SAMPLES_PER_TASK; s++) {
|
| 114 |
+
const label = `[${problem.task_id}] (${problem.complexity}) sample ${s + 1}/${SAMPLES_PER_TASK}`;
|
| 115 |
+
console.log(`→ ${label}: ${problem.description}`);
|
| 116 |
+
|
| 117 |
+
try {
|
| 118 |
+
const requirements = buildRequirements(problem);
|
| 119 |
+
const result = await coderAgent.invoke({ requirements: [requirements] });
|
| 120 |
+
|
| 121 |
+
const generated = result.contract;
|
| 122 |
+
const completion = extractCompletion(generated, problem.prompt);
|
| 123 |
+
|
| 124 |
+
samples.push({ task_id: problem.task_id, completion });
|
| 125 |
+
|
| 126 |
+
const hasErrors = result.compilationErrors.length > 0;
|
| 127 |
+
console.log(` ✓ Done${hasErrors ? ` (with ${result.compilationErrors.length} compile errors)` : ""}`);
|
| 128 |
+
} catch (error) {
|
| 129 |
+
const msg = error instanceof Error ? error.message : String(error);
|
| 130 |
+
console.error(` ✗ Failed: ${msg}`);
|
| 131 |
+
// Still emit an empty completion so FormalEval doesn't complain about missing tasks
|
| 132 |
+
samples.push({ task_id: problem.task_id, completion: "// generation failed\n}\n" });
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
completed++;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Write JSONL output
|
| 140 |
+
const jsonl = samples.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
| 141 |
+
writeFileSync(OUTPUT_PATH, jsonl, "utf-8");
|
| 142 |
+
|
| 143 |
+
console.log(`\n${"═".repeat(60)}`);
|
| 144 |
+
console.log(`Benchmark complete: ${completed} completions generated`);
|
| 145 |
+
console.log(`Output saved to: ${OUTPUT_PATH}`);
|
| 146 |
+
console.log(`\nTo evaluate, run:`);
|
| 147 |
+
console.log(` cd ../formal-eval && evaluate_formal_correctness ${resolve(OUTPUT_PATH)}`);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
main().catch((err) => {
|
| 151 |
+
console.error("Fatal error:", err);
|
| 152 |
+
process.exit(1);
|
| 153 |
+
});
|
benchmarks/samples.jsonl
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"task_id":"FormalEval/0","completion":" uint256 private storedValue;\n address public owner;\n bool private locked;\n\n event ValueStored(uint256 indexed newValue, address indexed sender);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n storedValue = 0;\n }\n\n /// @notice Stores a new value in the contract\n /// @param _value The value to be stored\n function store(uint256 _value) public onlyOwner nonReentrant {\n storedValue = _value;\n emit ValueStored(_value, msg.sender);\n }\n\n /// @notice Retrieves the currently stored value\n /// @return The stored uint256 value\n function retrieve() public view returns (uint256) {\n return storedValue;\n }\n}"}
|
| 2 |
+
{"task_id":"FormalEval/0","completion":" uint256 private storedValue;\n address public owner;\n bool private locked;\n\n event ValueStored(uint256 indexed newValue, address indexed sender);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n storedValue = 0;\n }\n\n /// @notice Stores a new value in the contract\n /// @param _value The value to be stored\n function store(uint256 _value) public onlyOwner nonReentrant {\n storedValue = _value;\n emit ValueStored(_value, msg.sender);\n }\n\n /// @notice Retrieves the currently stored value\n /// @return The stored uint256 value\n function retrieve() public view returns (uint256) {\n return storedValue;\n }\n}"}
|
| 3 |
+
{"task_id":"FormalEval/0","completion":" uint256 private storedValue;\n address private immutable owner;\n\n event ValueStored(uint256 indexed newValue, address indexed sender);\n\n error Unauthorized();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert Unauthorized();\n _;\n }\n\n constructor() {\n owner = msg.sender;\n storedValue = 0;\n }\n\n /// @notice Stores a new value in the contract\n /// @param _value The uint256 value to be stored\n function store(uint256 _value) external {\n storedValue = _value;\n emit ValueStored(_value, msg.sender);\n }\n\n /// @notice Retrieves the currently stored value\n /// @return The current uint256 value\n function retrieve() external view returns (uint256) {\n return storedValue;\n }\n}"}
|
| 4 |
+
{"task_id":"FormalEval/1","completion":"\n uint256 private count;\n bool private locked;\n\n event CounterIncremented(uint256 newValue);\n event CounterDecremented(uint256 newValue);\n\n modifier noReentrancy() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n count = 0;\n locked = false;\n }\n\n /// @notice Increases the counter by 1\n function increment() external noReentrancy {\n count += 1;\n emit CounterIncremented(count);\n }\n\n /// @notice Decreases the counter by 1\n /// @dev Reverts if the counter is 0 to prevent underflow\n function decrement() external noReentrancy {\n require(count > 0, \"Counter: cannot decrement below zero\");\n count -= 1;\n emit CounterDecremented(count);\n }\n\n /// @notice Returns the current counter value\n /// @return The current count\n function getCount() external view returns (uint256) {\n return count;\n }\n}"}
|
| 5 |
+
{"task_id":"FormalEval/1","completion":"\n uint256 private count;\n bool private locked;\n\n event Incremented(uint256 newValue);\n event Decremented(uint256 newValue);\n\n modifier noReentrancy() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n count = 0;\n locked = false;\n }\n\n /// @notice Increases the counter by 1\n function increment() external noReentrancy {\n count += 1;\n emit Incremented(count);\n }\n\n /// @notice Decreases the counter by 1\n /// @dev Reverts if the counter is already 0\n function decrement() external noReentrancy {\n require(count > 0, \"Counter: cannot decrement below zero\");\n count -= 1;\n emit Decremented(count);\n }\n\n /// @notice Returns the current counter value\n /// @return The current count\n function getCount() external view returns (uint256) {\n return count;\n }\n}"}
|
| 6 |
+
{"task_id":"FormalEval/1","completion":"\n uint256 private count;\n bool private locked;\n\n event Incremented(uint256 newValue);\n event Decremented(uint256 newValue);\n\n modifier noReentrancy() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n count = 0;\n locked = false;\n }\n\n /// @notice Increases the counter by 1\n function increment() external noReentrancy {\n count += 1;\n emit Incremented(count);\n }\n\n /// @notice Decreases the counter by 1\n /// @dev Reverts if the counter is 0 to prevent underflow\n function decrement() external noReentrancy {\n require(count > 0, \"Counter: cannot decrement below 0\");\n count -= 1;\n emit Decremented(count);\n }\n\n /// @notice Returns the current counter value\n /// @return The current count\n function getCount() external view returns (uint256) {\n return count;\n }\n}"}
|
| 7 |
+
{"task_id":"FormalEval/2","completion":"\n /// @notice Event emitted when an addition is performed\n event Addition(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a subtraction is performed\n event Subtraction(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a multiplication is performed\n event Multiplication(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a division is performed\n event Division(uint256 a, uint256 b, uint256 result);\n\n /// @notice Adds two numbers\n /// @param a First number\n /// @param b Second number\n /// @return result The sum of a and b\n function add(uint256 a, uint256 b) external returns (uint256 result) {\n result = a + b;\n emit Addition(a, b, result);\n }\n\n /// @notice Subtracts b from a\n /// @param a Minuend\n /// @param b Subtrahend\n /// @return result The difference of a and b\n function subtract(uint256 a, uint256 b) external returns (uint256 result) {\n require(a >= b, \"Calculator: subtraction result would be negative\");\n result = a - b;\n emit Subtraction(a, b, result);\n }\n\n /// @notice Multiplies two numbers\n /// @param a First number\n /// @param b Second number\n /// @return result The product of a and b\n function multiply(uint256 a, uint256 b) external returns (uint256 result) {\n result = a * b;\n emit Multiplication(a, b, result);\n }\n\n /// @notice Divides a by b\n /// @param a Dividend\n /// @param b Divisor\n /// @return result The quotient of a and b\n function divide(uint256 a, uint256 b) external returns (uint256 result) {\n require(b != 0, \"Calculator: division by zero\");\n result = a / b;\n emit Division(a, b, result);\n }\n}"}
|
| 8 |
+
{"task_id":"FormalEval/2","completion":"\n /// @notice Event emitted when an addition is performed\n event Addition(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a subtraction is performed\n event Subtraction(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a multiplication is performed\n event Multiplication(uint256 a, uint256 b, uint256 result);\n /// @notice Event emitted when a division is performed\n event Division(uint256 a, uint256 b, uint256 result);\n\n /// @notice Adds two numbers\n /// @param a First operand\n /// @param b Second operand\n /// @return result The sum of a and b\n function add(uint256 a, uint256 b) external returns (uint256 result) {\n result = a + b;\n emit Addition(a, b, result);\n }\n\n /// @notice Subtracts b from a\n /// @param a First operand\n /// @param b Second operand\n /// @return result The difference of a and b\n function subtract(uint256 a, uint256 b) external returns (uint256 result) {\n require(a >= b, \"Calculator: subtraction underflow\");\n result = a - b;\n emit Subtraction(a, b, result);\n }\n\n /// @notice Multiplies two numbers\n /// @param a First operand\n /// @param b Second operand\n /// @return result The product of a and b\n function multiply(uint256 a, uint256 b) external returns (uint256 result) {\n result = a * b;\n emit Multiplication(a, b, result);\n }\n\n /// @notice Divides a by b\n /// @param a First operand\n /// @param b Second operand\n /// @return result The quotient of a divided by b\n function divide(uint256 a, uint256 b) external returns (uint256 result) {\n require(b != 0, \"Calculator: division by zero\");\n result = a / b;\n emit Division(a, b, result);\n }\n}"}
|
| 9 |
+
{"task_id":"FormalEval/2","completion":"\n /// @notice Event emitted when an addition is performed\n event Addition(uint256 a, uint256 b, uint256 result);\n\n /// @notice Event emitted when a subtraction is performed\n event Subtraction(uint256 a, uint256 b, uint256 result);\n\n /// @notice Event emitted when a multiplication is performed\n event Multiplication(uint256 a, uint256 b, uint256 result);\n\n /// @notice Event emitted when a division is performed\n event Division(uint256 a, uint256 b, uint256 result);\n\n /// @notice Adds two numbers\n /// @param a First operand\n /// @param b Second operand\n /// @return The sum of a and b\n function add(uint256 a, uint256 b) external returns (uint256) {\n uint256 result = a + b;\n emit Addition(a, b, result);\n return result;\n }\n\n /// @notice Subtracts b from a\n /// @param a First operand\n /// @param b Second operand\n /// @return The difference of a and b\n /// @dev Reverts if b > a to prevent underflow\n function subtract(uint256 a, uint256 b) external returns (uint256) {\n require(a >= b, \"Calculator: subtraction result would be negative\");\n uint256 result = a - b;\n emit Subtraction(a, b, result);\n return result;\n }\n\n /// @notice Multiplies two numbers\n /// @param a First operand\n /// @param b Second operand\n /// @return The product of a and b\n function multiply(uint256 a, uint256 b) external returns (uint256) {\n uint256 result = a * b;\n emit Multiplication(a, b, result);\n return result;\n }\n\n /// @notice Divides a by b\n /// @param a First operand\n /// @param b Second operand\n /// @return The quotient of a and b\n /// @dev Reverts if b is 0\n function divide(uint256 a, uint256 b) external returns (uint256) {\n require(b != 0, \"Calculator: division by zero\");\n uint256 result = a / b;\n emit Division(a, b, result);\n return result;\n }\n}"}
|
| 10 |
+
{"task_id":"FormalEval/3","completion":" uint256[] private values;\n\n /// @notice Event emitted when a new value is added to the array\n event ValueAdded(uint256 value);\n\n /// @notice Appends a value to the array\n /// @param _value The value to be added\n function push(uint256 _value) external {\n values.push(_value);\n emit ValueAdded(_value);\n }\n\n /// @notice Returns the minimum value in the array\n /// @dev Reverts if the array is empty\n /// @return min The smallest value found\n function getMin() external view returns (uint256 min) {\n require(values.length > 0, \"Array is empty\");\n min = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] < min) {\n min = values[i];\n }\n }\n }\n\n /// @notice Returns the maximum value in the array\n /// @dev Reverts if the array is empty\n /// @return max The largest value found\n function getMax() external view returns (uint256 max) {\n require(values.length > 0, \"Array is empty\");\n max = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] > max) {\n max = values[i];\n }\n }\n }\n\n /// @notice Returns the number of elements in the array\n /// @return The length of the array\n function getLength() external view returns (uint256) {\n return values.length;\n }\n}"}
|
| 11 |
+
{"task_id":"FormalEval/3","completion":" uint256[] private values;\n\n /// @notice Event emitted when a new value is added\n event ValueAdded(uint256 value);\n\n /// @notice Appends a value to the array\n /// @param _value The value to be added\n function push(uint256 _value) external {\n values.push(_value);\n emit ValueAdded(_value);\n }\n\n /// @notice Returns the minimum value in the array\n /// @return min The smallest value found\n function getMin() external view returns (uint256 min) {\n require(values.length > 0, \"Array is empty\");\n min = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] < min) {\n min = values[i];\n }\n }\n }\n\n /// @notice Returns the maximum value in the array\n /// @return max The largest value found\n function getMax() external view returns (uint256 max) {\n require(values.length > 0, \"Array is empty\");\n max = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] > max) {\n max = values[i];\n }\n }\n }\n\n /// @notice Returns the number of elements in the array\n /// @return length The total count of elements\n function getLength() external view returns (uint256 length) {\n return values.length;\n }\n}"}
|
| 12 |
+
{"task_id":"FormalEval/3","completion":" uint256[] private values;\n\n /// @notice Event emitted when a new value is added to the array\n event ValueAdded(uint256 indexed value);\n\n /// @notice Appends a value to the array\n /// @param _value The value to be added\n function push(uint256 _value) external {\n values.push(_value);\n emit ValueAdded(_value);\n }\n\n /// @notice Returns the minimum value in the array\n /// @return min The smallest value found\n function getMin() external view returns (uint256 min) {\n require(values.length > 0, \"Array is empty\");\n min = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] < min) {\n min = values[i];\n }\n }\n }\n\n /// @notice Returns the maximum value in the array\n /// @return max The largest value found\n function getMax() external view returns (uint256 max) {\n require(values.length > 0, \"Array is empty\");\n max = values[0];\n for (uint256 i = 1; i < values.length; i++) {\n if (values[i] > max) {\n max = values[i];\n }\n }\n }\n\n /// @notice Returns the number of elements in the array\n /// @return length The total count of elements\n function getLength() external view returns (uint256 length) {\n return values.length;\n }\n}"}
|
| 13 |
+
{"task_id":"FormalEval/4","completion":" mapping(address => string) private _names;\n mapping(address => bool) private _registered;\n\n /// @notice Emitted when a new name is registered\n event Registered(address indexed user, string name);\n\n /// @notice Emitted when a registration is removed\n event Unregistered(address indexed user);\n\n /// @notice Registers the caller with a specific name\n /// @param _name The name to associate with the sender's address\n function register(string calldata _name) external {\n require(bytes(_name).length > 0, \"Name cannot be empty\");\n _names[msg.sender] = _name;\n _registered[msg.sender] = true;\n emit Registered(msg.sender, _name);\n }\n\n /// @notice Returns the name associated with an address\n /// @param _addr The address to query\n /// @return The registered name\n function getName(address _addr) external view returns (string memory) {\n return _names[_addr];\n }\n\n /// @notice Checks if an address is registered\n /// @param _addr The address to check\n /// @return True if registered, false otherwise\n function isRegistered(address _addr) external view returns (bool) {\n return _registered[_addr];\n }\n\n /// @notice Removes the registration for the caller\n function unregister() external {\n require(_registered[msg.sender], \"Address not registered\");\n delete _names[msg.sender];\n delete _registered[msg.sender];\n emit Unregistered(msg.sender);\n }\n}"}
|
| 14 |
+
{"task_id":"FormalEval/4","completion":" mapping(address => string) private _names;\n mapping(address => bool) private _registered;\n\n /// @notice Emitted when a new name is registered\n event Registered(address indexed user, string name);\n\n /// @notice Emitted when a registration is removed\n event Unregistered(address indexed user);\n\n /// @notice Registers the caller with a specific name\n /// @param _name The name to associate with the sender's address\n function register(string calldata _name) external {\n require(bytes(_name).length > 0, \"Name cannot be empty\");\n _names[msg.sender] = _name;\n _registered[msg.sender] = true;\n emit Registered(msg.sender, _name);\n }\n\n /// @notice Retrieves the name associated with an address\n /// @param _addr The address to query\n /// @return The registered name or an empty string\n function getName(address _addr) external view returns (string memory) {\n return _names[_addr];\n }\n\n /// @notice Checks if an address is registered\n /// @param _addr The address to check\n /// @return True if registered, false otherwise\n function isRegistered(address _addr) external view returns (bool) {\n return _registered[_addr];\n }\n\n /// @notice Removes the registration for the caller\n function unregister() external {\n require(_registered[msg.sender], \"Address not registered\");\n delete _names[msg.sender];\n delete _registered[msg.sender];\n emit Unregistered(msg.sender);\n }\n}"}
|
| 15 |
+
{"task_id":"FormalEval/4","completion":" mapping(address => string) private _names;\n mapping(address => bool) private _registered;\n\n event Registered(address indexed user, string name);\n event Unregistered(address indexed user);\n\n /// @notice Registers the caller with a specific name.\n /// @param _name The name to associate with the caller's address.\n function register(string calldata _name) external {\n require(bytes(_name).length > 0, \"Name cannot be empty\");\n _names[msg.sender] = _name;\n _registered[msg.sender] = true;\n emit Registered(msg.sender, _name);\n }\n\n /// @notice Retrieves the name associated with an address.\n /// @param _addr The address to query.\n /// @return The registered name or an empty string.\n function getName(address _addr) external view returns (string memory) {\n return _names[_addr];\n }\n\n /// @notice Checks if an address is registered.\n /// @param _addr The address to check.\n /// @return True if registered, false otherwise.\n function isRegistered(address _addr) external view returns (bool) {\n return _registered[_addr];\n }\n\n /// @notice Removes the registration for the caller.\n function unregister() external {\n require(_registered[msg.sender], \"Address not registered\");\n delete _names[msg.sender];\n delete _registered[msg.sender];\n emit Unregistered(msg.sender);\n }\n}"}
|
| 16 |
+
{"task_id":"FormalEval/5","completion":"\n string private _name;\n string private _symbol;\n uint8 private _decimals;\n uint256 private _totalSupply;\n address private _owner;\n mapping(address => uint256) private _balances;\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n modifier onlyOwner() {\n require(msg.sender == _owner, \"BasicToken: caller is not the owner\");\n _;\n }\n\n constructor(string memory name_, string memory symbol_, uint8 decimals_) {\n _name = name_;\n _symbol = symbol_;\n _decimals = decimals_;\n _owner = msg.sender;\n }\n\n function name() public view returns (string memory) {\n return _name;\n }\n\n function symbol() public view returns (string memory) {\n return _symbol;\n }\n\n function decimals() public view returns (uint8) {\n return _decimals;\n }\n\n function totalSupply() public view returns (uint256) {\n return _totalSupply;\n }\n\n function balanceOf(address account) public view returns (uint256) {\n return _balances[account];\n }\n\n function mint(address to, uint256 amount) public onlyOwner {\n require(to != address(0), \"BasicToken: mint to the zero address\");\n\n _totalSupply += amount;\n _balances[to] += amount;\n emit Transfer(address(0), to, amount);\n }\n\n function transfer(address to, uint256 amount) public returns (bool) {\n require(to != address(0), \"BasicToken: transfer to the zero address\");\n require(_balances[msg.sender] >= amount, \"BasicToken: insufficient balance\");\n\n _balances[msg.sender] -= amount;\n _balances[to] += amount;\n \n emit Transfer(msg.sender, to, amount);\n return true;\n }\n}"}
|
| 17 |
+
{"task_id":"FormalEval/5","completion":"\n string private _name;\n string private _symbol;\n uint8 private _decimals;\n uint256 private _totalSupply;\n address private _owner;\n\n mapping(address => uint256) private _balances;\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n modifier onlyOwner() {\n require(msg.sender == _owner, \"BasicToken: caller is not the owner\");\n _;\n }\n\n constructor(string memory name_, string memory symbol_, uint8 decimals_) {\n _name = name_;\n _symbol = symbol_;\n _decimals = decimals_;\n _owner = msg.sender;\n }\n\n function name() external view returns (string memory) {\n return _name;\n }\n\n function symbol() external view returns (string memory) {\n return _symbol;\n }\n\n function decimals() external view returns (uint8) {\n return _decimals;\n }\n\n function totalSupply() external view returns (uint256) {\n return _totalSupply;\n }\n\n function balanceOf(address account) external view returns (uint256) {\n return _balances[account];\n }\n\n function mint(address to, uint256 amount) external onlyOwner {\n require(to != address(0), \"BasicToken: mint to the zero address\");\n\n _totalSupply += amount;\n _balances[to] += amount;\n emit Transfer(address(0), to, amount);\n }\n\n function transfer(address to, uint256 amount) external returns (bool) {\n require(to != address(0), \"BasicToken: transfer to the zero address\");\n require(_balances[msg.sender] >= amount, \"BasicToken: insufficient balance\");\n\n _balances[msg.sender] -= amount;\n _balances[to] += amount;\n \n emit Transfer(msg.sender, to, amount);\n return true;\n }\n}"}
|
| 18 |
+
{"task_id":"FormalEval/5","completion":"\n string private _name;\n string private _symbol;\n uint8 private _decimals;\n uint256 private _totalSupply;\n address private _owner;\n mapping(address => uint256) private _balances;\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n modifier onlyOwner() {\n require(msg.sender == _owner, \"BasicToken: caller is not the owner\");\n _;\n }\n\n constructor(string memory name_, string memory symbol_, uint8 decimals_) {\n _name = name_;\n _symbol = symbol_;\n _decimals = decimals_;\n _owner = msg.sender;\n }\n\n function name() external view returns (string memory) {\n return _name;\n }\n\n function symbol() external view returns (string memory) {\n return _symbol;\n }\n\n function decimals() external view returns (uint8) {\n return _decimals;\n }\n\n function totalSupply() external view returns (uint256) {\n return _totalSupply;\n }\n\n function balanceOf(address account) external view returns (uint256) {\n return _balances[account];\n }\n\n function mint(address to, uint256 amount) external onlyOwner {\n require(to != address(0), \"BasicToken: mint to the zero address\");\n \n _totalSupply += amount;\n _balances[to] += amount;\n \n emit Transfer(address(0), to, amount);\n }\n\n function transfer(address to, uint256 amount) external returns (bool) {\n require(to != address(0), \"BasicToken: transfer to the zero address\");\n require(_balances[msg.sender] >= amount, \"BasicToken: insufficient balance\");\n\n _balances[msg.sender] -= amount;\n _balances[to] += amount;\n\n emit Transfer(msg.sender, to, amount);\n return true;\n }\n}"}
|
| 19 |
+
{"task_id":"FormalEval/6","completion":"\n address private _owner;\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error Unauthorized();\n error InvalidOwner();\n\n constructor() {\n _owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n modifier onlyOwner() {\n if (msg.sender != _owner) revert Unauthorized();\n _;\n }\n\n /// @notice Returns the address of the current owner.\n function owner() public view returns (address) {\n return _owner;\n }\n\n /// @notice Transfers ownership of the contract to a new account (`newOwner`).\n /// @param newOwner The address to transfer ownership to.\n function transferOwnership(address newOwner) public onlyOwner {\n if (newOwner == address(0)) revert InvalidOwner();\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n\n /// @notice Leaves the contract without owner. It will not be possible to call\n /// `onlyOwner` functions anymore.\n function renounceOwnership() public onlyOwner {\n address oldOwner = _owner;\n _owner = address(0);\n emit OwnershipTransferred(oldOwner, address(0));\n }\n}"}
|
| 20 |
+
{"task_id":"FormalEval/6","completion":"\n address private _owner;\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /// @notice Sets the initial owner of the contract as the deployer.\n constructor() {\n _owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n /// @notice Returns the address of the current owner.\n function owner() public view returns (address) {\n return _owner;\n }\n\n /// @notice Throws if called by any account other than the owner.\n modifier onlyOwner() {\n require(msg.sender == _owner, \"Ownable: caller is not the owner\");\n _;\n }\n\n /// @notice Transfers ownership of the contract to a new account (`newOwner`).\n /// @param newOwner The address to transfer ownership to.\n function transferOwnership(address newOwner) public onlyOwner {\n require(newOwner != address(0), \"Ownable: new owner is the zero address\");\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n\n /// @notice Leaves the contract without owner. It will not be possible to call\n /// `onlyOwner` functions anymore.\n function renounceOwnership() public onlyOwner {\n address oldOwner = _owner;\n _owner = address(0);\n emit OwnershipTransferred(oldOwner, address(0));\n }\n}"}
|
| 21 |
+
{"task_id":"FormalEval/6","completion":"\n address private _owner;\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /// @notice Sets the initial owner of the contract as the deployer.\n constructor() {\n _owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n /// @notice Returns the address of the current owner.\n function owner() public view returns (address) {\n return _owner;\n }\n\n /// @notice Throws if called by any account other than the owner.\n modifier onlyOwner() {\n require(msg.sender == _owner, \"Ownable: caller is not the owner\");\n _;\n }\n\n /// @notice Transfers ownership of the contract to a new account (`newOwner`).\n /// @param newOwner The address to transfer ownership to.\n function transferOwnership(address newOwner) public onlyOwner {\n require(newOwner != address(0), \"Ownable: new owner is the zero address\");\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n\n /// @notice Leaves the contract without owner. It will not be possible to call\n /// `onlyOwner` functions anymore.\n function renounceOwnership() public onlyOwner {\n address oldOwner = _owner;\n _owner = address(0);\n emit OwnershipTransferred(oldOwner, address(0));\n }\n}"}
|
| 22 |
+
{"task_id":"FormalEval/7","completion":"\n address public owner;\n uint256 public maxWhitelistedAddresses;\n uint256 public numWhitelistedCount;\n\n mapping(address => bool) public whitelistedAddresses;\n\n event AddedToWhitelist(address indexed account);\n event RemovedFromWhitelist(address indexed account);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n constructor(uint256 _maxWhitelistedAddresses) {\n require(_maxWhitelistedAddresses > 0, \"Max capacity must be greater than 0\");\n owner = msg.sender;\n maxWhitelistedAddresses = _maxWhitelistedAddresses;\n }\n\n /// @notice Adds the caller to the whitelist\n function addToWhitelist() external {\n require(!whitelistedAddresses[msg.sender], \"Address is already whitelisted\");\n require(numWhitelistedCount < maxWhitelistedAddresses, \"Whitelist is full\");\n\n whitelistedAddresses[msg.sender] = true;\n numWhitelistedCount += 1;\n\n emit AddedToWhitelist(msg.sender);\n }\n\n /// @notice Removes an address from the whitelist\n /// @param _addr The address to remove\n function removeFromWhitelist(address _addr) external onlyOwner {\n require(whitelistedAddresses[_addr], \"Address is not in the whitelist\");\n\n whitelistedAddresses[_addr] = false;\n numWhitelistedCount -= 1;\n\n emit RemovedFromWhitelist(_addr);\n }\n\n /// @notice Checks if an address is whitelisted\n /// @param _addr The address to check\n function isWhitelisted(address _addr) external view returns (bool) {\n return whitelistedAddresses[_addr];\n }\n\n /// @notice Returns the current number of whitelisted addresses\n function getNumWhitelisted() external view returns (uint256) {\n return numWhitelistedCount;\n }\n\n /// @notice Returns the maximum capacity\n function maxWhitelisted() external view returns (uint256) {\n return maxWhitelistedAddresses;\n }\n}"}
|
| 23 |
+
{"task_id":"FormalEval/7","completion":"\n address public owner;\n uint256 public maxWhitelistedAddresses;\n uint256 public numWhitelistedCount;\n\n mapping(address => bool) public whitelistedAddresses;\n\n event AddedToWhitelist(address indexed account);\n event RemovedFromWhitelist(address indexed account);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n constructor(uint256 _maxWhitelistedAddresses) {\n require(_maxWhitelistedAddresses > 0, \"Max capacity must be greater than 0\");\n owner = msg.sender;\n maxWhitelistedAddresses = _maxWhitelistedAddresses;\n }\n\n /// @notice Adds the caller to the whitelist\n function addToWhitelist() external {\n require(!whitelistedAddresses[msg.sender], \"Address is already whitelisted\");\n require(numWhitelistedCount < maxWhitelistedAddresses, \"Whitelist capacity reached\");\n\n whitelistedAddresses[msg.sender] = true;\n numWhitelistedCount += 1;\n\n emit AddedToWhitelist(msg.sender);\n }\n\n /// @notice Removes an address from the whitelist\n /// @param _addr The address to remove\n function removeFromWhitelist(address _addr) external onlyOwner {\n require(whitelistedAddresses[_addr], \"Address is not in the whitelist\");\n\n whitelistedAddresses[_addr] = false;\n numWhitelistedCount -= 1;\n\n emit RemovedFromWhitelist(_addr);\n }\n\n /// @notice Checks if an address is whitelisted\n function isWhitelisted(address _addr) external view returns (bool) {\n return whitelistedAddresses[_addr];\n }\n\n /// @notice Returns the current number of whitelisted addresses\n function getNumWhitelisted() external view returns (uint256) {\n return numWhitelistedCount;\n }\n\n /// @notice Returns the maximum capacity\n function maxWhitelisted() external view returns (uint256) {\n return maxWhitelistedAddresses;\n }\n}"}
|
| 24 |
+
{"task_id":"FormalEval/7","completion":"\n address public owner;\n uint256 public maxWhitelistedAddresses;\n uint256 public numWhitelistedCount;\n\n mapping(address => bool) public whitelistedAddresses;\n\n event AddedToWhitelist(address indexed account);\n event RemovedFromWhitelist(address indexed account);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n constructor(uint256 _maxWhitelistedAddresses) {\n require(_maxWhitelistedAddresses > 0, \"Max capacity must be greater than 0\");\n owner = msg.sender;\n maxWhitelistedAddresses = _maxWhitelistedAddresses;\n }\n\n /// @notice Adds the caller to the whitelist\n function addToWhitelist() external {\n require(!whitelistedAddresses[msg.sender], \"Address is already whitelisted\");\n require(numWhitelistedCount < maxWhitelistedAddresses, \"Whitelist is full\");\n\n whitelistedAddresses[msg.sender] = true;\n numWhitelistedCount += 1;\n\n emit AddedToWhitelist(msg.sender);\n }\n\n /// @notice Removes an address from the whitelist\n /// @param _addr The address to be removed\n function removeFromWhitelist(address _addr) external onlyOwner {\n require(whitelistedAddresses[_addr], \"Address is not in the whitelist\");\n\n whitelistedAddresses[_addr] = false;\n numWhitelistedCount -= 1;\n\n emit RemovedFromWhitelist(_addr);\n }\n\n /// @notice Checks if an address is whitelisted\n /// @param _addr The address to check\n function isWhitelisted(address _addr) external view returns (bool) {\n return whitelistedAddresses[_addr];\n }\n\n /// @notice Returns the current number of whitelisted addresses\n function getNumWhitelisted() external view returns (uint256) {\n return numWhitelistedCount;\n }\n\n /// @notice Returns the maximum capacity of the whitelist\n function maxWhitelisted() external view returns (uint256) {\n return maxWhitelistedAddresses;\n }\n}"}
|
| 25 |
+
{"task_id":"FormalEval/8","completion":" address private owner;\n bool private locked;\n\n struct LockInfo {\n uint256 amount;\n uint256 unlockTime;\n }\n\n mapping(address => LockInfo) private locks;\n\n event Deposit(address indexed sender, uint256 amount, uint256 unlockTime);\n event Withdrawal(address indexed sender, uint256 amount);\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Locks ETH until the specified unlockTime\n /// @param unlockTime The timestamp when the funds can be withdrawn\n function deposit(uint256 unlockTime) external payable {\n require(msg.value > 0, \"Amount must be greater than 0\");\n require(unlockTime > block.timestamp, \"Unlock time must be in the future\");\n require(locks[msg.sender].amount == 0, \"Existing lock found, withdraw first\");\n\n locks[msg.sender] = LockInfo({\n amount: msg.value,\n unlockTime: unlockTime\n });\n\n emit Deposit(msg.sender, msg.value, unlockTime);\n }\n\n /// @notice Withdraws all locked ETH if the unlock time has passed\n function withdraw() external nonReentrant {\n LockInfo storage userLock = locks[msg.sender];\n \n require(userLock.amount > 0, \"No locked funds\");\n require(block.timestamp >= userLock.unlockTime, \"Funds are still locked\");\n\n uint256 amountToWithdraw = userLock.amount;\n \n userLock.amount = 0;\n userLock.unlockTime = 0;\n\n (bool success, ) = payable(msg.sender).call{value: amountToWithdraw}(\"\");\n require(success, \"Transfer failed\");\n\n emit Withdrawal(msg.sender, amountToWithdraw);\n }\n\n /// @notice Returns the locked balance for a specific address\n /// @param _addr The address to query\n function getBalance(address _addr) external view returns (uint256) {\n return locks[_addr].amount;\n }\n\n /// @notice Returns the unlock time for a specific address\n /// @param _addr The address to query\n function getUnlockTime(address _addr) external view returns (uint256) {\n return locks[_addr].unlockTime;\n }\n}"}
|
| 26 |
+
{"task_id":"FormalEval/8","completion":" address private owner;\n bool private locked;\n\n mapping(address => uint256) private balances;\n mapping(address => uint256) private unlockTimes;\n\n event Deposit(address indexed sender, uint256 amount, uint256 unlockTime);\n event Withdrawal(address indexed sender, uint256 amount);\n\n modifier nonReentrant() {\n require(!locked, \"ReentrancyGuard: reentrant call\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Locks ETH until the specified unlock time.\n /// @param unlockTime The timestamp when the funds can be withdrawn.\n function deposit(uint256 unlockTime) external payable {\n require(msg.value > 0, \"Amount must be greater than 0\");\n require(unlockTime > block.timestamp, \"Unlock time must be in the future\");\n \n balances[msg.sender] += msg.value;\n unlockTimes[msg.sender] = unlockTime;\n\n emit Deposit(msg.sender, msg.value, unlockTime);\n }\n\n /// @notice Withdraws all locked ETH for the sender if the unlock time has passed.\n function withdraw() external nonReentrant {\n uint256 amount = balances[msg.sender];\n uint256 unlockTime = unlockTimes[msg.sender];\n\n require(amount > 0, \"No funds to withdraw\");\n require(block.timestamp >= unlockTime, \"Funds are still locked\");\n\n balances[msg.sender] = 0;\n unlockTimes[msg.sender] = 0;\n\n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n\n emit Withdrawal(msg.sender, amount);\n }\n\n /// @notice Returns the locked balance for a specific address.\n /// @param _addr The address to query.\n function getBalance(address _addr) external view returns (uint256) {\n return balances[_addr];\n }\n\n /// @notice Returns the unlock time for a specific address.\n /// @param _addr The address to query.\n function getUnlockTime(address _addr) external view returns (uint256) {\n return unlockTimes[_addr];\n }\n}"}
|
| 27 |
+
{"task_id":"FormalEval/8","completion":"\n address private owner;\n bool private locked;\n\n mapping(address => uint256) private balances;\n mapping(address => uint256) private unlockTimes;\n\n event Deposit(address indexed sender, uint256 amount, uint256 unlockTime);\n event Withdrawal(address indexed sender, uint256 amount);\n\n modifier nonReentrant() {\n require(!locked, \"ReentrancyGuard: reentrant call\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Locks ETH until the specified unlockTime\n /// @param unlockTime The timestamp when the funds can be withdrawn\n function deposit(uint256 unlockTime) external payable {\n require(msg.value > 0, \"Amount must be greater than 0\");\n require(unlockTime > block.timestamp, \"Unlock time must be in the future\");\n \n balances[msg.sender] += msg.value;\n unlockTimes[msg.sender] = unlockTime;\n\n emit Deposit(msg.sender, msg.value, unlockTime);\n }\n\n /// @notice Withdraws all locked ETH for the sender\n function withdraw() external nonReentrant {\n uint256 amount = balances[msg.sender];\n uint256 unlockTime = unlockTimes[msg.sender];\n\n require(amount > 0, \"No funds to withdraw\");\n require(block.timestamp >= unlockTime, \"Funds are still locked\");\n\n balances[msg.sender] = 0;\n unlockTimes[msg.sender] = 0;\n\n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n\n emit Withdrawal(msg.sender, amount);\n }\n\n /// @notice Returns the locked balance for a specific address\n /// @param _addr The address to query\n function getBalance(address _addr) external view returns (uint256) {\n return balances[_addr];\n }\n\n /// @notice Returns the unlock time for a specific address\n /// @param _addr The address to query\n function getUnlockTime(address _addr) external view returns (uint256) {\n return unlockTimes[_addr];\n }\n}"}
|
| 28 |
+
{"task_id":"FormalEval/9","completion":" address public owner;\n\n struct Proposal {\n string description;\n uint256 voteCount;\n }\n\n Proposal[] private proposals;\n mapping(uint256 => mapping(address => bool)) private votes;\n bool private locked;\n\n event ProposalCreated(uint256 indexed proposalId, string description);\n event Voted(uint256 indexed proposalId, address indexed voter);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Creates a new proposal\n /// @param description The text description of the proposal\n /// @return proposalId The index of the created proposal\n function createProposal(string calldata description) external onlyOwner returns (uint256) {\n uint256 proposalId = proposals.length;\n proposals.push(Proposal({\n description: description,\n voteCount: 0\n }));\n emit ProposalCreated(proposalId, description);\n return proposalId;\n }\n\n /// @notice Casts a vote for a specific proposal\n /// @param proposalId The ID of the proposal to vote for\n function vote(uint256 proposalId) external nonReentrant {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n require(!votes[proposalId][msg.sender], \"Already voted on this proposal\");\n\n votes[proposalId][msg.sender] = true;\n proposals[proposalId].voteCount += 1;\n\n emit Voted(proposalId, msg.sender);\n }\n\n /// @notice Retrieves proposal details\n /// @param proposalId The ID of the proposal\n /// @return description The proposal description\n /// @return voteCount The current number of votes\n function getProposal(uint256 proposalId) external view returns (string memory, uint256) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n Proposal storage p = proposals[proposalId];\n return (p.description, p.voteCount);\n }\n\n /// @notice Returns the total number of proposals created\n function getProposalCount() external view returns (uint256) {\n return proposals.length;\n }\n\n /// @notice Checks if an address has already voted on a proposal\n /// @param proposalId The ID of the proposal\n /// @param voter The address to check\n function hasVoted(uint256 proposalId, address voter) external view returns (bool) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n return votes[proposalId][voter];\n }\n}"}
|
| 29 |
+
{"task_id":"FormalEval/9","completion":" address public owner;\n\n struct Proposal {\n string description;\n uint256 voteCount;\n }\n\n Proposal[] private proposals;\n mapping(uint256 => mapping(address => bool)) private votes;\n bool private locked;\n\n event ProposalCreated(uint256 indexed proposalId, string description);\n event Voted(uint256 indexed proposalId, address indexed voter);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Creates a new proposal\n /// @param description The text description of the proposal\n /// @return proposalId The ID of the newly created proposal\n function createProposal(string calldata description) external onlyOwner returns (uint256) {\n uint256 proposalId = proposals.length;\n proposals.push(Proposal({\n description: description,\n voteCount: 0\n }));\n emit ProposalCreated(proposalId, description);\n return proposalId;\n }\n\n /// @notice Casts a vote for a specific proposal\n /// @param proposalId The ID of the proposal to vote for\n function vote(uint256 proposalId) external nonReentrant {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n require(!votes[proposalId][msg.sender], \"Already voted on this proposal\");\n\n votes[proposalId][msg.sender] = true;\n proposals[proposalId].voteCount += 1;\n\n emit Voted(proposalId, msg.sender);\n }\n\n /// @notice Retrieves proposal details\n /// @param proposalId The ID of the proposal\n /// @return description The proposal description\n /// @return voteCount The current number of votes\n function getProposal(uint256 proposalId) external view returns (string memory, uint256) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n Proposal storage p = proposals[proposalId];\n return (p.description, p.voteCount);\n }\n\n /// @notice Returns the total number of proposals\n function getProposalCount() external view returns (uint256) {\n return proposals.length;\n }\n\n /// @notice Checks if an address has already voted on a proposal\n /// @param proposalId The ID of the proposal\n /// @param voter The address to check\n function hasVoted(uint256 proposalId, address voter) external view returns (bool) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n return votes[proposalId][voter];\n }\n}"}
|
| 30 |
+
{"task_id":"FormalEval/9","completion":" address public owner;\n\n struct Proposal {\n string description;\n uint256 voteCount;\n }\n\n Proposal[] private proposals;\n mapping(uint256 => mapping(address => bool)) private votes;\n bool private locked;\n\n event ProposalCreated(uint256 indexed proposalId, string description);\n event Voted(uint256 indexed proposalId, address indexed voter);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the contract owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n /// @notice Creates a new proposal\n /// @param description The text description of the proposal\n /// @return proposalId The ID of the newly created proposal\n function createProposal(string calldata description) external onlyOwner returns (uint256) {\n uint256 proposalId = proposals.length;\n proposals.push(Proposal({\n description: description,\n voteCount: 0\n }));\n emit ProposalCreated(proposalId, description);\n return proposalId;\n }\n\n /// @notice Casts a vote for a specific proposal\n /// @param proposalId The ID of the proposal to vote for\n function vote(uint256 proposalId) external nonReentrant {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n require(!votes[proposalId][msg.sender], \"Already voted on this proposal\");\n\n votes[proposalId][msg.sender] = true;\n proposals[proposalId].voteCount += 1;\n\n emit Voted(proposalId, msg.sender);\n }\n\n /// @notice Retrieves details of a proposal\n /// @param proposalId The ID of the proposal\n /// @return description The proposal description\n /// @return voteCount The current number of votes\n function getProposal(uint256 proposalId) external view returns (string memory, uint256) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n Proposal storage p = proposals[proposalId];\n return (p.description, p.voteCount);\n }\n\n /// @notice Returns the total number of proposals\n function getProposalCount() external view returns (uint256) {\n return proposals.length;\n }\n\n /// @notice Checks if an address has voted on a specific proposal\n /// @param proposalId The ID of the proposal\n /// @param voter The address to check\n function hasVoted(uint256 proposalId, address voter) external view returns (bool) {\n require(proposalId < proposals.length, \"Invalid proposal ID\");\n return votes[proposalId][voter];\n }\n}"}
|
| 31 |
+
{"task_id":"FormalEval/10","completion":"\n address[] public owners;\n mapping(address => bool) public isOwner;\n uint256 public numConfirmationsRequired;\n\n struct Transaction {\n address to;\n uint256 value;\n bytes data;\n bool executed;\n uint256 numConfirmations;\n }\n\n Transaction[] public transactions;\n mapping(uint256 => mapping(address => bool)) public isConfirmed;\n\n event SubmitTransaction(address indexed owner, uint256 indexed txIndex, address indexed to, uint256 value, bytes data);\n event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);\n event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);\n event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);\n\n modifier onlyOwner() {\n require(isOwner[msg.sender], \"Not an owner\");\n _;\n }\n\n modifier txExists(uint256 _txIndex) {\n require(_txIndex < transactions.length, \"Transaction does not exist\");\n _;\n }\n\n modifier notExecuted(uint256 _txIndex) {\n require(!transactions[_txIndex].executed, \"Transaction already executed\");\n _;\n }\n\n modifier notConfirmed(uint256 _txIndex) {\n require(!isConfirmed[_txIndex][msg.sender], \"Transaction already confirmed\");\n _;\n }\n\n constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {\n require(_owners.length > 0, \"Owners required\");\n require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length, \"Invalid number of required confirmations\");\n\n for (uint256 i = 0; i < _owners.length; i++) {\n address owner = _owners[i];\n require(owner != address(0), \"Invalid owner\");\n require(!isOwner[owner], \"Owner not unique\");\n\n isOwner[owner] = true;\n owners.push(owner);\n }\n\n numConfirmationsRequired = _numConfirmationsRequired;\n }\n\n receive() external payable {}\n\n function submitTransaction(address _to, uint256 _value, bytes calldata _data) external onlyOwner {\n uint256 txIndex = transactions.length;\n transactions.push(Transaction({\n to: _to,\n value: _value,\n data: _data,\n executed: false,\n numConfirmations: 0\n }));\n emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);\n }\n\n function confirmTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations += 1;\n isConfirmed[_txIndex][msg.sender] = true;\n emit ConfirmTransaction(msg.sender, _txIndex);\n }\n\n function executeTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n require(transaction.numConfirmations >= numConfirmationsRequired, \"Cannot execute: insufficient confirmations\");\n\n transaction.executed = true;\n (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);\n require(success, \"Transaction failed\");\n\n emit ExecuteTransaction(msg.sender, _txIndex);\n }\n\n function revokeConfirmation(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n require(isConfirmed[_txIndex][msg.sender], \"Transaction not confirmed\");\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations -= 1;\n isConfirmed[_txIndex][msg.sender] = false;\n emit RevokeConfirmation(msg.sender, _txIndex);\n }\n\n function getTransactionCount() external view returns (uint256) {\n return transactions.length;\n }\n\n function getTransaction(uint256 _txIndex) external view returns (address to, uint256 value, bytes memory data, bool executed, uint256 numConfirmations) {\n Transaction storage transaction = transactions[_txIndex];\n return (transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations);\n }\n\n function getOwners() external view returns (address[] memory) {\n return owners;\n }\n}"}
|
| 32 |
+
{"task_id":"FormalEval/10","completion":"\n address[] public owners;\n mapping(address => bool) public isOwner;\n uint256 public numConfirmationsRequired;\n\n struct Transaction {\n address to;\n uint256 value;\n bytes data;\n bool executed;\n uint256 numConfirmations;\n }\n\n Transaction[] public transactions;\n mapping(uint256 => mapping(address => bool)) public isConfirmed;\n\n event SubmitTransaction(address indexed owner, uint256 indexed txIndex, address indexed to, uint256 value, bytes data);\n event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);\n event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);\n event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);\n\n modifier onlyOwner() {\n require(isOwner[msg.sender], \"Not an owner\");\n _;\n }\n\n modifier txExists(uint256 _txIndex) {\n require(_txIndex < transactions.length, \"Transaction does not exist\");\n _;\n }\n\n modifier notExecuted(uint256 _txIndex) {\n require(!transactions[_txIndex].executed, \"Transaction already executed\");\n _;\n }\n\n modifier notConfirmed(uint256 _txIndex) {\n require(!isConfirmed[_txIndex][msg.sender], \"Transaction already confirmed\");\n _;\n }\n\n constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {\n require(_owners.length > 0, \"Owners required\");\n require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length, \"Invalid number of required confirmations\");\n\n for (uint256 i = 0; i < _owners.length; i++) {\n address owner = _owners[i];\n require(owner != address(0), \"Invalid owner\");\n require(!isOwner[owner], \"Owner not unique\");\n\n isOwner[owner] = true;\n owners.push(owner);\n }\n\n numConfirmationsRequired = _numConfirmationsRequired;\n }\n\n receive() external payable {}\n\n function submitTransaction(address _to, uint256 _value, bytes calldata _data) external onlyOwner {\n uint256 txIndex = transactions.length;\n transactions.push(Transaction({\n to: _to,\n value: _value,\n data: _data,\n executed: false,\n numConfirmations: 0\n }));\n emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);\n }\n\n function confirmTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations += 1;\n isConfirmed[_txIndex][msg.sender] = true;\n emit ConfirmTransaction(msg.sender, _txIndex);\n }\n\n function executeTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n require(transaction.numConfirmations >= numConfirmationsRequired, \"Cannot execute: insufficient confirmations\");\n\n transaction.executed = true;\n (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);\n require(success, \"Transaction failed\");\n\n emit ExecuteTransaction(msg.sender, _txIndex);\n }\n\n function revokeConfirmation(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n require(isConfirmed[_txIndex][msg.sender], \"Transaction not confirmed\");\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations -= 1;\n isConfirmed[_txIndex][msg.sender] = false;\n emit RevokeConfirmation(msg.sender, _txIndex);\n }\n\n function getTransactionCount() external view returns (uint256) {\n return transactions.length;\n }\n\n function getTransaction(uint256 _txIndex) external view returns (address to, uint256 value, bytes memory data, bool executed, uint256 numConfirmations) {\n Transaction storage transaction = transactions[_txIndex];\n return (transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations);\n }\n\n function getOwners() external view returns (address[] memory) {\n return owners;\n }\n}"}
|
| 33 |
+
{"task_id":"FormalEval/10","completion":"\n address[] public owners;\n mapping(address => bool) public isOwner;\n uint256 public numConfirmationsRequired;\n\n struct Transaction {\n address to;\n uint256 value;\n bytes data;\n bool executed;\n uint256 numConfirmations;\n }\n\n Transaction[] public transactions;\n mapping(uint256 => mapping(address => bool)) public isConfirmed;\n\n event SubmitTransaction(address indexed owner, uint256 indexed txIndex, address indexed to, uint256 value, bytes data);\n event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);\n event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);\n event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);\n\n modifier onlyOwner() {\n require(isOwner[msg.sender], \"Not an owner\");\n _;\n }\n\n modifier txExists(uint256 _txIndex) {\n require(_txIndex < transactions.length, \"Transaction does not exist\");\n _;\n }\n\n modifier notExecuted(uint256 _txIndex) {\n require(!transactions[_txIndex].executed, \"Transaction already executed\");\n _;\n }\n\n modifier notConfirmed(uint256 _txIndex) {\n require(!isConfirmed[_txIndex][msg.sender], \"Transaction already confirmed\");\n _;\n }\n\n constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {\n require(_owners.length > 0, \"Owners required\");\n require(_numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length, \"Invalid number of required confirmations\");\n\n for (uint256 i = 0; i < _owners.length; i++) {\n address owner = _owners[i];\n require(owner != address(0), \"Invalid owner\");\n require(!isOwner[owner], \"Owner not unique\");\n\n isOwner[owner] = true;\n owners.push(owner);\n }\n\n numConfirmationsRequired = _numConfirmationsRequired;\n }\n\n receive() external payable {}\n\n function submitTransaction(address _to, uint256 _value, bytes calldata _data) external onlyOwner {\n uint256 txIndex = transactions.length;\n transactions.push(Transaction({\n to: _to,\n value: _value,\n data: _data,\n executed: false,\n numConfirmations: 0\n }));\n emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);\n }\n\n function confirmTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations += 1;\n isConfirmed[_txIndex][msg.sender] = true;\n emit ConfirmTransaction(msg.sender, _txIndex);\n }\n\n function executeTransaction(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n Transaction storage transaction = transactions[_txIndex];\n require(transaction.numConfirmations >= numConfirmationsRequired, \"Cannot execute: insufficient confirmations\");\n\n transaction.executed = true;\n (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);\n require(success, \"Transaction failed\");\n\n emit ExecuteTransaction(msg.sender, _txIndex);\n }\n\n function revokeConfirmation(uint256 _txIndex) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {\n require(isConfirmed[_txIndex][msg.sender], \"Transaction not confirmed\");\n Transaction storage transaction = transactions[_txIndex];\n transaction.numConfirmations -= 1;\n isConfirmed[_txIndex][msg.sender] = false;\n emit RevokeConfirmation(msg.sender, _txIndex);\n }\n\n function getTransactionCount() external view returns (uint256) {\n return transactions.length;\n }\n\n function getTransaction(uint256 _txIndex) external view returns (address to, uint256 value, bytes memory data, bool executed, uint256 numConfirmations) {\n Transaction storage transaction = transactions[_txIndex];\n return (transaction.to, transaction.value, transaction.data, transaction.executed, transaction.numConfirmations);\n }\n\n function getOwners() external view returns (address[] memory) {\n return owners;\n }\n}"}
|
| 34 |
+
{"task_id":"FormalEval/11","completion":"\n address private immutable _beneficiary;\n uint256 private immutable _start;\n uint256 private immutable _duration;\n uint256 private _released;\n bool private _locked;\n\n event TokensReleased(uint256 amount);\n\n modifier onlyBeneficiary() {\n require(msg.sender == _beneficiary, \"Not the beneficiary\");\n _;\n }\n\n modifier nonReentrant() {\n require(!_locked, \"Reentrancy guard\");\n _locked = true;\n _;\n _locked = false;\n }\n\n constructor(address beneficiary_, uint256 startTime_, uint256 duration_) {\n require(beneficiary_ != address(0), \"Beneficiary is zero address\");\n require(duration_ > 0, \"Duration must be greater than 0\");\n _beneficiary = beneficiary_;\n _start = startTime_;\n _duration = duration_;\n }\n\n receive() external payable {}\n\n function beneficiary() external view returns (address) {\n return _beneficiary;\n }\n\n function start() external view returns (uint256) {\n return _start;\n }\n\n function duration() external view returns (uint256) {\n return _duration;\n }\n\n function released() external view returns (uint256) {\n return _released;\n }\n\n function vestedAmount(uint256 timestamp) public view returns (uint256) {\n uint256 totalBalance = address(this).balance + _released;\n\n if (timestamp < _start) {\n return 0;\n } else if (timestamp >= _start + _duration) {\n return totalBalance;\n } else {\n return (totalBalance * (timestamp - _start)) / _duration;\n }\n }\n\n function releasable() public view returns (uint256) {\n return vestedAmount(block.timestamp) - _released;\n }\n\n function release() external onlyBeneficiary nonReentrant {\n uint256 amount = releasable();\n require(amount > 0, \"No tokens to release\");\n\n _released += amount;\n emit TokensReleased(amount);\n\n (bool success, ) = payable(_beneficiary).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n }\n}"}
|
| 35 |
+
{"task_id":"FormalEval/11","completion":"\n address private immutable _beneficiary;\n uint256 private immutable _start;\n uint256 private immutable _duration;\n uint256 private _released;\n bool private _locked;\n\n event TokensReleased(uint256 amount);\n\n modifier onlyBeneficiary() {\n require(msg.sender == _beneficiary, \"Not authorized\");\n _;\n }\n\n modifier nonReentrant() {\n require(!_locked, \"Reentrancy detected\");\n _locked = true;\n _;\n _locked = false;\n }\n\n constructor(address beneficiary_, uint256 startTime_, uint256 duration_) {\n require(beneficiary_ != address(0), \"Invalid beneficiary\");\n require(duration_ > 0, \"Duration must be > 0\");\n _beneficiary = beneficiary_;\n _start = startTime_;\n _duration = duration_;\n }\n\n receive() external payable {}\n\n function beneficiary() external view returns (address) {\n return _beneficiary;\n }\n\n function start() external view returns (uint256) {\n return _start;\n }\n\n function duration() external view returns (uint256) {\n return _duration;\n }\n\n function released() external view returns (uint256) {\n return _released;\n }\n\n function releasable() public view returns (uint256) {\n return vestedAmount(block.timestamp) - _released;\n }\n\n function release() external onlyBeneficiary nonReentrant {\n uint256 amount = releasable();\n require(amount > 0, \"Nothing to release\");\n\n _released += amount;\n (bool success, ) = payable(_beneficiary).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n\n emit TokensReleased(amount);\n }\n\n function vestedAmount(uint256 timestamp) public view returns (uint256) {\n uint256 totalBalance = address(this).balance + _released;\n\n if (timestamp < _start) {\n return 0;\n } else if (timestamp >= _start + _duration) {\n return totalBalance;\n } else {\n return (totalBalance * (timestamp - _start)) / _duration;\n }\n }\n}"}
|
| 36 |
+
{"task_id":"FormalEval/11","completion":"\n address private immutable _beneficiary;\n uint256 private immutable _start;\n uint256 private immutable _duration;\n uint256 private _released;\n\n event TokensReleased(uint256 amount);\n\n modifier onlyBeneficiary() {\n require(msg.sender == _beneficiary, \"Not the beneficiary\");\n _;\n }\n\n constructor(address beneficiary_, uint256 startTime_, uint256 duration_) {\n require(beneficiary_ != address(0), \"Beneficiary is zero address\");\n require(duration_ > 0, \"Duration must be greater than 0\");\n _beneficiary = beneficiary_;\n _start = startTime_;\n _duration = duration_;\n }\n\n receive() external payable {}\n\n function beneficiary() external view returns (address) {\n return _beneficiary;\n }\n\n function start() external view returns (uint256) {\n return _start;\n }\n\n function duration() external view returns (uint256) {\n return _duration;\n }\n\n function released() external view returns (uint256) {\n return _released;\n }\n\n function vestedAmount(uint256 timestamp) public view returns (uint256) {\n if (timestamp < _start) {\n return 0;\n } else if (timestamp >= _start + _duration) {\n return address(this).balance + _released;\n } else {\n return ((address(this).balance + _released) * (timestamp - _start)) / _duration;\n }\n }\n\n function releasable() public view returns (uint256) {\n return vestedAmount(block.timestamp) - _released;\n }\n\n function release() external onlyBeneficiary {\n uint256 amount = releasable();\n require(amount > 0, \"No tokens to release\");\n\n _released += amount;\n emit TokensReleased(amount);\n\n (bool success, ) = payable(_beneficiary).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n }\n}"}
|
| 37 |
+
{"task_id":"FormalEval/12","completion":" address public seller;\n uint256 public endAt;\n bool public ended;\n\n address public highestBidder;\n uint256 public highestBid;\n\n mapping(address => uint256) public pendingReturns;\n\n event BidPlaced(address indexed bidder, uint256 amount);\n event AuctionEnded(address indexed winner, uint256 amount);\n\n modifier onlyOwner() {\n require(msg.sender == seller, \"Not the seller\");\n _;\n }\n\n constructor(uint256 startingBid, uint256 auctionDuration) {\n seller = msg.sender;\n highestBid = startingBid;\n endAt = block.timestamp + auctionDuration;\n }\n\n /// @notice Place a bid in the auction\n function bid() external payable {\n require(block.timestamp < endAt, \"Auction expired\");\n require(msg.value > highestBid, \"Bid too low\");\n\n if (highestBidder != address(0)) {\n pendingReturns[highestBidder] += highestBid;\n }\n\n highestBidder = msg.sender;\n highestBid = msg.value;\n\n emit BidPlaced(msg.sender, msg.value);\n }\n\n /// @notice Withdraw overbid funds\n function withdraw() external returns (bool) {\n uint256 amount = pendingReturns[msg.sender];\n if (amount > 0) {\n pendingReturns[msg.sender] = 0;\n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n return true;\n }\n return false;\n }\n\n /// @notice End the auction and transfer funds to seller\n function end() external onlyOwner {\n require(block.timestamp >= endAt, \"Auction not yet ended\");\n require(!ended, \"Auction already ended\");\n\n ended = true;\n \n (bool success, ) = payable(seller).call{value: highestBid}(\"\");\n require(success, \"Transfer to seller failed\");\n\n emit AuctionEnded(highestBidder, highestBid);\n }\n\n function getSeller() external view returns (address) { return seller; }\n function getHighestBidder() external view returns (address) { return highestBidder; }\n function getHighestBid() external view returns (uint256) { return highestBid; }\n function getEnded() external view returns (bool) { return ended; }\n function getEndAt() external view returns (uint256) { return endAt; }\n}"}
|
| 38 |
+
{"task_id":"FormalEval/12","completion":" address public seller;\n uint256 public endAt;\n bool public ended;\n\n address public highestBidder;\n uint256 public highestBid;\n\n mapping(address => uint256) public pendingReturns;\n\n event BidPlaced(address indexed bidder, uint256 amount);\n event AuctionEnded(address indexed winner, uint256 amount);\n\n modifier onlyOwner() {\n require(msg.sender == seller, \"Not the seller\");\n _;\n }\n\n constructor(uint256 _startingBid, uint256 _auctionDuration) {\n seller = msg.sender;\n highestBid = _startingBid;\n endAt = block.timestamp + _auctionDuration;\n }\n\n function bid() external payable {\n require(block.timestamp < endAt, \"Auction expired\");\n require(msg.value > highestBid, \"Bid too low\");\n\n if (highestBidder != address(0)) {\n pendingReturns[highestBidder] += highestBid;\n }\n\n highestBidder = msg.sender;\n highestBid = msg.value;\n\n emit BidPlaced(msg.sender, msg.value);\n }\n\n function withdraw() external returns (bool) {\n uint256 amount = pendingReturns[msg.sender];\n if (amount > 0) {\n pendingReturns[msg.sender] = 0;\n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n }\n return true;\n }\n\n function end() external onlyOwner {\n require(block.timestamp >= endAt, \"Auction not yet ended\");\n require(!ended, \"Auction already ended\");\n\n ended = true;\n emit AuctionEnded(highestBidder, highestBid);\n\n (bool success, ) = payable(seller).call{value: highestBid}(\"\");\n require(success, \"Transfer to seller failed\");\n }\n\n function getSeller() external view returns (address) { return seller; }\n function getHighestBidder() external view returns (address) { return highestBidder; }\n function getHighestBid() external view returns (uint256) { return highestBid; }\n function getEnded() external view returns (bool) { return ended; }\n function getEndAt() external view returns (uint256) { return endAt; }\n}"}
|
| 39 |
+
{"task_id":"FormalEval/12","completion":" address public seller;\n uint256 public endAt;\n bool public ended;\n\n address public highestBidder;\n uint256 public highestBid;\n\n mapping(address => uint256) public pendingReturns;\n\n event BidPlaced(address indexed bidder, uint256 amount);\n event AuctionEnded(address indexed winner, uint256 amount);\n\n modifier onlySeller() {\n require(msg.sender == seller, \"Not the seller\");\n _;\n }\n\n constructor(uint256 _startingBid, uint256 _auctionDuration) {\n seller = msg.sender;\n highestBid = _startingBid;\n endAt = block.timestamp + _auctionDuration;\n }\n\n function bid() external payable {\n require(block.timestamp < endAt, \"Auction already ended\");\n require(msg.value > highestBid, \"Bid too low\");\n\n if (highestBidder != address(0)) {\n pendingReturns[highestBidder] += highestBid;\n }\n\n highestBidder = msg.sender;\n highestBid = msg.value;\n\n emit BidPlaced(msg.sender, msg.value);\n }\n\n function withdraw() external returns (bool) {\n uint256 amount = pendingReturns[msg.sender];\n if (amount > 0) {\n pendingReturns[msg.sender] = 0;\n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n }\n return true;\n }\n\n function end() external onlySeller {\n require(block.timestamp >= endAt, \"Auction not yet ended\");\n require(!ended, \"Auction already ended\");\n\n ended = true;\n emit AuctionEnded(highestBidder, highestBid);\n\n (bool success, ) = payable(seller).call{value: highestBid}(\"\");\n require(success, \"Transfer to seller failed\");\n }\n}"}
|
| 40 |
+
{"task_id":"FormalEval/13","completion":" address public owner;\n uint256 public rewardPool;\n bool private locked;\n\n struct StakeInfo {\n uint256 amount;\n uint256 lastUpdateTime;\n uint256 pendingRewards;\n }\n\n mapping(address => StakeInfo) public stakes;\n\n event Staked(address indexed user, uint256 amount);\n event Unstaked(address indexed user, uint256 amount);\n event RewardsClaimed(address indexed user, uint256 amount);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n function _calculateRewards(address _addr) internal view returns (uint256) {\n StakeInfo storage s = stakes[_addr];\n if (s.amount == 0) return 0;\n uint256 timeElapsed = block.timestamp - s.lastUpdateTime;\n return s.pendingRewards + (s.amount * timeElapsed);\n }\n\n function stake() external payable {\n require(msg.value > 0, \"Cannot stake 0\");\n \n _updateRewards(msg.sender);\n stakes[msg.sender].amount += msg.value;\n \n emit Staked(msg.sender, msg.value);\n }\n\n function unstake(uint256 amount) external nonReentrant {\n require(amount > 0, \"Amount must be > 0\");\n require(stakes[msg.sender].amount >= amount, \"Insufficient stake\");\n\n _updateRewards(msg.sender);\n stakes[msg.sender].amount -= amount;\n \n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n \n emit Unstaked(msg.sender, amount);\n }\n\n function claimRewards() external nonReentrant {\n _updateRewards(msg.sender);\n uint256 reward = stakes[msg.sender].pendingRewards;\n require(reward > 0, \"No rewards to claim\");\n require(rewardPool >= reward, \"Insufficient reward pool\");\n\n stakes[msg.sender].pendingRewards = 0;\n rewardPool -= reward;\n\n (bool success, ) = payable(msg.sender).call{value: reward}(\"\");\n require(success, \"Transfer failed\");\n\n emit RewardsClaimed(msg.sender, reward);\n }\n\n function _updateRewards(address _addr) internal {\n stakes[_addr].pendingRewards = _calculateRewards(_addr);\n stakes[_addr].lastUpdateTime = block.timestamp;\n }\n\n function fundRewards() external payable onlyOwner {\n rewardPool += msg.value;\n }\n\n function getStake(address _addr) external view returns (uint256) {\n return stakes[_addr].amount;\n }\n\n function getRewards(address _addr) external view returns (uint256) {\n return _calculateRewards(_addr);\n }\n}"}
|
| 41 |
+
{"task_id":"FormalEval/13","completion":" address public owner;\n uint256 public rewardPool;\n bool private locked;\n\n struct User {\n uint256 stakedAmount;\n uint256 lastUpdateTime;\n uint256 pendingRewards;\n }\n\n mapping(address => User) public users;\n\n event Staked(address indexed user, uint256 amount);\n event Unstaked(address indexed user, uint256 amount);\n event RewardsClaimed(address indexed user, uint256 amount);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n function _calculateRewards(address _addr) internal view returns (uint256) {\n User storage user = users[_addr];\n if (user.stakedAmount == 0) return 0;\n uint256 timeElapsed = block.timestamp - user.lastUpdateTime;\n return user.stakedAmount * timeElapsed;\n }\n\n function _updateRewards(address _addr) internal {\n users[_addr].pendingRewards += _calculateRewards(_addr);\n users[_addr].lastUpdateTime = block.timestamp;\n }\n\n function stake() external payable {\n require(msg.value > 0, \"Cannot stake 0\");\n _updateRewards(msg.sender);\n users[msg.sender].stakedAmount += msg.value;\n emit Staked(msg.sender, msg.value);\n }\n\n function unstake(uint256 amount) external nonReentrant {\n require(amount > 0, \"Amount must be > 0\");\n require(users[msg.sender].stakedAmount >= amount, \"Insufficient stake\");\n \n _updateRewards(msg.sender);\n users[msg.sender].stakedAmount -= amount;\n \n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n \n emit Unstaked(msg.sender, amount);\n }\n\n function claimRewards() external nonReentrant {\n _updateRewards(msg.sender);\n uint256 reward = users[msg.sender].pendingRewards;\n require(reward > 0, \"No rewards to claim\");\n require(rewardPool >= reward, \"Insufficient reward pool\");\n\n users[msg.sender].pendingRewards = 0;\n rewardPool -= reward;\n\n (bool success, ) = payable(msg.sender).call{value: reward}(\"\");\n require(success, \"Reward transfer failed\");\n\n emit RewardsClaimed(msg.sender, reward);\n }\n\n function fundRewards() external payable onlyOwner {\n rewardPool += msg.value;\n }\n\n function getStake(address _addr) external view returns (uint256) {\n return users[_addr].stakedAmount;\n }\n\n function getRewards(address _addr) external view returns (uint256) {\n return users[_addr].pendingRewards + _calculateRewards(_addr);\n }\n}"}
|
| 42 |
+
{"task_id":"FormalEval/13","completion":" address public owner;\n uint256 public rewardPool;\n bool private locked;\n\n struct StakeInfo {\n uint256 amount;\n uint256 lastUpdateTime;\n uint256 pendingRewards;\n }\n\n mapping(address => StakeInfo) public stakes;\n\n event Staked(address indexed user, uint256 amount);\n event Unstaked(address indexed user, uint256 amount);\n event RewardsClaimed(address indexed user, uint256 amount);\n\n modifier onlyOwner() {\n require(msg.sender == owner, \"Not the owner\");\n _;\n }\n\n modifier nonReentrant() {\n require(!locked, \"Reentrancy detected\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor() {\n owner = msg.sender;\n }\n\n function _calculateRewards(address _addr) internal view returns (uint256) {\n StakeInfo storage s = stakes[_addr];\n if (s.amount == 0) return 0;\n uint256 timeElapsed = block.timestamp - s.lastUpdateTime;\n return s.pendingRewards + (s.amount * timeElapsed);\n }\n\n function stake() external payable {\n require(msg.value > 0, \"Cannot stake 0\");\n \n _updateRewards(msg.sender);\n stakes[msg.sender].amount += msg.value;\n \n emit Staked(msg.sender, msg.value);\n }\n\n function unstake(uint256 amount) external nonReentrant {\n require(amount > 0, \"Amount must be > 0\");\n require(stakes[msg.sender].amount >= amount, \"Insufficient stake\");\n\n _updateRewards(msg.sender);\n stakes[msg.sender].amount -= amount;\n \n (bool success, ) = payable(msg.sender).call{value: amount}(\"\");\n require(success, \"Transfer failed\");\n \n emit Unstaked(msg.sender, amount);\n }\n\n function claimRewards() external nonReentrant {\n _updateRewards(msg.sender);\n uint256 reward = stakes[msg.sender].pendingRewards;\n require(reward > 0, \"No rewards to claim\");\n require(rewardPool >= reward, \"Insufficient reward pool\");\n\n stakes[msg.sender].pendingRewards = 0;\n rewardPool -= reward;\n\n (bool success, ) = payable(msg.sender).call{value: reward}(\"\");\n require(success, \"Reward transfer failed\");\n\n emit RewardsClaimed(msg.sender, reward);\n }\n\n function _updateRewards(address _addr) internal {\n stakes[_addr].pendingRewards = _calculateRewards(_addr);\n stakes[_addr].lastUpdateTime = block.timestamp;\n }\n\n function fundRewards() external payable onlyOwner {\n rewardPool += msg.value;\n }\n\n function getStake(address _addr) external view returns (uint256) {\n return stakes[_addr].amount;\n }\n\n function getRewards(address _addr) external view returns (uint256) {\n return _calculateRewards(_addr);\n }\n}"}
|
| 43 |
+
{"task_id":"FormalEval/14","completion":" address public immutable tokenA;\n address public immutable tokenB;\n\n uint256 public reserveA;\n uint256 public reserveB;\n uint256 public totalSupply;\n\n mapping(address => uint256) public balanceOf;\n\n bool private locked;\n\n event LiquidityAdded(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event LiquidityRemoved(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event Swap(address indexed user, address tokenIn, uint256 amountIn, uint256 amountOut);\n\n modifier nonReentrant() {\n require(!locked, \"ReentrancyGuard: reentrant call\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor(address _tokenA, address _tokenB) {\n require(_tokenA != address(0) && _tokenB != address(0), \"Invalid token address\");\n require(_tokenA != _tokenB, \"Tokens must be different\");\n tokenA = _tokenA;\n tokenB = _tokenB;\n }\n\n function _sqrt(uint256 y) internal pure returns (uint256 z) {\n if (y > 3) {\n z = y;\n uint256 x = y / 2 + 1;\n while (x < z) {\n z = x;\n x = (y / x + x) / 2;\n }\n } else if (y != 0) {\n z = 1;\n }\n }\n\n function getReserves() external view returns (uint256, uint256) {\n return (reserveA, reserveB);\n }\n\n function addLiquidity(uint256 amountA, uint256 amountB) external nonReentrant returns (uint256 shares) {\n IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);\n IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);\n\n if (totalSupply == 0) {\n shares = _sqrt(amountA * amountB);\n } else {\n uint256 sharesA = (amountA * totalSupply) / reserveA;\n uint256 sharesB = (amountB * totalSupply) / reserveB;\n shares = sharesA < sharesB ? sharesA : sharesB;\n require(shares > 0, \"Insufficient liquidity minted\");\n }\n\n balanceOf[msg.sender] += shares;\n totalSupply += shares;\n reserveA += amountA;\n reserveB += amountB;\n\n emit LiquidityAdded(msg.sender, amountA, amountB, shares);\n }\n\n function removeLiquidity(uint256 shares) external nonReentrant returns (uint256 amountA, uint256 amountB) {\n require(shares <= balanceOf[msg.sender], \"Insufficient shares\");\n\n amountA = (shares * reserveA) / totalSupply;\n amountB = (shares * reserveB) / totalSupply;\n\n balanceOf[msg.sender] -= shares;\n totalSupply -= shares;\n reserveA -= amountA;\n reserveB -= amountB;\n\n IERC20(tokenA).transfer(msg.sender, amountA);\n IERC20(tokenB).transfer(msg.sender, amountB);\n\n emit LiquidityRemoved(msg.sender, amountA, amountB, shares);\n }\n\n function swap(address tokenIn, uint256 amountIn) external nonReentrant returns (uint256 amountOut) {\n require(tokenIn == tokenA || tokenIn == tokenB, \"Invalid token\");\n require(amountIn > 0, \"Amount must be > 0\");\n\n bool isA = tokenIn == tokenA;\n (uint256 resIn, uint256 resOut) = isA ? (reserveA, reserveB) : (reserveB, reserveA);\n address tokenOut = isA ? tokenB : tokenA;\n\n IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);\n\n uint256 amountInWithFee = (amountIn * 997) / 1000;\n amountOut = (resOut * amountInWithFee) / (resIn + amountInWithFee);\n\n if (isA) {\n reserveA += amountIn;\n reserveB -= amountOut;\n } else {\n reserveB += amountIn;\n reserveA -= amountOut;\n }\n\n IERC20(tokenOut).transfer(msg.sender, amountOut);\n\n emit Swap(msg.sender, tokenIn, amountIn, amountOut);\n }\n}"}
|
| 44 |
+
{"task_id":"FormalEval/14","completion":" address public immutable tokenA;\n address public immutable tokenB;\n\n uint256 public reserveA;\n uint256 public reserveB;\n uint256 public totalSupply;\n\n mapping(address => uint256) public balanceOf;\n\n bool private locked;\n\n event LiquidityAdded(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event LiquidityRemoved(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event Swap(address indexed user, address tokenIn, uint256 amountIn, uint256 amountOut);\n\n modifier nonReentrant() {\n require(!locked, \"ReentrancyGuard: reentrant call\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor(address _tokenA, address _tokenB) {\n require(_tokenA != address(0) && _tokenB != address(0), \"Invalid token addresses\");\n require(_tokenA != _tokenB, \"Tokens must be different\");\n tokenA = _tokenA;\n tokenB = _tokenB;\n }\n\n function _sqrt(uint256 y) internal pure returns (uint256 z) {\n if (y > 3) {\n z = y;\n uint256 x = y / 2 + 1;\n while (x < z) {\n z = x;\n x = (y / x + x) / 2;\n }\n } else if (y != 0) {\n z = 1;\n }\n }\n\n function addLiquidity(uint256 amountA, uint256 amountB) external nonReentrant returns (uint256 shares) {\n IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);\n IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);\n\n if (totalSupply == 0) {\n shares = _sqrt(amountA * amountB);\n } else {\n uint256 sharesA = (amountA * totalSupply) / reserveA;\n uint256 sharesB = (amountB * totalSupply) / reserveB;\n shares = sharesA < sharesB ? sharesA : sharesB;\n require(shares > 0, \"Insufficient liquidity minted\");\n }\n\n balanceOf[msg.sender] += shares;\n totalSupply += shares;\n reserveA += amountA;\n reserveB += amountB;\n\n emit LiquidityAdded(msg.sender, amountA, amountB, shares);\n }\n\n function removeLiquidity(uint256 shares) external nonReentrant returns (uint256 amountA, uint256 amountB) {\n require(shares <= balanceOf[msg.sender], \"Insufficient shares\");\n\n amountA = (shares * reserveA) / totalSupply;\n amountB = (shares * reserveB) / totalSupply;\n\n balanceOf[msg.sender] -= shares;\n totalSupply -= shares;\n reserveA -= amountA;\n reserveB -= amountB;\n\n IERC20(tokenA).transfer(msg.sender, amountA);\n IERC20(tokenB).transfer(msg.sender, amountB);\n\n emit LiquidityRemoved(msg.sender, amountA, amountB, shares);\n }\n\n function swap(address tokenIn, uint256 amountIn) external nonReentrant returns (uint256 amountOut) {\n require(tokenIn == tokenA || tokenIn == tokenB, \"Invalid token\");\n require(amountIn > 0, \"Invalid amount\");\n\n bool isTokenA = tokenIn == tokenA;\n (uint256 resIn, uint256 resOut) = isTokenA ? (reserveA, reserveB) : (reserveB, reserveA);\n address tokenOut = isTokenA ? tokenB : tokenA;\n\n IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);\n\n uint256 amountInWithFee = (amountIn * 997) / 1000;\n amountOut = (resOut * amountInWithFee) / (resIn + amountInWithFee);\n\n if (isTokenA) {\n reserveA += amountIn;\n reserveB -= amountOut;\n } else {\n reserveB += amountIn;\n reserveA -= amountOut;\n }\n\n IERC20(tokenOut).transfer(msg.sender, amountOut);\n emit Swap(msg.sender, tokenIn, amountIn, amountOut);\n }\n\n function getReserves() external view returns (uint256, uint256) {\n return (reserveA, reserveB);\n }\n\n function totalShares() external view returns (uint256) {\n return totalSupply;\n }\n}"}
|
| 45 |
+
{"task_id":"FormalEval/14","completion":" address public immutable tokenA;\n address public immutable tokenB;\n\n uint256 public reserveA;\n uint256 public reserveB;\n uint256 public totalSupply;\n\n mapping(address => uint256) public balanceOf;\n\n bool private locked;\n\n event LiquidityAdded(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event LiquidityRemoved(address indexed provider, uint256 amountA, uint256 amountB, uint256 shares);\n event Swap(address indexed user, address tokenIn, uint256 amountIn, uint256 amountOut);\n\n modifier nonReentrant() {\n require(!locked, \"ReentrancyGuard: reentrant call\");\n locked = true;\n _;\n locked = false;\n }\n\n constructor(address _tokenA, address _tokenB) {\n require(_tokenA != address(0) && _tokenB != address(0), \"Invalid token addresses\");\n require(_tokenA != _tokenB, \"Tokens must be different\");\n tokenA = _tokenA;\n tokenB = _tokenB;\n }\n\n function _sqrt(uint256 y) internal pure returns (uint256 z) {\n if (y > 3) {\n z = y;\n uint256 x = y / 2 + 1;\n while (x < z) {\n z = x;\n x = (y / x + x) / 2;\n }\n } else if (y != 0) {\n z = 1;\n }\n }\n\n function getReserves() external view returns (uint256, uint256) {\n return (reserveA, reserveB);\n }\n\n function addLiquidity(uint256 amountA, uint256 amountB) external nonReentrant returns (uint256 shares) {\n IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);\n IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);\n\n if (totalSupply == 0) {\n shares = _sqrt(amountA * amountB);\n } else {\n uint256 sharesA = (amountA * totalSupply) / reserveA;\n uint256 sharesB = (amountB * totalSupply) / reserveB;\n shares = sharesA < sharesB ? sharesA : sharesB;\n require(shares > 0, \"Insufficient liquidity minted\");\n }\n\n balanceOf[msg.sender] += shares;\n totalSupply += shares;\n reserveA += amountA;\n reserveB += amountB;\n\n emit LiquidityAdded(msg.sender, amountA, amountB, shares);\n }\n\n function removeLiquidity(uint256 shares) external nonReentrant returns (uint256 amountA, uint256 amountB) {\n require(shares > 0 && balanceOf[msg.sender] >= shares, \"Invalid shares\");\n\n amountA = (shares * reserveA) / totalSupply;\n amountB = (shares * reserveB) / totalSupply;\n\n balanceOf[msg.sender] -= shares;\n totalSupply -= shares;\n reserveA -= amountA;\n reserveB -= amountB;\n\n IERC20(tokenA).transfer(msg.sender, amountA);\n IERC20(tokenB).transfer(msg.sender, amountB);\n\n emit LiquidityRemoved(msg.sender, amountA, amountB, shares);\n }\n\n function swap(address tokenIn, uint256 amountIn) external nonReentrant returns (uint256 amountOut) {\n require(tokenIn == tokenA || tokenIn == tokenB, \"Invalid token\");\n require(amountIn > 0, \"Invalid amount\");\n\n bool isA = tokenIn == tokenA;\n (uint256 resIn, uint256 resOut) = isA ? (reserveA, reserveB) : (reserveB, reserveA);\n address tokenOut = isA ? tokenB : tokenA;\n\n IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);\n\n uint256 amountInWithFee = (amountIn * 997) / 1000;\n amountOut = (resOut * amountInWithFee) / (resIn + amountInWithFee);\n\n if (isA) {\n reserveA += amountIn;\n reserveB -= amountOut;\n } else {\n reserveB += amountIn;\n reserveA -= amountOut;\n }\n\n IERC20(tokenOut).transfer(msg.sender, amountOut);\n\n emit Swap(msg.sender, tokenIn, amountIn, amountOut);\n }\n}"}
|
benchmarks/samples.jsonl_results.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
biome.json
CHANGED
|
@@ -1,55 +1,53 @@
|
|
| 1 |
{
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
| 3 |
+
"vcs": {
|
| 4 |
+
"enabled": true,
|
| 5 |
+
"clientKind": "git",
|
| 6 |
+
"useIgnoreFile": true
|
| 7 |
+
},
|
| 8 |
+
"files": {
|
| 9 |
+
"includes": ["**", "!!**/dist"]
|
| 10 |
+
},
|
| 11 |
+
"formatter": {
|
| 12 |
+
"enabled": true,
|
| 13 |
+
"lineWidth": 120,
|
| 14 |
+
"indentStyle": "space"
|
| 15 |
+
},
|
| 16 |
+
"linter": {
|
| 17 |
+
"enabled": true,
|
| 18 |
+
"rules": {
|
| 19 |
+
"recommended": true,
|
| 20 |
+
"suspicious": {
|
| 21 |
+
"noUnknownAtRules": "off",
|
| 22 |
+
"noArrayIndexKey": "off",
|
| 23 |
+
"noExplicitAny": "off",
|
| 24 |
+
"noAssignInExpressions": "off"
|
| 25 |
+
},
|
| 26 |
+
"complexity": {
|
| 27 |
+
"noStaticOnlyClass": "off"
|
| 28 |
+
},
|
| 29 |
+
"correctness": {
|
| 30 |
+
"useExhaustiveDependencies": "off",
|
| 31 |
+
"useParseIntRadix": "off"
|
| 32 |
+
},
|
| 33 |
+
"a11y": "off",
|
| 34 |
+
"style": {
|
| 35 |
+
"noNonNullAssertion": "off",
|
| 36 |
+
"useTemplate": "off"
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"javascript": {
|
| 41 |
+
"formatter": {
|
| 42 |
+
"quoteStyle": "double"
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"assist": {
|
| 46 |
+
"enabled": true,
|
| 47 |
+
"actions": {
|
| 48 |
+
"source": {
|
| 49 |
+
"organizeImports": "on"
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
|
|
|
|
|
|
| 53 |
}
|
package-lock.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
"license": "ISC",
|
| 12 |
"dependencies": {
|
| 13 |
"@hono/node-server": "^2.0.3",
|
| 14 |
-
"@langchain/anthropic": "^1.
|
| 15 |
"@langchain/core": "^1.1.45",
|
| 16 |
"@langchain/google-genai": "^2.1.31",
|
| 17 |
"@langchain/langgraph": "^1.3.2",
|
|
@@ -36,9 +36,9 @@
|
|
| 36 |
}
|
| 37 |
},
|
| 38 |
"node_modules/@anthropic-ai/sdk": {
|
| 39 |
-
"version": "0.
|
| 40 |
-
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.
|
| 41 |
-
"integrity": "sha512-
|
| 42 |
"license": "MIT",
|
| 43 |
"dependencies": {
|
| 44 |
"json-schema-to-ts": "^3.1.1",
|
|
@@ -199,23 +199,25 @@
|
|
| 199 |
}
|
| 200 |
},
|
| 201 |
"node_modules/@langchain/anthropic": {
|
| 202 |
-
"version": "1.4.
|
| 203 |
-
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.
|
| 204 |
-
"integrity": "sha512-
|
| 205 |
"license": "MIT",
|
| 206 |
"dependencies": {
|
| 207 |
-
"@anthropic-ai/sdk": "^0.
|
| 208 |
"zod": "^3.25.76 || ^4"
|
| 209 |
},
|
| 210 |
"engines": {
|
| 211 |
"node": ">=20"
|
| 212 |
},
|
| 213 |
"peerDependencies": {
|
| 214 |
-
"@langchain/core": "^1.1.
|
| 215 |
}
|
| 216 |
},
|
| 217 |
"node_modules/@langchain/core": {
|
| 218 |
-
"version": "1.1.
|
|
|
|
|
|
|
| 219 |
"license": "MIT",
|
| 220 |
"dependencies": {
|
| 221 |
"@cfworker/json-schema": "^4.0.2",
|
|
|
|
| 11 |
"license": "ISC",
|
| 12 |
"dependencies": {
|
| 13 |
"@hono/node-server": "^2.0.3",
|
| 14 |
+
"@langchain/anthropic": "^1.4.1",
|
| 15 |
"@langchain/core": "^1.1.45",
|
| 16 |
"@langchain/google-genai": "^2.1.31",
|
| 17 |
"@langchain/langgraph": "^1.3.2",
|
|
|
|
| 36 |
}
|
| 37 |
},
|
| 38 |
"node_modules/@anthropic-ai/sdk": {
|
| 39 |
+
"version": "0.103.0",
|
| 40 |
+
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.103.0.tgz",
|
| 41 |
+
"integrity": "sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==",
|
| 42 |
"license": "MIT",
|
| 43 |
"dependencies": {
|
| 44 |
"json-schema-to-ts": "^3.1.1",
|
|
|
|
| 199 |
}
|
| 200 |
},
|
| 201 |
"node_modules/@langchain/anthropic": {
|
| 202 |
+
"version": "1.4.1",
|
| 203 |
+
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.1.tgz",
|
| 204 |
+
"integrity": "sha512-h3b6hxThcfh0WdmpuWr+qBi74MN+0BpNI/4H681vwXxbD3hLr2qMYN6ghqcPQhCxGJjg8ufs85qu2/ldSWonYQ==",
|
| 205 |
"license": "MIT",
|
| 206 |
"dependencies": {
|
| 207 |
+
"@anthropic-ai/sdk": "^0.103.0",
|
| 208 |
"zod": "^3.25.76 || ^4"
|
| 209 |
},
|
| 210 |
"engines": {
|
| 211 |
"node": ">=20"
|
| 212 |
},
|
| 213 |
"peerDependencies": {
|
| 214 |
+
"@langchain/core": "^1.1.49"
|
| 215 |
}
|
| 216 |
},
|
| 217 |
"node_modules/@langchain/core": {
|
| 218 |
+
"version": "1.1.49",
|
| 219 |
+
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.49.tgz",
|
| 220 |
+
"integrity": "sha512-7wkN3Qv/qZqsY0p3h48CNu6E6y5GMYatYxj+JrX4uVNBiqIVQm1Z528QrmayJWVW9SQTQicqRNoyTCzl+K9F8Q==",
|
| 221 |
"license": "MIT",
|
| 222 |
"dependencies": {
|
| 223 |
"@cfworker/json-schema": "^4.0.2",
|
package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
"start": "npm run build && node dist/index.js",
|
| 12 |
"server": "npm run build && node dist/server.js",
|
| 13 |
"dev:server": "npx tsx src/server.ts",
|
|
|
|
| 14 |
"postinstall": "patch-package"
|
| 15 |
},
|
| 16 |
"repository": {
|
|
@@ -33,7 +34,7 @@
|
|
| 33 |
},
|
| 34 |
"dependencies": {
|
| 35 |
"@hono/node-server": "^2.0.3",
|
| 36 |
-
"@langchain/anthropic": "^1.
|
| 37 |
"@langchain/core": "^1.1.45",
|
| 38 |
"@langchain/google-genai": "^2.1.31",
|
| 39 |
"@langchain/langgraph": "^1.3.2",
|
|
|
|
| 11 |
"start": "npm run build && node dist/index.js",
|
| 12 |
"server": "npm run build && node dist/server.js",
|
| 13 |
"dev:server": "npx tsx src/server.ts",
|
| 14 |
+
"benchmark": "npx tsx benchmarks/run-formal-eval.ts",
|
| 15 |
"postinstall": "patch-package"
|
| 16 |
},
|
| 17 |
"repository": {
|
|
|
|
| 34 |
},
|
| 35 |
"dependencies": {
|
| 36 |
"@hono/node-server": "^2.0.3",
|
| 37 |
+
"@langchain/anthropic": "^1.4.1",
|
| 38 |
"@langchain/core": "^1.1.45",
|
| 39 |
"@langchain/google-genai": "^2.1.31",
|
| 40 |
"@langchain/langgraph": "^1.3.2",
|
src/agents/auditor/agent.ts
CHANGED
|
@@ -1,61 +1,27 @@
|
|
| 1 |
import fs from "node:fs";
|
| 2 |
-
import path from "node:path";
|
| 3 |
|
| 4 |
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
| 5 |
import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
|
| 6 |
import { z } from "zod";
|
| 7 |
|
| 8 |
-
import { logger } from "../../logger.ts";
|
| 9 |
import { createLLM } from "../../config/llm.ts";
|
| 10 |
-
import { emitStep } from "../../logger.ts";
|
| 11 |
-
import {
|
| 12 |
-
import { AuditorState, JudgeReviewSchema, CandidateFindingSchema } from "./state.ts";
|
| 13 |
-
import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
|
| 14 |
-
import { buildRepoTree } from "./tools/repo-tree-tool.ts";
|
| 15 |
import {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
} from "./
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
const
|
| 29 |
-
|
| 30 |
-
const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
|
| 31 |
-
if (depth > MAX_DEPTH) return;
|
| 32 |
-
|
| 33 |
-
let entries: fs.Dirent[];
|
| 34 |
-
try {
|
| 35 |
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
| 36 |
-
} catch {
|
| 37 |
-
return;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
for (const entry of entries) {
|
| 41 |
-
if (entry.isDirectory()) {
|
| 42 |
-
if (!SKIP_DIRS.has(entry.name)) {
|
| 43 |
-
walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
|
| 44 |
-
}
|
| 45 |
-
} else if (entry.isFile()) {
|
| 46 |
-
const fullPath = path.join(dir, entry.name);
|
| 47 |
-
const ext = path.extname(entry.name).toLowerCase();
|
| 48 |
-
const base = path.basename(entry.name, ext).toLowerCase();
|
| 49 |
-
|
| 50 |
-
if (ext === SOL_EXT) {
|
| 51 |
-
const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
|
| 52 |
-
if (!isTest) solFiles.push(fullPath);
|
| 53 |
-
} else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
|
| 54 |
-
docFiles.push(fullPath);
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
};
|
| 59 |
|
| 60 |
const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
| 61 |
emitStep({ agent: "auditor", step: "scope", status: "running" });
|
|
@@ -68,18 +34,48 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 68 |
|
| 69 |
const fileTree = buildRepoTree(state.repoPath);
|
| 70 |
|
| 71 |
-
logger.info(
|
|
|
|
|
|
|
| 72 |
logger.debug(`[Auditor] defineScope: arquivos Solidity: ${JSON.stringify(solFiles)}`);
|
| 73 |
logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
|
| 74 |
logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
|
| 75 |
|
| 76 |
emitStep({ agent: "auditor", step: "scope", status: "done" });
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
};
|
| 79 |
|
| 80 |
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
| 81 |
emitStep({ agent: "auditor", step: "ctx", status: "running" });
|
| 82 |
-
logger.info(
|
|
|
|
|
|
|
| 83 |
|
| 84 |
const readFile = (filePath: string): string => {
|
| 85 |
try {
|
|
@@ -89,102 +85,120 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 89 |
}
|
| 90 |
};
|
| 91 |
|
| 92 |
-
// Read and analyze each Solidity file
|
| 93 |
const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
|
| 94 |
for (const filePath of state.scope) {
|
| 95 |
const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
|
| 96 |
if (!source) continue;
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
solidityEntries.push({ filePath, source, analysis });
|
| 99 |
}
|
| 100 |
|
| 101 |
-
// Read documentation files
|
| 102 |
const docEntries: { filePath: string; content: string }[] = [];
|
| 103 |
for (const filePath of state.docs) {
|
| 104 |
const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
|
| 105 |
if (content) docEntries.push({ filePath, content });
|
| 106 |
}
|
| 107 |
|
| 108 |
-
// Build the LLM input
|
| 109 |
const parts: string[] = [];
|
| 110 |
|
| 111 |
if (docEntries.length > 0) {
|
| 112 |
-
parts.push("##
|
| 113 |
for (const { filePath, content } of docEntries) {
|
| 114 |
parts.push(`### ${filePath}\n${content}`);
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
| 118 |
-
parts.push(
|
| 119 |
-
for (const { filePath, analysis } of solidityEntries) {
|
| 120 |
-
parts.push(`### ${filePath}\n${analysis}`);
|
| 121 |
-
}
|
| 122 |
|
| 123 |
-
parts.push("##
|
| 124 |
-
for (const {
|
| 125 |
-
parts.push(
|
| 126 |
}
|
| 127 |
|
| 128 |
-
const model =
|
| 129 |
-
const result = await model.invoke([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
logger.info(`[Auditor] gatherContext: contexto construído (${parts.join("\n\n").length} caracteres)`);
|
| 132 |
logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
|
|
|
|
|
|
|
| 133 |
|
| 134 |
emitStep({ agent: "auditor", step: "ctx", status: "done" });
|
| 135 |
-
|
|
|
|
| 136 |
};
|
| 137 |
|
| 138 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 139 |
-
const model =
|
| 140 |
-
|
| 141 |
-
const
|
| 142 |
-
state.judgeReviews.length > 0
|
| 143 |
-
? state.judgeReviews
|
| 144 |
-
.map((r, i) => {
|
| 145 |
-
const title = state.candidateFindings[i]?.title ?? `Finding ${i + 1}`;
|
| 146 |
-
return `- "${title}": ${r.isFalsePositive ? "FALSE POSITIVE" : "TRUE POSITIVE"}\n Judge: ${r.review}`;
|
| 147 |
-
})
|
| 148 |
-
.join("\n")
|
| 149 |
-
: null;
|
| 150 |
|
| 151 |
logger.info(
|
| 152 |
`[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
|
| 153 |
);
|
|
|
|
| 154 |
emitStep({ agent: "auditor", step: "find", status: "running", detail: `iter ${state.reflectionCount + 1}` });
|
| 155 |
|
| 156 |
-
const
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
})
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
|
|
|
| 188 |
logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
|
| 189 |
|
| 190 |
emitStep({ agent: "auditor", step: "find", status: "done" });
|
|
@@ -203,30 +217,46 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 203 |
};
|
| 204 |
}
|
| 205 |
|
| 206 |
-
const model =
|
| 207 |
|
| 208 |
-
logger.info(
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
const
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
} catch {
|
| 216 |
-
source = "";
|
| 217 |
-
}
|
| 218 |
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
const confirmedEntries = state.candidateFindings
|
| 232 |
.map((finding, i) => ({ finding, review: reviews[i] }))
|
|
|
|
| 1 |
import fs from "node:fs";
|
|
|
|
| 2 |
|
| 3 |
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
| 4 |
import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
|
| 5 |
import { z } from "zod";
|
| 6 |
|
|
|
|
| 7 |
import { createLLM } from "../../config/llm.ts";
|
| 8 |
+
import { emitStep, logger } from "../../logger.ts";
|
| 9 |
+
import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
|
|
|
|
|
|
|
|
|
|
| 10 |
import {
|
| 11 |
+
FIND_VULNERABILITIES_PROMPT,
|
| 12 |
+
GATHER_CONTEXT_PROMPT,
|
| 13 |
+
JUDGE_FINDINGS_PROMPT,
|
| 14 |
+
RANK_FILES_PROMPT,
|
| 15 |
+
REFINE_VULNERABILITIES_PROMPT,
|
| 16 |
+
} from "./prompts.ts";
|
| 17 |
+
import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
|
| 18 |
+
import { buildRepoTree } from "./tools/repo-tree/tool.ts";
|
| 19 |
+
import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
|
| 20 |
+
import { buildReviewBlocks, matchLines, walkDirectory } from "./utils.ts";
|
| 21 |
+
|
| 22 |
+
const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
|
| 23 |
+
const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
|
| 24 |
+
const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
| 27 |
emitStep({ agent: "auditor", step: "scope", status: "running" });
|
|
|
|
| 34 |
|
| 35 |
const fileTree = buildRepoTree(state.repoPath);
|
| 36 |
|
| 37 |
+
logger.info(
|
| 38 |
+
`[Auditor] defineScope: encontrado(s) ${solFiles.length} arquivo(s) Solidity e ${docFiles.length} arquivo(s) de documentação`,
|
| 39 |
+
);
|
| 40 |
logger.debug(`[Auditor] defineScope: arquivos Solidity: ${JSON.stringify(solFiles)}`);
|
| 41 |
logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
|
| 42 |
logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
|
| 43 |
|
| 44 |
emitStep({ agent: "auditor", step: "scope", status: "done" });
|
| 45 |
+
|
| 46 |
+
logger.info("[Auditor] defineScope: rankeando arquivos por importância");
|
| 47 |
+
|
| 48 |
+
const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
|
| 49 |
+
const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
|
| 50 |
+
|
| 51 |
+
const { rankings } = await rankingModel.invoke([
|
| 52 |
+
new SystemMessage({ content: [{ type: "text", text: RANK_FILES_PROMPT, cache_control: { type: "ephemeral" } }] }),
|
| 53 |
+
new HumanMessage(
|
| 54 |
+
`Árvore de arquivos:\n\`\`\`\n${fileTree}\n\`\`\`\n\nArquivos Solidity para classificar:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
|
| 55 |
+
),
|
| 56 |
+
]);
|
| 57 |
+
|
| 58 |
+
const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
|
| 59 |
+
logger.info(
|
| 60 |
+
`[Auditor] defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} — ${r.reasoning}`).join("\n")}`,
|
| 61 |
+
);
|
| 62 |
+
|
| 63 |
+
const importantFiles = sorted.filter((r) => r.importance >= MIN_FILE_IMPORTANCE).map((r) => r.filePath);
|
| 64 |
+
const skipped = solFiles.length - importantFiles.length;
|
| 65 |
+
if (skipped > 0) {
|
| 66 |
+
logger.info(
|
| 67 |
+
`[Auditor] defineScope: pulando ${skipped} arquivo(s) de baixa importância (importância < ${MIN_FILE_IMPORTANCE})`,
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return { scope: importantFiles, docs: docFiles, fileTree, fileRankings: sorted };
|
| 72 |
};
|
| 73 |
|
| 74 |
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
| 75 |
emitStep({ agent: "auditor", step: "ctx", status: "running" });
|
| 76 |
+
logger.info(
|
| 77 |
+
`[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`,
|
| 78 |
+
);
|
| 79 |
|
| 80 |
const readFile = (filePath: string): string => {
|
| 81 |
try {
|
|
|
|
| 85 |
}
|
| 86 |
};
|
| 87 |
|
|
|
|
| 88 |
const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
|
| 89 |
for (const filePath of state.scope) {
|
| 90 |
const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
|
| 91 |
if (!source) continue;
|
| 92 |
+
|
| 93 |
+
const ranking = state.fileRankings.find((r) => r.filePath === filePath);
|
| 94 |
+
const mode = ranking && ranking.importance >= 4 ? "full" : "short";
|
| 95 |
+
const analysis = await analyzeSolidityFile(source, mode, filePath, ranking?.importance);
|
| 96 |
solidityEntries.push({ filePath, source, analysis });
|
| 97 |
}
|
| 98 |
|
|
|
|
| 99 |
const docEntries: { filePath: string; content: string }[] = [];
|
| 100 |
for (const filePath of state.docs) {
|
| 101 |
const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
|
| 102 |
if (content) docEntries.push({ filePath, content });
|
| 103 |
}
|
| 104 |
|
|
|
|
| 105 |
const parts: string[] = [];
|
| 106 |
|
| 107 |
if (docEntries.length > 0) {
|
| 108 |
+
parts.push("## Documentação\n");
|
| 109 |
for (const { filePath, content } of docEntries) {
|
| 110 |
parts.push(`### ${filePath}\n${content}`);
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
+
parts.push(`## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
parts.push("## Análise Estrutural\n");
|
| 117 |
+
for (const { analysis } of solidityEntries) {
|
| 118 |
+
parts.push(analysis);
|
| 119 |
}
|
| 120 |
|
| 121 |
+
const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
|
| 122 |
+
const result = await model.invoke([
|
| 123 |
+
new SystemMessage({
|
| 124 |
+
content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }],
|
| 125 |
+
}),
|
| 126 |
+
new HumanMessage(parts.join("\n\n")),
|
| 127 |
+
]);
|
| 128 |
+
|
| 129 |
+
const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
|
| 130 |
+
const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
|
| 131 |
+
const repoContext = [result.context, fileTreeBlock, structuralBlock].join("\n\n");
|
| 132 |
|
|
|
|
| 133 |
logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
|
| 134 |
+
logger.info(`[Auditor] gatherContext: contexto construído (${repoContext.length} caracteres)`);
|
| 135 |
+
logger.debug(`[Auditor] gatherContext: contexto compactado:\n${repoContext}`);
|
| 136 |
|
| 137 |
emitStep({ agent: "auditor", step: "ctx", status: "done" });
|
| 138 |
+
|
| 139 |
+
return { repoContext };
|
| 140 |
};
|
| 141 |
|
| 142 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 143 |
+
const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
|
| 144 |
+
|
| 145 |
+
const isReflection = state.judgeReviews.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
logger.info(
|
| 148 |
`[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
|
| 149 |
);
|
| 150 |
+
|
| 151 |
emitStep({ agent: "auditor", step: "find", status: "running", detail: `iter ${state.reflectionCount + 1}` });
|
| 152 |
|
| 153 |
+
const cachedContext = {
|
| 154 |
+
type: "text" as const,
|
| 155 |
+
text: `Contexto do Protocolo:\n${state.repoContext}`,
|
| 156 |
+
cache_control: { type: "ephemeral" as const },
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
const processFile = async (filePath: string) => {
|
| 160 |
+
let source: string;
|
| 161 |
+
try {
|
| 162 |
+
source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
|
| 163 |
+
} catch {
|
| 164 |
+
return [];
|
| 165 |
+
}
|
| 166 |
+
if (!source) return [];
|
| 167 |
+
|
| 168 |
+
const fileEntries = isReflection
|
| 169 |
+
? state.candidateFindings
|
| 170 |
+
.map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
|
| 171 |
+
.filter(({ finding }) => finding.path === filePath)
|
| 172 |
+
: [];
|
| 173 |
+
|
| 174 |
+
const isRefinement = fileEntries.length > 0;
|
| 175 |
+
const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
|
| 176 |
+
const contractText = isRefinement
|
| 177 |
+
? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
|
| 178 |
+
: `Contrato (${filePath}):\n\n${source}`;
|
| 179 |
+
|
| 180 |
+
logger.debug(`[Auditor] findVulnerabilities: processando ${filePath}`);
|
| 181 |
+
|
| 182 |
+
const result = await model.invoke([
|
| 183 |
+
new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
|
| 184 |
+
new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
|
| 185 |
+
]);
|
| 186 |
+
|
| 187 |
+
return result.findings.map((finding: any) => ({
|
| 188 |
+
...finding,
|
| 189 |
+
path: filePath,
|
| 190 |
+
location: matchLines(source, finding.codeSnippet) ?? "",
|
| 191 |
+
}));
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
const [firstFile, ...restFiles] = state.scope;
|
| 195 |
+
const firstFindings = firstFile ? await processFile(firstFile) : [];
|
| 196 |
+
const restFindings = await Promise.all(restFiles.map(processFile));
|
| 197 |
+
const candidateFindings = [firstFindings, ...restFindings].flat();
|
| 198 |
|
| 199 |
+
logger.info(
|
| 200 |
+
`[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`,
|
| 201 |
+
);
|
| 202 |
logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
|
| 203 |
|
| 204 |
emitStep({ agent: "auditor", step: "find", status: "done" });
|
|
|
|
| 217 |
};
|
| 218 |
}
|
| 219 |
|
| 220 |
+
const model = llmSonnet.withStructuredOutput(JudgeReviewSchema);
|
| 221 |
|
| 222 |
+
logger.info(
|
| 223 |
+
`[Auditor] judgeFindings: revisando ${state.candidateFindings.length} finding(s) candidato(s) em paralelo`,
|
| 224 |
+
);
|
| 225 |
|
| 226 |
+
const cachedContext = {
|
| 227 |
+
type: "text" as const,
|
| 228 |
+
text: `Contexto do Protocolo:\n${state.repoContext}`,
|
| 229 |
+
cache_control: { type: "ephemeral" as const },
|
| 230 |
+
};
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
const reviewFinding = async (finding: (typeof state.candidateFindings)[number], i: number) => {
|
| 233 |
+
let source: string;
|
| 234 |
+
try {
|
| 235 |
+
source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
|
| 236 |
+
} catch {
|
| 237 |
+
source = "";
|
| 238 |
+
}
|
| 239 |
|
| 240 |
+
const findingText = `[Achado ${i + 1}] ${finding.title}\nSeveridade: ${finding.severity}\nDescrição: ${finding.description}\nLocalização: ${finding.path} linhas ${finding.location}\nCódigo:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
|
| 241 |
+
|
| 242 |
+
logger.debug(`[Auditor] judgeFindings: revisando finding ${i + 1}: ${finding.title}`);
|
| 243 |
+
return model.invoke([
|
| 244 |
+
new SystemMessage({
|
| 245 |
+
content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
|
| 246 |
+
}),
|
| 247 |
+
new HumanMessage({
|
| 248 |
+
content: [
|
| 249 |
+
cachedContext,
|
| 250 |
+
{ type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
|
| 251 |
+
],
|
| 252 |
+
}),
|
| 253 |
+
]);
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
const [firstFinding, ...restFindings] = state.candidateFindings;
|
| 257 |
+
const firstReview = await reviewFinding(firstFinding, 0);
|
| 258 |
+
const restReviews = await Promise.all(restFindings.map((f, i) => reviewFinding(f, i + 1)));
|
| 259 |
+
const reviews = [firstReview, ...restReviews];
|
| 260 |
|
| 261 |
const confirmedEntries = state.candidateFindings
|
| 262 |
.map((finding, i) => ({ finding, review: reviews[i] }))
|
src/agents/auditor/config.ts
CHANGED
|
@@ -20,4 +20,5 @@ export const SKIP_DIRS = new Set([
|
|
| 20 |
export const MAX_DEPTH = 6;
|
| 21 |
export const MAX_DOC_CHARS = 12_000;
|
| 22 |
export const MAX_SOL_CHARS = 40_000;
|
| 23 |
-
export const MAX_REFLECTIONS =
|
|
|
|
|
|
| 20 |
export const MAX_DEPTH = 6;
|
| 21 |
export const MAX_DOC_CHARS = 12_000;
|
| 22 |
export const MAX_SOL_CHARS = 40_000;
|
| 23 |
+
export const MAX_REFLECTIONS = 3;
|
| 24 |
+
export const MIN_FILE_IMPORTANCE = 3;
|
src/agents/auditor/prompts.ts
CHANGED
|
@@ -1,50 +1,131 @@
|
|
| 1 |
-
export const
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
Liste todas as variáveis de estado relevantes entre os contratos, o que representam e quais funções as leem ou escrevem. Sinalize armazenamento compartilhado ou herdado.
|
| 10 |
|
| 11 |
-
|
| 12 |
-
Trace os principais caminhos de execução e transições de estado de ponta a ponta entre contratos (ex.: depósito → cunhar shares → atualizar recompensas; saque → queimar shares → transferir ETH). Inclua chamadas entre contratos.
|
| 13 |
|
| 14 |
-
|
| 15 |
-
Condições que devem sempre ser verdadeiras (ex.: "o supply total deve ser igual à soma de todos os saldos", "o saldo de ETH do contrato ≥ soma de todos os depósitos dos usuários"). Derive-as tanto do código-fonte quanto da documentação.
|
| 16 |
|
| 17 |
-
|
| 18 |
-
O que o protocolo assume sobre chamadores, contratos externos, oráculos, chaves de administrador e comportamento de tokens (ex.: "tokens são compatíveis com ERC-20", "o admin é confiável", "sem tokens com taxa de transferência").
|
| 19 |
|
| 20 |
-
|
| 21 |
-
Controles de acesso, estruturas de taxas, timelocks, limites, mecanismos de pausa, padrões de atualização e quaisquer outras restrições de domínio.
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
- **
|
| 32 |
-
- **
|
| 33 |
-
- **codeSnippet**: O bloco de código vulnerável exatamente como aparece no código-fonte.
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
- **isFalsePositive**: true se o achado NÃO for explorável na prática; false se for uma vulnerabilidade real.
|
| 45 |
-
- **confidence**: Número inteiro de 0 a 100 refletindo sua confiança no veredicto.
|
| 46 |
-
- **exploitablePaths**: Array de strings. Se for verdadeiro positivo, forneça caminhos concretos confirmando a explorabilidade com valores reais. Cada rastreamento deve descrever os passos do atacante com entradas/valores realistas (ex.: "1. Atacante chama deposit(100 ETH) 2. Contrato do atacante no fallback chama withdraw() novamente antes da atualização do saldo 3. Atacante drena 100 ETH duas vezes"). Se for falso positivo, forneça o raciocínio que bloqueia o exploit.
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const RANK_FILES_PROMPT = `Você é um especialista em segurança de smart contracts. Dada a árvore de arquivos do repositório e uma lista de contratos Solidity, classifique cada arquivo pela sua importância para a descoberta de vulnerabilidades de segurança.
|
| 2 |
|
| 3 |
+
Para cada arquivo atribua:
|
| 4 |
+
- importance: inteiro de 1 (menos importante) a 5 (mais importante)
|
| 5 |
+
- reasoning: uma frase concisa justificando a classificação (mencione o papel específico do contrato, não categorias genéricas)
|
| 6 |
|
| 7 |
+
Critérios de classificação:
|
| 8 |
+
- Importância 5: lógica central do protocolo (vaults, pools, engines), custódia de tokens/ETH, mecanismos de upgrade/proxy, cálculos financeiros críticos (preço, juros, liquidação), controle de acesso raiz.
|
| 9 |
+
- Importância 4: contratos que movem valor e interagem diretamente com os de importância 5, máquinas de estado complexas, distribuição de taxas/recompensas, roteadores de entrada.
|
| 10 |
+
- Importância 3: helpers periféricos com lógica de negócio, bibliotecas reutilizáveis com efeitos colaterais, governança com timelocks.
|
| 11 |
+
- Importância 2: interfaces, adaptadores, wrappers triviais, contratos utilitários sem lógica crítica.
|
| 12 |
+
- Importância 1: views somente-leitura, arquivos de constantes/configuração pura, mocks e scripts de deploy.
|
| 13 |
|
| 14 |
+
Retorne a classificação de TODOS os arquivos fornecidos, sem omitir nenhum.`;
|
|
|
|
| 15 |
|
| 16 |
+
export const GATHER_CONTEXT_PROMPT = `Você é um especialista em segurança de smart contracts criando um modelo mental preciso do protocolo para auditoria. Você receberá documentação e a análise estrutural dos contratos Solidity em escopo.
|
|
|
|
| 17 |
|
| 18 |
+
**REGRA DE FIDELIDADE FACTUAL — NÃO NEGOCIÁVEL**: Baseie-se EXCLUSIVAMENTE no que está EXPLICITAMENTE presente no código-fonte (linhas de código, NatSpec, comentários inline) ou na documentação fornecida. NUNCA infira, suponha, extrapole ou "complete" informações que não estejam literalmente escritas nas fontes. Se uma informação não aparece explicitamente, NÃO a inclua e NÃO a invente. Esta regra se aplica a TODAS as seções. **Na seção Trust Assumptions esta proibição é ABSOLUTA: se não há declaração explícita (código, NatSpec, comentário ou doc) sobre uma suposição, ela não existe para você.**
|
|
|
|
| 19 |
|
| 20 |
+
Produza um contexto factual e detalhado de auditoria sem introduções, sem padding e sem repetições. Preserve nomes concretos (funções, variáveis, tipos, valores numéricos) exatamente como aparecem no código. Sem prosa explicativa.
|
|
|
|
| 21 |
|
| 22 |
+
A árvore de arquivos do repositório e a análise estrutural completa de cada contrato serão anexadas automaticamente ao final do contexto — **não as duplique**. Concentre-se nas seções de síntese abaixo.
|
|
|
|
| 23 |
|
| 24 |
+
## Visão geral
|
| 25 |
+
Descreva o propósito do protocolo, o fluxo econômico principal, os participantes envolvidos e os ativos protegidos — apenas o que estiver explicitamente declarado no código ou na documentação.
|
| 26 |
|
| 27 |
+
## Estado Crítico
|
| 28 |
+
Todas as variáveis de estado com impacto em lógica de negócio, segurança ou contabilidade, extraídas diretamente da seção Storage da análise estrutural.
|
| 29 |
+
Formato por bullet: \`filePath::nomeVar (tipo, visibilidade) — NatSpec/comentário se presente — funções que escrevem nela — impacto se manipulada\`.
|
| 30 |
+
Omita apenas constantes e imutáveis puramente administrativas (nome do token, símbolo, decimals, versão de string).
|
| 31 |
|
| 32 |
+
## Fluxos Principais (máx. 5 fluxos, 3–6 passos cada)
|
| 33 |
+
Apenas os caminhos críticos ponta a ponta que movem valor ou alteram estado relevante, derivados das funções e call graphs observados no código.
|
| 34 |
+
Formato por passo: \`ação (função) → efeito colateral → variável/estado alterado\`.
|
| 35 |
+
Inclua chamadas cross-contract quando materiais para entender superfície de ataque.
|
| 36 |
|
| 37 |
+
## Invariantes e Propriedades
|
| 38 |
+
Condições que devem ser verdadeiras para o protocolo operar corretamente, derivadas APENAS de \`require\`/\`assert\`/\`revert\` explícitos no código, NatSpec \`@dev\`, ou comentários que as declarem literalmente. Separe em dois grupos:
|
| 39 |
+
- **Econômicas**: balanços, totais, proporções (ex.: \`totalDebt == Σ userDebt[i]\`, \`reservas >= totalSupply * exchangeRate\`)
|
| 40 |
+
- **De controle**: acesso, sequência de operações, transições de estado permitidas
|
|
|
|
| 41 |
|
| 42 |
+
## Trust Assumptions
|
| 43 |
+
**SOMENTE o que estiver EXPLICITAMENTE declarado** em código-fonte (require, NatSpec, comentários inline) ou na documentação. **NÃO inferir. NÃO supor. NÃO extrapolar.** Se não há declaração explícita sobre confiança em um componente externo ou comportamento esperado, ele NÃO entra nesta seção — mesmo que pareça óbvio.
|
| 44 |
+
Bullets curtos com referência à fonte (ex.: "owner pode pausar o contrato — \`onlyOwner\` em \`pause()\`").
|
| 45 |
|
| 46 |
+
## Regras de Negócio e Restrições de Segurança
|
| 47 |
+
Em bullets: roles e modifiers relevantes (nomes exatos do código), limites numéricos (apenas valores literais presentes no código-fonte), taxas e destinatários, timelocks, pausabilidade, condições de upgrade, restrições de whitelist/blacklist. Inclua apenas regras com impacto direto em vetores de ataque.
|
| 48 |
|
| 49 |
+
**Formato obrigatório**: bullets e frases curtas. Dados concretos (nomes de funções, variáveis, valores numéricos) exatamente como aparecem no código. Sem prosa explicativa.`;
|
| 50 |
|
| 51 |
+
export const FIND_VULNERABILITIES_PROMPT = `Você é um auditor especialista em segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Assuma que todos os usuários são adversariais e estão ativamente tentando explorar o contrato.
|
| 52 |
|
| 53 |
+
Sua tarefa é analisar o código-fonte fornecido linha a linha e identificar todas as vulnerabilidades de segurança, design, lógica e econômicas. Qualquer discrepância entre a implementação e o comportamento esperado, as suposições do protocolo ou o design econômico deve ser reportada como vulnerabilidade, mesmo que o contrato execute sem erros em runtime.
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
**Escopo obrigatório da análise**
|
| 56 |
|
| 57 |
+
Além das categorias técnicas listadas abaixo, sua análise deve cobrir:
|
| 58 |
+
|
| 59 |
+
- **Invariantes de protocolo**: identifique invariantes implícitas e explícitas (ex.: depósitos devem igualar saques, ativos devem permanecer colateralizados, recompensas devem corresponder aos inputs) e verifique se podem ser quebradas.
|
| 60 |
+
- **Fluxos de valor**: analise exaustivamente todas as transferências de valor (taxas, saldos, depósitos, saques, recompensas, deltas), verificando: quem provê os fundos, quem os recebe, e se os fundos são corretamente custodiados (escrowed) antes da transferência.
|
| 61 |
+
- **Validação de ownership**: verifique se usuários podem interagir com tokens, NFTs ou permissões que não controlam.
|
| 62 |
+
- **Observabilidade**: avalie se eventos, logs e chamadas de métodos representam corretamente as ações do protocolo. Emissão ausente, incorreta ou enganosa é uma vulnerabilidade.
|
| 63 |
+
- **Implementação vs. intenção**: qualquer desvio entre o comportamento implementado e o design pretendido do protocolo deve ser reportado.
|
| 64 |
+
|
| 65 |
+
## Categorias técnicas a verificar sistematicamente
|
| 66 |
+
|
| 67 |
+
Reentrância (simples, cross-function, cross-contract, read-only), controle de acesso (funções privilegiadas desprotegidas, erros em herança de roles), overflow/underflow (Solidity <0.8 ou uso de \`unchecked\`), manipulação de oráculo (TWAP curto, preço spot, valor de reserves), ataques de flash loan (price impact, liquidações artificiais), front-running e MEV (sandwich, race condition em aprovações), replay de assinatura (nonce ausente, falta de chainId), colisões de storage (proxies, delegatecall), proxies não inicializados (initializer sem proteção), delegatecall inseguro (destino controlável pelo usuário), griefing de gas (loops ilimitados, arrays crescentes), negação de serviço (push payments, dependência de chamada externa), perda de precisão (divisão antes de multiplicação, truncamento acumulativo), lógica de negócio (violação de invariantes, casos de borda em math financeira, race conditions de estado).
|
| 68 |
+
|
| 69 |
+
## Formato de saída
|
| 70 |
+
|
| 71 |
+
Para cada vulnerabilidade encontrada, forneça OBRIGATORIAMENTE todos os campos abaixo. Cada entrada deve ser **atômica**: reporte exatamente um problema por entrada. Não agrupe múltiplos problemas em um único achado, mesmo que ocorram na mesma função ou linha. Não há limite para o número de vulnerabilidades reportadas.
|
| 72 |
+
|
| 73 |
+
- **title**: Nome curto e preciso (ex.: "Reentrância em \`withdraw\`", "Controle de acesso ausente em \`setFee\`").
|
| 74 |
+
- **description**: Descreva (a) o comportamento **esperado** pelo protocolo, (b) o comportamento **observado** no código vulnerável, e (c) o impacto concreto se explorado. Mínimo 3 frases, máximo 5.
|
| 75 |
+
- **exploit_scenario**: Descreva um cenário concreto e passo a passo de como um atacante exploraria a vulnerabilidade.
|
| 76 |
+
- **recommendation**: Correção específica e acionável com referência ao padrão ou mecanismo correto (ex.: "Aplicar checks-effects-interactions: mover \`balances[msg.sender] -= amount\` para antes da chamada externa").
|
| 77 |
+
- **severity**: Exatamente um de: \`"high"\` (perda direta de fundos ou tomada de controle do contrato), \`"medium"\` (risco indireto ou condicional), \`"low"\` (problema de boas práticas, sem risco financeiro imediato).
|
| 78 |
+
- **location**: Função e/ou número de linha onde o problema ocorre.
|
| 79 |
+
- **codeSnippet**: O trecho exato e completo do código vulnerável, copiado literalmente do código-fonte. **Proibido** usar reticências (\`...\`), omissões, pseudocódigo ou paráfrases. Inclua as linhas exatas conforme aparecem no arquivo, com indentação original preservada. Se o snippet for maior que 40 linhas, inclua o intervalo completo sem cortes.
|
| 80 |
+
|
| 81 |
+
## Regra de completude
|
| 82 |
+
|
| 83 |
+
Reporte vulnerabilidades mesmo que não sejam imediatamente exploráveis. Vulnerabilidades podem ser de segurança, inconsistências de design, desalinhamentos econômicos ou falhas de observabilidade. Se nenhuma vulnerabilidade for encontrada, retorne um array vazio.`;
|
| 84 |
+
|
| 85 |
+
export const REFINE_VULNERABILITIES_PROMPT = `Você é um auditor sênior de segurança de smart contracts refinando seus próprios achados com base no feedback de um revisor especialista independente.
|
| 86 |
+
|
| 87 |
+
Na iteração anterior, você analisou um contrato Solidity e gerou uma lista de vulnerabilidades candidatas. Um revisor especialista avaliou cada achado e forneceu: veredicto (verdadeiro ou falso positivo), análise técnica detalhada, nível de confiança e caminhos de exploit ou razões de bloqueio.
|
| 88 |
+
|
| 89 |
+
**Sua tarefa**: produzir uma lista final e refinada de vulnerabilidades incorporando o feedback do revisor.
|
| 90 |
+
|
| 91 |
+
## Regras de refinamento
|
| 92 |
+
|
| 93 |
+
1. **Falso positivo com confiança ≥ 80%**: remova o achado sem exceção.
|
| 94 |
+
2. **Falso positivo com confiança < 80%**: reavalie com base na análise do revisor. Mantenha apenas se encontrar evidência nova ou argumento técnico que o revisor não considerou — e reflita isso na descrição.
|
| 95 |
+
3. **Verdadeiro positivo**: mantenha o achado. Incorpore melhorias sugeridas pelo revisor (descrição mais precisa, snippet mais completo, recomendação mais específica, caminhos de exploit detalhados).
|
| 96 |
+
4. **Novos achados**: se o revisor apontou superfícies de ataque não cobertas em seus achados originais, investigue o código-fonte e adicione novos achados para elas.
|
| 97 |
+
5. Não adicione achados que não sejam suportados pelo código-fonte ou pelo feedback do revisor.
|
| 98 |
+
|
| 99 |
+
## Formato de saída
|
| 100 |
+
|
| 101 |
+
Idêntico ao da análise inicial. Para cada vulnerabilidade:
|
| 102 |
+
- **title**: nome curto e preciso
|
| 103 |
+
- **description**: (a) comportamento esperado, (b) comportamento observado, (c) impacto concreto — mínimo 3 frases, máximo 5
|
| 104 |
+
- **recommendation**: correção específica e acionável com referência ao padrão correto
|
| 105 |
+
- **severity**: \`"high"\` / \`"medium"\` / \`"low"\`
|
| 106 |
+
- **codeSnippet**: trecho exato e completo copiado literalmente do código-fonte, sem omissões, reticências ou pseudocódigo
|
| 107 |
+
|
| 108 |
+
Se nenhuma vulnerabilidade restar após o refinamento, retorne um array vazio.`;
|
| 109 |
+
|
| 110 |
+
export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
|
| 111 |
+
|
| 112 |
+
Para cada achado, forneça OBRIGATORIAMENTE todos os campos abaixo:
|
| 113 |
+
|
| 114 |
+
- **review**: Análise técnica detalhada (3 a 6 frases) explicando o veredicto. Referencie: (a) o código específico envolvido, (b) invariantes ou premissas do protocolo que confirmam ou bloqueiam o exploit, (c) pré-condições necessárias para exploração, (d) controles mitigadores existentes que o auditor pode ter ignorado. Seja preciso — cite nomes de funções, variáveis e valores.
|
| 115 |
+
- **isFalsePositive**: \`true\` se o achado NÃO for explorável na prática; \`false\` se for uma vulnerabilidade real.
|
| 116 |
+
- **confidence**: Inteiro de 0 a 100 refletindo sua certeza no veredicto. Use < 60 apenas quando existir ambiguidade genuína no código.
|
| 117 |
+
- **exploitablePaths**: Retorne uma array de strings.
|
| 118 |
+
- Se verdadeiro positivo (\`isFalsePositive: false\`): forneça 1 a 3 caminhos concretos de exploit, cada um com passos numerados, entradas realistas e estado do contrato antes/depois. Ex.: ["1. Atacante chama flashLoan(500k USDC). 2. No callback, chama deposit() inflando reserves. 3. Chama withdraw() com preço manipulado. 4. Lucra 50k USDC. Estado: reserves inflado temporariamente, totalShares inalterado."].
|
| 119 |
+
- Se falso positivo (\`isFalsePositive: true\`): forneça o raciocínio exato que bloqueia cada caminho de exploit tentado pelo auditor.
|
| 120 |
+
|
| 121 |
+
## Critérios para falso positivo (aplique com rigor — não seja permissivo)
|
| 122 |
+
|
| 123 |
+
1. O caminho de exploit é bloqueado por controle de acesso verificável no código.
|
| 124 |
+
2. A vulnerabilidade já é totalmente mitigada por outro mecanismo no código (ex.: nonReentrant, onlyOwner, require com validação suficiente).
|
| 125 |
+
3. A condição necessária para o exploit é impossível ou economicamente inviável dado o modelo do protocolo (ex.: requer ser o próprio contrato, ou lucro < custo de gas em qualquer cenário realista).
|
| 126 |
+
4. O comportamento é explicitamente documentado como intencional nas premissas de design do protocolo.
|
| 127 |
+
|
| 128 |
+
## Critérios para verdadeiro positivo
|
| 129 |
+
- Existe pelo menos um caminho de exploit concreto e realista que viola uma invariante ou permite extração de valor não autorizada.
|
| 130 |
+
- Não exige condições impossíveis nem assume acesso privilegiado não disponível ao atacante.
|
| 131 |
+
- Inclui qualquer discrepância entre a implementação e a intenção, premissas ou objetivos documentados — mesmo que o contrato opere sem erros em runtime. Isso abrange problemas de segurança, inconsistências de design, desalinhamentos econômicos e falhas de observabilidade, independentemente de exploitabilidade direta.`;
|
src/agents/auditor/state.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
import { StateSchema } from "@langchain/langgraph";
|
| 2 |
import { z } from "zod";
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
export const CandidateFindingSchema = z.object({
|
| 5 |
title: z.string(),
|
| 6 |
description: z.string(),
|
|
@@ -34,6 +40,7 @@ export const AuditorState = new StateSchema({
|
|
| 34 |
scope: z.array(z.string()).default([]),
|
| 35 |
docs: z.array(z.string()).default([]),
|
| 36 |
fileTree: z.string().default(""),
|
|
|
|
| 37 |
repoContext: z.string().default(""),
|
| 38 |
candidateFindings: z.array(LocatedFindingSchema).default([]),
|
| 39 |
judgeReviews: z.array(JudgeReviewSchema).default([]),
|
|
|
|
| 1 |
import { StateSchema } from "@langchain/langgraph";
|
| 2 |
import { z } from "zod";
|
| 3 |
|
| 4 |
+
export const FileRankingSchema = z.object({
|
| 5 |
+
filePath: z.string(),
|
| 6 |
+
importance: z.number().int().min(1).max(5),
|
| 7 |
+
reasoning: z.string(),
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
export const CandidateFindingSchema = z.object({
|
| 11 |
title: z.string(),
|
| 12 |
description: z.string(),
|
|
|
|
| 40 |
scope: z.array(z.string()).default([]),
|
| 41 |
docs: z.array(z.string()).default([]),
|
| 42 |
fileTree: z.string().default(""),
|
| 43 |
+
fileRankings: z.array(FileRankingSchema).default([]),
|
| 44 |
repoContext: z.string().default(""),
|
| 45 |
candidateFindings: z.array(LocatedFindingSchema).default([]),
|
| 46 |
judgeReviews: z.array(JudgeReviewSchema).default([]),
|
src/agents/auditor/tools/{repo-tree-tool.ts → repo-tree/tool.ts}
RENAMED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
| 4 |
import { tool } from "langchain";
|
| 5 |
import { z } from "zod";
|
| 6 |
|
| 7 |
-
import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../config.ts";
|
| 8 |
|
| 9 |
const CONFIG_FILES = new Set([
|
| 10 |
"foundry.toml",
|
|
@@ -90,14 +90,11 @@ export const buildRepoTree = (repoPath: string): string => {
|
|
| 90 |
return `${repoName}/\n${renderTree(nodes, "")}`;
|
| 91 |
};
|
| 92 |
|
| 93 |
-
export const repoTreeTool = tool(
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
}),
|
| 102 |
-
},
|
| 103 |
-
);
|
|
|
|
| 4 |
import { tool } from "langchain";
|
| 5 |
import { z } from "zod";
|
| 6 |
|
| 7 |
+
import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../../config.ts";
|
| 8 |
|
| 9 |
const CONFIG_FILES = new Set([
|
| 10 |
"foundry.toml",
|
|
|
|
| 90 |
return `${repoName}/\n${renderTree(nodes, "")}`;
|
| 91 |
};
|
| 92 |
|
| 93 |
+
export const repoTreeTool = tool(async ({ repoPath }) => buildRepoTree(repoPath), {
|
| 94 |
+
name: "repo_tree",
|
| 95 |
+
description:
|
| 96 |
+
"Walk a repository and return a file-system tree of relevant files tagged by kind: [sol] for auditable Solidity contracts, [test] for Solidity test files, [doc] for documentation, and [config] for project config files. Use this during Define Scope to understand repository layout before selecting which files to audit.",
|
| 97 |
+
schema: z.object({
|
| 98 |
+
repoPath: z.string().describe("Absolute path to the repository root."),
|
| 99 |
+
}),
|
| 100 |
+
});
|
|
|
|
|
|
|
|
|
src/agents/auditor/tools/solidity-analyzer-tool.ts
DELETED
|
@@ -1,567 +0,0 @@
|
|
| 1 |
-
import { parse, visit } from "@solidity-parser/parser";
|
| 2 |
-
import { tool } from "langchain";
|
| 3 |
-
import { z } from "zod";
|
| 4 |
-
|
| 5 |
-
const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
|
| 6 |
-
const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
|
| 7 |
-
|
| 8 |
-
interface StateVar {
|
| 9 |
-
name: string;
|
| 10 |
-
type: string;
|
| 11 |
-
visibility: string;
|
| 12 |
-
constant: boolean;
|
| 13 |
-
immutable: boolean;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
interface EventDef {
|
| 17 |
-
name: string;
|
| 18 |
-
params: string[];
|
| 19 |
-
anonymous: boolean;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
interface ModifierDef {
|
| 23 |
-
name: string;
|
| 24 |
-
params: string[];
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
interface FunctionDef {
|
| 28 |
-
name: string;
|
| 29 |
-
isConstructor: boolean;
|
| 30 |
-
isReceive: boolean;
|
| 31 |
-
isFallback: boolean;
|
| 32 |
-
visibility: string;
|
| 33 |
-
mutability: string;
|
| 34 |
-
params: string[];
|
| 35 |
-
returns: string[];
|
| 36 |
-
modifiers: string[];
|
| 37 |
-
internalCalls: string[];
|
| 38 |
-
externalCalls: string[];
|
| 39 |
-
stateReads: string[];
|
| 40 |
-
stateWrites: string[];
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
interface ContractAnalysis {
|
| 44 |
-
name: string;
|
| 45 |
-
kind: string;
|
| 46 |
-
baseContracts: string[];
|
| 47 |
-
usingFor: string[];
|
| 48 |
-
stateVars: StateVar[];
|
| 49 |
-
events: EventDef[];
|
| 50 |
-
modifiers: ModifierDef[];
|
| 51 |
-
functions: FunctionDef[];
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
const typeToString = (node: any): string => {
|
| 55 |
-
if (!node) return "unknown";
|
| 56 |
-
|
| 57 |
-
switch (node.type) {
|
| 58 |
-
case "ElementaryTypeName":
|
| 59 |
-
return node.name as string;
|
| 60 |
-
case "UserDefinedTypeName":
|
| 61 |
-
return (node.namePath ?? node.name) as string;
|
| 62 |
-
case "ArrayTypeName":
|
| 63 |
-
return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
|
| 64 |
-
case "Mapping":
|
| 65 |
-
return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
|
| 66 |
-
case "FunctionTypeName":
|
| 67 |
-
return "function";
|
| 68 |
-
default:
|
| 69 |
-
return "unknown";
|
| 70 |
-
}
|
| 71 |
-
};
|
| 72 |
-
|
| 73 |
-
const paramToString = (p: any) => {
|
| 74 |
-
if (!p) return "?";
|
| 75 |
-
const type = typeToString(p.typeName);
|
| 76 |
-
return p.name ? `${type} ${p.name}` : type;
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
const collectLHSRoots = (node: any, targets: Set<string>) => {
|
| 80 |
-
if (!node) return;
|
| 81 |
-
switch (node.type) {
|
| 82 |
-
case "Identifier":
|
| 83 |
-
targets.add(node.name as string);
|
| 84 |
-
break;
|
| 85 |
-
case "MemberAccess":
|
| 86 |
-
collectLHSRoots(node.expression, targets);
|
| 87 |
-
break;
|
| 88 |
-
case "IndexAccess":
|
| 89 |
-
collectLHSRoots(node.base, targets);
|
| 90 |
-
break;
|
| 91 |
-
case "TupleExpression":
|
| 92 |
-
for (const c of node.components ?? []) collectLHSRoots(c, targets);
|
| 93 |
-
break;
|
| 94 |
-
}
|
| 95 |
-
};
|
| 96 |
-
|
| 97 |
-
const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
|
| 98 |
-
const internalCalls = new Set<string>();
|
| 99 |
-
const externalCalls = new Set<string>();
|
| 100 |
-
const writeTargets = new Set<string>();
|
| 101 |
-
const allStateAccesses = new Set<string>();
|
| 102 |
-
const localVars = new Set<string>();
|
| 103 |
-
|
| 104 |
-
if (!funcNode.body) {
|
| 105 |
-
return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
// Collect function params and return params as locals so they don't shadow state vars
|
| 109 |
-
for (const p of funcNode.parameters ?? []) {
|
| 110 |
-
if (p?.name) localVars.add(p.name as string);
|
| 111 |
-
}
|
| 112 |
-
for (const p of funcNode.returnParameters ?? []) {
|
| 113 |
-
if (p?.name) localVars.add(p.name as string);
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
// Collect local variable declarations
|
| 117 |
-
visit(funcNode.body, {
|
| 118 |
-
VariableDeclarationStatement: (node: any) => {
|
| 119 |
-
for (const v of node.variables ?? []) {
|
| 120 |
-
if (v?.name) localVars.add(v.name as string);
|
| 121 |
-
}
|
| 122 |
-
},
|
| 123 |
-
});
|
| 124 |
-
|
| 125 |
-
const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
|
| 126 |
-
|
| 127 |
-
// Collect write targets from assignment LHS, unary mutations, and delete
|
| 128 |
-
visit(funcNode.body, {
|
| 129 |
-
ExpressionStatement: (node: any) => {
|
| 130 |
-
const expr = node.expression;
|
| 131 |
-
if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
|
| 132 |
-
collectLHSRoots(expr.left, writeTargets);
|
| 133 |
-
}
|
| 134 |
-
// Handle ++, --, and delete — all work on any lvalue (arr[i]++, delete s.field, etc.)
|
| 135 |
-
if (
|
| 136 |
-
expr?.type === "UnaryOperation" &&
|
| 137 |
-
(expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
|
| 138 |
-
) {
|
| 139 |
-
collectLHSRoots(expr.subExpression, writeTargets);
|
| 140 |
-
}
|
| 141 |
-
},
|
| 142 |
-
});
|
| 143 |
-
|
| 144 |
-
// Collect calls and state-var identifier accesses
|
| 145 |
-
visit(funcNode.body, {
|
| 146 |
-
FunctionCall: (node: any) => {
|
| 147 |
-
const expr = node.expression;
|
| 148 |
-
if (expr?.type === "Identifier") {
|
| 149 |
-
internalCalls.add(expr.name as string);
|
| 150 |
-
} else if (expr?.type === "MemberAccess") {
|
| 151 |
-
const base = expr.expression;
|
| 152 |
-
if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
|
| 153 |
-
internalCalls.add(expr.memberName as string);
|
| 154 |
-
} else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
|
| 155 |
-
// abi.encode, block.xxx, msg.xxx, etc. — not external calls
|
| 156 |
-
} else {
|
| 157 |
-
const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
|
| 158 |
-
externalCalls.add(`${baseStr}.${expr.memberName as string}`);
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
},
|
| 162 |
-
Identifier: (node: any) => {
|
| 163 |
-
if (effectiveStateVars.has(node.name as string)) {
|
| 164 |
-
allStateAccesses.add(node.name as string);
|
| 165 |
-
}
|
| 166 |
-
},
|
| 167 |
-
});
|
| 168 |
-
|
| 169 |
-
const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
|
| 170 |
-
// A var can be in both — e.g. x = x + 1 is both a read and a write.
|
| 171 |
-
const stateReads = [...allStateAccesses];
|
| 172 |
-
|
| 173 |
-
return {
|
| 174 |
-
internalCalls: [...internalCalls],
|
| 175 |
-
externalCalls: [...externalCalls],
|
| 176 |
-
stateReads,
|
| 177 |
-
stateWrites,
|
| 178 |
-
};
|
| 179 |
-
};
|
| 180 |
-
|
| 181 |
-
const hasCycle = (start: string, current: string, callMap: Map<string, string[]>, visited: Set<string>) => {
|
| 182 |
-
for (const callee of callMap.get(current) ?? []) {
|
| 183 |
-
if (callee === start) return true;
|
| 184 |
-
if (!visited.has(callee)) {
|
| 185 |
-
visited.add(callee);
|
| 186 |
-
if (hasCycle(start, callee, callMap, visited)) return true;
|
| 187 |
-
}
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
return false;
|
| 191 |
-
};
|
| 192 |
-
|
| 193 |
-
const fnLabel = (fn: FunctionDef) => {
|
| 194 |
-
if (fn.isConstructor) return "constructor";
|
| 195 |
-
if (fn.isReceive) return "receive";
|
| 196 |
-
if (fn.isFallback) return "fallback";
|
| 197 |
-
return fn.name;
|
| 198 |
-
};
|
| 199 |
-
|
| 200 |
-
const generateShortMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
|
| 201 |
-
const lines: string[] = [];
|
| 202 |
-
lines.push("# Solidity Analysis\n");
|
| 203 |
-
|
| 204 |
-
if (imports.length > 0) {
|
| 205 |
-
lines.push(`**Imports:** ${imports.map((i) => `\`${i}\``).join(", ")}\n`);
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
for (const contract of contracts) {
|
| 209 |
-
const inheritance =
|
| 210 |
-
contract.baseContracts.length > 0 ? ` : ${contract.baseContracts.map((b) => `\`${b}\``).join(", ")}` : "";
|
| 211 |
-
lines.push(`---\n\n## \`${contract.name}\` (${contract.kind})${inheritance}\n`);
|
| 212 |
-
|
| 213 |
-
// State variables — one line, name:type
|
| 214 |
-
if (contract.stateVars.length > 0) {
|
| 215 |
-
const vars = contract.stateVars.map((v) => {
|
| 216 |
-
const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean);
|
| 217 |
-
const suffix = flags.length > 0 ? `, ${flags.join(", ")}` : "";
|
| 218 |
-
return `\`${v.name}: ${v.type}\` (${v.visibility}${suffix})`;
|
| 219 |
-
});
|
| 220 |
-
lines.push(`**State:** ${vars.join(", ")}\n`);
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
// Modifiers — names only
|
| 224 |
-
if (contract.modifiers.length > 0) {
|
| 225 |
-
const mods = contract.modifiers.map(
|
| 226 |
-
(m) => `\`${m.name}${m.params.length > 0 ? `(${m.params.join(", ")})` : ""}\``,
|
| 227 |
-
);
|
| 228 |
-
lines.push(`**Modifiers:** ${mods.join(", ")}\n`);
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
// Events — name + params, one line each
|
| 232 |
-
if (contract.events.length > 0) {
|
| 233 |
-
const evts = contract.events.map((e) => `\`${e.name}(${e.params.join(", ")})\``);
|
| 234 |
-
lines.push(`**Events:** ${evts.join(", ")}\n`);
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
// Function list — compact, one line per function
|
| 238 |
-
if (contract.functions.length > 0) {
|
| 239 |
-
lines.push("**Functions:**");
|
| 240 |
-
for (const fn of contract.functions) {
|
| 241 |
-
const label = fnLabel(fn);
|
| 242 |
-
const params = fn.params.join(", ");
|
| 243 |
-
const ret = fn.returns.length > 0 ? ` → ${fn.returns.join(", ")}` : "";
|
| 244 |
-
const mods = fn.modifiers.length > 0 ? ` [${fn.modifiers.join(", ")}]` : "";
|
| 245 |
-
lines.push(`- \`${label}(${params})${ret}\` — ${fn.visibility} ${fn.mutability}${mods}`);
|
| 246 |
-
}
|
| 247 |
-
lines.push("");
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
// External calls — only functions that make them
|
| 251 |
-
const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
|
| 252 |
-
if (externalFuncs.length > 0) {
|
| 253 |
-
lines.push("**External Calls:**");
|
| 254 |
-
for (const fn of externalFuncs) {
|
| 255 |
-
lines.push(`- \`${fnLabel(fn)}\`: ${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}`);
|
| 256 |
-
}
|
| 257 |
-
lines.push("");
|
| 258 |
-
}
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
return lines.join("\n");
|
| 262 |
-
};
|
| 263 |
-
|
| 264 |
-
const generateMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
|
| 265 |
-
const lines: string[] = [];
|
| 266 |
-
lines.push("# Solidity Contract Analysis\n");
|
| 267 |
-
|
| 268 |
-
// Imports
|
| 269 |
-
lines.push("## Imports\n");
|
| 270 |
-
if (imports.length === 0) {
|
| 271 |
-
lines.push("_No imports._\n");
|
| 272 |
-
} else {
|
| 273 |
-
for (const imp of imports) lines.push(`- \`${imp}\``);
|
| 274 |
-
lines.push("");
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
for (const contract of contracts) {
|
| 278 |
-
const kindLabel = contract.kind.charAt(0).toUpperCase() + contract.kind.slice(1);
|
| 279 |
-
lines.push(`---\n\n## ${kindLabel}: \`${contract.name}\`\n`);
|
| 280 |
-
|
| 281 |
-
// Inheritance
|
| 282 |
-
lines.push("### Inheritance\n");
|
| 283 |
-
if (contract.baseContracts.length === 0) {
|
| 284 |
-
lines.push("_None._\n");
|
| 285 |
-
} else {
|
| 286 |
-
for (const base of contract.baseContracts) lines.push(`- \`${base}\``);
|
| 287 |
-
lines.push("");
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
// Using For
|
| 291 |
-
if (contract.usingFor.length > 0) {
|
| 292 |
-
lines.push("### Using For\n");
|
| 293 |
-
for (const u of contract.usingFor) lines.push(`- ${u}`);
|
| 294 |
-
lines.push("");
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
// Storage layout
|
| 298 |
-
lines.push("### Storage Layout (State Variables)\n");
|
| 299 |
-
if (contract.stateVars.length === 0) {
|
| 300 |
-
lines.push("_No state variables._\n");
|
| 301 |
-
} else {
|
| 302 |
-
lines.push("| Slot | Name | Type | Visibility | Flags |");
|
| 303 |
-
lines.push("|------|------|------|------------|-------|");
|
| 304 |
-
contract.stateVars.forEach((v, i) => {
|
| 305 |
-
const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ");
|
| 306 |
-
lines.push(`| ${i} | \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} |`);
|
| 307 |
-
});
|
| 308 |
-
lines.push("");
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
// Events
|
| 312 |
-
lines.push("### Events\n");
|
| 313 |
-
if (contract.events.length === 0) {
|
| 314 |
-
lines.push("_No events._\n");
|
| 315 |
-
} else {
|
| 316 |
-
for (const evt of contract.events) {
|
| 317 |
-
const params = evt.params.join(", ");
|
| 318 |
-
lines.push(`- **\`${evt.name}\`**\`(${params})\`${evt.anonymous ? " _(anonymous)_" : ""}`);
|
| 319 |
-
}
|
| 320 |
-
lines.push("");
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
// Modifiers
|
| 324 |
-
lines.push("### Modifiers\n");
|
| 325 |
-
if (contract.modifiers.length === 0) {
|
| 326 |
-
lines.push("_No modifiers._\n");
|
| 327 |
-
} else {
|
| 328 |
-
for (const mod of contract.modifiers) {
|
| 329 |
-
lines.push(`- **\`${mod.name}\`**\`(${mod.params.join(", ")})\``);
|
| 330 |
-
}
|
| 331 |
-
lines.push("");
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
// Function list
|
| 335 |
-
lines.push("### Function List\n");
|
| 336 |
-
if (contract.functions.length === 0) {
|
| 337 |
-
lines.push("_No functions._\n");
|
| 338 |
-
} else {
|
| 339 |
-
lines.push("| Name | Visibility | Mutability | Parameters | Returns | Modifiers |");
|
| 340 |
-
lines.push("|------|------------|------------|------------|---------|-----------|");
|
| 341 |
-
for (const fn of contract.functions) {
|
| 342 |
-
lines.push(
|
| 343 |
-
`| \`${fnLabel(fn)}\` | ${fn.visibility} | ${fn.mutability} | \`${fn.params.join(", ")}\` | \`${fn.returns.join(", ")}\` | ${fn.modifiers.join(", ")} |`,
|
| 344 |
-
);
|
| 345 |
-
}
|
| 346 |
-
lines.push("");
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
// Call graph
|
| 350 |
-
lines.push("### Call Graph\n");
|
| 351 |
-
const hasCalls = contract.functions.some((f) => f.internalCalls.length > 0 || f.externalCalls.length > 0);
|
| 352 |
-
if (!hasCalls) {
|
| 353 |
-
lines.push("_No function calls detected._\n");
|
| 354 |
-
} else {
|
| 355 |
-
for (const fn of contract.functions) {
|
| 356 |
-
if (fn.internalCalls.length === 0 && fn.externalCalls.length === 0) continue;
|
| 357 |
-
lines.push(`**\`${fnLabel(fn)}\`**`);
|
| 358 |
-
for (const call of fn.internalCalls) lines.push(` - → \`${call}\` _(internal)_`);
|
| 359 |
-
for (const call of fn.externalCalls) lines.push(` - → \`${call}\` _(external)_`);
|
| 360 |
-
}
|
| 361 |
-
lines.push("");
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
// External calls
|
| 365 |
-
lines.push("### External Calls\n");
|
| 366 |
-
const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
|
| 367 |
-
if (externalFuncs.length === 0) {
|
| 368 |
-
lines.push("_No external calls detected._\n");
|
| 369 |
-
} else {
|
| 370 |
-
for (const fn of externalFuncs) {
|
| 371 |
-
lines.push(`**\`${fnLabel(fn)}\`**`);
|
| 372 |
-
for (const call of fn.externalCalls) lines.push(` - \`${call}\``);
|
| 373 |
-
}
|
| 374 |
-
lines.push("");
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
// Internal recursion
|
| 378 |
-
lines.push("### Internal Recursion\n");
|
| 379 |
-
const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
|
| 380 |
-
const recursiveFns = contract.functions.filter((fn) => hasCycle(fnLabel(fn), fnLabel(fn), callMap, new Set()));
|
| 381 |
-
if (recursiveFns.length === 0) {
|
| 382 |
-
lines.push("_No recursive functions detected._\n");
|
| 383 |
-
} else {
|
| 384 |
-
for (const fn of recursiveFns) lines.push(`- **\`${fnLabel(fn)}\`** is recursive`);
|
| 385 |
-
lines.push("");
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
// State variable touchpoints
|
| 389 |
-
lines.push("### State Variable Touchpoints\n");
|
| 390 |
-
const touchedFns = contract.functions.filter((f) => f.stateReads.length > 0 || f.stateWrites.length > 0);
|
| 391 |
-
if (touchedFns.length === 0) {
|
| 392 |
-
lines.push("_No state variable accesses detected._\n");
|
| 393 |
-
} else {
|
| 394 |
-
lines.push("| Function | Reads | Writes |");
|
| 395 |
-
lines.push("|----------|-------|--------|");
|
| 396 |
-
for (const fn of touchedFns) {
|
| 397 |
-
const reads = fn.stateReads.map((r) => `\`${r}\``).join(", ");
|
| 398 |
-
const writes = fn.stateWrites.map((w) => `\`${w}\``).join(", ");
|
| 399 |
-
lines.push(`| \`${fnLabel(fn)}\` | ${reads} | ${writes} |`);
|
| 400 |
-
}
|
| 401 |
-
lines.push("");
|
| 402 |
-
}
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
// External dependencies summary
|
| 406 |
-
lines.push("---\n\n## External Dependencies\n");
|
| 407 |
-
|
| 408 |
-
lines.push("### Import Paths\n");
|
| 409 |
-
if (imports.length === 0) {
|
| 410 |
-
lines.push("_No imports._\n");
|
| 411 |
-
} else {
|
| 412 |
-
for (const imp of imports) lines.push(`- \`${imp}\``);
|
| 413 |
-
lines.push("");
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
const externalTargets = new Set<string>();
|
| 417 |
-
for (const contract of contracts) {
|
| 418 |
-
for (const fn of contract.functions) {
|
| 419 |
-
for (const call of fn.externalCalls) {
|
| 420 |
-
const target = call.split(".")[0];
|
| 421 |
-
if (target && target !== "<expr>") externalTargets.add(target);
|
| 422 |
-
}
|
| 423 |
-
}
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
lines.push("### External Contract Interactions\n");
|
| 427 |
-
if (externalTargets.size === 0) {
|
| 428 |
-
lines.push("_No external contract interactions detected._\n");
|
| 429 |
-
} else {
|
| 430 |
-
for (const dep of externalTargets) lines.push(`- \`${dep}\``);
|
| 431 |
-
lines.push("");
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
return lines.join("\n");
|
| 435 |
-
};
|
| 436 |
-
|
| 437 |
-
export const analyzeSolidityFile = async (soliditySource: string, mode: "full" | "short") => {
|
| 438 |
-
let ast: any;
|
| 439 |
-
|
| 440 |
-
try {
|
| 441 |
-
ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
|
| 442 |
-
} catch (e: any) {
|
| 443 |
-
return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
|
| 444 |
-
}
|
| 445 |
-
|
| 446 |
-
const imports: string[] = [];
|
| 447 |
-
const contracts: ContractAnalysis[] = [];
|
| 448 |
-
|
| 449 |
-
for (const node of ast.children ?? []) {
|
| 450 |
-
if (node.type === "ImportDirective") {
|
| 451 |
-
imports.push(node.path as string);
|
| 452 |
-
}
|
| 453 |
-
}
|
| 454 |
-
|
| 455 |
-
for (const node of ast.children ?? []) {
|
| 456 |
-
if (node.type !== "ContractDefinition") continue;
|
| 457 |
-
|
| 458 |
-
const contract: ContractAnalysis = {
|
| 459 |
-
name: node.name as string,
|
| 460 |
-
kind: (node.kind as string) ?? "contract",
|
| 461 |
-
baseContracts: (node.baseContracts ?? []).map(
|
| 462 |
-
(bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
|
| 463 |
-
),
|
| 464 |
-
usingFor: [],
|
| 465 |
-
stateVars: [],
|
| 466 |
-
events: [],
|
| 467 |
-
modifiers: [],
|
| 468 |
-
functions: [],
|
| 469 |
-
};
|
| 470 |
-
|
| 471 |
-
const stateVarNames = new Set<string>();
|
| 472 |
-
|
| 473 |
-
for (const member of node.subNodes ?? []) {
|
| 474 |
-
switch (member.type) {
|
| 475 |
-
case "StateVariableDeclaration":
|
| 476 |
-
for (const v of member.variables ?? []) {
|
| 477 |
-
stateVarNames.add(v.name as string);
|
| 478 |
-
contract.stateVars.push({
|
| 479 |
-
name: v.name as string,
|
| 480 |
-
type: typeToString(v.typeName),
|
| 481 |
-
visibility: (v.visibility as string) ?? "internal",
|
| 482 |
-
constant: (v.isDeclaredConst as boolean) ?? false,
|
| 483 |
-
immutable: (v.isImmutable as boolean) ?? false,
|
| 484 |
-
});
|
| 485 |
-
}
|
| 486 |
-
break;
|
| 487 |
-
|
| 488 |
-
case "EventDefinition": {
|
| 489 |
-
const params = (member.parameters ?? []).map((p: any) => {
|
| 490 |
-
const indexed = p.isIndexed ? "indexed " : "";
|
| 491 |
-
const name = p.name ? ` ${p.name as string}` : "";
|
| 492 |
-
return `${indexed}${typeToString(p.typeName)}${name}`;
|
| 493 |
-
});
|
| 494 |
-
contract.events.push({
|
| 495 |
-
name: member.name as string,
|
| 496 |
-
params,
|
| 497 |
-
anonymous: (member.isAnonymous as boolean) ?? false,
|
| 498 |
-
});
|
| 499 |
-
break;
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
case "ModifierDefinition":
|
| 503 |
-
contract.modifiers.push({
|
| 504 |
-
name: member.name as string,
|
| 505 |
-
params: (member.parameters ?? []).map(paramToString),
|
| 506 |
-
});
|
| 507 |
-
break;
|
| 508 |
-
|
| 509 |
-
case "FunctionDefinition": {
|
| 510 |
-
const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
|
| 511 |
-
contract.functions.push({
|
| 512 |
-
name: (member.name as string) ?? "",
|
| 513 |
-
isConstructor: (member.isConstructor as boolean) ?? false,
|
| 514 |
-
isReceive: (member.isReceiveEther as boolean) ?? false,
|
| 515 |
-
isFallback: (member.isFallback as boolean) ?? false,
|
| 516 |
-
visibility: (member.visibility as string) ?? "internal",
|
| 517 |
-
mutability: (member.stateMutability as string) ?? "nonpayable",
|
| 518 |
-
params: (member.parameters ?? []).map(paramToString),
|
| 519 |
-
returns: (member.returnParameters ?? []).map(paramToString),
|
| 520 |
-
modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
|
| 521 |
-
internalCalls,
|
| 522 |
-
externalCalls,
|
| 523 |
-
stateReads,
|
| 524 |
-
stateWrites,
|
| 525 |
-
});
|
| 526 |
-
break;
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
case "UsingForDeclaration": {
|
| 530 |
-
const forType = member.typeName ? typeToString(member.typeName) : "*";
|
| 531 |
-
if (member.libraryName) {
|
| 532 |
-
contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
|
| 533 |
-
} else {
|
| 534 |
-
// New-style: using {fn1, fn2, ...} for T
|
| 535 |
-
const fns = (member.functions ?? [])
|
| 536 |
-
.map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
|
| 537 |
-
.join(", ");
|
| 538 |
-
contract.usingFor.push(`{${fns}} for \`${forType}\``);
|
| 539 |
-
}
|
| 540 |
-
break;
|
| 541 |
-
}
|
| 542 |
-
}
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
contracts.push(contract);
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
return mode === "short" ? generateShortMarkdown(imports, contracts) : generateMarkdown(imports, contracts);
|
| 549 |
-
};
|
| 550 |
-
|
| 551 |
-
export const solidityAnalyzerTool = tool(
|
| 552 |
-
async ({ solidityFile, mode }) => {
|
| 553 |
-
return analyzeSolidityFile(solidityFile, mode);
|
| 554 |
-
},
|
| 555 |
-
{
|
| 556 |
-
name: "solidity_analyzer",
|
| 557 |
-
description:
|
| 558 |
-
"Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact token-efficient summary (imports, state, modifiers, events, function signatures, external calls). Use mode='full' for the complete report including storage layout table, call graph, recursion detection, state variable touchpoints, and external dependencies.",
|
| 559 |
-
schema: z.object({
|
| 560 |
-
solidityFile: z.string().describe("The full Solidity source code to analyze."),
|
| 561 |
-
mode: z
|
| 562 |
-
.enum(["full", "short"])
|
| 563 |
-
.default("full")
|
| 564 |
-
.describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
|
| 565 |
-
}),
|
| 566 |
-
},
|
| 567 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/agents/auditor/tools/solidity-analyzer/tool.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { parse } from "@solidity-parser/parser";
|
| 2 |
+
import { tool } from "langchain";
|
| 3 |
+
import { z } from "zod";
|
| 4 |
+
|
| 5 |
+
import {
|
| 6 |
+
analyzeFunction,
|
| 7 |
+
buildCommentBlocks,
|
| 8 |
+
type ContractAnalysis,
|
| 9 |
+
extractSolcVersion,
|
| 10 |
+
findCommentFor,
|
| 11 |
+
generateBriefMarkdown,
|
| 12 |
+
generateFullMarkdown,
|
| 13 |
+
paramToString,
|
| 14 |
+
type RenderOptions,
|
| 15 |
+
typeToString,
|
| 16 |
+
} from "./utils.ts";
|
| 17 |
+
|
| 18 |
+
export const analyzeSolidityFile = async (
|
| 19 |
+
soliditySource: string,
|
| 20 |
+
mode: "full" | "short",
|
| 21 |
+
filePath?: string,
|
| 22 |
+
importance?: number,
|
| 23 |
+
): Promise<string> => {
|
| 24 |
+
let ast: any;
|
| 25 |
+
try {
|
| 26 |
+
ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
|
| 27 |
+
} catch (e: any) {
|
| 28 |
+
return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const comments = buildCommentBlocks(soliditySource);
|
| 32 |
+
const imports: string[] = [];
|
| 33 |
+
const contracts: ContractAnalysis[] = [];
|
| 34 |
+
|
| 35 |
+
for (const node of ast.children ?? []) {
|
| 36 |
+
if (node.type === "ImportDirective") imports.push(node.path as string);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
for (const node of ast.children ?? []) {
|
| 40 |
+
if (node.type !== "ContractDefinition") continue;
|
| 41 |
+
|
| 42 |
+
const contractComment = node.loc ? findCommentFor(node.loc.start.line, comments) : undefined;
|
| 43 |
+
|
| 44 |
+
const contract: ContractAnalysis = {
|
| 45 |
+
name: node.name as string,
|
| 46 |
+
kind: (node.kind as string) ?? "contract",
|
| 47 |
+
baseContracts: (node.baseContracts ?? []).map(
|
| 48 |
+
(bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
|
| 49 |
+
),
|
| 50 |
+
usingFor: [],
|
| 51 |
+
stateVars: [],
|
| 52 |
+
events: [],
|
| 53 |
+
errors: [],
|
| 54 |
+
modifiers: [],
|
| 55 |
+
functions: [],
|
| 56 |
+
natspec: contractComment?.natspec,
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const stateVarNames = new Set<string>();
|
| 60 |
+
|
| 61 |
+
for (const member of node.subNodes ?? []) {
|
| 62 |
+
const memberComment = member.loc ? findCommentFor(member.loc.start.line, comments) : undefined;
|
| 63 |
+
|
| 64 |
+
switch (member.type) {
|
| 65 |
+
case "StateVariableDeclaration":
|
| 66 |
+
for (const v of member.variables ?? []) {
|
| 67 |
+
stateVarNames.add(v.name as string);
|
| 68 |
+
contract.stateVars.push({
|
| 69 |
+
name: v.name as string,
|
| 70 |
+
type: typeToString(v.typeName),
|
| 71 |
+
visibility: (v.visibility as string) ?? "internal",
|
| 72 |
+
constant: (v.isDeclaredConst as boolean) ?? false,
|
| 73 |
+
immutable: (v.isImmutable as boolean) ?? false,
|
| 74 |
+
natspec: memberComment?.natspec,
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
break;
|
| 78 |
+
|
| 79 |
+
case "EventDefinition": {
|
| 80 |
+
const params = (member.parameters ?? []).map((p: any) => {
|
| 81 |
+
const indexed = p.isIndexed ? "indexed " : "";
|
| 82 |
+
const name = p.name ? ` ${p.name as string}` : "";
|
| 83 |
+
return `${indexed}${typeToString(p.typeName)}${name}`;
|
| 84 |
+
});
|
| 85 |
+
contract.events.push({
|
| 86 |
+
name: member.name as string,
|
| 87 |
+
params,
|
| 88 |
+
anonymous: (member.isAnonymous as boolean) ?? false,
|
| 89 |
+
natspec: memberComment?.natspec,
|
| 90 |
+
});
|
| 91 |
+
break;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
case "CustomErrorDefinition":
|
| 95 |
+
contract.errors.push({
|
| 96 |
+
name: member.name as string,
|
| 97 |
+
params: (member.parameters ?? []).map((p: any) => paramToString(p)),
|
| 98 |
+
natspec: memberComment?.natspec,
|
| 99 |
+
});
|
| 100 |
+
break;
|
| 101 |
+
|
| 102 |
+
case "ModifierDefinition":
|
| 103 |
+
contract.modifiers.push({
|
| 104 |
+
name: member.name as string,
|
| 105 |
+
params: (member.parameters ?? []).map(paramToString),
|
| 106 |
+
natspec: memberComment?.natspec,
|
| 107 |
+
});
|
| 108 |
+
break;
|
| 109 |
+
|
| 110 |
+
case "FunctionDefinition": {
|
| 111 |
+
const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
|
| 112 |
+
contract.functions.push({
|
| 113 |
+
name: (member.name as string) ?? "",
|
| 114 |
+
isConstructor: (member.isConstructor as boolean) ?? false,
|
| 115 |
+
isReceive: (member.isReceiveEther as boolean) ?? false,
|
| 116 |
+
isFallback: (member.isFallback as boolean) ?? false,
|
| 117 |
+
visibility: (member.visibility as string) ?? "internal",
|
| 118 |
+
mutability: (member.stateMutability as string) ?? "nonpayable",
|
| 119 |
+
params: (member.parameters ?? []).map(paramToString),
|
| 120 |
+
returns: (member.returnParameters ?? []).map(paramToString),
|
| 121 |
+
modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
|
| 122 |
+
internalCalls,
|
| 123 |
+
externalCalls,
|
| 124 |
+
stateReads,
|
| 125 |
+
stateWrites,
|
| 126 |
+
natspec: memberComment?.natspec,
|
| 127 |
+
});
|
| 128 |
+
break;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
case "UsingForDeclaration": {
|
| 132 |
+
const forType = member.typeName ? typeToString(member.typeName) : "*";
|
| 133 |
+
if (member.libraryName) {
|
| 134 |
+
contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
|
| 135 |
+
} else {
|
| 136 |
+
const fns = (member.functions ?? [])
|
| 137 |
+
.map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
|
| 138 |
+
.join(", ");
|
| 139 |
+
contract.usingFor.push(`{${fns}} for \`${forType}\``);
|
| 140 |
+
}
|
| 141 |
+
break;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
contracts.push(contract);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
const opts: RenderOptions = {
|
| 150 |
+
filePath,
|
| 151 |
+
importance,
|
| 152 |
+
lineCount: soliditySource.split("\n").length,
|
| 153 |
+
solcVersion: extractSolcVersion(soliditySource),
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
return mode === "short"
|
| 157 |
+
? generateBriefMarkdown(imports, contracts, opts)
|
| 158 |
+
: generateFullMarkdown(imports, contracts, opts);
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
export const solidityAnalyzerTool = tool(async ({ solidityFile, mode }) => analyzeSolidityFile(solidityFile, mode), {
|
| 162 |
+
name: "solidity_analyzer",
|
| 163 |
+
description:
|
| 164 |
+
"Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact summary (meta, external calls, function table). Use mode='full' for the complete report including storage, events, errors, per-function call graph, recursion detection, and state variable touchpoints.",
|
| 165 |
+
schema: z.object({
|
| 166 |
+
solidityFile: z.string().describe("The full Solidity source code to analyze."),
|
| 167 |
+
mode: z
|
| 168 |
+
.enum(["full", "short"])
|
| 169 |
+
.default("full")
|
| 170 |
+
.describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
|
| 171 |
+
}),
|
| 172 |
+
});
|
src/agents/auditor/tools/solidity-analyzer/utils.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { visit } from "@solidity-parser/parser";
|
| 2 |
+
|
| 3 |
+
const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
|
| 4 |
+
const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
|
| 5 |
+
|
| 6 |
+
export interface NatSpec {
|
| 7 |
+
title?: string;
|
| 8 |
+
author?: string;
|
| 9 |
+
notice?: string;
|
| 10 |
+
dev?: string;
|
| 11 |
+
params: Record<string, string>;
|
| 12 |
+
returns: string[];
|
| 13 |
+
inheritdoc?: string;
|
| 14 |
+
custom: Record<string, string>;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export interface ParsedComment {
|
| 18 |
+
text: string;
|
| 19 |
+
startLine: number;
|
| 20 |
+
endLine: number;
|
| 21 |
+
isNatSpec: boolean;
|
| 22 |
+
natspec?: NatSpec;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface StateVar {
|
| 26 |
+
name: string;
|
| 27 |
+
type: string;
|
| 28 |
+
visibility: string;
|
| 29 |
+
constant: boolean;
|
| 30 |
+
immutable: boolean;
|
| 31 |
+
natspec?: NatSpec;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface EventDef {
|
| 35 |
+
name: string;
|
| 36 |
+
params: string[];
|
| 37 |
+
anonymous: boolean;
|
| 38 |
+
natspec?: NatSpec;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface ErrorDef {
|
| 42 |
+
name: string;
|
| 43 |
+
params: string[];
|
| 44 |
+
natspec?: NatSpec;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export interface ModifierDef {
|
| 48 |
+
name: string;
|
| 49 |
+
params: string[];
|
| 50 |
+
natspec?: NatSpec;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export interface FunctionDef {
|
| 54 |
+
name: string;
|
| 55 |
+
isConstructor: boolean;
|
| 56 |
+
isReceive: boolean;
|
| 57 |
+
isFallback: boolean;
|
| 58 |
+
visibility: string;
|
| 59 |
+
mutability: string;
|
| 60 |
+
params: string[];
|
| 61 |
+
returns: string[];
|
| 62 |
+
modifiers: string[];
|
| 63 |
+
internalCalls: string[];
|
| 64 |
+
externalCalls: string[];
|
| 65 |
+
stateReads: string[];
|
| 66 |
+
stateWrites: string[];
|
| 67 |
+
natspec?: NatSpec;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export interface ContractAnalysis {
|
| 71 |
+
name: string;
|
| 72 |
+
kind: string;
|
| 73 |
+
baseContracts: string[];
|
| 74 |
+
usingFor: string[];
|
| 75 |
+
stateVars: StateVar[];
|
| 76 |
+
events: EventDef[];
|
| 77 |
+
errors: ErrorDef[];
|
| 78 |
+
modifiers: ModifierDef[];
|
| 79 |
+
functions: FunctionDef[];
|
| 80 |
+
natspec?: NatSpec;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export interface RenderOptions {
|
| 84 |
+
filePath?: string;
|
| 85 |
+
importance?: number;
|
| 86 |
+
lineCount: number;
|
| 87 |
+
solcVersion: string;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export const extractSolcVersion = (source: string): string => {
|
| 91 |
+
const match = source.match(/pragma\s+solidity\s+([^;]+);/);
|
| 92 |
+
return match ? match[1].trim() : "—";
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
const parseNatSpecTags = (text: string): NatSpec => {
|
| 96 |
+
const result: NatSpec = { params: {}, returns: [], custom: {} };
|
| 97 |
+
|
| 98 |
+
const firstTag = text.search(/@(?:title|author|notice|dev|param|return|inheritdoc|custom:)/);
|
| 99 |
+
if (firstTag > 0) {
|
| 100 |
+
const implicit = text.slice(0, firstTag).trim();
|
| 101 |
+
if (implicit) result.notice = implicit.replace(/\n\s*/g, " ");
|
| 102 |
+
} else if (firstTag === -1 && text.trim()) {
|
| 103 |
+
result.notice = text.trim().replace(/\n\s*/g, " ");
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
const tagRe = /@(custom:\S+|\w+)([^@]*)/g;
|
| 107 |
+
let m: RegExpExecArray | null;
|
| 108 |
+
while ((m = tagRe.exec(text)) !== null) {
|
| 109 |
+
const tag = m[1];
|
| 110 |
+
const value = m[2].trim().replace(/\n\s*/g, " ");
|
| 111 |
+
if (tag === "title") result.title = value;
|
| 112 |
+
else if (tag === "author") result.author = value;
|
| 113 |
+
else if (tag === "notice") result.notice = value;
|
| 114 |
+
else if (tag === "dev") result.dev = value;
|
| 115 |
+
else if (tag === "inheritdoc") result.inheritdoc = value;
|
| 116 |
+
else if (tag === "param") {
|
| 117 |
+
const sp = value.indexOf(" ");
|
| 118 |
+
if (sp > 0) result.params[value.slice(0, sp)] = value.slice(sp + 1);
|
| 119 |
+
else if (value) result.params[value] = "";
|
| 120 |
+
} else if (tag === "return") result.returns.push(value);
|
| 121 |
+
else if (tag.startsWith("custom:")) result.custom[tag.slice(7)] = value;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
return result;
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
export const buildCommentBlocks = (source: string): ParsedComment[] => {
|
| 128 |
+
const blocks: ParsedComment[] = [];
|
| 129 |
+
const lines = source.split("\n");
|
| 130 |
+
let i = 0;
|
| 131 |
+
|
| 132 |
+
while (i < lines.length) {
|
| 133 |
+
const raw = lines[i];
|
| 134 |
+
const trimmed = raw.trimStart();
|
| 135 |
+
|
| 136 |
+
if (trimmed.startsWith("///")) {
|
| 137 |
+
const startLine = i + 1;
|
| 138 |
+
const texts: string[] = [];
|
| 139 |
+
while (i < lines.length && lines[i].trimStart().startsWith("///")) {
|
| 140 |
+
texts.push(lines[i].trimStart().slice(3).replace(/^ /, ""));
|
| 141 |
+
i++;
|
| 142 |
+
}
|
| 143 |
+
const text = texts.join("\n");
|
| 144 |
+
blocks.push({ text, startLine, endLine: i, isNatSpec: true, natspec: parseNatSpecTags(text) });
|
| 145 |
+
continue;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const mlStart = raw.indexOf("/*");
|
| 149 |
+
if (mlStart !== -1) {
|
| 150 |
+
const isNatSpec = raw[mlStart + 2] === "*" && raw[mlStart + 3] !== "/";
|
| 151 |
+
const startLine = i + 1;
|
| 152 |
+
const closeOnSame = raw.indexOf("*/", mlStart + 2);
|
| 153 |
+
|
| 154 |
+
if (closeOnSame !== -1) {
|
| 155 |
+
const inner = raw.slice(mlStart + (isNatSpec ? 3 : 2), closeOnSame).trim();
|
| 156 |
+
blocks.push({
|
| 157 |
+
text: inner,
|
| 158 |
+
startLine,
|
| 159 |
+
endLine: startLine,
|
| 160 |
+
isNatSpec,
|
| 161 |
+
natspec: isNatSpec ? parseNatSpecTags(inner) : undefined,
|
| 162 |
+
});
|
| 163 |
+
i++;
|
| 164 |
+
continue;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const rawLines: string[] = [raw.slice(mlStart + (isNatSpec ? 3 : 2))];
|
| 168 |
+
i++;
|
| 169 |
+
while (i < lines.length) {
|
| 170 |
+
const closeIdx = lines[i].indexOf("*/");
|
| 171 |
+
if (closeIdx !== -1) {
|
| 172 |
+
rawLines.push(lines[i].slice(0, closeIdx));
|
| 173 |
+
i++;
|
| 174 |
+
break;
|
| 175 |
+
}
|
| 176 |
+
rawLines.push(lines[i]);
|
| 177 |
+
i++;
|
| 178 |
+
}
|
| 179 |
+
const text = rawLines
|
| 180 |
+
.map((l) => l.replace(/^\s*\*\s?/, ""))
|
| 181 |
+
.join("\n")
|
| 182 |
+
.trim();
|
| 183 |
+
blocks.push({ text, startLine, endLine: i, isNatSpec, natspec: isNatSpec ? parseNatSpecTags(text) : undefined });
|
| 184 |
+
continue;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (trimmed.startsWith("//")) {
|
| 188 |
+
blocks.push({ text: trimmed.slice(2).trim(), startLine: i + 1, endLine: i + 1, isNatSpec: false });
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
i++;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
return blocks;
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
export const findCommentFor = (line: number, comments: ParsedComment[]): ParsedComment | undefined =>
|
| 198 |
+
comments.find((c) => c.endLine === line - 1) ?? comments.find((c) => c.startLine === line && !c.isNatSpec);
|
| 199 |
+
|
| 200 |
+
export const typeToString = (node: any): string => {
|
| 201 |
+
if (!node) return "unknown";
|
| 202 |
+
switch (node.type) {
|
| 203 |
+
case "ElementaryTypeName":
|
| 204 |
+
return node.name as string;
|
| 205 |
+
case "UserDefinedTypeName":
|
| 206 |
+
return (node.namePath ?? node.name) as string;
|
| 207 |
+
case "ArrayTypeName":
|
| 208 |
+
return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
|
| 209 |
+
case "Mapping":
|
| 210 |
+
return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
|
| 211 |
+
case "FunctionTypeName":
|
| 212 |
+
return "function";
|
| 213 |
+
default:
|
| 214 |
+
return "unknown";
|
| 215 |
+
}
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
export const paramToString = (p: any): string => {
|
| 219 |
+
if (!p) return "?";
|
| 220 |
+
const type = typeToString(p.typeName);
|
| 221 |
+
return p.name ? `${type} ${p.name}` : type;
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
const collectLHSRoots = (node: any, targets: Set<string>) => {
|
| 225 |
+
if (!node) return;
|
| 226 |
+
switch (node.type) {
|
| 227 |
+
case "Identifier":
|
| 228 |
+
targets.add(node.name as string);
|
| 229 |
+
break;
|
| 230 |
+
case "MemberAccess":
|
| 231 |
+
collectLHSRoots(node.expression, targets);
|
| 232 |
+
break;
|
| 233 |
+
case "IndexAccess":
|
| 234 |
+
collectLHSRoots(node.base, targets);
|
| 235 |
+
break;
|
| 236 |
+
case "TupleExpression":
|
| 237 |
+
for (const c of node.components ?? []) collectLHSRoots(c, targets);
|
| 238 |
+
break;
|
| 239 |
+
}
|
| 240 |
+
};
|
| 241 |
+
|
| 242 |
+
export const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
|
| 243 |
+
const internalCalls = new Set<string>();
|
| 244 |
+
const externalCalls = new Set<string>();
|
| 245 |
+
const writeTargets = new Set<string>();
|
| 246 |
+
const allStateAccesses = new Set<string>();
|
| 247 |
+
const localVars = new Set<string>();
|
| 248 |
+
|
| 249 |
+
if (!funcNode.body) {
|
| 250 |
+
return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
for (const p of funcNode.parameters ?? []) {
|
| 254 |
+
if (p?.name) localVars.add(p.name as string);
|
| 255 |
+
}
|
| 256 |
+
for (const p of funcNode.returnParameters ?? []) {
|
| 257 |
+
if (p?.name) localVars.add(p.name as string);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
visit(funcNode.body, {
|
| 261 |
+
VariableDeclarationStatement: (node: any) => {
|
| 262 |
+
for (const v of node.variables ?? []) {
|
| 263 |
+
if (v?.name) localVars.add(v.name as string);
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
|
| 269 |
+
|
| 270 |
+
visit(funcNode.body, {
|
| 271 |
+
ExpressionStatement: (node: any) => {
|
| 272 |
+
const expr = node.expression;
|
| 273 |
+
if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
|
| 274 |
+
collectLHSRoots(expr.left, writeTargets);
|
| 275 |
+
}
|
| 276 |
+
if (
|
| 277 |
+
expr?.type === "UnaryOperation" &&
|
| 278 |
+
(expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
|
| 279 |
+
) {
|
| 280 |
+
collectLHSRoots(expr.subExpression, writeTargets);
|
| 281 |
+
}
|
| 282 |
+
},
|
| 283 |
+
});
|
| 284 |
+
|
| 285 |
+
visit(funcNode.body, {
|
| 286 |
+
FunctionCall: (node: any) => {
|
| 287 |
+
const expr = node.expression;
|
| 288 |
+
if (expr?.type === "Identifier") {
|
| 289 |
+
internalCalls.add(expr.name as string);
|
| 290 |
+
} else if (expr?.type === "MemberAccess") {
|
| 291 |
+
const base = expr.expression;
|
| 292 |
+
if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
|
| 293 |
+
internalCalls.add(expr.memberName as string);
|
| 294 |
+
} else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
|
| 295 |
+
// builtin namespace — skip
|
| 296 |
+
} else {
|
| 297 |
+
const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
|
| 298 |
+
externalCalls.add(`${baseStr}.${expr.memberName as string}`);
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
Identifier: (node: any) => {
|
| 303 |
+
if (effectiveStateVars.has(node.name as string)) {
|
| 304 |
+
allStateAccesses.add(node.name as string);
|
| 305 |
+
}
|
| 306 |
+
},
|
| 307 |
+
});
|
| 308 |
+
|
| 309 |
+
const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
|
| 310 |
+
|
| 311 |
+
return {
|
| 312 |
+
internalCalls: [...internalCalls],
|
| 313 |
+
externalCalls: [...externalCalls],
|
| 314 |
+
stateReads: [...allStateAccesses],
|
| 315 |
+
stateWrites,
|
| 316 |
+
};
|
| 317 |
+
};
|
| 318 |
+
|
| 319 |
+
export const hasCycle = (
|
| 320 |
+
start: string,
|
| 321 |
+
current: string,
|
| 322 |
+
callMap: Map<string, string[]>,
|
| 323 |
+
visited: Set<string>,
|
| 324 |
+
): boolean => {
|
| 325 |
+
for (const callee of callMap.get(current) ?? []) {
|
| 326 |
+
if (callee === start) return true;
|
| 327 |
+
if (!visited.has(callee)) {
|
| 328 |
+
visited.add(callee);
|
| 329 |
+
if (hasCycle(start, callee, callMap, visited)) return true;
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
return false;
|
| 333 |
+
};
|
| 334 |
+
|
| 335 |
+
export const fnLabel = (fn: FunctionDef): string => {
|
| 336 |
+
if (fn.isConstructor) return "constructor";
|
| 337 |
+
if (fn.isReceive) return "receive";
|
| 338 |
+
if (fn.isFallback) return "fallback";
|
| 339 |
+
return fn.name;
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
const renderNatSpec = (ns: NatSpec | undefined): string => {
|
| 343 |
+
if (!ns) return "—";
|
| 344 |
+
const parts: string[] = [];
|
| 345 |
+
if (ns.title) parts.push(`@title "${ns.title}"`);
|
| 346 |
+
if (ns.author) parts.push(`@author "${ns.author}"`);
|
| 347 |
+
if (ns.notice) parts.push(`@notice "${ns.notice}"`);
|
| 348 |
+
if (ns.dev) parts.push(`@dev "${ns.dev}"`);
|
| 349 |
+
for (const [k, v] of Object.entries(ns.params)) {
|
| 350 |
+
parts.push(v ? `@param ${k}: "${v}"` : `@param ${k}`);
|
| 351 |
+
}
|
| 352 |
+
for (const r of ns.returns) {
|
| 353 |
+
if (r) parts.push(`@return "${r}"`);
|
| 354 |
+
}
|
| 355 |
+
return parts.length > 0 ? parts.join(" · ") : "—";
|
| 356 |
+
};
|
| 357 |
+
|
| 358 |
+
const renderContractFull = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
|
| 359 |
+
lines.push("## Meta");
|
| 360 |
+
const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "—";
|
| 361 |
+
lines.push(`- kind: ${contract.kind} · inherits: ${inherits}`);
|
| 362 |
+
if (contract.usingFor.length > 0) lines.push(`- uses: [${contract.usingFor.join(", ")}]`);
|
| 363 |
+
lines.push(`- imports: ${imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "—"}`);
|
| 364 |
+
lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
|
| 365 |
+
lines.push("");
|
| 366 |
+
|
| 367 |
+
if (contract.stateVars.length > 0) {
|
| 368 |
+
lines.push("## Storage");
|
| 369 |
+
lines.push("| name | type | vis | flags | desc |");
|
| 370 |
+
lines.push("|------|------|-----|-------|------|");
|
| 371 |
+
for (const v of contract.stateVars) {
|
| 372 |
+
const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ") || "—";
|
| 373 |
+
const desc = v.natspec?.notice ?? v.natspec?.dev ?? "—";
|
| 374 |
+
lines.push(`| \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} | ${desc} |`);
|
| 375 |
+
}
|
| 376 |
+
lines.push("");
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
if (contract.events.length > 0) {
|
| 380 |
+
lines.push("## Events");
|
| 381 |
+
for (const e of contract.events) {
|
| 382 |
+
const notice = e.natspec?.notice ? ` — ${e.natspec.notice}` : "";
|
| 383 |
+
lines.push(`- \`${e.name}(${e.params.join(", ")})\`${e.anonymous ? " _(anon)_" : ""}${notice}`);
|
| 384 |
+
}
|
| 385 |
+
lines.push("");
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
if (contract.errors.length > 0) {
|
| 389 |
+
lines.push("## Errors");
|
| 390 |
+
for (const e of contract.errors) lines.push(`- \`${e.name}(${e.params.join(", ")})\``);
|
| 391 |
+
lines.push("");
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
if (contract.modifiers.length > 0) {
|
| 395 |
+
lines.push("## Modifiers");
|
| 396 |
+
for (const m of contract.modifiers) {
|
| 397 |
+
const notice = m.natspec?.notice ? ` — ${m.natspec.notice}` : "";
|
| 398 |
+
lines.push(`- \`${m.name}(${m.params.join(", ")})\`${notice}`);
|
| 399 |
+
}
|
| 400 |
+
lines.push("");
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
|
| 404 |
+
if (allExternalCalls.size > 0) {
|
| 405 |
+
lines.push("## External Calls");
|
| 406 |
+
for (const call of allExternalCalls) lines.push(`- \`${call}\``);
|
| 407 |
+
lines.push("");
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
if (contract.functions.length > 0) {
|
| 411 |
+
lines.push("## Functions");
|
| 412 |
+
lines.push("");
|
| 413 |
+
|
| 414 |
+
const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
|
| 415 |
+
|
| 416 |
+
for (const fn of contract.functions) {
|
| 417 |
+
const label = fnLabel(fn);
|
| 418 |
+
lines.push(`### ${label}`);
|
| 419 |
+
|
| 420 |
+
const modsStr = fn.modifiers.length > 0 ? ` · modifiers: [${fn.modifiers.join(", ")}]` : "";
|
| 421 |
+
lines.push(`- visibility: ${fn.visibility} · mutability: ${fn.mutability}${modsStr}`);
|
| 422 |
+
|
| 423 |
+
const paramsStr = fn.params.length > 0 ? fn.params.join(", ") : "—";
|
| 424 |
+
const returnsStr = fn.returns.length > 0 ? `\`${fn.returns.join(", ")}\`` : "—";
|
| 425 |
+
lines.push(`- parameters: \`(${paramsStr})\` · returns: ${returnsStr}`);
|
| 426 |
+
|
| 427 |
+
if (fn.externalCalls.length > 0) {
|
| 428 |
+
lines.push(`- calls: [${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}]`);
|
| 429 |
+
}
|
| 430 |
+
if (fn.internalCalls.length > 0) {
|
| 431 |
+
lines.push(`- graph: \`${fn.internalCalls.map((c) => `${label} → ${c}`).join(", ")}\``);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
lines.push(`- recurse: ${hasCycle(label, label, callMap, new Set()) ? "yes ⚠" : "no"}`);
|
| 435 |
+
|
| 436 |
+
if (fn.stateReads.length > 0 || fn.stateWrites.length > 0) {
|
| 437 |
+
const reads = fn.stateReads.length > 0 ? fn.stateReads.map((r) => `\`${r}\``).join(", ") : "—";
|
| 438 |
+
const writes = fn.stateWrites.length > 0 ? fn.stateWrites.map((w) => `\`${w}\``).join(", ") : "—";
|
| 439 |
+
lines.push(`- state: reads [${reads}] · writes [${writes}]`);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
lines.push(`- docs: ${renderNatSpec(fn.natspec)}`);
|
| 443 |
+
lines.push("");
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
};
|
| 447 |
+
|
| 448 |
+
const renderContractBrief = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
|
| 449 |
+
lines.push("## Meta");
|
| 450 |
+
const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "—";
|
| 451 |
+
const importsList = imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "—";
|
| 452 |
+
lines.push(`- kind: ${contract.kind} · inherits: ${inherits}`);
|
| 453 |
+
lines.push(`- imports: ${importsList}`);
|
| 454 |
+
lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
|
| 455 |
+
lines.push("");
|
| 456 |
+
|
| 457 |
+
const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
|
| 458 |
+
if (allExternalCalls.size > 0) {
|
| 459 |
+
lines.push("## External Calls");
|
| 460 |
+
lines.push([...allExternalCalls].map((c) => `\`${c}\``).join(" · "));
|
| 461 |
+
lines.push("");
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
if (contract.functions.length > 0) {
|
| 465 |
+
lines.push("## Functions");
|
| 466 |
+
lines.push("| function | visibility | mutability | parameters | returns | modifiers |");
|
| 467 |
+
lines.push("|----------|------------|------------|------------|---------|-----------|");
|
| 468 |
+
for (const fn of contract.functions) {
|
| 469 |
+
const label = fnLabel(fn);
|
| 470 |
+
const params = fn.params.length > 0 ? fn.params.join(", ") : "—";
|
| 471 |
+
const returns = fn.returns.length > 0 ? fn.returns.join(", ") : "—";
|
| 472 |
+
const mods = fn.modifiers.length > 0 ? fn.modifiers.join(", ") : "—";
|
| 473 |
+
lines.push(`| \`${label}\` | ${fn.visibility} | ${fn.mutability} | ${params} | ${returns} | ${mods} |`);
|
| 474 |
+
}
|
| 475 |
+
lines.push("");
|
| 476 |
+
}
|
| 477 |
+
};
|
| 478 |
+
|
| 479 |
+
const fileHeader = (contracts: ContractAnalysis[], mode: "full" | "short", opts: RenderOptions): string[] => {
|
| 480 |
+
const names = contracts.map((c) => c.name).join(", ");
|
| 481 |
+
const label = mode === "full" ? "FULL" : "BRIEF";
|
| 482 |
+
const rankStr = opts.importance !== undefined ? ` | importance: ${opts.importance}/5` : "";
|
| 483 |
+
return [
|
| 484 |
+
`# ${names} · ${label}`,
|
| 485 |
+
`> path: \`${opts.filePath ?? "—"}\` | lines: ${opts.lineCount} | solc: ${opts.solcVersion}${rankStr}`,
|
| 486 |
+
"",
|
| 487 |
+
];
|
| 488 |
+
};
|
| 489 |
+
|
| 490 |
+
export const generateFullMarkdown = (imports: string[], contracts: ContractAnalysis[], opts: RenderOptions): string => {
|
| 491 |
+
const lines: string[] = fileHeader(contracts, "full", opts);
|
| 492 |
+
|
| 493 |
+
for (let i = 0; i < contracts.length; i++) {
|
| 494 |
+
if (contracts.length > 1) {
|
| 495 |
+
if (i > 0) lines.push("---", "");
|
| 496 |
+
lines.push(`## ◆ ${contracts[i].name}`, "");
|
| 497 |
+
}
|
| 498 |
+
renderContractFull(contracts[i], imports, lines);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
return lines.join("\n");
|
| 502 |
+
};
|
| 503 |
+
|
| 504 |
+
export const generateBriefMarkdown = (
|
| 505 |
+
imports: string[],
|
| 506 |
+
contracts: ContractAnalysis[],
|
| 507 |
+
opts: RenderOptions,
|
| 508 |
+
): string => {
|
| 509 |
+
const lines: string[] = fileHeader(contracts, "short", opts);
|
| 510 |
+
|
| 511 |
+
for (let i = 0; i < contracts.length; i++) {
|
| 512 |
+
if (contracts.length > 1) {
|
| 513 |
+
if (i > 0) lines.push("---", "");
|
| 514 |
+
lines.push(`## ◆ ${contracts[i].name}`, "");
|
| 515 |
+
}
|
| 516 |
+
renderContractBrief(contracts[i], imports, lines);
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
return lines.join("\n");
|
| 520 |
+
};
|
src/agents/auditor/utils.ts
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
|
| 2 |
const fileLines = fileContent.split("\n");
|
| 3 |
const snippetLines = codeSnippet.split("\n").map((line) => line.trim());
|
|
@@ -34,3 +69,49 @@ export const matchLines = (fileContent: string, codeSnippet: string): string | n
|
|
| 34 |
|
| 35 |
return null;
|
| 36 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from "node:fs";
|
| 2 |
+
import path from "node:path";
|
| 3 |
+
|
| 4 |
+
import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "./config.ts";
|
| 5 |
+
|
| 6 |
+
export const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
|
| 7 |
+
if (depth > MAX_DEPTH) return;
|
| 8 |
+
|
| 9 |
+
let entries: fs.Dirent[];
|
| 10 |
+
try {
|
| 11 |
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
| 12 |
+
} catch {
|
| 13 |
+
return;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
for (const entry of entries) {
|
| 17 |
+
if (entry.isDirectory()) {
|
| 18 |
+
if (!SKIP_DIRS.has(entry.name)) {
|
| 19 |
+
walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
|
| 20 |
+
}
|
| 21 |
+
} else if (entry.isFile()) {
|
| 22 |
+
const fullPath = path.join(dir, entry.name);
|
| 23 |
+
const ext = path.extname(entry.name).toLowerCase();
|
| 24 |
+
const base = path.basename(entry.name, ext).toLowerCase();
|
| 25 |
+
|
| 26 |
+
if (ext === SOL_EXT) {
|
| 27 |
+
const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
|
| 28 |
+
if (!isTest) solFiles.push(fullPath);
|
| 29 |
+
} else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
|
| 30 |
+
docFiles.push(fullPath);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
|
| 37 |
const fileLines = fileContent.split("\n");
|
| 38 |
const snippetLines = codeSnippet.split("\n").map((line) => line.trim());
|
|
|
|
| 69 |
|
| 70 |
return null;
|
| 71 |
};
|
| 72 |
+
|
| 73 |
+
type ReviewEntry = {
|
| 74 |
+
finding: {
|
| 75 |
+
title: string;
|
| 76 |
+
severity: string;
|
| 77 |
+
location: string;
|
| 78 |
+
description: string;
|
| 79 |
+
codeSnippet: string;
|
| 80 |
+
};
|
| 81 |
+
review: {
|
| 82 |
+
isFalsePositive: boolean;
|
| 83 |
+
confidence: number;
|
| 84 |
+
review: string;
|
| 85 |
+
exploitablePaths: string[];
|
| 86 |
+
};
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
export const buildReviewBlocks = (fileEntries: ReviewEntry[], iterationCount: number): string => {
|
| 90 |
+
const blocks = fileEntries.map(({ finding, review }, i) => {
|
| 91 |
+
const verdict = review.isFalsePositive
|
| 92 |
+
? `FALSO POSITIVO (confiança: ${review.confidence}/100)`
|
| 93 |
+
: `VERDADEIRO POSITIVO (confiança: ${review.confidence}/100)`;
|
| 94 |
+
const pathsLabel = review.isFalsePositive ? "Razão de Bloqueio" : "Caminhos de Exploração";
|
| 95 |
+
const pathsContent =
|
| 96 |
+
review.exploitablePaths.length > 0
|
| 97 |
+
? review.exploitablePaths.map((p) => ` - ${p}`).join("\n")
|
| 98 |
+
: " (nenhum fornecido)";
|
| 99 |
+
|
| 100 |
+
return `[Achado ${i + 1}/${fileEntries.length}] ${finding.title}
|
| 101 |
+
Severidade: ${finding.severity}
|
| 102 |
+
Localização: linhas ${finding.location}
|
| 103 |
+
Descrição: ${finding.description}
|
| 104 |
+
|
| 105 |
+
Código:
|
| 106 |
+
\`\`\`solidity
|
| 107 |
+
${finding.codeSnippet}
|
| 108 |
+
\`\`\`
|
| 109 |
+
|
| 110 |
+
Veredito do Revisor: ${verdict}
|
| 111 |
+
Análise do Revisor: ${review.review}
|
| 112 |
+
${pathsLabel}:
|
| 113 |
+
${pathsContent}`;
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
return `=== Iteração ${iterationCount} — Achados e Revisões do Especialista (${fileEntries.length} achado(s)) ===\n\n${blocks.join("\n\n---\n\n")}`;
|
| 117 |
+
};
|
src/config/llm.ts
CHANGED
|
@@ -5,30 +5,39 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
| 5 |
|
| 6 |
export type LLMProvider = "google" | "openrouter" | "anthropic";
|
| 7 |
|
| 8 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
|
| 10 |
|
| 11 |
switch (provider) {
|
| 12 |
case "openrouter":
|
| 13 |
return new ChatOpenRouter({
|
| 14 |
-
model: process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
|
| 15 |
-
temperature: 0.2,
|
| 16 |
apiKey: process.env.OPENROUTER_API_KEY,
|
| 17 |
-
maxTokens: 4096,
|
| 18 |
});
|
|
|
|
| 19 |
case "anthropic":
|
| 20 |
return new ChatAnthropic({
|
| 21 |
-
model: process.env.ANTHROPIC_MODEL || "claude-
|
| 22 |
-
temperature: 0.2,
|
| 23 |
-
|
|
|
|
| 24 |
});
|
|
|
|
| 25 |
case "google":
|
| 26 |
default:
|
| 27 |
return new ChatGoogleGenerativeAI({
|
| 28 |
apiKey: process.env.GOOGLE_API_KEY || "",
|
| 29 |
-
model: process.env.MODEL_NAME || "gemini-2.5-flash",
|
| 30 |
-
temperature: 0.2,
|
| 31 |
-
maxOutputTokens: 4096,
|
| 32 |
});
|
| 33 |
}
|
| 34 |
}
|
|
|
|
| 5 |
|
| 6 |
export type LLMProvider = "google" | "openrouter" | "anthropic";
|
| 7 |
|
| 8 |
+
export interface LLMOptions {
|
| 9 |
+
model?: string;
|
| 10 |
+
temperature?: number | null;
|
| 11 |
+
maxTokens?: number;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export function createLLM(overrideProvider?: LLMProvider, options?: LLMOptions): BaseChatModel {
|
| 15 |
const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
|
| 16 |
|
| 17 |
switch (provider) {
|
| 18 |
case "openrouter":
|
| 19 |
return new ChatOpenRouter({
|
| 20 |
+
model: options?.model || process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
|
| 21 |
+
...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
|
| 22 |
apiKey: process.env.OPENROUTER_API_KEY,
|
| 23 |
+
maxTokens: options?.maxTokens ?? 4096,
|
| 24 |
});
|
| 25 |
+
|
| 26 |
case "anthropic":
|
| 27 |
return new ChatAnthropic({
|
| 28 |
+
model: options?.model || process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
|
| 29 |
+
...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
|
| 30 |
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
| 31 |
+
maxTokens: options?.maxTokens ?? 4096,
|
| 32 |
});
|
| 33 |
+
|
| 34 |
case "google":
|
| 35 |
default:
|
| 36 |
return new ChatGoogleGenerativeAI({
|
| 37 |
apiKey: process.env.GOOGLE_API_KEY || "",
|
| 38 |
+
model: options?.model || process.env.MODEL_NAME || "gemini-2.5-flash",
|
| 39 |
+
...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
|
| 40 |
+
maxOutputTokens: options?.maxTokens ?? 4096,
|
| 41 |
});
|
| 42 |
}
|
| 43 |
}
|