{"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":"c4579349-2671-450c-b6ba-3e65d8bb27b2","kind":"audit","nodes":[{"acceptedSubmissionHash":"9f1ad68850a1d43b79f8552a418321958a9d205a6940eab50893f646a8722b78","dependsOn":[],"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":"accepted"},{"acceptedSubmissionHash":"bf934e27560dbe0f48a692e0fd85852bf68ced6501d4452dde15c1981eee5985","dependsOn":[],"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":"accepted"},{"acceptedSubmissionHash":"b633fcf46e09e85fd3acc27b7c153ed6662e078c6716f5d95dbae855d0c9d81c","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"2bdb54f4e6c8a858223f1e81dac03e390c08315acd09999913e48837cfe0b22b","dependsOn":[],"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":"accepted"},{"acceptedSubmissionHash":"baa6c380b6456312d1462be242ac4206cb68af60d9572d3f5032afdc2352c158","dependsOn":[],"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"}],"objective":"DeploymentBatcher Phase 1 finalize. Look hardest at the owner check, setMinter only for the wrapper, setTrustedAdapter, the batcher-registry setRegistry call, and the ShareOFT salt rule. Do not change vault, wrapper, or ShareOFT creation. Lottery, FriendKey, Ajna, Permit2, and the app-registry rebind are out of scope.","parentJobId":null,"planHash":"3e4cf494160a2fddadf6765dcf43fa68c4f670ad168425fe8c2f8e36b206b213","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"c4579349-2671-450c-b6ba-3e65d8bb27b2","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"50962","feedbackHash":"652a24ea4b39975dddc6d8c219a8c12c8f5707ba0509bd2f67c632c15c412875","nodeKey":"audit_economics","submissionHash":"9f1ad68850a1d43b79f8552a418321958a9d205a6940eab50893f646a8722b78","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50956","feedbackHash":"a7cdcccbb1c2eecc48780cf170599e5ce2fa768fe8e5a6ed1f16ce280c9498a8","nodeKey":"audit_flow","submissionHash":"bf934e27560dbe0f48a692e0fd85852bf68ced6501d4452dde15c1981eee5985","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51226","feedbackHash":"170cca9e407ac1b71d05a50f46bfc755c4ca5081c26d69c457ca2ba478396556","nodeKey":"audit_judge","submissionHash":"b633fcf46e09e85fd3acc27b7c153ed6662e078c6716f5d95dbae855d0c9d81c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50971","feedbackHash":"926474ea8fb5f81465e3e9ea75e788a2045b63a01785863de89d9cfbce35fdd9","nodeKey":"audit_math","submissionHash":"2bdb54f4e6c8a858223f1e81dac03e390c08315acd09999913e48837cfe0b22b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"801585268ae36720bdb52b9336060494577fa6878fb42fc32a4e41d485fb2640","nodeKey":"audit_permissions","submissionHash":"baa6c380b6456312d1462be242ac4206cb68af60d9572d3f5032afdc2352c158","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"cf55a305f1286f2f3b3a58245c1154f74d478bd928b8094e8acc5f037e1c221d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"35c52a5b502e847c","findings":[],"hash":"2bdb54f4e6c8a858223f1e81dac03e390c08315acd09999913e48837cfe0b22b","nodeId":"ea919675-8bb3-416d-a693-359a181507d0","outcome":"completed","summary":"Wrote `{\"findings\":[]}` to [.imd-findings.json](/home/identitymd5/.identitymd/work/c4579349-2671-450c-b6ba-3e65d8bb27b2/ea919675-8bb3-416d-a693-359a181507d0/.imd-findings.json).\n\nNo substantiated defects in the reviewed owner checks, minter/adapter grants, registry wiring, or salt/adoption boundaries.\n\nValidation was limited: Foundry compilation failed because dependencies are missing; concrete vault, wrapper, and ShareOFT implementations are also absent. No production files changed.","treeHash":null,"usage":{"cachedInputTokens":838784,"inputTokens":115753,"model":"gpt-6-astra","outputTokens":5137,"runtime":"codex","turns":5,"wallClockMs":208537}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3f6a9bdd601cb99f","findings":[{"citation":"resolved","description":"finalizePhase1Split runs under delegatecall, so `registry` at line 947 (`IShareOFT4626(out.shareOFT).setRegistry(address(registry));`) resolves to DeploymentBatcherPhase1Module's own immutable, which is baked into the module bytecode at its construction, not to the shell's `registry()` as IMD_BATCHER_REVIEW.md states (\"setRegistry receives the shell's own registry()\"). The same holds for the module's create2Deployer, bytecodeStore, vaultActivationBatcher and utilsHelper immutables. setPhase1Module (and wireDeploymentHelpers for utilsHelper) validates only the approved codehash and `batcher() == address(this)`; none of the module immutables that must agree with the shell are compared. A Phase 1 module that was built with the wrong registry constructor argument (an honest operator mistake during the module hot-swap the shell is designed for) passes wiring, and every ShareOFT finalized afterwards is wired to that registry while the vaultActivationBatcher flags, CREATE2 factory and store may likewise diverge from the shell. On the live Base deployment both values are currently 0x7773767b72Ea5c7d768E91212f024FD920fC4626 (module 0x4074E614... and shell 0x9326942e... read on 2026-09-30), so today's click is unaffected; the gap is in the wiring check that is supposed to keep it that way across the swap path the shell explicitly supports. Minimal fix: in setPhase1Module require `module.registry() == address(registry)`, `module.create2Deployer() == create2Deployer`, `module.bytecodeStore() == bytecodeStore`, `module.vaultActivationBatcher() == vaultActivationBatcher` and `module.utilsHelper() == utilsHelper` (or have finalize call the shell's own getters), and correct the review note.","line":2766,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"State: shell constructed with registry R1; DeploymentBatcherPhase1Module constructed with registry R2 != R1 and batcher = shell; treasury calls approvePhaseModuleCodehash(module, extcodehash(module)) then setPhase1Module(module). Input: owner calls deployPhase1CoreWithSalt(params, codeIds, 0) then finalizePhase1WithSalt(params, codeIds, 0). Expected: setPhase1Module rejects the module, or the ShareOFT ends with registry() == shell.registry() == R1. Actual: setPhase1Module succeeds and ShareOFT.registry() == R2. The attached test fails on the current code with `ShareOFT registry must be the shell's batcher registry: 0x2e23... != 0x5615...` and passes once setPhase1Module also checks `module.registry() != address(registry)` (verified locally with a temporary one-line patch, then reverted).","severity":"low","snippet":"        if (DeploymentBatcherPhase1Module(_phase1Module).batcher() != address(this)) revert InvalidPhase1Module();","title":"setPhase1Module binds only batcher(); the ShareOFT registry finalize writes is the module's immutable, not the shell's registry()"},{"citation":"resolved","description":"In the catch branch of finalizePhase1Split, `shareOftInitCodeHash` (line 926) is `keccak256(bytecodeStore.get(codeIds.shareOFT) ++ shareOftArgs)` and `verifyHash` (line 937) is the identical expression evaluated a few instructions later in the same transaction with no intervening write path to the store (the only external call in between is create2Deployer.deploy, and the store at 0xcD60040f... exposes no setter; store(bytes) is append-only and content-addressed). The comment says the branch verifies the occupant before adopting it, but nothing about the occupant is read: the real guarantee is the CREATE2 binding between computeAddress(salt, initCodeHash) and the occupant's code, which is fine, but the explicit check is dead code and gives a false impression of an extra integrity gate. Fix: delete lines 937-938, or make the check meaningful by comparing the occupant's runtime codehash to the codehash of a known-good ShareOFT for that codeId.","line":938,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"Input: any finalizePhase1WithSalt call whose create2Deployer.deploy reverts and where code exists at computeAddress(state.shareOftSalt, shareOftInitCodeHash) (for example the project's own test test_finalizePhase1_reusesPredeployedShareOFTOnCreate2Collision). Expected per the comment: the occupant's init code is verified and a mismatched occupant reverts Phase1StateMismatch. Actual: verifyHash == shareOftInitCodeHash for every possible input because both are computed from the same store read and the same args; the revert at line 938 is unreachable.","severity":"info","snippet":"            if (verifyHash != shareOftInitCodeHash) revert Phase1StateMismatch();","title":"ShareOFT adopt path 'verifyHash' check compares a value to itself and can never fail"},{"citation":"resolved","description":"finalizePhase1Split wraps create2Deployer.deploy in try/catch to allow adoption of an existing occupant, but the catch swallows the deployer's revert data. When deploy fails for any reason other than an occupied address (the live deployer 0x18DCcdB3... is permissioned via authorizedDeployers and its owner 0xB05Cf012... is not the protocol treasury; other failures are DeployFailed from a reverting ShareOFT constructor, or out-of-gas inside the call), the branch finds no code at the predicted address and reverts Phase1ShareOFTMissing. deployPhase1Core calls the same deployer without try/catch and surfaces NotAuthorizedDeployer / DeployFailed directly, so the two halves of Phase 1 diagnose the same failure differently. Fix: capture the revert data in `catch (bytes memory err)`, and only fall through to the adopt path when code already exists at expectedAddr; otherwise bubble `err` up so the operator sees the deployer's error.","line":936,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"State: deployer owner calls setAuthorizedDeployer(batcher, false) after deployPhase1CoreWithSalt succeeded (vault and wrapper deployed, coreDone = true). Input: owner calls finalizePhase1WithSalt(params, codeIds, 0). Expected: revert NotAuthorizedDeployer() (as deployPhase1CoreWithSalt would). Actual: create2Deployer.deploy reverts inside the try, the catch computes expectedAddr, finds code.length == 0, and reverts Phase1ShareOFTMissing(); the same output is produced for a ShareOFT constructor revert, so the operator cannot tell a permission revocation from a broken codeId.","severity":"info","snippet":"            if (expectedAddr.code.length == 0) revert Phase1ShareOFTMissing();","title":"try/catch around ShareOFT deploy reports every deploy failure as Phase1ShareOFTMissing, hiding the real cause"},{"citation":"resolved","description":"Everywhere else in the module the CREATE2 init-code hash is derived from the store (`_deriveInitCodeHash(codeId, args)` = keccak256(bytecodeStore.get(codeId) ++ args)); for the bootstrap the raw codeId is passed to computeAddress instead. This is only correct when codeId == keccak256(creationCode). Checked on Base on 2026-09-30: the live store 0xcD60040f... is content-addressed (an eth_call simulation of store(bytes) with the compiled OFTBootstrapRegistry creation code returns exactly keccak256(code), and it has no repoint/setter selector), and the deployer's computeAddress equals the plain CREATE2 formula, so the live path is consistent today. The assumption is undocumented, contradicts the ODA-464-F02 comment on setApprovedCodeId which treats codeIds as repointable labels, and is hidden by the test fixture (MockUniversalCreate2Deployer.configureBootstrap special-cases salt+codeId). Fix: use `_deriveInitCodeHash(codeIds.oftBootstrap, \"\")` on line 849 so the bootstrap follows the same derivation as vault, wrapper and ShareOFT, and add a comment stating the content-addressing invariant of the store.","line":849,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"State: a bytecode store whose ids are not keccak256(creationCode) (e.g. the repository's own MockBytecodeStore with OFT_BOOTSTRAP_CODE_ID = bytes32(4)) behind a deployer whose computeAddress implements the real CREATE2 formula. Input: deployPhase1CoreWithSalt(params, codeIds, 0). Expected: out.oftBootstrapRegistry is the address the bootstrap is (or will be) deployed at. Actual: computeAddress(salt, bytes32(4)) is an address with no code, deploy() then creates the bootstrap at the address for keccak256(creationCode), state.oftBootstrapRegistry records the empty address, and finalizePhase1WithSalt fails in the ShareOFT constructor's getLayerZeroEndpoint call (empty target) and surfaces Phase1ShareOFTMissing; a second token's deployPhase1Core then reverts on the CREATE2 collision. With the live content-addressed store the two addresses coincide and nothing fails.","severity":"info","snippet":"        out.oftBootstrapRegistry = create2Deployer.computeAddress(oftBootstrapSalt, codeIds.oftBootstrap);","title":"Bootstrap registry address is computed with codeIds.oftBootstrap used as the init-code hash"},{"citation":"resolved","description":"The only Phase 1 finalize coverage (DeploymentBatcherPhase1EndpointPoisoningTest) uses MockShareOFT.setMinter, MockVault.setWhitelist (line 56) and MockVault.setTrustedAdapter (line 58) that record nothing, so no test in the snapshot asserts that exactly one minter (the wrapper) is granted, that the wrapper, batcher and vaultActivationBatcher are flagged as trusted adapters on the vault the click deploys, that a stranger cannot call finalizePhase1WithSalt for another owner's params, or that setRegistry receives the batcher registry. These are the four properties the review brief singles out. The salt-override test (ThreeWaySplit) only proves the free-form revert. Fix: extend the mocks to record calls and add tests for: minter set == {wrapper}; trustedAdapter set == {wrapper, batcher, vaultActivationBatcher}; finalize from a non-owner reverts NotOwner; ShareOFT.registry() == batcher.registry().","line":110,"path":"imd-batcher/test/DeploymentBatcher.Phase1EndpointPoisoning.t.sol","reproduction":"Input: change line 950 of DeploymentBatcher.sol to `setMinter(address(this), true)` (a second minter, the ShareOFT backing bug from job 3ed2b921 F-1) or delete lines 957-962 (both setTrustedAdapter calls and the vaultActivationBatcher block). Expected: at least one test fails. Actual: `forge test --match-contract 'DeploymentBatcher(ThreeWaySplitTest|Phase1EndpointPoisoningTest|OVaultRuntimeConfigTest)'` still reports 23 passed, 0 failed, because the mocks discard the setMinter/setTrustedAdapter/setWhitelist arguments.","severity":"info","snippet":"    function setMinter(address, bool) external {}","title":"Phase 1 tests never assert the minter, whitelist or trusted-adapter effects: the mocks are no-ops"}],"hash":"9f1ad68850a1d43b79f8552a418321958a9d205a6940eab50893f646a8722b78","nodeId":"f19f4d70-d5d6-420a-8a85-4cf91aeabf59","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds five findings, one low and four info, and the tree is back to the pinned commit apart from that file.\n\n**Outcome on the five focus areas**\n\n- **Owner check holds.** Every Phase 1 entry point runs `_requireOwner(params.owner)` in the shell and the module refuses direct calls with `NotBatcherContext`. State is keyed by (creatorToken, owner, version), so a stranger can only touch keys containing their own address. Authorized phase callers may act for any owner, which is a trust assumption, not a defect.\n- **Minter is only the wrapper.** No other path in Phase 1 or Phase 2 grants a minter.\n- **Trusted adapter flags** are set on the vault stored in `state.vault`, for the wrapper, the shell and the nonzero `vaultActivationBatcher`. I could not confirm what the vault does with the flag, since the vault source is not in the snapshot.\n- **Registry.** Phase 1 never writes the app registry. The ShareOFT gets the module's immutable registry, not the shell's; on live Base both read `0x7773767b…`, so the click is unaffected today. That mismatch in the wiring check is the low finding, with a Foundry proof that fails now and passes with a one-line check in `setPhase1Module`.\n- **Salt rule holds.** A nonzero override must equal the derived salt, and a ShareOFT bound to another vault is refused. The \"verify occupant\" check in the adopt path is dead code because it compares identical computations; the real guarantee is the CREATE2 binding.\n\n**What I verified beyond the source**\n\n- The live CREATE2 deployer is permissioned, so third parties cannot squat the predicted addresses.\n- The live bytecode store is content-addressed, which is the only reason the bootstrap address computation on line 849 is correct. That latent assumption is reported as info.\n- The module has zero storage slots, so delegatecall cannot clobber shell storage.\n- With the minter switched to the shell and all trusted-adapter calls deleted, all 23 baseline tests still pass, because the mocks are no-ops. That coverage gap is reported as info.\n\n**Trust assumptions to record, not findings.** The protocol treasury approves module codehashes and hot-swaps the Phase 1 module in the same role with no delay, so it can point finalize at a module that omits `setMinter` or `setTrustedAdapter`. The deployer owner, which is not the treasury, can revoke the batcher's deploy authorization and stall finalize.\n\n**Limitations.** Vault, wrapper and ShareOFT implementations are absent from the snapshot, so the semantics of whitelist and trusted-adapter flags and the ShareOFT setters were not audited. No second reviewer seat was used. No Slither run.","treeHash":null,"usage":{"cachedInputTokens":3753521,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":56360,"runtime":"claude","turns":44,"wallClockMs":794746}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"720122d0ca9f60ca","findings":[{"citation":"resolved","description":"finalizePhase1Split executes by delegatecall, but registry is the Phase 1 module's immutable constructor value (lines 771 and 798), not the shell's independent immutable registry. setPhase1Module checks the approved runtime codehash and batcher() only (lines 2763-2767). A replacement of the supplied module built with a different registry is accepted and subsequent finalizations pass that different registry to ShareOFT, violating the requested batcher-registry wiring. This is a treasury configuration consistency defect, not an unprivileged authorization bypass; deliberate malicious module upgrades remain an intended trust assumption. Minimal fix: read the shell registry through IDeploymentBatcherRegistryAccess(batcher).registry() at this call, or reject a module whose registry() differs from address(registry) in setPhase1Module. Neither fix changes vault, wrapper, or ShareOFT creation. The two specialists' registry-mismatch reports are merged here.","line":947,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"Source-level reproduction against the pinned implementation: on chain 8453 construct shell B with registry R1 = address(0x1001). Construct the supplied DeploymentBatcherPhase1Module M with registry R2 = address(0x1002), batcher = B, and the same valid store, CREATE2 deployer, helpers and vault modules as B. As B.protocolTreasury(), call approvePhaseModuleCodehash(M, M.codehash), then setPhase1Module(M). The nonzero, approved-codehash and batcher checks all pass; there is no registry equality check. As Alice = address(0xA11CE), use params {creatorToken: address(0xCAFE), owner: Alice, vaultName: Creator Vault, vaultSymbol: cvTOKEN, shareName: Creator Shares, shareSymbol: sTOK, version: v1, vaultKind: Creator}, valid approved codeIds and salt override zero, and call deployPhase1CoreWithSalt followed by finalizePhase1WithSalt. Use a content-addressed bootstrap codeId so the separate bootstrap-address assumption does not interfere. At line 947 the delegatecalled module calls setRegistry(address(0x1002)) on out.shareOFT while B.registry() remains address(0x1001). Expected: reject M during wiring or call setRegistry(R1). Actual by tracing the immutable assignment, all installation guards and delegatecall: M is installed and finalize calls setRegistry(R2). Foundry execution was attempted with forge test --offline --match-contract 'DeploymentBatcher(ThreeWaySplitTest|Phase1EndpointPoisoningTest|OVaultRuntimeConfigTest)' --summary, but exited 1 before compilation because solc 0.8.30 is absent; imd-batcher/node_modules and imd-batcher/lib are also absent. This reproduction is a source trace, not a claimed executed EVM test.","severity":"low","snippet":"        IShareOFT4626(out.shareOFT).setRegistry(address(registry));","title":"A replacement Phase 1 module can wire a different registry than the batcher registry"}],"hash":"b633fcf46e09e85fd3acc27b7c153ed6662e078c6716f5d95dbae855d0c9d81c","nodeId":"1c313aa9-87fb-48f6-9d40-00e15e986a97","outcome":"completed","summary":"Saved one merged low-severity registry-wiring finding to [.imd-findings.json](/Users/hhyy/.lobster_wallets/identitymd/work/c4579349-2671-450c-b6ba-3e65d8bb27b2/1c313aa9-87fb-48f6-9d40-00e15e986a97/.imd-findings.json).\n\nThe reproduction is a source trace. Foundry execution was blocked by missing solc 0.8.30 and dependencies. JSON and source references were validated; no contracts or configuration changed.","treeHash":null,"usage":{"cachedInputTokens":950656,"inputTokens":121877,"model":"gpt-6-astra","outputTokens":7401,"runtime":"codex","turns":5,"wallClockMs":323324}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"468e82a89b9bfe18","findings":[{"citation":"resolved","description":"finalizePhase1Split runs under delegatecall and calls `IShareOFT4626(out.shareOFT).setRegistry(address(registry))` (line 947) where `registry` is the DeploymentBatcherPhase1Module's own immutable, not the shell's `DeploymentBatcher.registry`. setPhase1Module only checks the approved codehash and `batcher() == address(this)`; it never checks `phase1Module.registry() == registry` (nor create2Deployer, bytecodeStore, utilsHelper or vaultActivationBatcher, which the module also carries as independent immutables). The requester's own review note (IMD_BATCHER_REVIEW.md, 'Registry' priority) states that setRegistry receives the shell's registry(); that is only true while the two immutables happen to agree. Expected: a module whose registry differs from the shell's is rejected at setPhase1Module (as a mismatched `batcher()` is), or finalize reads the shell's registry. Actual: the module is accepted and every subsequent Phase 1 finalize binds the ShareOFT to the module's registry. This is an operator-side consistency gap, not an unprivileged bypass: only protocolTreasury can approve and install a module. Fix: in setPhase1Module (and wireDeploymentHelpers for the Phase 2 module) add `if (DeploymentBatcherPhase1Module(_phase1Module).registry() != address(registry)) revert InvalidPhase1Module();` and the same for create2Deployer/bytecodeStore/utilsHelper/vaultActivationBatcher, so a hot-swapped module cannot silently diverge from the shell's configuration.","line":2766,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"State: shell constructed with registry R1. Deploy DeploymentBatcherPhase1Module with `_registry = R2 != R1` and `_batcher = shell`. As protocolTreasury: approvePhaseModuleCodehash(module, extcodehash(module)); setPhase1Module(module). Expected: revert InvalidPhase1Module. Actual: accepted. Then owner calls deployPhase1CoreWithSalt + finalizePhase1WithSalt. Expected: ShareOFT.registry() == R1 (shell.registry()). Actual: ShareOFT.registry() == R2. Scratch test `forge test --offline --match-path imd-batcher/test/scratch/Phase1ModuleRegistryPin.t.sol` fails with 'ShareOFT wired to non-shell registry: 0xe28d...7b5 != 0xf5b3...e96'.","severity":"low","snippet":"        if (DeploymentBatcherPhase1Module(_phase1Module).batcher() != address(this)) revert InvalidPhase1Module();","title":"Phase 1 module hot-swap does not pin the module's registry to the shell's; finalize wires the module's registry into the ShareOFT"},{"citation":"resolved","description":"deployPhase1Core passes `codeIds.oftBootstrap` directly as the `initCodeHash` argument of `create2Deployer.computeAddress`, while the vault, wrapper and ShareOFT addresses are derived through `_deriveInitCodeHash` = keccak256(bytecodeStore.get(codeId) ++ args). The result is only correct if the operator registered the bootstrap under `codeId == keccak256(creationCode)`. The ODA-464-F02 comments describe codeIds as labels that can be repointed, so nothing on-chain enforces that convention (requireApprovedCodeId snapshots the hash but does not compare it to the id). If the id is a label: (a) the first launch on a chain deploys the bootstrap at the real CREATE2 address but records a codeless address in `state.oftBootstrapRegistry`; finalize then passes that codeless address to the ShareOFT constructor, the CREATE2 deploy reverts, and the catch path reports Phase1ShareOFTMissing, so the tuple can never finalize; (b) every later launch sees `code.length == 0` at the wrong address and re-attempts the CREATE2, which collides and reverts, so Phase 1 core is blocked chain-wide. Live launches on Base evidently work, which implies the convention currently holds there; the defect is that a routine re-registration of the bootstrap bytecode under a new label, or a fresh chain rollout, silently breaks Phase 1 with no on-chain guard. Fix: `out.oftBootstrapRegistry = create2Deployer.computeAddress(oftBootstrapSalt, _deriveInitCodeHash(codeIds.oftBootstrap, bytes(\"\")));` which is byte-identical to today's answer whenever the convention holds and correct when it does not. The existing test fixture (MockUniversalCreate2Deployer.configureBootstrap) hard-wires computeAddress(salt, codeId) to the bootstrap address, so it cannot detect this.","line":849,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"State: bytecode store holds OFTBootstrapRegistry creation code under label id bytes32(4) (keccak256(creationCode) != bytes32(4)); a real CREATE2 deployer (address = keccak(0xff, deployer, salt, keccak(initCode))). Owner calls deployPhase1CoreWithSalt(params, codeIds{oftBootstrap: bytes32(4)}, 0). Expected: out.oftBootstrapRegistry == computeAddress(keccak256('4626:OFTBootstrapRegistry:v1'), keccak256(creationCode)) and has code. Actual: bootstrap is deployed at that real address but out.oftBootstrapRegistry is a different, codeless address. Then finalizePhase1WithSalt: expected ShareOFT deployed; actual revert Phase1ShareOFTMissing(). Then a second owner calls deployPhase1CoreWithSalt: expected success; actual revert (CREATE2 collision on the bootstrap salt). Scratch test `forge test --offline --match-path imd-batcher/test/scratch/Phase1BootstrapCodeId.t.sol`: 3 failures as described. Control: the same harness with the bootstrap registered under keccak256(creationCode) completes core and finalize (see Phase1ModuleRegistryPin.t.sol, which reaches the registry assertion).","severity":"low","snippet":"        out.oftBootstrapRegistry = create2Deployer.computeAddress(oftBootstrapSalt, codeIds.oftBootstrap);","title":"OFT bootstrap registry address is computed from the codeId label instead of the store bytecode hash (asymmetric with vault/wrapper/ShareOFT derivation)"},{"citation":"resolved","description":"In the catch branch of finalizePhase1Split, `shareOftInitCodeHash` (line 926) is computed as keccak256(bytecodeStore.get(codeIds.shareOFT) ++ shareOftArgs) via _deriveInitCodeHash, and `verifyHash` (line 937) is computed as the identical expression on the identical inputs in the same call. The two values are always equal, so the `Phase1StateMismatch` revert on line 938 is unreachable. The AUDIT-2026-07-08-C01 comment presents this as part of the squat protection; it provides none. The actual protections on this path are (1) the CREATE2 address binding, which ties the occupant at `expectedAddr` to the store bytecode + constructor args (including owner = batcher), and (2) the `vault()` check on lines 939-942. Those two are sufficient; the dead check should be removed or replaced with something that can fail (for example comparing `expectedAddr.codehash` against a treasury-snapshotted runtime hash, or checking `IOwnableView(expectedAddr).owner() == address(this)` so an occupant whose ownership has already moved is not re-wired). No unprivileged failing input exists for this line; it is reported because the comment asserts a guarantee the code does not deliver.","line":938,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"No input makes `verifyHash != shareOftInitCodeHash` true: both are keccak256(bytes.concat(bytecodeStore.get(codeIds.shareOFT), shareOftArgs)) evaluated in the same transaction. Delete line 938 and every existing test (23) plus the scratch suite produce identical results.","severity":"info","snippet":"            if (verifyHash != shareOftInitCodeHash) revert Phase1StateMismatch();","title":"ShareOFT adopt path: `verifyHash` integrity check is tautological and can never fire"},{"citation":"resolved","description":"Reviewed and confirmed correct as designed; recorded here so the powers are explicit. (1) Owner check: both deployPhase1CoreWithSalt and finalizePhase1WithSalt call _requireOwner(params.owner); a stranger cannot finalize another owner's tuple because the state key baseSalt includes params.owner. Any address in `authorizedPhaseCallers` may deploy/finalize for ANY params.owner, so each authorized phase caller must itself bind msg.sender to owner. (2) Module hot-swap: protocolTreasury can approvePhaseModuleCodehash + setPhase1Module in one batch with no timelock and no behavioural constraint; a replacement module can omit setMinter/setTrustedAdapter or add a second minter, and it runs with full write access to the shell's storage (phase1SplitStates, pendingAuctions) and to every vault/wrapper/ShareOFT still owned by the batcher. Answer to review priority 6: yes, and the only control is treasury key custody. (3) Trusted adapter / whitelist: finalize grants both flags to the wrapper, the batcher itself, and vaultActivationBatcher (if set) on the vault; finalizePhase2Execution never revokes the batcher's flags, so the batcher remains a cooldown-exempt whitelisted depositor on every vault it launched until the creator (after acceptOwnership) revokes it. All batcher entry points that act on a vault are per-tuple owner-gated, so this is not third-party reachable. (4) Confirmed negatives: setMinter is granted only to `out.wrapper`; setRegistry targets the Registry4626 the module was built with, and Phase 1 issues no writes to any registry (setVault/setWrapperForToken/setShareOFTForToken are Phase 2 only); a nonzero shareOftSaltOverride must equal the derived salt; a ShareOFT occupant bound to a different vault reverts Phase1ShareOFTAlreadyBound; the module rejects direct calls (NotBatcherContext) and holds no storage of its own.","line":2920,"path":"imd-batcher/contracts/shared/deploy/batchers/DeploymentBatcher.sol","reproduction":"Privileged-only states: (a) protocolTreasury: approvePhaseModuleCodehash(M, extcodehash(M)); setPhase1Module(M) where M.finalizePhase1Split omits setMinter -> next finalize yields a ShareOFT with no minter. (b) authorized phase caller X: setAuthorizedPhaseCaller(X, true) by treasury; X calls finalizePhase1WithSalt with params.owner = Alice -> succeeds without Alice's consent. Neither is reachable by an unprivileged actor; no fix proposed beyond key custody and the registry-pin check in the first finding.","severity":"info","snippet":"        if (msg.sender != owner && !authorizedPhaseCallers[msg.sender]) revert NotOwner();","title":"Trust assumptions on the Phase 1 finalize path (documented, not bypasses)"}],"hash":"baa6c380b6456312d1462be242ac4206cb68af60d9572d3f5032afdc2352c158","nodeId":"738e2330-9f9c-4f3e-9aaf-a2981a7fc697","outcome":"completed","summary":"Review complete. The findings file is written and validated: four entries, every quoted snippet matches the tree, and the two proofs are self-contained Foundry tests that fail on the current code.\n\n**Outcome.** The Phase 1 finalize permission surface holds up on every point the task named. A stranger cannot deploy or finalize another owner's tuple, because both entry points call the owner check and the state key includes the owner address. The module refuses direct calls outside delegatecall context and holds no storage of its own. `setMinter` is granted only to the wrapper. The salt override is confirmation-only, and a ShareOFT occupant already bound to another vault is rejected. Phase 1 issues no registry writes at all; the app-registry setters are Phase 2 only. No critical, high, or medium defect was found.\n\n**What I reported** in `.imd-findings.json`:\n\n- **Low, hot-swap does not pin the module's registry.** Finalize writes the Phase 1 module's own `registry` immutable into the ShareOFT, and `setPhase1Module` never compares it to the shell's. The requester's review note assumes they are the same slot. A module built with a different registry is accepted and every later finalize binds ShareOFTs to it. Treasury-only precondition, so low. Proof test attached.\n- **Low, bootstrap address derived from the codeId label.** Phase 1 core passes the codeId straight into `computeAddress` as if it were an init-code hash, unlike the vault, wrapper and ShareOFT paths which hash the store bytecode. With a label id the first launch records a codeless bootstrap and finalize reverts with `Phase1ShareOFTMissing`, and every later launch collides on the bootstrap CREATE2. Live Base launches work, which implies the operator registers the bootstrap under its content hash today, but nothing on-chain enforces that. The existing test mock hard-wires this case and cannot see it. Proof test attached, plus a control showing the content-hash case completes end to end.\n- **Info, tautological integrity check.** The `verifyHash` comparison in the ShareOFT adopt path recomputes the same expression on the same inputs and can never revert. The real protections there are the CREATE2 address binding and the `vault()` check, which are sufficient.\n- **Info, trust assumptions.** Treasury can swap the Phase 1 module to arbitrary logic in one batch with no timelock, which answers the requester's priority 6. Authorized phase callers may act for any owner. The batcher keeps its whitelist and trusted-adapter flags on every vault after handoff until the creator revokes them.\n\n**Verification run.** Baseline suite passes 23 of 23 offline. Scratch suite under `imd-batcher/test/scratch/` fails 4 of 4 for the stated reasons. To compile, I copied forge-std from the local mirror into `imd-batcher/lib/` and installed the npm packages from the lockfile; neither is a source change. No production file was edited.\n\n**Not covered.** Vault, wrapper and ShareOFT implementations are outside this tree, so their setter access control is assumed owner-only. Phase 2 and Phase 3, Lottery, FriendKey, Ajna, Permit2 and the app-registry rebind were out of scope and only read for context.","treeHash":null,"usage":{"cachedInputTokens":2584073,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":49054,"runtime":"claude","turns":42,"wallClockMs":680352}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bb0a3bf63233e5e5","findings":[],"hash":"bf934e27560dbe0f48a692e0fd85852bf68ced6501d4452dde15c1981eee5985","nodeId":"1d8b831e-ea62-4d31-a411-7adf0f265353","outcome":"completed","summary":"Saved [`.imd-findings.json`](/home/imd-worker/.identitymd/work/c4579349-2671-450c-b6ba-3e65d8bb27b2/1d8b831e-ea62-4d31-a411-7adf0f265353/.imd-findings.json) with `{\"findings\":[]}`.\n\nNo substantiated defects found in the scoped owner checks, minter/adapter wiring, registry assignment, or salt/adoption rules. Treasury-approved module behavior and configuration remain trust assumptions.\n\nValidation limits: Foundry exited before compilation because dependencies were missing. Production vault, wrapper, ShareOFT, and CREATE2 deployer implementations are absent, preventing end-to-end verification.\n\nProduction files are unchanged.","treeHash":null,"usage":{"cachedInputTokens":928896,"inputTokens":117992,"model":"gpt-6-astra","outputTokens":5910,"runtime":"codex","turns":5,"wallClockMs":242784}}],"verification":[]}