{"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":"d9395c95-982b-4ad5-9284-c96f8750f707","kind":"skill:adversarial-review","nodes":[{"acceptedSubmissionHash":"bd6d5ab99fc37398a9ec99bad35160ca286ecc96b3be695e6693f8ae5c971d85","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","state":"accepted"}],"objective":"Final pre-launch adversarial review of the deployment: script/DeployMiyamints.s.sol, script/config/1.json and script/config/4663.json, DEPLOY.md, and test/deploy/DeployMiyamints.t.sol, plus what changed in src/ since the three area audits (git diff a991e8c..7906d3d, summarised under \"External audit round 1\" in AUDIT-NOTES.md). Run forge and report what you executed: state plainly anything you could only check by reading. Questions:\n1. Is every external address in the configs the intended contract on its chain?\n2. Does the deploy order leave any window where a half-wired system can be used or front-run?\n3. Does ownership end with OWNER, with nothing left with the deployer?\n4. Can the CreateX salts be squatted?\n5. Do the fee and alignment parameters match the README?\n6. Does the fork test actually prove what it claims?\n7. Vault conversions after the fixes: UniAlignmentVault now swaps and LPs from everything it holds beyond what it owes (`owed` in convertAndAddLiquidity, and _ethToSwap). Is `owed` complete on every path? Can any of this spend fees, protocol fees, pending contributions or the caller reward, underflow and block conversions, or let a contributor capture more than the notes state? Do leftovers settle, or can they build up?\n8. UniswapVaultPriceValidator after the fixes: can anyone still block a conversion cheaply through any pool, and is the proportion right at every price? Does the depth floor ignore pools it should trust?\n9. The remaining fixes: the gas caps and guards in MiyaPool.revealRedeem and ERC721AuctionInstance.settleAuction, the creator-bound CREATE3 salts, owner-only deployVault, merged parked vault cuts, permissionless ERC1155Instance.withdraw. Is each one complete, and does any break a legitimate flow?\n\nmiyamints is an NFT launchpad on Ethereum (later Robinhood Chain 4663) built on ms2fun-contracts. MiyaRegistry (src/registry/MiyaRegistry.sol) is a slim, non-upgradeable registry of factories, collections and alignment vaults; it holds the treasury and the fee terms each collection snapshots at launch: a 1% protocol fee to the treasury, a collection-chosen alignment share (minimum 10%) sent to a Uniswap v4 alignment vault, the rest to the artist. Factories: ERC721 fixed-price drops, ERC721 auctions, ERC1155 editions (also used for music drops) with an optional dynamic pricing module. Vaults (src/vaults/uni/) buy the alignment token with collected ETH through zRouter and add full-range v4 liquidity; benefactors claim LP fees pro rata. MiyaPool (src/pools/) is an NFTX-v2-style zero-fee pool: deposit an NFT for 1e18 pool tokens, redeem TARGETED (pick the id) or RANDOM (commit-reveal, ERC721 only), fixed per pool at creation. The deployment is script/DeployMiyamints.s.sol (runbook DEPLOY.md, per-chain config script/config/). An internal audit, three external area audits and their fixes are already in: AUDIT-NOTES.md lists the findings with their status, including what was accepted or deferred and the trust the keeper model adds. A finding already listed counts only if its fix is wrong or incomplete.\n\nAnswer each numbered question, including the ones where nothing is wrong, and report anything else you find in scope.\n","parentJobId":null,"planHash":"4ce2f863ed98bb096550be43c7aaeb58136444bfa9c6db3c2e0416feb009891c","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"d9395c95-982b-4ad5-9284-c96f8750f707","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":"51484","feedbackHash":"61c2d0b9cd35900a263d40df90b7319675c3bd7eb37b70aca865975dd97283be","nodeKey":"adversarial_review","submissionHash":"bd6d5ab99fc37398a9ec99bad35160ca286ecc96b3be695e6693f8ae5c971d85","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"70772ba94c917dba7d440e4c2e5dcbfb0168ad438419ca459d6e46fb9da2152d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6ed09c2eb1ff6aec","findings":[{"citation":"resolved","description":"convertAndAddLiquidity must process every entry of conversionParticipants in one transaction; there is no partial batch and no way to remove a participant. For each fresh benefactor, once accFeePerShare > 0, _settleFees writes feeCheckpoint from zero (20,000 gas) and benefactorShares is written from zero (20,000 gas). Those two SSTOREs alone cost 20M gas across 500 participants, above mainnet's 16,777,216 per-transaction cap (EIP-7825, the same cap test/pools/MiyaPool.t.sol:246 uses). Adding cold loads and clears gives about 56k per participant, about 28M in total (hand-counted), so the limit is near 295 participants. Gas refunds do not help because the gas must be available during execution. receiveContribution takes an arbitrary benefactor, so one griefer can fill the list with 500 made-up addresses at MIN_CONTRIBUTION. Result: pending ETH in the vault can never convert, new benefactors revert TooManyConversionParticipants, every collection's cut parks in pendingVaultCut, and vaults and collections are not upgradeable. AUDIT-NOTES accepts this griefing as a delay 'until the keeper converts', but that conversion cannot be mined on mainnet. A grep of test/ finds no reference to the constant or its error. NOT EXECUTED: forge could not run in this session because the Bash sandbox failed to start, so the gas figures are hand-counted and the proof below has not been compiled or run.","line":88,"path":"src/vaults/uni/UniAlignmentVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport { Test } from \"forge-std/Test.sol\";\nimport { UniAlignmentVault } from \"src/vaults/uni/UniAlignmentVault.sol\";\nimport { IVaultPriceValidator } from \"src/interfaces/IVaultPriceValidator.sol\";\nimport { IMiyaRegistry } from \"src/registry/interfaces/IMiyaRegistry.sol\";\nimport { MiyaRegistry } from \"src/registry/MiyaRegistry.sol\";\nimport { Currency } from \"v4-core/types/Currency.sol\";\nimport { IHooks } from \"v4-core/interfaces/IHooks.sol\";\nimport { PoolKey } from \"v4-core/types/PoolKey.sol\";\n\ncontract StubToken {\n    function decimals() external pure returns (uint8) { return 18; }\n    function balanceOf(address) external pure returns (uint256) { return 0; }\n}\n\ncontract StubRouter {\n    function swapV4(address, bool, uint24, int24, address, address, uint256 swapAmount, uint256, uint256)\n        external\n        payable\n        returns (uint256, uint256)\n    {\n        return (swapAmount, 1e18);\n    }\n}\n\ncontract StubValidator {\n    function validatePrice(address, uint256) external pure { }\n    function calculateSwapProportion(address, int24, int24, address, bytes32) external pure returns (uint256) {\n        return 5e17;\n    }\n}\n\n/// @dev Stubbing the v4 add makes this conservative: the real conversion costs more gas, not less.\ncontract LpStubVault is UniAlignmentVault {\n    function _addToLpPosition(uint256, uint256, int24, int24) internal override returns (uint128) {\n        return 1e18;\n    }\n}\n\ncontract FullBatchGasCapTest is Test {\n    LpStubVault vault;\n    address keeper = makeAddr(\"keeper\");\n\n    // Contributions run in setUp so the test call sees cold slots and committed original values, as a real tx would.\n    function setUp() public {\n        vm.deal(address(this), 100 ether);\n        StubToken token = new StubToken();\n        MiyaRegistry registry = new MiyaRegistry(address(this), address(0xBEEF), 100, 1000);\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(token);\n        uint256 targetId = registry.addTarget(\"t\", tokens);\n        registry.setConversionKeeper(keeper);\n\n        vault = new LpStubVault();\n        vault.initialize(\n            address(this),\n            address(0x1111),\n            address(0xdead), // no code: fee collection returns (0, 0)\n            address(token),\n            address(new StubRouter()),\n            3000,\n            60,\n            IVaultPriceValidator(address(new StubValidator())),\n            IMiyaRegistry(address(registry)),\n            targetId\n        );\n        vault.setV4PoolKey(\n            PoolKey({\n                currency0: Currency.wrap(address(0)),\n                currency1: Currency.wrap(address(token)),\n                fee: 3000,\n                tickSpacing: 60,\n                hooks: IHooks(address(0))\n            })\n        );\n\n        // Existing holder and accrued yield, so accFeePerShare > 0.\n        vault.receiveContribution{ value: 1 ether }(Currency.wrap(address(0)), 1 ether, address(0xA11CE));\n        vm.prank(keeper);\n        vault.convertAndAddLiquidity(1, 0);\n        vault.depositFees{ value: 1 ether }();\n\n        // Fill the batch up to whatever cap the contract enforces.\n        for (uint256 i; i < 2000; ++i) {\n            try vault.receiveContribution{ value: 0.001 ether }(\n                Currency.wrap(address(0)), 0.001 ether, address(uint160(0x100000 + i))\n            ) { } catch {\n                break;\n            }\n        }\n    }\n\n    function test_fullBatchConvertsWithinMainnetTxGasCap() public {\n        assertGt(vault.totalPendingETH(), 0);\n        vm.prank(keeper);\n        vault.convertAndAddLiquidity{ gas: 16_777_216 }(1, 0);\n        assertEq(vault.totalPendingETH(), 0);\n    }\n}","reproduction":"Mainnet state: accFeePerShare > 0 (any collected yield, or depositFees of any amount). Griefer calls receiveContribution{value: 0.001 ether}(ETH, 0.001 ether, address(0x100000 + i)) for i in 0..499, costing 0.5 ETH plus gas. Keeper calls convertAndAddLiquidity(1, 1) with gas 16,777,216. Expected: the batch converts and totalPendingETH is 0. Actual: out of gas, on every retry, with no recovery path. The batch is also stuck organically above roughly 295 fresh benefactors.","severity":"high","snippet":"    uint256 public constant MAX_CONVERSION_PARTICIPANTS = 500;","title":"A full conversion batch cannot fit in a mainnet transaction, so 0.5 ETH of contributions permanently stops a vault's conversions"},{"citation":"resolved","description":"When pendingFeeTokens is non-zero, _convertPendingFeeTokens requires minEthOut >= 1 and passes it to zRouter as amountLimit. zRouter's unlockCallback reverts Slippage when amountLimit != 0 and amountOut < amountLimit (checked by reading zRouter's source on GitHub). If the pending tokens are worth under 1 wei of ETH, output is 0, so a floor of 1 reverts Slippage and a floor of 0 reverts AmountMustBePositive. convertAndAddLiquidity and convertFeeTokens both collect fees and then call this path, and the collection is forced, so the keeper cannot skip it. The block lasts until pendingFeeTokens is worth at least 1 wei. Organic token sells or a keeper sell bundled ahead of the conversion clear it, but DEPLOY.md does not mention this, and the griefer can repeat it after each clearing. Found by reading; not run.","line":609,"path":"src/vaults/uni/UniAlignmentVault.sol","reproduction":"Mainnet INFER pool at the fork test's price, about 2.7M INFER per ETH (1643^2). The vault holds a position. With no token fees pending, a griefer sells 1e6 wei of INFER through the pool, a fee of 1e4 wei, of which the vault earns a few thousand wei. Keeper calls convertAndAddLiquidity(x, 1): collection sets pendingFeeTokens to about 3000 wei, the swap outputs 0 ETH, and it reverts Slippage. convertAndAddLiquidity(x, 0) and convertFeeTokens(0) revert AmountMustBePositive. Expected: dust fee tokens are skipped and the conversion proceeds. Actual: no argument succeeds until pendingFeeTokens is worth at least 1 wei of ETH after the swap fee.","severity":"medium","snippet":"        if (minEthOut == 0) revert AmountMustBePositive();","title":"Dust fee tokens block both keeper entry points, and a griefer can create them for almost nothing"},{"citation":"resolved","description":"Shares are issued only for liquidity actually added, while tokens left over from a batch are spent on the next one and credited pro rata to whoever is pending then. When leftover token value covers the new ETH, _ethToSwap returns 0 and the contributor's ETH is paired entirely with the previous batch's tokens. That contributor receives liquidity for about 2x their ETH. Shares are not redeemable for principal, so what moves is the LP-fee yield. AUDIT-NOTES calls this a small windfall and quotes leftovers under 2% of a batch, but the fork test only measures the end state after all rounds, not the leftover after one conversion, while DEPLOY.md itself says about half of a 10 ETH batch can go unused on the first conversion. Anyone can time this by sending ETH to receive() after seeing a conversion. Numbers are hand-computed from the swap maths and were not run.","line":395,"path":"src/vaults/uni/UniAlignmentVault.sol","reproduction":"Pool as seeded in test/deploy/DeployMiyamints.t.sol: L = 8000e18, sqrtP = 1643 (about 4.87 ETH and 13.1M INFER). Batch A = 10 ETH: swaps about 5 ETH, receives about 6.63M INFER, price moves to about 664k INFER per ETH. Only about 5.0 ETH can pair, so about 3.3M INFER (about 4.97 ETH of value) stays unused and A gets about 4074e18 shares. Batch B = 4.97 ETH: alreadyHeld >= target so ethToSwap = 0, and B gets about 4049e18 shares. Expected per the notes: B's shares per ETH are about A's plus a small windfall. Actual: B holds about 49.8% of the combined new shares for about 33% of the ETH, about 2x A's rate.","severity":"medium","snippet":"        uint256 ethAvailable = address(this).balance - owed;","title":"After a large thin-pool batch, the next contributors get about twice the shares per ETH, not the 'small windfall' the notes state"},{"citation":"resolved","description":"AUDIT-NOTES accepts the virtual-depth weakness on the basis that blocking also needs a v2 pair holding 10+ WETH of the griefer's own. That condition is met for free by any alignment token that already has a genuine v2 pair of 10+ WETH and no 0.3% v3 pool. _getV3PriceAndLiquidity returns the first tier that passes, starting at 3000, and a very narrow position reaches 10 ETH of virtual depth with dust capital. The griefer needs only gas, dust and 30 minutes for the TWAP. The defaults do not qualify: per the notes INFER has no v2 or v3 pool, and 4663 has no v2 factory. It applies to any later target the owner adds through a new vault. The keeper can reset it by swapping the griefer's pool out of range, and the griefer can then re-add. Found by reading; not run.","line":122,"path":"src/peripherals/UniswapVaultPriceValidator.sol","reproduction":"Token T has a real v2 pair with 50 WETH and no v3 pool at fee 3000. Griefer creates the v3 T/WETH 3000 pool at a price 10% away from v2, adds a one-tick-spacing position with liquidity() giving at least 10 ETH of virtual depth, and waits 1800 seconds. validatePrice(T, x) then reverts PriceDeviationTooHigh, so every convertAndAddLiquidity reverts. The notes' condition of 10+ WETH of the griefer's own is not required.","severity":"low","snippet":"        if (hasV2Pool && reserveWETH >= MIN_REFERENCE_WETH && hasV3Pool) {","title":"The depth floor can still be used to block tokens that have a real v2 pair, without the griefer's own 10 WETH"},{"citation":"resolved","description":"loadConfig runs _requireCode on the external dependencies, but not on OWNER or TREASURY, and transferOwnership is one-step. DEPLOY.md warns that the mainnet governance Safe has no code on 4663, which makes this mistake likely. Passing that address gives a run where every _check passes, because they only compare addresses. If no Safe can ever be deployed there, the registry, vault factory and collection factories are lost: no keeper replacement, no vaults, no treasury change. Collections launched meanwhile also snapshot the treasury immutably. Found by reading; not run.","line":278,"path":"script/DeployMiyamints.s.sol","reproduction":"On 4663: OWNER=<address with no code on 4663, such as the mainnet governance Safe> TREASURY=<any address with no code> forge script script/DeployMiyamints.s.sol --broadcast. Expected: a ConfigError like the other dependencies. Actual: the deployment succeeds and _check passes with r.owner() == a codeless address.","severity":"low","snippet":"            registry.transferOwnership(c.owner);","title":"The script hands ownership and fixes the treasury without checking the address has code on this chain"},{"citation":"resolved","description":"The mainnet config says the hookless ETH/INFER pool does not exist yet, and PoolManager.initialize is permissionless. A griefer can initialize it first at any price. A later seeding transaction that also initializes then reverts, and one that adds liquidity without a price check deposits at the griefer's price. _requirePool only checks sqrtPriceX96 != 0 and liquidity != 0, so the script's only pool gate passes on such a pool. DEPLOY.md requires seeding but gives no procedure, price check or minimum depth. This is the one pre-deploy window I found; the deployment's own transaction order has none. The keeper's floors still protect swaps, provided they do not come from this same pool's quote. Found by reading; not run.","line":463,"path":"script/DeployMiyamints.s.sol","reproduction":"Before seeding, an attacker calls PoolManager.initialize(ETH/INFER/10000/200/no hook, TickMath.getSqrtPriceAtTick(800000)) and adds liquidity 1 full range. Running the script passes _requirePool. Expected: the script stops on a pool at a nonsense price with no depth. Actual: deployment proceeds, and InferPoolSeeder-style seeding reverts with PoolAlreadyInitialized.","severity":"low","snippet":"        if (IPoolManager(c.poolManager).getLiquidity(id) == 0) {","title":"The pool check accepts one unit of liquidity at any price, and the required seeding can be front-run"},{"citation":"resolved","description":"setUp always calls MANAGER.initialize for the same pool key the launch must seed. The runbook needs that pool before deploying and defaults the fork to latest block, so after seeding, setUp reverts PoolAlreadyInitialized and every fork test fails. This is when the suite matters most. Even with an older FORK_BLOCK it tests a synthetic pool (sqrtP 1643, L 8000e18), never the pool the deployment will use. Found by reading; not run.","line":107,"path":"test/deploy/DeployMiyamints.t.sol","reproduction":"Seed the hookless ETH/INFER 10000/200 pool on mainnet, leave FORK_BLOCK unset, and run forge test --match-path test/deploy/DeployMiyamints.t.sol. Expected: the tests exercise the seeded pool. Actual: setUp reverts in MANAGER.initialize with PoolAlreadyInitialized.","severity":"low","snippet":"        seeder.seed{ value: 100 ether }();","title":"The fork suite stops working at latest block once the real INFER pool is seeded"},{"citation":"resolved","description":"On the current validator these two tests no longer exercise the fix. validatePrice can only revert when a v2 pair holds 10+ WETH, and per the notes INFER has no v2 pair, so a mocked v3 pool cannot block regardless of depth. calculateSwapProportion reads only the vault's own v4 pool, so the dust v4 pool in test_fork_dustV4PoolAtExtremeTickDoesNotBlockConversion is never read. The depth floor is covered by mocks in test/peripherals/UniswapVaultPriceValidator.t.sol. The fork suite also never calls convertFeeTokens, claimFees or a non-zero fee-token conversion against the real zRouter, never runs on 4663, and shows as skipped rather than failed without an RPC. Found by reading; not run.","line":357,"path":"test/deploy/DeployMiyamints.t.sol","reproduction":"Remove the _hasReferenceDepth check from _queryV3PoolForFee, then run test_fork_dustV3PoolDoesNotBlockLaterConversions. Predicted: it still passes, because no v2 reference exists. Mock-based unit test test_dustV3PoolAtWrongPriceIsIgnored would fail.","severity":"info","snippet":"    function test_fork_dustV3PoolDoesNotBlockLaterConversions() public onFork {","title":"Two fork regressions cannot fail, and the fork suite skips fee-token conversion and fee claims"}],"hash":"bd6d5ab99fc37398a9ec99bad35160ca286ecc96b3be695e6693f8ae5c971d85","nodeId":"64517f59-1611-493a-89f7-0d1024d2fec1","outcome":"completed","summary":"# Final pre-launch review: one high finding, nothing executed\n\n**I ran no forge, cast or git commands.** Every Bash call failed with `Sandbox is required but failed to initialize`, so everything below comes from reading the code.\n- **Proof not run:** the proof for the high finding has never been compiled, so it may need fixes. Its gas figures are hand-counted.\n- **Diff not read:** I couldn't run `git diff a991e8c..7906d3d`, so I reviewed the current tree against the \"External audit round 1\" notes.\n- **No on-chain checks:** nothing was queried on chain, and Robinhood's Blockscout API returned 403.\n\nEight findings are in `.imd-findings.json`, the only file I changed.\n\n## Findings by severity\n\n| # | Severity | Finding | Location |\n|---|---|---|---|\n| 1 | **High** | A full 500-participant batch needs about 28M gas (20M from two storage writes alone), but mainnet's per-transaction cap is 16.78M. About 0.5 ETH sent for made-up benefactors permanently stops that vault's conversions. | `UniAlignmentVault.sol:88` |\n| 2 | Medium | If pending fee tokens are worth under 1 wei of ETH, both keeper functions revert for any floor. A griefer can cause this with one tiny sell. | `UniAlignmentVault.sol:609` |\n| 3 | Medium | After a large batch in a thin pool, the next contributor gets about 2x the shares per ETH, not the \"small windfall\" the notes state. | `UniAlignmentVault.sol:395` |\n| 4 | Low | For a token with a real v2 pair and no 0.3% v3 pool, a griefer can block conversions with dust, without the 10 WETH of their own the notes assume. | `UniswapVaultPriceValidator.sol:122` |\n| 5 | Low | `OWNER` and `TREASURY` aren't checked for code, so the one-step handoff can go to a codeless address and every check still passes. | `DeployMiyamints.s.sol:278` |\n| 6 | Low | The pool check accepts liquidity of 1 at any price, and anyone can initialize the unseeded INFER pool first. | `DeployMiyamints.s.sol:463` |\n| 7 | Low | Once the real pool is seeded, fork test `setUp` reverts at latest block. | `DeployMiyamints.t.sol:107` |\n| 8 | Info | Two fork regressions can't fail on the current validator, and the fork suite has coverage gaps (see question 6). | `DeployMiyamints.t.sol:357` |\n\nFinding 1 is not the delay `AUDIT-NOTES.md` accepted: the notes assume the keeper can still convert, but on mainnet that transaction can't be mined. It also happens without an attacker above about 295 new benefactors in one batch.\n\n## Answers to your questions\n\n**1. Are the config addresses the intended contracts? Partly confirmed.**\n\n| Address | Status |\n|---|---|\n| Mainnet WETH, v2 and v3 factories, CreateX, Permit2 | Match the canonical addresses from memory, not fetched in this session |\n| Mainnet PoolManager | Also matches zRouter's hardcoded `V4_POOL_MANAGER` |\n| 4663 WETH and USDG | Confirmed by [Robinhood's contract docs](https://docs.robinhood.com/chain/contracts/) |\n| 4663 PoolManager | Third-party match only ([rhcswap](https://github.com/jumpboxtech/rhcswap)) |\n| INFER, imdUSD Treasury | Not confirmed; a search found no page for the INFER address |\n| Mainnet zRouter | Config and `DEPLOY.md` cite the README; I did not confirm the deployed address |\n| 4663 zRouter build, 4663 v3 factory | Not confirmed; the config notes say only that code exists there |\n\nPlease check the unconfirmed ones on chain before broadcast.\n\n**2. Is there a half-wired window? Not in the deploy order.**\n- Collections need a registered vault, and that only exists after wiring, so a 1155 collection can't launch without its pricing module.\n- `deployVault` is owner-only and initializes in the same transaction. Fee terms are set in the constructor.\n- The real gap comes before deployment, in pool seeding (finding 6).\n\n**3. Does ownership end with `OWNER`? Yes, when `OWNER` is set and is not the deployer.**\n- All Ownable contracts are transferred, and the deployer keeps only the power to use its own salts.\n- If `OWNER` is unset, the deployer keeps ownership. That is documented and prints a warning, bu","treeHash":null,"usage":{"cachedInputTokens":1012936,"inputTokens":15,"model":"claude-opus-5-5","outputTokens":66889,"runtime":"claude","turns":46,"wallClockMs":718701}}],"verification":[]}