{"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":"6ffe5f84-08ae-4ab0-acdc-201d2bed27e8","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":null,"dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","state":"failed"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_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":"waiting"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_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":"waiting"},{"acceptedSubmissionHash":"e0ab419b44c5603a6779229306563eb377b3181cbf0ffd124b5e1dfef47bcbb6","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_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":["adapt_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":"waiting"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_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":"waiting"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"waiting"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_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":"waiting"}],"objective":"Token name: INK. Token symbol: INK. Launch it with the InkHook from this repository on its pool, paired with IMD, on Ethereum mainnet. Keep the hook's logic as written. Constructor values: portraits = 0x54fa3515848440ddde2aefe45a382bd868cacd15, vault = 0x33c85714c7d78fdb253fe1cf761d2d5c3c12d747, imd = the pool's IMD currency, treasury = 0xbc10B3662231c830d2ee10bc6359dB17bEa4b3d4, feeBps = 25 (a 0.25% hook fee on top of the pool's own fee, waived for wallets holding an agent portrait; IMD fees go to the treasury and INK fees are sent to the vault's liquidity contract (vault.liquidity()), which adds them back to the pool; nothing is burned; do not sell hook fees for ETH). Trading lock: the hook rejects every swap until vault.released() is true; there is no time-based fallback (anyone can call release() once the vault's 24-hour claim window ends, and it adds the unclaimed INK to the pool in the same transaction). The agent-holder fee waiver requires portraits.isAgentHolder(tx.origin), which needs the portrait held since an earlier block. Nobody can change any of these after deployment: the hook has no owner, admin or upgrade role, and none may be added. The pool trading fee share goes to the paying wallet, which is 0x824E12b38CfF2D3e7ad9607a0212f7F9Eb6a06E3.","parentJobId":null,"planHash":"4a386e8966d100a94f4ed39dc9148e1c5d155ed54d5939ad23e4fabc7cef3806","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6ffe5f84-08ae-4ab0-acdc-201d2bed27e8","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2317-src-inkhook-sol"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51450","feedbackHash":"e9d78b37583dd6633e0d735bb0c69fabb89068f0c1dc84a9f9bf5ec1acf451c6","nodeKey":"audit_imported_code","submissionHash":"e0ab419b44c5603a6779229306563eb377b3181cbf0ffd124b5e1dfef47bcbb6","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"61cdbb5fd9d7771620f751c49e4022ed5d2a52181929f54cc7966714162e9de4","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f74e4c7229c480f9","findings":[],"hash":"8ffad96438834f2529d08d59ea8699b1869a66dbd3672866d86427f979ad1b4a","nodeId":"331ce20f-3e0d-4c3c-9717-23884ba619a3","outcome":"failed","summary":"needs input: The exact source and reproducible build inputs for the existing Ethereum InkVault at 0x33c85714c7d78fdb253fe1cf761d2d5c3c12d747 and InkPortraits at 0x54fa3515848440ddde2aefe45a382bd868cacd15 are missing. Audit finding deaf4145e415468cdad4fc18e939c0d368ecb1d9d407b2de5a3aa883a6afced9 reproduces at block 26235009: their runtimes are 12400 and 9307 bytes, respectively, versus 11120 and 8078 bytes compiled from this repository. The deployed contracts expose additional entry points, so this tree cannot establish their release, liquidity-destination and fee-waiver guarantees. Sourcify returns no match for both addresses; Blockscout provides bytecode but no verified source; Etherscan source pages return HTTP 403. The task requires fixing reproduced findings while retaining these deployed dependencies, which cannot be completed by editing the hook or guessing their missing source. No implementation changes have been made. — Please provide the exact source repository and commit, compiler setting","treeHash":null,"usage":{"cachedInputTokens":682240,"inputTokens":66060,"model":"gpt-6-astra","outputTokens":4881,"runtime":"codex","turns":4,"wallClockMs":145159}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f2101c240e0b52a9","findings":[{"citation":"resolved","description":"InkHook.beforeInitialize only checks msg.sender == poolManager, key.hooks == this and that one of the two currencies is IMD. It ignores the `sender` argument (the account that called PoolManager.initialize), the other currency, the fee tier, the tick spacing and the opening sqrtPriceX96, and it is one-shot: the first successful initialize sets poolCreated and every later one reverts with PoolAlreadyCreated. The hook has no owner and no reset, so the flag is permanent. Between the moment the hook's code exists at its mined address and the moment the launch factory calls initialize for the real INK/IMD pool, anyone can call PoolManager.initialize with (IMD, any token X, any fee, any spacing, this hook): the hook binds `ink` to X, sets poolCreated, and the factory's own initialize then reverts, so the launch cannot proceed with this hook and the hook must be redeployed at a new mined address. The same window also lets a stranger initialize the correct INK/IMD key at an arbitrary price (the PoolManager then rejects the factory's initialize with PoolAlreadyInitialized and the seed goes in at the stranger's price). The task's launch flow states the factory both deploys the hook and initializes its pool; if those happen in one transaction the window is closed and this is unreachable, but nothing in the hook enforces that and the hook's own comment claims 'One pool only: INK paired with IMD'. The reference manifest example for this launch kind restricts beforeInitialize to the launch factory. Fix without changing the constructor: record `deployer = msg.sender` as an immutable in the constructor and require `sender == deployer` in beforeInitialize (the factory deploys and initializes), or take the factory address as a constructor argument; optionally also pin fee and tickSpacing.","line":123,"path":"src/InkHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MockERC20} from \"solmate/src/test/utils/mocks/MockERC20.sol\";\nimport {Deployers} from \"v4-core-test/utils/Deployers.sol\";\nimport {IHooks} from \"v4-core/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/types/Currency.sol\";\n\nimport {InkHook, IAgentPortraits, IInkVault} from \"src/InkHook.sol\";\n\n/// Stand-ins for the vault and portraits: the hook only reads them.\ncontract StubVault {\n    bool public released;\n    address public liquidity;\n}\n\ncontract StubPortraits {\n    function isAgentHolder(address) external pure returns (bool) {\n        return false;\n    }\n}\n\n/// InkHook.beforeInitialize accepts any initializer and any paired token / fee / tick spacing, and is one-shot\n/// (poolCreated). Once the hook exists at its mined address and before the launch factory initializes the\n/// INK/IMD pool, anyone can initialize a pool of IMD against any token with this hook. That flips\n/// poolCreated, so the factory's own initialize reverts with PoolAlreadyCreated and the launch cannot use the\n/// hook. The test plays the factory: it deploys the hook, an attacker initializes a junk pool, then the\n/// factory initializes the real pool, which must succeed.\ncontract InkHookInitFrontrunTest is Test, Deployers {\n    address attacker = makeAddr(\"attacker\");\n    address treasury = makeAddr(\"treasury\");\n\n    MockERC20 imd;\n    MockERC20 ink;\n    MockERC20 junk;\n    InkHook hook;\n\n    function setUp() public {\n        deployFreshManagerAndRouters();\n        imd = new MockERC20(\"Identity.md\", \"IMD\", 18);\n        ink = new MockERC20(\"INK\", \"INK\", 18);\n        junk = new MockERC20(\"JUNK\", \"JUNK\", 18);\n\n        address hookAddr = address(\n            uint160(\n                Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n                    | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG\n            ) ^ (0x4444 << 144)\n        );\n        // The test contract is the launch factory: it deploys the hook and later initializes its pool.\n        deployCodeTo(\n            \"InkHook.sol:InkHook\",\n            abi.encode(\n                manager,\n                IAgentPortraits(address(new StubPortraits())),\n                IInkVault(address(new StubVault())),\n                Currency.wrap(address(imd)),\n                treasury,\n                uint16(25)\n            ),\n            hookAddr\n        );\n        hook = InkHook(hookAddr);\n    }\n\n    function _key(address a, address b, uint24 fee, int24 spacing) internal view returns (PoolKey memory) {\n        (address c0, address c1) = a < b ? (a, b) : (b, a);\n        return PoolKey({\n            currency0: Currency.wrap(c0),\n            currency1: Currency.wrap(c1),\n            fee: fee,\n            tickSpacing: spacing,\n            hooks: IHooks(address(hook))\n        });\n    }\n\n    function test_strangerCannotPreemptTheLaunchPool() public {\n        // 1. A stranger initializes IMD/JUNK with the launch hook, between hook deployment and the factory's\n        //    initialize. Its outcome is not asserted: on fixed code this call reverts, on current code it passes.\n        PoolKey memory junkKey = _key(address(imd), address(junk), 3000, 60);\n        vm.prank(attacker, attacker);\n        (bool ok,) = address(manager).call(\n            abi.encodeWithSelector(manager.initialize.selector, junkKey, TickMath.getSqrtPriceAtTick(0))\n        );\n        ok; // ignored on purpose\n\n        // 2. The launch factory initializes the real INK/IMD pool. This must succeed and bind the hook to INK.\n        PoolKey memory realKey = _key(address(imd), address(ink), 12_500, 200);\n        manager.initialize(realKey, TickMath.getSqrtPriceAtTick(0));\n\n        assertTrue(hook.poolCreated(), \"the launch pool exists\");\n        assertEq(Currency.unwrap(hook.ink()), address(ink), \"the hook is bound to INK, not to the stranger's token\");\n    }\n}","reproduction":"State: InkHook deployed at its flagged address with imd = IMD token, no pool initialized yet. Attacker (any EOA) calls PoolManager.initialize(PoolKey{currency0/1 = sorted(IMD, JUNK), fee 3000, tickSpacing 60, hooks = InkHook}, sqrtPriceAtTick(0)). Expected: the hook rejects a pool that is not the launch's INK/IMD pool. Actual: the call succeeds, hook.poolCreated() == true, hook.ink() == JUNK. The factory then calls PoolManager.initialize(PoolKey{sorted(IMD, INK), 12500, 200, InkHook}, price) and it reverts with WrappedError(hook, 0xdc98354e beforeInitialize, 0x02937516 PoolAlreadyCreated, 0xa9e35b2f HookCallFailed). Run: forge test --match-path test/scratch/InkHookInitFrontrun.t.sol (fails on current code with exactly that WrappedError).","severity":"medium","snippet":"        if (poolCreated) revert PoolAlreadyCreated();","title":"beforeInitialize accepts any initializer and any paired token, so a stranger can consume the one-shot pool slot before the factory initializes INK/IMD"},{"citation":"resolved","description":"The hook trusts two immutable dependencies for its only two guarantees: vault.released() gates every swap and vault.liquidity() receives every INK fee; portraits.isAgentHolder() decides the fee waiver. The requester gives mainnet addresses for them (vault 0x33c85714c7d78fdb253fe1cf761d2d5c3c12d747, portraits 0x54fa3515848440ddde2aefe45a382bd868cacd15). Both have code, are wired to each other (portraits.vault() == the vault, vault.portraits() == the portraits) and are owned by the paying wallet 0x824E12b38CfF2D3e7ad9607a0212f7F9Eb6a06E3, but their runtime bytecode is not what this repository compiles. The deployed vault is 12,400 bytes against 11,120 compiled, and its dispatcher contains four selectors absent from src/InkVault.sol: 0xe30c3978 pendingOwner(), 0x79ba5097 acceptOwnership() (OpenZeppelin Ownable2Step, while the source inherits plain Ownable) plus 0x6eae2807 and 0xb25e26ac, which match no known signature. The deployed portraits is 9,307 bytes against 8,078 and adds the same two Ownable2Step selectors plus 0x11db7435 and 0x2bee3bc9, also unidentified. Neither address has verified source on Sourcify, and Etherscan's keyless endpoint is retired, so the extra functions could not be read. Every selector the repository source declares is present on-chain, so the hook's calls will not revert for a missing function, but the audited guarantees that matter to the hook (liquidity set once in setup(), released() set once by release(), isAgentHolder's held-since-an-earlier-block rule) cannot be confirmed for the code that will actually answer those calls, and two unknown owner-reachable functions per contract may change them. The requester's statement 'Nobody can change any of these after deployment' is therefore unverified for the fee destination and the trading lock. The adapter should obtain and pin the exact source and commit the two addresses were built from (or redeploy from this tree), and the launch's reviewers should audit that source rather than this one.","line":37,"path":"src/InkVault.sol","reproduction":"RPC https://ethereum-rpc.publicnode.com at block 26235009. `cast code 0x33c85714c7d78fdb253fe1cf761d2d5c3c12d747` returns 12,400 bytes whose dispatcher includes PUSH4 0xe30c3978, 0x79ba5097, 0x6eae2807, 0xb25e26ac; `forge build` then out/InkVault.sol/InkVault.json deployedBytecode is 11,120 bytes and contains none of those four. `cast code 0x54fa3515848440ddde2aefe45a382bd868cacd15` returns 9,307 bytes including 0xe30c3978, 0x79ba5097, 0x11db7435, 0x2bee3bc9; out/InkPortraits.sol/InkPortraits.json deployedBytecode is 8,078 bytes with none of them. `cast call <vault> 'pendingOwner()(address)'` succeeds on-chain, while InkVault in this tree has no such function (`grep -rn pendingOwner src/` is empty). Expected: on-chain runtime bytecode equal to the compiled artifact modulo immutables. Actual: different code with extra entry points.","severity":"medium","snippet":"contract InkVault is Ownable, ReentrancyGuard, IERC721Receiver {","title":"The vault and portraits the hook will be wired to on mainnet are not the contracts in this repository: the deployed bytecode exposes functions the audited source does not have"},{"citation":"resolved","description":"Read in full from this tree: src/InkHook.sol (all state-changing paths: beforeInitialize, beforeSwap, afterSwap, collectFees/unlockCallback/_payOut, constructor), src/InkVault.sol, src/InkLiquidity.sol, src/InkFeeRouter.sol, src/InkPortraits.sol, the v4-core Hooks and PoolManager.initialize paths the hook depends on, launch/mainnet-2-hook.json, launch/README.md, AUDIT_BRIEF.md and test/InkSystem.t.sol's launch simulation. Not read: src/InkRenderer.sol (art rendering, no hook interaction) and script/. Reached on Ethereum mainnet through https://ethereum-rpc.publicnode.com at block 26235009: portraits 0x54fa3515848440ddde2aefe45a382bd868cacd15 (code, 9,307 bytes; vault() == the vault, owner() == 0x824E12b38CfF2D3e7ad9607a0212f7F9Eb6a06E3, renderer 0x9853b489003aDD907fF0849d899f5CB44e8c17d2 with code, 0 minted), vault 0x33c85714c7d78fdb253fe1cf761d2d5c3c12d747 (code, 12,400 bytes; portraits() == the portraits, seats() == 0x0000eC93127BAA929E58E97dd0095A2BFb38ec1D with code, ink() == 0, liquidity() == 0, statsRoot == 0, released() == false, owner() == the paying wallet), treasury 0xbc10B3662231c830d2ee10bc6359dB17bEa4b3d4 (no code, EOA) and the paying wallet 0x824E12b38CfF2D3e7ad9607a0212f7F9Eb6a06E3 (no code, EOA). Could not be reached: the source of the deployed vault and portraits (no Sourcify match for either; Etherscan's keyless source endpoint is retired), the IMD token the pool pairs with and the IMD launch factory/PoolManager flow (the deployer resolves $poolManager and the pairing; no address was given), and the InkLiquidity instance the vault will point at (vault.liquidity() is still zero, so no InkLiquidity is deployed or wired yet). Hook checks that passed on this source: callback caller restricted to the PoolManager on every callback and on unlockCallback; no owner, admin, initializer, proxy, delegatecall, selfdestruct, mint, pause or blacklist path; permissions in getHookPermissions match the flags the constructor validates (beforeInitialize, beforeSwap, afterSwap, afterSwapReturnDelta) and the IMD manifest must list exactly those; fee side selection is correct for all four swap shapes (exact-in/out, both directions) and the returned afterSwap delta is matched by the ERC-6909 mint so the hook's pool delta nets to zero; fee rounds down (zero below 400 wei of the unspecified currency, dust only); feeBps capped at 100 in the constructor; collectFees can only run outside a swap (nested unlock reverts) and only pays to the two fixed destinations; INK fees cannot exist before release, and release requires setup, so vault.liquidity() is never zero when INK is paid out. Design points the requester explicitly accepts and that were not reported as defects: the tx.origin fee waiver (contract wallets never qualify), no time-based fallback if release() can never succeed (release depends on the owner having called InkLiquidity.setPool and on addLiquidity not reverting), and the release-price floor in InkLiquidity (fee INK re-added just above the current price, never below the floor, which is sandwichable only down to the floor).","line":38,"path":"src/InkHook.sol","reproduction":"Not a defect. Commands used: cast code <addr> --rpc-url https://ethereum-rpc.publicnode.com; cast call <vault> 'liquidity()(address)' etc.; curl https://sourcify.dev/server/v2/contract/1/<addr>; forge build; forge test --match-path test/InkSystem.t.sol --match-test 'hookFee|agentHolder'.","severity":"info","snippet":"contract InkHook is IHooks, IUnlockCallback {","title":"Coverage: contracts read in full, contracts reached on mainnet, and what could not be reached"}],"hash":"e0ab419b44c5603a6779229306563eb377b3181cbf0ffd124b5e1dfef47bcbb6","nodeId":"7e4a52e3-52e5-4a26-b785-6f2fb7afc1e2","outcome":"completed","summary":"The review is complete. The findings file at `.imd-findings.json` holds two medium findings and one coverage entry, and no tracked file was changed.\n\n**Findings**\n\n- **Open pool-initialization window (medium, with proof).** `beforeInitialize` in `src/InkHook.sol:123` ignores who initializes, which token pairs with IMD, the fee tier, the tick spacing and the opening price, and it is one-shot with no reset. Between hook deployment and the factory's initialize, anyone can initialize IMD against a junk token with this hook. The factory's real initialize then reverts with PoolAlreadyCreated and the hook is dead. The same window allows initializing the correct key at a stranger's price. If the IMD factory deploys and initializes in one transaction the window is closed, but the hook does not enforce that. The proof test at `test/scratch/InkHookInitFrontrun.t.sol` fails on the current code and passes with a deployer-equals-initializer check, which keeps the constructor signature.\n\n- **Deployed vault and portraits are not this repository's code (medium).** Both mainnet addresses the requester gave have code and are wired to each other, but each exposes four selectors the audited source lacks. Two are OpenZeppelin Ownable2Step's `pendingOwner` and `acceptOwnership`, while the source inherits plain Ownable. Two per contract match no known signature and no verified source exists on Sourcify. The hook's trading lock, INK fee destination and fee waiver all depend on those contracts, so the claim that nothing can change after deployment is unverified for them.\n\n**Coverage**\n\nRead in full: InkHook, InkVault, InkLiquidity, InkFeeRouter, InkPortraits, the v4-core initialize and hook-call paths, the launch drafts and the system test. Not read: InkRenderer and the scripts. Reached on mainnet: portraits, vault, their seats and renderer, the treasury and the paying wallet, both EOAs. Not reachable: the deployed vault and portraits source, the IMD token and factory, and the InkLiquidity instance, which does not exist yet since the vault's liquidity address is still zero.\n\n**Checks that passed on the hook source**\n\nCallback callers are restricted to the PoolManager. There is no owner, initializer, proxy, delegatecall, selfdestruct, mint, pause or blacklist path. Permissions match the validated flags. Fee side selection is correct for all four swap shapes and the afterSwap delta nets to zero against the ERC-6909 mint. Fee collection only pays the two fixed destinations and cannot run mid-swap. The requester's accepted design points, the tx.origin waiver and the absence of a release fallback, were noted but not reported as defects.","treeHash":null,"usage":{"cachedInputTokens":1539497,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":37433,"runtime":"claude","turns":34,"wallClockMs":635859}}],"verification":[]}