{"jobId":"582906e4-3ecf-48b2-ab13-e3a2c52743e8","repoUrl":"https://github.com/DmitryiBond/imdirector-contracts","baseCommit":"9701276d06b5c0f9840da7d5b144d51a22514dd9","count":2,"submissions":[{"hash":"2916081fe4d66624b86703e7349cd5d6d8355de307ecb660598e44908877efcc","nodeKey":"adapt_contract_project","role":"implement","attempt":1,"deviceKey":"87f02002d6a2b915040fc35904c8618d01c4b273d9a76505351682413140c9e9","seat":{"tokenId":"1318","agentId":"51434"},"outcome":"failed","accepted":false,"failureReason":"needs_input","failureClass":"task","safetyRefusal":false,"usage":{"model":"gpt-6-astra","turns":4,"runtime":"codex","inputTokens":86974,"wallClockMs":133320,"outputTokens":4225,"cachedInputTokens":606336},"baseCommit":"9701276d06b5c0f9840da7d5b144d51a22514dd9","startCommit":"9701276d06b5c0f9840da7d5b144d51a22514dd9","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"needs input: Authorization to resolve the reproduced conflict between the required afterSwap-only permissions (0x0040), preserved SPEC.md section 1 and existing permission assertions, and the pinned LaunchFactory admission requirement for beforeInitialize or afterInitialize. Finding da49d6eb05e02dfe90291dbe22f3b6da2b9b6a7fdf6b871ac5fdb1aa0a2d5ebb reproduces: the supplied proof, copied unchanged to test/scratch/InitFrontRun.t.sol, fails both test_launchHookDeclaresAnInitializationCallback and test_predictedLaunchPoolCannotBeInitializedBeforeHookExists. PoolManager accepts initialization at an attacker-selected price while the predicted afterSwap-only hook has no code. The protected Hook.protected.t.sol test_permissionsMatchTheDeclaredFlags independently requires an initialization callback. The unchanged project passes forge build and all 28 original tests, including 512 fuzz runs with FOUNDRY_FUZZ_SEED=0xc0f4cbc9c001bf0bdf6f8edfba39e7a51eb0412f21ab9d7a70be24ff673f5aba. Local PoolManager integration ","createdAt":"2026-10-21T19:25:00.574Z","verdict":null,"findings":[],"coverage":null},{"hash":"858ec5fcdb8180a79769bbf6fa2494aa8252fb25cab4d87e29436d800fa1b155","nodeKey":"audit_imported_code","role":"review","attempt":1,"deviceKey":"2f1d693f218c1c18c218a1f700bd525389e6df9ee96e2fb77d7677ad58d77dd0","seat":{"tokenId":"52","agentId":"51194"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":16,"runtime":"claude","inputTokens":32,"wallClockMs":264239,"outputTokens":23776,"cachedInputTokens":1304448},"baseCommit":"9701276d06b5c0f9840da7d5b144d51a22514dd9","startCommit":"9701276d06b5c0f9840da7d5b144d51a22514dd9","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"I found one blocking problem, a real incompatibility with the IMD LaunchFactory. As written, `IMDPhysicalHook` can't pass admission, and the launch pool can be blocked before the factory opens it. The findings are in `.imd-findings.json`; no repository file was changed. I deleted the scratch test and the `out/` and `cache/` build folders my run created.\n\n**High: the hook has no initialization callback** (`src/IMDPhysicalHook.sol:64`)\n- **What goes wrong:** `getHookPermissions()` turns on only `afterSwap` (flags `0x0040`). The factory deploys the hook and initializes its pool in one transaction, and both addresses can be predicted in advance. Because the hook has no init callback, the PoolManager never calls it at initialization, so anyone can initialize the exact Director/IMD pool (fee 12500, spacing 60) at any price before the hook exists.\n- **Consequence:** the factory's own initialize then reverts with `PoolAlreadyInitialized`, so the launch fails. Separately, the protected admission test `test_permissionsMatchTheDeclaredFlags` requires `beforeInitialize || afterInitialize`, so the hook is rejected regardless.\n- **Proof:** a Foundry test is attached to the finding. On the current code both of its tests fail for the reasons above. With `beforeInitialize` enabled, initializing against a hook address with no code reverts with `InvalidHookResponse` in v4-core's `Hooks.callHook`, so the test should pass once fixed. I didn't run it against a fixed build, since writing one would mean editing `src/`.\n- **Why I didn't propose fixing it silently:** it conflicts with SPEC.md §1, which says the hook uses only `afterSwap` with mask `0x0040`. Resolving it needs your approval of a SPEC change:\n  - enable `beforeInitialize` (flags `0x2040`);\n  - accept the call only from the PoolManager, with `sender == $factory` as a new constructor argument;\n  - return the selector, and don't bind or enable inside it, so your manual `bindPool` → gateway → `setEnabled(true)` sequence stays as specified.\n  \n  `launch.json` would then need permissions `[\"beforeInitialize\",\"afterSwap\"]`, constructor args `[\"$owner\",\"$factory\"]` and updated notes.\n\n**Low: `bindPool` can't be corrected** (`src/IMDPhysicalHook.sol:72`)\nIt never checks that the key is an initialized pool or that the non-IMD side is Director, and it can only be called once. For example, binding a key with fee 3000 by mistake succeeds. After that, buys on the real 12500 pool trigger nothing, and binding the correct key reverts with `PoolAlreadyBound`. The only fix is deploying a new hook.\n\n**Info: hard-coded PoolManager** (`src/IMDPhysicalHook.sol:19`)\nThe IMD guidance asks for a `$poolManager` constructor argument rather than a constant. I checked the constant on chain 4663: `0x8366…0951` holds PoolManager code, and the factory address your tests use (`0xa25b…4645`) returns exactly that address from `poolManager()`. It's correct for this launch. The one risk is in testing: a harness that builds the PoolManager at any other address will see every swap revert with `OnlyPoolManager`.\n\n**Info: scope and the token**\n- **Read:** both contracts, every external function of the hook (including the inherited ownership functions), the tests, SPEC/README/VALIDATION, `launch.json`, the protected admission tests and v4-core's `Hooks`/`PoolManager`. On chain 4663 I also read the PoolManager, the IMD token (symbol IMD, 18 decimals) and the factory's `poolManager()`.\n- **Token:** `IMDirectorToken` is clean: it mints exactly 1e27 once to the deploying factory, with no owner, mint, fee, pause or blacklist.\n- **Hook swap logic:** the buy/sell detection, the int128.min handling, the threshold and cooldown boundaries, and the zero return delta are all correct.\n- **Tests:** all 28 local tests pass.\n- **Not reached:** the LaunchFactory source and the IMD admission/deployer code (not in this repository), the IMD token source (bytecode only) and the external gateway. I didn't re-run the 8 fork tests.","createdAt":"2026-10-21T19:22:32.409Z","verdict":null,"findings":[{"severity":"high","title":"Factory incompatibility: hook declares no initialization callback, so the predicted launch pool can be initialized by anyone before the hook exists and admission rejects the hook","path":"src/IMDPhysicalHook.sol","line":64,"description":"getHookPermissions() enables only afterSwap (mask 0x0040); beforeInitialize/afterInitialize are disabled and their implementations revert. The IMD LaunchFactory deploys the hook (CREATE2, mined salt) and calls PoolManager.initialize in one transaction. The hook and token addresses are predictable, and PoolManager.initialize does not call a hook that has no init flag, so it does not care that the hook address has no code yet. Anyone can therefore initialize the exact Director/IMD launch PoolKey (fee 12500, tickSpacing 60, predicted hook) at any sqrtPrice before the factory runs. The factory initialize then reverts with PoolAlreadyInitialized, so the launch transaction fails, or the pool opens at an attacker-chosen price instead of the policy-derived 2500 IMD cap. Separately, the protected admission floor (Hook.protected.t.sol test_permissionsMatchTheDeclaredFlags) asserts `p.beforeInitialize || p.afterInitialize` and fails on this hook, so it cannot be admitted. This is the concrete admission incompatibility with SPEC.md section 1 (\"agree on only afterSwap, mask 0x0040\"). It cannot be fixed without a SPEC change the requester approves: enable beforeInitialize (flags 0x2040), require msg.sender == PoolManager, accept only sender == a constructor-supplied $factory, return IHooks.beforeInitialize.selector, and do NOT bind or enable inside it, so manual owner bindPool/setEnabled stays as specified. launch.json would then need permissions [\"beforeInitialize\",\"afterSwap\"], constructorArgs [\"$owner\",\"$factory\"] in declaration order, and an updated notes line (it currently says initialization callbacks are disabled). Contracts read for this finding: src/IMDPhysicalHook.sol, lib/v4-core Hooks.sol/PoolManager.sol, and on chain 4663 the PoolManager at 0x8366a39cc670b4001a1121b8f6a443a643e40951 (code present).","reproduction":"Local PoolManager at 0x8366…0951. Predicted hook address H whose low 14 bits equal the implementation flags (0x0040) and which has no code. Attacker 0xBAD calls PoolManager.initialize(PoolKey{IMD, 0x9000…0000 (predicted Director), 12500, 60, H}, 2^96/1000). Expected: revert, because the launch pool can only open through the hook factory. Actual: it succeeds, and a later factory initialize of the same key reverts PoolAlreadyInitialized. Also, getHookPermissions() returns beforeInitialize=false and afterInitialize=false, which fails the protected floor assertion \"launch hooks require an initialization callback\". `forge test --match-path test/scratch/InitFrontRun.t.sol` gives 2 failing tests on the current code. With beforeInitialize enabled, the codeless-hook initialize reverts with InvalidHookResponse in Hooks.callHook and both tests pass.","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDPhysicalHook} from \"src/IMDPhysicalHook.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 {Hooks} from \"@uniswap/v4-core/src/libraries/Hooks.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\n\n/// The launch factory deploys the hook and initializes its pool in one transaction. Both the hook\n/// address (CREATE2, mined) and the token address are predictable, so the pool can be initialized\n/// by anyone before the hook has code unless the hook declares an initialization callback.\ncontract InitFrontRunTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant ATTACKER = address(0xBAD);\n\n    function _flags() internal returns (uint160 flags) {\n        // The hook has no immutables, so its runtime code can be read at any address.\n        address probe = address(0x1234);\n        vm.etch(probe, vm.getDeployedCode(\"IMDPhysicalHook.sol:IMDPhysicalHook\"));\n        Hooks.Permissions memory p = IMDPhysicalHook(probe).getHookPermissions();\n        if (p.beforeInitialize) flags |= Hooks.BEFORE_INITIALIZE_FLAG;\n        if (p.afterInitialize) flags |= Hooks.AFTER_INITIALIZE_FLAG;\n        if (p.beforeAddLiquidity) flags |= Hooks.BEFORE_ADD_LIQUIDITY_FLAG;\n        if (p.afterAddLiquidity) flags |= Hooks.AFTER_ADD_LIQUIDITY_FLAG;\n        if (p.beforeRemoveLiquidity) flags |= Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG;\n        if (p.afterRemoveLiquidity) flags |= Hooks.AFTER_REMOVE_LIQUIDITY_FLAG;\n        if (p.beforeSwap) flags |= Hooks.BEFORE_SWAP_FLAG;\n        if (p.afterSwap) flags |= Hooks.AFTER_SWAP_FLAG;\n        if (p.beforeDonate) flags |= Hooks.BEFORE_DONATE_FLAG;\n        if (p.afterDonate) flags |= Hooks.AFTER_DONATE_FLAG;\n        if (p.beforeSwapReturnDelta) flags |= Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG;\n        if (p.afterSwapReturnDelta) flags |= Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n        if (p.afterAddLiquidityReturnDelta) flags |= Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG;\n        if (p.afterRemoveLiquidityReturnDelta) flags |= Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG;\n    }\n\n    function test_launchHookDeclaresAnInitializationCallback() public {\n        vm.etch(address(0x1234), vm.getDeployedCode(\"IMDPhysicalHook.sol:IMDPhysicalHook\"));\n        Hooks.Permissions memory p = IMDPhysicalHook(address(0x1234)).getHookPermissions();\n        assertTrue(p.beforeInitialize || p.afterInitialize, \"launch hooks require an initialization callback\");\n    }\n\n    function test_predictedLaunchPoolCannotBeInitializedBeforeHookExists() public {\n        deployCodeTo(\"PoolManager.sol:PoolManager\", abi.encode(address(this)), MANAGER);\n        uint160 flags = _flags();\n        // The address the factory will deploy the hook to: carries the declared flags, has no code yet.\n        address predictedHook =\n            address((uint160(uint256(keccak256(\"predicted hook\"))) & ~uint160(Hooks.ALL_HOOK_MASK)) | flags);\n        // The launch token address the factory will deploy (predictable from the factory nonce/salt).\n        address predictedToken = address(0x9000000000000000000000000000000000000000);\n        assertEq(predictedHook.code.length, 0);\n        PoolKey memory launchKey = PoolKey({\n            currency0: Currency.wrap(IMD),\n            currency1: Currency.wrap(predictedToken),\n            fee: 12500,\n            tickSpacing: 60,\n            hooks: IHooks(predictedHook)\n        });\n\n        vm.prank(ATTACKER);\n        (bool frontRun,) =\n            MANAGER.call(abi.encodeCall(IPoolManager.initialize, (launchKey, uint160(1 << 96) / 1000)));\n        assertFalse(frontRun, \"anyone initialized the launch pool before the hook was deployed\");\n    }\n}","snippet":"    function getHookPermissions() public pure returns (Hooks.Permissions memory p) {\n        p.afterSwap = true;\n    }","citation":"resolved","blocking":true},{"severity":"low","title":"bindPool is one-shot and does not check that the key is an initialized pool or holds the launch token, so one wrong key permanently disables reactions","path":"src/IMDPhysicalHook.sol","line":72,"description":"bindPool only checks ordering, the hook address and exactly one IMD side. It does not check that the pool is initialized (StateLibrary.getSlot0(poolManager, id).sqrtPriceX96 != 0) or that the non-IMD side is the Director token. Because the hook has no beforeInitialize gate, anyone can create look-alike pools on this hook (any token/IMD at any fee or spacing). Binding cannot be redone (PoolAlreadyBound) and there is no rebind path, so binding a mistyped or look-alike key leaves the hook unable ever to react to the real pool, and the only remedy is a new hook deployment. This guards an owner error, so it is low. Suggested fix, which keeps SPEC's manual one-time binding: require the bound pool to be initialized, and/or take the launch token as a $token constructor argument and require it as the non-IMD currency.","reproduction":"Owner calls bindPool(PoolKey{IMD, Director, fee: 3000, tickSpacing: 60, hooks: hook}), a key that was never initialized (the real pool uses 12500). Expected: revert InvalidPool or not-initialized. Actual: it succeeds (the existing unit test testBindingIsOnceAndSavesExactKey binds a never-initialized key the same way). Then setEnabled(true). A real 1 IMD buy on the 12500 launch pool emits no RelayTriggered, and bindPool with the correct key reverts PoolAlreadyBound.","snippet":"        if (a == address(0) || a >= b || address(key.hooks) != address(this) || (a == imdToken) == (b == imdToken)) {","citation":"resolved","blocking":false},{"severity":"info","title":"PoolManager is a compiled constant, not the $poolManager constructor argument the IMD launch guidance asks for","path":"src/IMDPhysicalHook.sol","line":19,"description":"The requester asked for this constant. I checked it against chain 4663 (public RPC https://rpc.mainnet.chain.robinhood.com): eth_chainId is 0x1237, 0x8366…0951 has PoolManager runtime code, and the factory address the tests use (0xa25b02a1e93903b790e6aaf7da1b8b8d50294645) returns exactly this address from poolManager(). So it is correct for this launch and not a defect on 4663. The only consequence: if the hook is admitted or tested with a manager at any other address (for example a harness that builds PoolManager elsewhere because the manifest has no $poolManager argument), every swap on any pool using the hook reverts with OnlyPoolManager. The adapter should keep this in mind when reading protected-harness results.","reproduction":"Deploy PoolManager with `new PoolManager(owner)` at a non-canonical address, initialize a pool with this hook, add liquidity and swap. Expected under the IMD guidance: the swap completes. Actual: afterSwap reverts OnlyPoolManager. On chain 4663 the constant equals the live factory poolManager(), so production is unaffected.","snippet":"    IPoolManager public constant poolManager = IPoolManager(0x8366a39CC670B4001A1121B8F6A443A643e40951);","citation":"resolved","blocking":false},{"severity":"info","title":"Review scope: IMDirectorToken checked clean; contracts read and not reached","path":"src/IMDirectorToken.sol","line":9,"description":"Read: src/IMDPhysicalHook.sol (every external function: bindPool, setRule, setEnabled, afterSwap, the disabled callbacks, and inherited Ownable2Step transferOwnership/acceptOwnership/renounceOwnership), src/IMDirectorToken.sol, test/*.sol, SPEC.md, README.md, VALIDATION.md, launch.json, the protected floor tests, and v4-core Hooks/PoolManager. On chain 4663 (read-only): PoolManager code at 0x8366…0951, IMD 0x5f7b…7127 (symbol IMD, decimals 18), and factory 0xa25b…4645 poolManager(). Token: plain OZ ERC20 \"IMDirector\"/\"Director\", 18 decimals, mints exactly 1e27 once to the deployer (the factory). No owner, mint, fee, pause or blacklist, and no DELEGATECALL/SELFDESTRUCT. No defect found. Hook afterSwap: the buy/sell sign logic, int128.min widening, inclusive threshold/cooldown, zero return delta and the PoolManager-only caller check are correct. All 28 local tests pass on forge 1.8.3. launch.json fits the schema for the current code (constructorArgs [\"$owner\"] matches constructor(address initialOwner); permissions match getHookPermissions; fee 12500; tickSpacing 60) but must change with the high finding. Not reached: the LaunchFactory source and the IMD service admission/deployer code (not in this repository; only the factory poolManager() getter was read), the IMD token source (bytecode only), and the external relay gateway. The 8-test canonical fork run was not repeated.","reproduction":"Deploy IMDirectorToken from address F: totalSupply() == balanceOf(F) == 1e27, symbol() == \"Director\", decimals() == 18. Calls to mint/pause/blacklist selectors revert. Expected and actual match, so there is no defect.","snippet":"        _mint(msg.sender, 1_000_000_000 ether);","citation":"resolved","blocking":false}],"coverage":null}]}