{"assessments":[],"deployments":[{"attestationHash":"cd6aada1b90b718439fcefb7f4d71cf2fa6aeb276b95d76728cf5a11af71bd8d","chainId":1,"contracts":[{"address":"0x8926bf608c039ce9beebc8cc8727171102efcf6a","blockNumber":26256833,"name":"LaunchToken","role":"token","txHash":"0x7e1fd5de3e1d8b860bfde3607a9e58b96b730670a89316fc50b93feb630a643b"},{"address":"0x4cf7f23cc89f999b39c6ac24f9e222b953e82023","blockNumber":26256833,"name":"MerkleDistributor","role":"distributor","txHash":"0x7e1fd5de3e1d8b860bfde3607a9e58b96b730670a89316fc50b93feb630a643b"},{"address":"0xd95fa8af73a5b7ce57c33b909d94590c105b10cc","blockNumber":26256833,"name":"LaunchHook","role":"hook","txHash":"0x7e1fd5de3e1d8b860bfde3607a9e58b96b730670a89316fc50b93feb630a643b"}],"id":"fca1a579-567b-4d06-9583-70f6c90df1e5","manifestHash":"52b027bafdd21d2e6a270df51989b2be4f3d55f409fe7a0f676746f02257d4e7","status":"live"}],"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":"05958691-0bdb-481d-94e6-bbe8cfa54ffb","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"7a05d0b61dc2d3ae255646efaf5ac724331bb6f894a7918e7f7b1b3f489f81c7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2d2e8dd90ee59ee52c9ff7a9335b07b7f593e4c348c91bb3a91e34d7e1ae30d8","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"ac33645e6e5d590b43b55b34be228b31711a2ca2180837c394ebafab2e19c82a","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"75bb9d581fd28f4ef27dae40a26336fc04e2d8b9f30a87b02625652200e4013f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"9d438f934500fd0f1d1e70ccea8c0a4643ca3de7b035050f9d783ebdf0bdbfb6","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"3864f5a8a4e4053237f9d24b8be0a55fa57ed6bb777d61d9a10142ae714984ec","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"816fe39c7efd64504a19175b0f4dd9186d61069f37a22684c90358a18c60bddc","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"816fe39c7efd64504a19175b0f4dd9186d61069f37a22684c90358a18c60bddc","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"2ec1669a815576e7201c87b04d5f23fa61cecf5e36c33b4fa89e2d963e7cd8af","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"4d0d0884bf201584d01884104a40bd85b1bd6747a734824e96afed20a2a4f658","dependsOn":["build_contract_project"],"execution":{"forkChains":[1],"network":false,"profile":"foundry","requires":[],"skillHash":"ab3f8f2aa4e7f3b3ada558c0dcf522ba733ca3df3b1da92c707a0fe305816d5f","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"ab3f8f2aa4e7f3b3ada558c0dcf522ba733ca3df3b1da92c707a0fe305816d5f","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build a Uniswap v4 hook for this test launch. The pool pairs the new token with native ETH. The hook takes a fee in ETH on every swap and forwards it to two fixed recipients that fund research jobs.\n\nRecipients (immutable, set in the constructor):\n- Research vault: 0x087C0CF0A103acA2c487c5017DbC121ad6C5afEE (a contract with a payable receive)\n- Dev wallet: 0x5Cf44fb6a49156125b5f9Dfea73E4011534C7944\n- Owner of the admin functions: 0x5Cf44fb6a49156125b5f9Dfea73E4011534C7944\n\nFee schedule:\n- Launch window: the first 3600 seconds after the pool is initialized (record block.timestamp in afterInitialize, once).\n- Buys (ETH in, token out) inside the window pay a fee that decays linearly from 30% at t=0 to 2% at t=3600.\n- Sells (token in, ETH out) pay 2% at all times.\n- After the window every swap pays 2%.\n- Split of each fee: 0.5% of the swap's ETH amount goes to the dev wallet; everything else (1.5% plus any launch premium above 2%) goes to the research vault.\n\nRequirements:\n- The fee is always charged in ETH, never in the token, for both directions and for exact-input and exact-output swaps. Use beforeSwap / afterSwap with the return-delta flags as needed so that no swap type escapes the fee.\n- Fees are paid out in the same transaction. If an ETH transfer to a recipient fails, the swap must not revert: credit the amount to that recipient and let it withdraw later with a pull function.\n- The owner can only lower the fees, never raise them: the 30% launch cap and the 2% base fee are hardcoded maximums. The base fee can never go below the 0.5% dev share.\n- The owner can transfer or renounce ownership. There is no function that moves pool liquidity, mints tokens, pauses swaps or withdraws ETH that belongs to a recipient.\n- The hook only accepts one pool: an ETH pool with this launch's token. afterInitialize reverts on any second initialization.\n- Use transient storage (EIP-1153) for any state passed from beforeSwap to afterSwap.\n- Liquidity is single-sided: the launch supplies only the token, buyers bring the ETH.\n\nTests (Foundry, mainnet fork against the real PoolManager):\n- Exact-input and exact-output buys and sells: each pays the right fee and the vault and dev wallet receive the right amounts.\n- Fee at t=0, t=1800 (16%) and t=3600 (2%) after initialization.\n- A recipient that rejects ETH does not block swaps and can withdraw its credit.\n- The owner cannot raise any fee; non-owners cannot call admin functions.\n\nToken name: TEST\nToken symbol: TEST","parentJobId":null,"planHash":"156fe49b8419036fe4c576bd2aec1c53e3677b806674fbc42aac7218186ebde7","previousHash":"2f13bbeb396aea0e2f3d89d0dcccca12d7af47af2aba5976a1f3cb22f8d7a0b9","projectId":"05958691-0bdb-481d-94e6-bbe8cfa54ffb","publication":{"commit":"176874fe5ac5dd9971532f6e60737609e718f69e","deliveredAt":"2026-10-23T17:30:00.122Z","repoUrl":"https://github.com/identity-md-launches/launch-2501-test"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52188","feedbackHash":"32d30de1ef3210efff3da210b335ee34fde9e9ca23911faae39c96d032e54433","nodeKey":"audit_economics","submissionHash":"7a05d0b61dc2d3ae255646efaf5ac724331bb6f894a7918e7f7b1b3f489f81c7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"54235","feedbackHash":"39ebabf015d84b0883bbd3f09568ca807befe0097e23219f111d2588051735a2","nodeKey":"audit_flow","submissionHash":"2d2e8dd90ee59ee52c9ff7a9335b07b7f593e4c348c91bb3a91e34d7e1ae30d8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52129","feedbackHash":"be44259d3f8dec1896f4f46b8527780b5c7d7acd30160b6a849257c21185717b","nodeKey":"audit_judge","submissionHash":"bdab4b8551c4269d617543f24fc7a09f7b8364964d370c24aedead0b78345c45","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52480","feedbackHash":"faaf37aaa5a022de371a548eecce9272bc87aa6ccb10929433a21366f4daf983","nodeKey":"audit_judge","submissionHash":"ac33645e6e5d590b43b55b34be228b31711a2ca2180837c394ebafab2e19c82a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52135","feedbackHash":"0c0237b685101487bd186ae90ba9a91253c7e0c95c78cad023804295f920302d","nodeKey":"audit_math","submissionHash":"75bb9d581fd28f4ef27dae40a26336fc04e2d8b9f30a87b02625652200e4013f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52676","feedbackHash":"6aedae557c2f61d942536989dbf139c0a72b211b226afa59e2eb7bc592c1028c","nodeKey":"audit_permissions","submissionHash":"9d438f934500fd0f1d1e70ccea8c0a4643ca3de7b035050f9d783ebdf0bdbfb6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52160","feedbackHash":"02c78d6962d0820ae903b34fe1311aa13b456c16919fb59a3b3c8190165ea001","nodeKey":"build_contract_project","submissionHash":"3864f5a8a4e4053237f9d24b8be0a55fa57ed6bb777d61d9a10142ae714984ec","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"d07736fcea3353ab0dd3514dc8529be0037e482b57f1153ad47b33240bc1a983","nodeKey":"manifest","submissionHash":"39d8afd2fe9f991a7d9ea7d85d934ea0f123cbc286c0449d954e3acfa07cb803","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52176","feedbackHash":"ec5724c5f9c4ee4b93abd40313fd90df19cc72beb7a2aa27c113ffecd8b4a7ca","nodeKey":"manifest","submissionHash":"2ec1669a815576e7201c87b04d5f23fa61cecf5e36c33b4fa89e2d963e7cd8af","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"53917","feedbackHash":"2cf240f204e5f5c73dab5121ab305578c2f2b4caf4b6c638306a2d450e9ed8a1","nodeKey":"write_foundry_tests","submissionHash":"4d0d0884bf201584d01884104a40bd85b1bd6747a734824e96afed20a2a4f658","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52282","feedbackHash":"0f50a875a61bc57dee6c861cb3b386a5174c14eb4bc3d83264761ee723a51020","nodeKey":"write_foundry_tests","submissionHash":"c4313b831650251780d4a6ca37fe30bd27c6598b75c86f88c5f7ed23e720effa","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"c698c0c927d35debc438adf8a7b01519401566d01397e9a6a4244af6313ec9f8","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0be47287abf7c3bd","findings":[],"hash":"05e04803ad6b0e44be2dbfa690d4ab2c680ab55e3aa1edaef74c62338ed3467c","nodeId":"c136adb4-e7dd-430c-af43-6305f4f010df","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":35}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"76df9026b98e4277","findings":[],"hash":"0e49947cb5e77fb453281709a55e0f35fc8074b77da635083131496e06c11441","nodeId":"ecd3543a-c517-4ec8-888a-c09798e86e43","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":56}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"89eed35842d609d7","findings":[],"hash":"13cd1eb3877f4fb556d4d845ac100c390259a7e9c071d5fd0c8f632eb73ec8a7","nodeId":"85191cfa-dc14-4e47-a6e2-f22d7aa7a257","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":161}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"bc39660800b5f882","findings":[{"citation":"resolved","description":"For an exact-input buy and an exact-output sell the hook decides the whole fee in beforeSwap from params.amountSpecified and returns it as the before-swap delta on ETH. afterSwap then takes exactly that amount (src/LaunchHook.sol:337-338 reuses feeFromBefore and sets gross from params.amountSpecified) and never compares it with the ETH the pool actually moved (delta.amount0()). Uniswap v4 does not guarantee a full fill: a swap stops at sqrtPriceLimitX96 or when the liquidity range runs out, and the remainder is simply not swapped. The fee, however, stays what beforeSwap fixed, so the effective rate on the executed ETH is unbounded. The two afterSwap-charged types (exact-output buy, exact-input sell) use the executed delta and are correct, so the four swap types are not charged on the same base, which the brief requires ('the right fee' for each). The launch pool is seeded single-sided with tokens only, so it holds little ETH in its first hours: an exact-output sell asking for more ETH than the pool holds is an ordinary request at launch, and v4-periphery's V4Router does not revert on a partially filled exact output (it only bounds the token input). When the pool holds less ETH than the fee fixed in beforeSwap, the seller's ETH delta becomes negative: the seller settles tokens and ETH and receives nothing, while the hook pays the recipients 'fee' ETH that was never part of an executed swap. Fix: in afterSwap, for the two specified-ETH types, derive the executed ETH from delta.amount0() (exact-input buy: executed = -delta.amount0() + fee; exact-output sell: executed = delta.amount0()) and revert when it differs from what beforeSwap assumed (amountSpecified minus/plus fee), or recompute the fee from the executed amount and refuse the swap when the fee cannot be reconciled. A partial fill of a specified-ETH swap must never be charged more than the scheduled rate of the ETH it moved.","line":303,"path":"src/LaunchHook.sol","reproduction":"Fixture: real PoolManager, ETH/TEST pool initialized at sqrtPrice 2^96, token-only liquidity 5e26 in [minUsableTick, 0], manager funded with 1000 ETH (mainnet-like). (A) Exact-input buy, t=0: swap(zeroForOne=true, amountSpecified=-1e18, sqrtPriceLimitX96 = 2^96 - 2^96/5e10). Expected: fee = 30% of the ETH the swap used. Actual: beforeSwap takes 0.300000000000000000 ETH; the pool consumes only ~0.0101 ETH before the limit; the swapper pays ~0.3101 ETH for ~0.0101 ETH of tokens, a 96.7% effective fee (scheduled: 0.004339963833721519 ETH). (B) Exact-output sell, t=3600 (2%): first buy 1 ETH so the pool holds ~0.97 ETH, then swap(zeroForOne=false, amountSpecified=+5e18, limit=MAX_SQRT_PRICE-1). Expected fee: 2% of the 0.96775 ETH actually paid out = 0.019354999999999999 ETH. Actual: 0.102040816326530612 ETH (10.5% of the executed gross); the seller receives 0.8657 ETH. (C) Same as (B) but the pool holds only ~0.048 ETH (prime with a 0.05 ETH buy). Actual: the hook takes 0.102040816326530612 ETH, the pool pays ~0.048 ETH, the seller's ETH balance drops by 0.053653316326530613 ETH while it also hands over its tokens and receives no ETH. Expected: a seller never pays ETH. All three are test/scratch/PartialFillFee.t.sol (3 failing tests on the current code).","severity":"high","snippet":"        if (buy && params.amountSpecified < 0) {\n            // Exact-input buy: the specified amount is the swapper's ETH. Take `rate` of it up front.\n            fee = uint256(-params.amountSpecified) * rate / BPS;\n        } else if (!buy && params.amountSpecified > 0) {\n            // Exact-output sell: the specified amount is the ETH the swapper wants net. The pool pays out\n            // gross = net / (1 - rate); the difference is the fee.\n            fee = uint256(params.amountSpecified) * rate / (BPS - rate);\n        }","title":"Fee of specified-ETH swaps is charged on the requested amount, not the executed one: partial fills overpay, and an exact-output sell can make the seller pay ETH"},{"citation":"resolved","description":"withdraw pays owed[msg.sender] only when the recipient itself is the caller. The research vault 0x087C0CF0A103acA2c487c5017DbC121ad6C5afEE is a deployed mainnet contract (11,101 bytes of runtime code, owner() = 0x5Cf44fb6a49156125b5f9Dfea73E4011534C7944) whose selectors are approveJob, cancelJob, postJob, releaseJob, initiateWithdraw/executeWithdraw/cancelWithdraw, setOperator, setMonthlyBudget, transferOwnership/acceptOwnership and views; none of them makes an arbitrary external call, so the vault can never execute hook.withdraw(to). Today its receive costs ~22.8k gas (a plain payable receive, under the 100k push allowance), so pushes succeed and this path is dormant. It becomes live the moment the vault's receive starts reverting (a pause, an upgrade, a budget guard) or needs more than PAYOUT_GAS: every swap then credits owed[vault], redeemClaims() re-pushes through the same failing receive, and no other address, the owner included (by design), can move the credit. The ETH is stranded in the hook for as long as the vault rejects it. Consider letting the vault's owner() (the dev wallet, already a party to this launch) or a vault-chosen address pull on the vault's behalf, or confirm with the vault's source that its receive is unconditional.","line":240,"path":"src/LaunchHook.sol","reproduction":"State: researchVault's receive reverts (test stand-in: ToggleVault.setAccepting(false)) and the vault, like the mainnet contract, has no function that calls out. Swap: exact-input buy of 1 ETH at t=0 -> owed[vault] = 0.295 ETH, hook balance 0.295 ETH. Then: hook.withdraw(x) from any address but the vault reverts NothingOwed; hook.redeemClaims() leaves owed[vault] = 0.295 ETH because _flush fails on the same receive; the owner has no function to move it. Expected per the brief: the recipient can withdraw its credit later. Actual for this vault: nothing can ever call withdraw as the vault, so the credit is stranded until the vault accepts pushes again.","severity":"low","snippet":"    function withdraw(address to) external {\n        if (to == address(0)) revert ZeroAddress();\n        uint256 amount = owed[msg.sender];\n        if (amount == 0) revert NothingOwed();\n        owed[msg.sender] = 0;\n        if (address(this).balance < amount) _redeemClaims();\n        (bool ok,) = to.call{value: amount}(\"\");\n        if (!ok) revert WithdrawFailed();\n        emit Withdrawn(msg.sender, to, amount);\n    }","title":"The credited-ETH fallback cannot be pulled by the real research vault: it has no function that can call withdraw(address)"}],"hash":"2d2e8dd90ee59ee52c9ff7a9335b07b7f593e4c348c91bb3a91e34d7e1ae30d8","nodeId":"85191cfa-dc14-4e47-a6e2-f22d7aa7a257","outcome":"completed","summary":"Review complete. The findings file is in place and verified.\n\n**Outcome**\n\n`.imd-findings.json` holds 2 findings and 18 coverage rows (all 14 listed entry points plus 4 invariant/periphery rows). Both snippets match the tree exactly. The scratch proof fails 3 of 3 on the current code and passes 3 of 3 after a minimal partial-fill revert in a throwaway copy under /tmp. The working tree is unchanged apart from the findings file and the gitignored scratch test.\n\n**Finding 1, high: fee of specified-ETH swaps is charged on the requested amount, not the executed one** (`src/LaunchHook.sol:303`)\n- For an exact-input buy and an exact-output sell, `beforeSwap` fixes the fee from `params.amountSpecified` and `afterSwap` reuses it without comparing against the pool's actual delta. v4 allows partial fills, so the effective rate is unbounded.\n- Reproduced three ways: a 1 ETH buy stopped by a price limit paid 0.3 ETH fee on about 0.01 ETH swapped (96.7% effective). An exact-output sell of 5 ETH from a pool holding about 0.97 ETH paid 10.5% instead of 2%. With only about 0.048 ETH in the pool, the seller handed over tokens plus 0.054 ETH and received nothing, while the recipients were paid a fee from ETH no swap moved.\n- This matters at launch specifically: single-sided seeding means the pool holds little ETH, and the V4Router does not revert a partially filled exact output. The proof test is attached to this finding.\n\n**Finding 2, low: the credited-ETH fallback is unreachable for the real research vault** (`src/LaunchHook.sol:240`)\n- I pulled the vault's mainnet code. Its `receive` costs about 22.8k gas, so pushes succeed today, but none of its functions can make an external call, so it can never be `msg.sender` of `withdraw`. If its receive ever starts reverting, the credit is stranded with no path out.\n\n**Coverage**: every entry point traced. The admin functions, the init callback, `receive`, `redeemClaims`, `unlockCallback` and the token's ERC-20 functions hold. I confirmed the hook's PoolManager delta nets to zero on all four swap paths and that the transient slots cannot be interleaved. The static-analysis leads (strict equality, uninitialized locals, reentrancy, unused return) all checked out as benign and were left out.","treeHash":null,"usage":{"cachedInputTokens":1714463,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":42712,"runtime":"claude","turns":35,"wallClockMs":789968}},{"artifacts":[],"attempt":1,"bundleHash":"ec7d4ac2dd1d05e9295f8ae7b57ad5a3e2402881b435ae6d1032c8e4cdb3a1a3","device":"ee2cc12e7dfe571a","findings":[],"hash":"2ec1669a815576e7201c87b04d5f23fa61cecf5e36c33b4fa89e2d963e7cd8af","nodeId":"3a5e9473-fb3e-4d9b-94d5-89f5d16cbf74","outcome":"completed","summary":"Build and tests pass, and the working tree is clean. The previous attempt failed only on an infrastructure error, so the accepted manifest stands as is.\n\n**Outcome:** launch.json is correct and needs no change. The earlier rejection was an internal \"fetch failed\" error, not a review finding.\n\n**What I verified against the source:**\n- The first field is `\"kind\": \"univ4_hook\"`.\n- Hook contract name is LaunchHook, token contract is LaunchToken with name TEST, symbol TEST, 18 decimals.\n- Constructor arguments match declaration order: pool manager, token, owner, research vault, dev wallet. The first three use the required placeholders and the two recipients are the lowercase addresses the brief gave.\n- Declared permissions match getHookPermissions exactly: afterInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta.\n- Pool fields are the launch's own: native ETH pair, fee 12500, tick spacing 60, the stated initialPrice.\n- Notes are one string of about 2100 characters, under the 4000 cap.\n\n**Checks run:** `forge build` succeeds and `forge test` with the verifier's fuzz seed passes all 67 tests across 8 suites. No file was modified, so git status is clean and only the manifest is in scope.","treeHash":"5f9e4a37da1b0ef1f65291a7a5090d7577f91587","usage":{"cachedInputTokens":163521,"inputTokens":98,"model":"claude-fable-5-1","outputTokens":1644,"runtime":"claude","turns":4,"wallClockMs":29617}},{"artifacts":[],"attempt":1,"bundleHash":"4f28960c0b0af38ff08f3f7b8afba4da937af8a368f3310dd697028f6c95ee01","device":"cd8076510aad8b8e","findings":[],"hash":"3864f5a8a4e4053237f9d24b8be0a55fa57ed6bb777d61d9a10142ae714984ec","nodeId":"f6c7b9db-ba2e-4d10-a226-5b67b44e3f1e","outcome":"completed","summary":"Both findings are resolved, the proof passes, and the full suite is green.\n\n**Finding 1 (high, partial fills), fixed.** I reproduced the reviewer's proof on the starting tree: both tests failed exactly as reported. I confirmed the cause in v4-core: afterSwap receives the pool's own delta, and a swap may stop at the price limit or when the pool's ETH runs out, while the specified-side delta committed in beforeSwap cannot be changed later. In `src/LaunchHook.sol` afterSwap now compares the ETH the pool actually swapped with the amount beforeSwap sized the fee for, and reverts with a new `PartialFill(assumed, executed)` error on any difference. The whole swap is undone and nobody is charged. I chose the revert over a refund because afterSwapReturnDelta can only move the token side, and sending excess ETH to the swap's sender would strand it in a router with no sweep step. Exact-output buys and exact-input sells are untouched and still fill partially with the correct fee.\n\n**Finding 2 (low, stuck credit), fixed.** I reproduced it with a gas-hungry vault: the push failed, `redeemClaims` retried with the same bound and failed, and nobody else could deliver the credit. The hook now has a permissionless `withdrawFor(recipient)` that pays the recipient's whole credit to the recipient itself with all gas, redeeming claims first if needed. The ETH can only go to the fixed recipient, so a caller gains nothing.\n\n**Tests and checks.** New file `test/LaunchHookPartialFill.t.sol` adds 14 tests: price-limited and ETH-exhausted partial fills reverting with the expected values and leaving every balance unchanged, a non-binding limit still filling, partial exact-output buys and exact-input sells paying exactly 30% or 2% of the executed gross, and `withdrawFor` delivery, rejection, nothing-owed, redirection, and claim-redemption cases. The test router gained a price-limited swap entry point.\n\n| Check | Result |\n| --- | --- |\n| forge build | passes |\n| forge test with the verifier's fuzz seed | 69 passed, 0 failed |\n| Reviewer's proof (copied under test/scratch) | 2 passed |\n| forge fmt --check | passes |\n\nThe README documents the partial-fill rule, the `withdrawFor` path, and the trader-facing note. Answers to both findings are in `.imd-responses.json`, both marked fixed. Nothing is committed; the changed files are the hook, the README, the test router mock, and the new test file.","treeHash":"170ba2509e28fb144a34df3443cdd7fbee3f9860","usage":{"cachedInputTokens":1361041,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":30044,"runtime":"claude","turns":35,"wallClockMs":390632}},{"artifacts":[],"attempt":3,"bundleHash":"a9101d1a4ae5a44de2f61bbb4521e692cd854637d4d483adc324147cf61f8888","device":"2bb9ca7686987b64","findings":[],"hash":"39d8afd2fe9f991a7d9ea7d85d934ea0f123cbc286c0449d954e3acfa07cb803","nodeId":"3a5e9473-fb3e-4d9b-94d5-89f5d16cbf74","outcome":"completed","summary":"Created [launch.json](/root/.identitymd-1731/work/05958691-0bdb-481d-94e6-bbe8cfa54ffb/3a5e9473-fb3e-4d9b-94d5-89f5d16cbf74/launch.json) with matching contract names, constructor arguments, permissions, recipients, and required pool settings.\n\n- Manifest structure and compiled constructor ABI checked.\n- `forge build` passed with existing lint warnings.\n- `forge test`: 53 passed using the specified fuzz seed.\n\nNotes document deferred payouts when ETH is insufficient and that tests run locally, without a mainnet fork. Only `launch.json` changed.","treeHash":"2bd3771980f7691a3546daec0a8d9522528889b0","usage":{"cachedInputTokens":494848,"inputTokens":56747,"model":"gpt-6-astra","outputTokens":4014,"runtime":"codex","turns":3,"wallClockMs":134856}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"644eb561f6d70a85","findings":[],"hash":"3ccbddac9ec017f32c9d812162d2596ac983beb867dfa2937867a530f0469cb0","nodeId":"1278a2d3-bd4a-424d-aa46-f9e58cdabe79","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":49}},{"artifacts":[],"attempt":1,"bundleHash":"96b7d45c585afbae47786635d133993909bbff42a16f67eea635d2601ad2ee7d","device":"ee9fbaf2480d1034","findings":[],"hash":"4bace423548b4f65cc74698feac577f4ab78839f8ed2108cd088dd41686848a3","nodeId":"f6c7b9db-ba2e-4d10-a226-5b67b44e3f1e","outcome":"completed","summary":"The deliverable is complete and committed. The fresh clone builds offline, passes `forge fmt --check`, and passes all tests with the verifier's fuzz seed.\n\n**What was built** (commit `65dab94` on `main`)\n\n- `src/LaunchToken.sol`: fixed-supply ERC-20 \"TEST\", 10^27 units minted once to the deployer, no admin functions.\n- `src/LaunchHook.sol`: the fee hook. `afterInitialize` binds the single ETH/TEST pool and records the launch timestamp once. `beforeSwap` plus `afterSwap`, with both return-delta flags, charge the fee on the ETH side for all four swap types. The rate and amount decided in `beforeSwap` reach `afterSwap` through transient storage.\n- Fee schedule: buys decay linearly from 30% to 2% over 3600 seconds, sells pay 2% always, 0.5% of the gross ETH goes to the dev wallet and the rest to the research vault.\n- Payout happens inside the swap. A recipient that rejects ETH is credited and pulls with `withdraw`. A PoolManager without enough ETH yields an ERC-6909 claim that anyone redeems with `redeemClaims`.\n- Owner functions only lower the two fees, never below the dev share or below the base fee. Ownership is transferable and renounceable. No liquidity, mint, pause or recipient-ETH functions exist.\n- `script/Deploy.s.sol` records the fixed recipient and owner addresses and mines a flagged CREATE2 address. `README.md` documents the fee definition, payout paths, constructor arguments for the manifest, operational duties and assumptions.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| `forge build` (offline, fresh clone) | passes |\n| `forge test` with the verifier's fuzz seed | 53 passed, 0 failed |\n| `forge fmt --check` | passes |\n| Pinned floor tests run in scratch with a simulated verifier environment | 16 passed, 0 failed |\n\n**Two things to know**\n\n- The brief asked for mainnet fork tests. The verifier runs with no network and an empty environment, so the tests deploy the real PoolManager code from v4-core locally instead and run every scenario against it, with a funded and an ETH-less manager fixture. The README states this.\n- Dependencies are vendored as plain files with no submodules. The sandbox mounted two nested `.gitmodules` files read-only inside `lib/v4-core` and `lib/openzeppelin-contracts`, so they could not be deleted. They are harmless since git only reads the root file and no gitlink entries exist.","treeHash":"4a6502180eeb76ad8b17e53e8631d73543270a82","usage":{"cachedInputTokens":3947581,"inputTokens":930,"model":"claude-fable-5-1","outputTokens":90512,"runtime":"claude","turns":53,"wallClockMs":1286201}},{"artifacts":[],"attempt":1,"bundleHash":"5cf7c2d4cf51577aa807ce2866540bdf19bf91da93c190c3b6901f291923446c","device":"1eba9b0f789fcc77","findings":[{"description":"The brief requires fees to be paid out in the same transaction, with crediting only for a recipient whose ETH transfer fails. `_collect` instead mints an ERC-6909 ETH claim and credits both recipients whenever `address(poolManager).balance < fee`. Neither recipient is paid until someone calls `redeemClaims`, `withdraw` or `withdrawFor` in a later transaction, even when both recipients accept ETH. The deferral keeps the value and lets anyone recover it, which avoids the NativeTransferFailed revert the direct take would hit. But the fee is not paid in the same transaction. On the mainnet PoolManager at block 26255649 the manager holds far more ETH than any fee, and every fork test pays out directly, so this only matters on a manager holding less ETH than one fee. That is why it is low. Severity was lowered from medium in the previous round. The partial-fill finding from that round has since been fixed in the implementation (PartialFill revert) and is not re-reported.","line":468,"path":"src/LaunchHook.sol","reproduction":"Fresh PoolManager with no ETH, pool seeded with TEST only, both recipients accept ETH. A 1 ETH exact-input buy at t=0. Expected (brief): dev wallet +0.005 ETH and research vault +0.295 ETH in that transaction. Actual: both balances unchanged, hook.pendingClaims() == 0.3 ETH, owed(dev) == 0.005 ETH, owed(vault) == 0.295 ETH, paid only after a separate redeemClaims() call. The existing passing test test/LaunchHook.t.sol::test_firstBuyOnFreshManager_mintsClaim_thenRedeems (lines 422-445) shows this behaviour. It checks that the value can be recovered, not that the same-transaction requirement is met.","severity":"low","title":"Fee is not paid out in the swap's transaction when the PoolManager holds less ETH than the fee"}],"hash":"4d0d0884bf201584d01884104a40bd85b1bd6747a734824e96afed20a2a4f658","nodeId":"c136adb4-e7dd-430c-af43-6305f4f010df","outcome":"completed","summary":"The revision is done. The suite passes against the revised hook: `forge test` reports 91 passed, 0 failed, 0 skipped. That includes all 10 tests forked from Ethereum block 26255649, run with `MAINNET_RPC_URL` set and the verifier's fuzz seed. With the variable empty, the fork suite skips cleanly.\n\n**What changed since my last accepted round:** the hook's author fixed my high finding (the fee was charged on ETH that a stopped-early swap never traded). A buy with a fixed ETH input or a sell asking for a fixed ETH output now reverts with `PartialFill` when it can't fill completely. The author also added `withdrawFor`, which lets anyone deliver a recipient's credit to that recipient. This round tests both on the real PoolManager and in the random-sequence (invariant) tests. All changes are under `test/`.\n\n- **Fork tests, `test/LaunchHookFork.t.sol`:**\n  - Partial fills at t=0 and t=3600. Three swaps are refused with nothing moved: a price-limited exact-input buy, a price-limited exact-output sell, and an exact-output sell asking for more ETH than the pool holds. Two swaps are allowed to fill partly, and each is checked to pay the fee only on the ETH actually traded: a price-limited exact-output buy and a price-limited exact-input sell.\n  - A test that `withdrawFor`, called by a stranger, fails without losing the credit while the recipient rejects ETH. Once the recipient accepts, the credit reaches only the recipient, and the caller gains nothing.\n- **Invariant tests, `test/LaunchHookInvariant.t.sol`:**\n  - A new price-limited trade action covering all four swap types, and a `withdrawFor` action.\n  - Every trade now goes through one path: a successful swap is checked against the ETH it actually traded, and the only accepted revert is the hook's own `PartialFill`. I checked that runs under the verifier seed really produce both partial fills and refusals.\n- **Shared helpers:** a new decoder, `test/support/PartialFillRevert.sol`, and a helper in `test/support/HookScenario.sol` for swaps with a price limit.\n\n**Test fixes:** while building the invariant tests, two failures turned out to be in my test setup, not the hook, and I fixed the setup.\n- My model of the pool's ETH includes trading fees the pool keeps and can't pay out. So a sell sized from that number can find the pool empty, and the hook is right to refuse it.\n- A sell with the most extreme price limit can push the price to its maximum. After that the PoolManager itself rejects every further sell, so the test switches to buying.\n\n**Still open, reported in `.imd-findings.json`:** one finding, now rated low (it was medium). If the PoolManager holds less ETH than one fee, the hook mints a claim instead of paying the vault and dev wallet in the same transaction, as the brief requires. Someone has to call `redeemClaims` (or a withdraw function) later to pay them. On mainnet at the pinned block the PoolManager holds plenty of ETH, and every fork swap paid out directly. The existing `test_firstBuyOnFreshManager_mintsClaim_thenRedeems` shows the behaviour. I didn't re-report the partial-fill finding, since it's fixed.\n\n`test/TESTING.md` now reflects all of this. I haven't committed; the changes are in the working tree.","treeHash":"e14ee4c818691df0f5a3e3d382279825b0cb847e","usage":{"cachedInputTokens":2967400,"inputTokens":60,"model":"claude-opus-5-5","outputTokens":34042,"runtime":"claude","turns":30,"wallClockMs":404619}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7236faf97e42979a","findings":[],"hash":"676a3f528d0bc33bbfab7dea8a1cce6ff4fad5b19658a4adf394234d702d6458","nodeId":"ecd3543a-c517-4ec8-888a-c09798e86e43","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":197}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"2717ac8e1d58fd36","findings":[],"hash":"72bf594f5eb06cba9ff79b8b38dcde50c4a88cb951f2d66351b5b136d889fd96","nodeId":"c136adb4-e7dd-430c-af43-6305f4f010df","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":79}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"b97527597dd934a0","findings":[{"citation":"resolved","description":"For an exact-output sell `beforeSwap` computes `fee = net * rate / (BPS - rate)` from `params.amountSpecified` (the ETH the seller asks for) and commits it as the before-swap specified delta, so the hook is owed `fee` ETH before the pool runs. The pool then pays out only what its liquidity and the price limit allow. The launch pool is single-sided (tokens only), so its ETH reserve is exactly what buyers have paid in minus fees; any exact-output sell larger than that reserve is partially filled: the swap reaches the top of the seeded range at tick 0, walks zero liquidity to the price limit and stops. The swapper's ETH delta becomes `paidOut - fee`, where `fee` was sized on the full request. `afterSwap` does not compare the executed amount with the request (line 338 uses `uint256(params.amountSpecified) + fee` as `gross`), so the fee is paid out to the vault and dev wallet in full. Measured against the real PoolManager: (a) pool with 0.7 ETH, request 10 ETH out: pool pays 0.6912 ETH, fee = 10e18*200/9800 = 0.2041 ETH, seller nets 0.4872 ETH, an effective 29.5% against the hardcoded 2% maximum; (b) pool with 0 ETH (the state of every launch pool before its first buy), request 1 ETH out: 0 tokens sold, 0 ETH received, seller pays 20408163265306122 wei which the hook forwards to the recipients. The same `amountSpecified`-based sizing is also what overcharges exact-input buys with a price limit (finding 2). Fix: in `afterSwap`, for the two swap types whose fee was fixed in `beforeSwap`, compare the executed ETH amount (`delta.amount0()` for a sell, `-delta.amount0() + fee` for a buy) with the amount the fee was sized on and revert (e.g. `PartialFill()`) when they differ; alternatively recompute the fee from the executed amount and return the surplus to the swapper (the only in-swap channel is `poolManager.mint(sender, 0, surplus)`, an ERC-6909 claim, which routers do not expect, so reverting is the simpler safe choice). The proof passes with either fix.","line":309,"path":"src/LaunchHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {LaunchHook} from \"src/LaunchHook.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\n\n/// @dev Minimal router: pays the manager from its own balances and keeps what it receives.\ncontract LimitRouter is IUnlockCallback {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager m) {\n        manager = m;\n    }\n\n    receive() external payable {}\n\n    function swap(PoolKey memory key, bool zeroForOne, int256 amountSpecified, uint160 limit)\n        external\n        returns (BalanceDelta)\n    {\n        bytes memory r = manager.unlock(abi.encode(uint8(1), abi.encode(key, zeroForOne, amountSpecified, limit)));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function addLiquidity(PoolKey memory key, int24 lower, int24 upper, int256 liq) external returns (BalanceDelta) {\n        bytes memory r = manager.unlock(abi.encode(uint8(2), abi.encode(key, lower, upper, liq)));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        BalanceDelta delta;\n        PoolKey memory key;\n        (uint8 action, bytes memory payload) = abi.decode(data, (uint8, bytes));\n        if (action == 1) {\n            (PoolKey memory k, bool zeroForOne, int256 amountSpecified, uint160 limit) =\n                abi.decode(payload, (PoolKey, bool, int256, uint160));\n            key = k;\n            delta = manager.swap(key, SwapParams(zeroForOne, amountSpecified, limit), \"\");\n        } else {\n            (PoolKey memory k, int24 lower, int24 upper, int256 liq) =\n                abi.decode(payload, (PoolKey, int24, int24, int256));\n            key = k;\n            (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, liq, bytes32(0)), \"\");\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency c, int128 a) private {\n        if (a < 0) {\n            uint256 owed = uint256(uint128(-a));\n            if (c.isAddressZero()) {\n                manager.settle{value: owed}();\n            } else {\n                manager.sync(c);\n                IERC20(Currency.unwrap(c)).transfer(address(manager), owed);\n                manager.settle();\n            }\n        } else if (a > 0) {\n            manager.take(c, address(this), uint256(uint128(a)));\n        }\n    }\n}\n\n/// @notice Exact-output sells are charged 2% of the ETH the seller *asked for*, fixed in `beforeSwap`, even\n/// when the pool can only pay out part of it (or none of it). Passes once the fee follows the executed\n/// amount, or once the hook refuses a partially filled ETH-specified swap.\ncontract SellPartialFillProof is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint256 constant BPS = 10_000;\n    uint160 constant FLAGS = uint160(\n        Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n            | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG\n    );\n\n    PoolManager manager;\n    LaunchToken token;\n    LaunchHook hook;\n    LimitRouter router;\n    LimitRouter seeder;\n    PoolKey key;\n    address vault = makeAddr(\"vault\");\n    address dev = makeAddr(\"dev\");\n    address owner = makeAddr(\"owner\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new LaunchToken();\n        address at = address((uint160(0x4242) << 144) | FLAGS);\n        deployCodeTo(\"LaunchHook.sol:LaunchHook\", abi.encode(manager, address(token), owner, vault, dev), at);\n        hook = LaunchHook(payable(at));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        vm.warp(1_700_000_000);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // Single-sided seed, as the launch does: tokens only in [minTick, 0].\n        seeder = new LimitRouter(IPoolManager(address(manager)));\n        token.transfer(address(seeder), 800_000_000 ether);\n        seeder.addLiquidity(key, TickMath.minUsableTick(60), 0, 5e26);\n\n        // The mainnet manager holds ETH of other pools.\n        vm.deal(address(manager), 1_000 ether);\n\n        router = new LimitRouter(IPoolManager(address(manager)));\n        vm.deal(address(router), 1_000 ether);\n        token.transfer(address(router), 10_000_000 ether);\n    }\n\n    /// Exact-output sell of 1 ETH on a pool that holds no ETH yet: the seller sells nothing, receives nothing,\n    /// and still pays 1e18 * 200 / 9800 = 20408163265306122 wei to the vault and dev wallet.\n    function test_exactOutputSell_onEmptyPool_chargesFeeForNothing() public {\n        uint256 routerEthBefore = address(router).balance;\n        uint256 routerTokBefore = token.balanceOf(address(router));\n\n        try router.swap(key, false, int256(1 ether), TickMath.MAX_SQRT_PRICE - 1) {} catch {}\n\n        uint256 ethReceived = address(router).balance > routerEthBefore ? address(router).balance - routerEthBefore : 0;\n        uint256 tokensSold = routerTokBefore - token.balanceOf(address(router));\n        uint256 feePaid = vault.balance + dev.balance;\n        emit log_named_uint(\"tokens sold\", tokensSold);\n        emit log_named_uint(\"ETH received\", ethReceived);\n        emit log_named_uint(\"fee taken\", feePaid);\n        // A sell must never cost the seller ETH, and the fee must stay at 2% of the ETH the pool paid out.\n        assertGe(address(router).balance, routerEthBefore, \"seller lost ETH on a sell\");\n        assertLe(feePaid * BPS, (ethReceived + feePaid) * 200, \"fee above 2% of gross ETH paid out\");\n    }\n\n    /// Pool holds 0.7 ETH after one 1 ETH buy. An exact-output sell for 10 ETH is filled with ~0.69 ETH, but the\n    /// fee is 10e18 * 200 / 9800 = 0.204 ETH: the seller nets 0.487 ETH, an effective rate of 29.5%, not 2%.\n    function test_exactOutputSell_partialFill_overcharges() public {\n        router.swap(key, true, -int256(1 ether), TickMath.MIN_SQRT_PRICE + 1);\n        uint256 vaultBefore = vault.balance;\n        uint256 devBefore = dev.balance;\n        uint256 routerEthBefore = address(router).balance;\n\n        try router.swap(key, false, int256(10 ether), TickMath.MAX_SQRT_PRICE - 1) {} catch {}\n\n        uint256 fee = (vault.balance - vaultBefore) + (dev.balance - devBefore);\n        uint256 net = address(router).balance > routerEthBefore ? address(router).balance - routerEthBefore : 0;\n        emit log_named_uint(\"net ETH to seller\", net);\n        emit log_named_uint(\"fee taken\", fee);\n        assertGe(address(router).balance, routerEthBefore, \"seller lost ETH on a sell\");\n        assertLe(fee * BPS, (net + fee) * 200, \"fee above 2% of gross ETH paid out\");\n    }\n}","reproduction":"Setup: real PoolManager, LaunchHook with vault/dev EOAs, pool ETH/TEST fee 12500 spacing 60 at sqrtPrice 2^96, seeded with tokens only (liquidity 5e26 in [minUsableTick(60), 0]), manager pre-funded with 1000 ETH of other pools. Case A: no buy yet; a router holding TEST calls poolManager.swap(key, zeroForOne=false, amountSpecified=+1e18, sqrtPriceLimit=MAX_SQRT_PRICE-1). Expected: either revert or a 0-ETH, 0-fee no-op. Actual: swap succeeds with amount0 = -20408163265306122, amount1 = 0; the router must settle 0.0204 ETH; vault receives 15306122448979592 wei and dev 5102040816326530 wei. Case B: after one exact-input buy of 1 ETH (pool keeps 0.7 ETH), the same router swaps oneForZero exact output 10e18. Expected: 2% of the ETH paid out (about 0.0138 ETH on 0.69 ETH). Actual: fee 204081632653061224 wei, seller nets 487168367346938775 wei. Run: forge test --match-path test/scratch/SellPartialFillProof.t.sol -vv (both tests fail on the current code, pass once the fee follows the executed amount or partial fills are refused).","severity":"high","snippet":"            fee = uint256(params.amountSpecified) * rate / (BPS - rate);","title":"Exact-output sell fee is fixed on the ETH requested, not the ETH the pool pays out: partial fills pay far above 2%, and a sell on an ETH-less pool costs the seller the fee for nothing"},{"citation":"resolved","description":"For an exact-input buy `beforeSwap` takes `fee = ethIn * rate / BPS` of the whole `params.amountSpecified` through the before-swap specified delta, and `afterSwap` (line 338) keeps `gross = uint256(-params.amountSpecified)` without looking at how much ETH the pool actually consumed (`-delta.amount0()`). A swapper who passes a `sqrtPriceLimitX96` as slippage protection, which is how a limit-priced v4 swap is expressed, is charged the launch rate on ETH that never entered the pool. With a limit the pool reaches after about 1 ETH, a 100 ETH exact-input buy at t=0 costs the buyer 31.02 ETH in total: 1.02 ETH swapped, 30 ETH fee, an effective 96.7% against the hardcoded 30% cap, all of it forwarded to the vault and dev wallet inside the swap. Same root cause and same fix as finding 1: compare the executed ETH amount with the amount the fee was sized on in `afterSwap` and refuse partial fills, or size the fee on the executed amount.","line":305,"path":"src/LaunchHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {LaunchHook} from \"src/LaunchHook.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\n\n/// @dev Minimal router: pays the manager from its own balances and keeps what it receives.\ncontract LimitRouter is IUnlockCallback {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager m) {\n        manager = m;\n    }\n\n    receive() external payable {}\n\n    function swap(PoolKey memory key, bool zeroForOne, int256 amountSpecified, uint160 limit)\n        external\n        returns (BalanceDelta)\n    {\n        bytes memory r = manager.unlock(abi.encode(uint8(1), abi.encode(key, zeroForOne, amountSpecified, limit)));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function addLiquidity(PoolKey memory key, int24 lower, int24 upper, int256 liq) external returns (BalanceDelta) {\n        bytes memory r = manager.unlock(abi.encode(uint8(2), abi.encode(key, lower, upper, liq)));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        BalanceDelta delta;\n        PoolKey memory key;\n        (uint8 action, bytes memory payload) = abi.decode(data, (uint8, bytes));\n        if (action == 1) {\n            (PoolKey memory k, bool zeroForOne, int256 amountSpecified, uint160 limit) =\n                abi.decode(payload, (PoolKey, bool, int256, uint160));\n            key = k;\n            delta = manager.swap(key, SwapParams(zeroForOne, amountSpecified, limit), \"\");\n        } else {\n            (PoolKey memory k, int24 lower, int24 upper, int256 liq) =\n                abi.decode(payload, (PoolKey, int24, int24, int256));\n            key = k;\n            (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, liq, bytes32(0)), \"\");\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency c, int128 a) private {\n        if (a < 0) {\n            uint256 owed = uint256(uint128(-a));\n            if (c.isAddressZero()) {\n                manager.settle{value: owed}();\n            } else {\n                manager.sync(c);\n                IERC20(Currency.unwrap(c)).transfer(address(manager), owed);\n                manager.settle();\n            }\n        } else if (a > 0) {\n            manager.take(c, address(this), uint256(uint128(a)));\n        }\n    }\n}\n\n/// @notice An exact-input buy with a price limit is charged 30% of the full `amountSpecified`, fixed in\n/// `beforeSwap`, although the pool stops at the limit and consumes only part of it. Passes once the fee follows\n/// the executed amount, or once the hook refuses a partially filled ETH-specified swap.\ncontract BuyPriceLimitProof is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint256 constant BPS = 10_000;\n    uint160 constant FLAGS = uint160(\n        Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n            | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG\n    );\n\n    PoolManager manager;\n    LaunchToken token;\n    LaunchHook hook;\n    LimitRouter router;\n    LimitRouter seeder;\n    PoolKey key;\n    address vault = makeAddr(\"vault\");\n    address dev = makeAddr(\"dev\");\n    address owner = makeAddr(\"owner\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new LaunchToken();\n        address at = address((uint160(0x4242) << 144) | FLAGS);\n        deployCodeTo(\"LaunchHook.sol:LaunchHook\", abi.encode(manager, address(token), owner, vault, dev), at);\n        hook = LaunchHook(payable(at));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        vm.warp(1_700_000_000);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // Single-sided seed, as the launch does: tokens only in [minTick, 0].\n        seeder = new LimitRouter(IPoolManager(address(manager)));\n        token.transfer(address(seeder), 800_000_000 ether);\n        seeder.addLiquidity(key, TickMath.minUsableTick(60), 0, 5e26);\n\n        // The mainnet manager holds ETH of other pools.\n        vm.deal(address(manager), 1_000 ether);\n\n        router = new LimitRouter(IPoolManager(address(manager)));\n        vm.deal(address(router), 1_000 ether);\n    }\n\n    /// Buyer specifies 100 ETH exact input with a price limit the pool reaches after ~1.02 ETH. The hook takes\n    /// 100e18 * 3000 / 10000 = 30 ETH in `beforeSwap`; the buyer pays 31.02 ETH in total for a 1.02 ETH swap,\n    /// an effective rate of 96.7% against the hardcoded 30% cap.\n    function test_exactInputBuy_priceLimit_chargesFeeOnUnswappedEth() public {\n        uint256 routerEthBefore = address(router).balance;\n        uint160 limit = SQRT_PRICE_1_1 - 1.6e20; // a limit the pool reaches after consuming about 1 ETH\n\n        try router.swap(key, true, -int256(100 ether), limit) {} catch {}\n\n        uint256 paid = routerEthBefore - address(router).balance;\n        uint256 fee = vault.balance + dev.balance;\n        emit log_named_uint(\"ETH paid by buyer in total\", paid);\n        emit log_named_uint(\"ETH consumed by the pool\", paid - fee);\n        emit log_named_uint(\"fee taken\", fee);\n        assertLe(fee * BPS, paid * 3000, \"fee above the 30% cap of what the buyer actually paid\");\n    }\n}","reproduction":"Setup as in finding 1 (real PoolManager, tokens-only seed of liquidity 5e26 in [minUsableTick(60), 0], sqrtPrice 2^96, t = launchedAt). Router with 1000 ETH calls poolManager.swap(key, zeroForOne=true, amountSpecified=-100e18, sqrtPriceLimitX96 = 79228162514264337593543950336 - 1.6e20). Expected: fee <= 30% of the ETH the buyer actually pays. Actual: buyer pays 31022523504528661025 wei in total, the pool consumes 1022523504528661025 wei, the hook takes 30000000000000000000 wei (29.5 ETH to the vault, 0.5 ETH to the dev wallet). Run: forge test --match-path test/scratch/BuyPriceLimitProof.t.sol -vv (fails on the current code, passes once the fee follows the executed amount or partial fills are refused).","severity":"medium","snippet":"            fee = uint256(-params.amountSpecified) * rate / BPS;","title":"Exact-input buy fee is 30% of the full amountSpecified even when a price limit stops the pool after a fraction of it, so the effective rate breaks the 30% cap"},{"citation":"resolved","description":"`fee = gross * rate / BPS` (lines 305 and 348) and `fee = x * rate / (BPS - rate)` (lines 309 and 342) all truncate toward zero, in the swapper's favour by less than 1 wei per swap; the dev share `gross * DEV_SHARE_BPS / BPS` truncates the same way and is clamped to `fee`, so the split never underflows. The loss is at most 1 wei per swap and does not compound; it is recorded here only because the math guide asks for every zero-rounding boundary to be named. No change is required; if the author wants fees to round up, use a mulDivUp for the four formulas.","line":348,"path":"src/LaunchHook.sol","reproduction":"After the launch window (rate = 200): an exact-input buy of 49 wei pays fee = 49*200/10000 = 0; an exact-input sell whose pool payout is 49 wei pays 0; an exact-output sell of 48 wei pays 48*200/9800 = 0. With rate = 3000 a 3 wei exact-input buy pays 0. Any amount above these thresholds pays within 1 wei of the exact rate.","severity":"info","snippet":"            fee = gross * rate / BPS;","title":"All four fee formulas round down, so swaps whose ETH leg is below BPS/rate wei pay no fee (dust only)"}],"hash":"75bb9d581fd28f4ef27dae40a26336fc04e2d8b9f30a87b02625652200e4013f","nodeId":"61f13992-8b74-4bd1-b3d1-5be49fbff085","outcome":"completed","summary":"The review is complete. The tracked tree is unchanged, and `.imd-findings.json` holds the findings, the coverage record for all 14 entry points, and two proof tests.\n\n**What I found**\n\nThe fee math is exact for every fully filled swap and for the two swap types whose fee is sized in `afterSwap`. The defect is at the seam between `beforeSwap` and the pool: for an exact-input buy and an exact-output sell the hook commits a fee sized on `amountSpecified` before the pool runs, and `afterSwap` never compares that with what the pool actually executed.\n\n1. **High, exact-output sells** (`src/LaunchHook.sol:309`). The launch pool is single-sided, so its ETH reserve is only what buyers have paid in. An exact-output sell larger than that reserve is partially filled, but the fee stays 2% of the request. Measured against the real PoolManager: with 0.7 ETH in the pool a request for 10 ETH out nets the seller 0.487 ETH after a 0.204 ETH fee, an effective 29.5%. Before the first buy, a 1 ETH exact-output sell sells nothing, returns nothing, and still costs the seller 0.0204 ETH, which the hook forwards to the recipients.\n\n2. **Medium, exact-input buys with a price limit** (`src/LaunchHook.sol:305`). A 100 ETH exact-input buy with a limit the pool reaches after about 1 ETH costs the buyer 31.02 ETH in total, of which 30 ETH is fee, far above the hardcoded 30% cap.\n\n3. **Info, rounding** (`src/LaunchHook.sol:348`). All fee formulas truncate toward zero, so ETH legs under 50 wei pay no fee. Dust only, recorded for completeness.\n\n**Proofs.** `test/scratch/SellPartialFillProof.t.sol` and `test/scratch/BuyPriceLimitProof.t.sol` are self-contained, fail on the current code for the stated reasons, and pass under a minimal fix I tried temporarily and then reverted: in `afterSwap`, revert when the executed ETH amount differs from the amount the fee was sized on. The existing 53 tests still pass with that fix.\n\n**Coverage.** Every entry point has a row. The decay formula, the dev/vault split, the owner setters' bounds (no underflow possible in the decay term), the claim and credit conservation in `withdraw` and `redeemClaims`, the casts, and the transient-storage handoff all hold. Nothing in my area was left unreached. The Slither leads at lines 181, 302, 334 and 483 are benign and were not reported.","treeHash":null,"usage":{"cachedInputTokens":2051715,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":49405,"runtime":"claude","turns":34,"wallClockMs":704841}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"d4b53d656a3fbe07","findings":[{"citation":"resolved","description":"For the two swap types where ETH is the specified currency, beforeSwap computes the fee from params.amountSpecified (line 305: exact-input buy, fee = specified * rate / BPS; line 309: exact-output sell, fee = specified * rate / (BPS - rate)) and returns it as the specified-side BeforeSwapDelta, so the PoolManager credits the hook that full amount no matter how much the pool actually swaps. afterSwap then takes feeFromBefore at face value (lines 335-338) and sets gross from params.amountSpecified, not from the realized swapDelta. Uniswap v4 fills partially whenever sqrtPriceLimitX96 is reached or liquidity runs out: it does not revert. This launch pool is seeded with tokens only, so its ETH is exactly what buyers have brought; any exact-output sell asking for more ETH than that is a partial fill. Consequences: (a) the effective fee is far above the advertised 2%/30% of the swap's ETH amount; (b) when the precomputed fee exceeds the pool's payout, the seller's ETH delta turns negative and they pay ETH out of pocket while also handing over tokens, and the vault and dev wallet receive ETH that was never part of any swap. The two afterSwap-side types (exact-output buy, exact-input sell) are correct because they read delta.amount0(); the asymmetry is the bug. Fix: afterSwap already receives the realized swapDelta, so for the two beforeSwap-priced types compute realizedFee = rate * realizedGross / BPS (exact-output sell: realizedGross = delta.amount0(); exact-input buy: realizedGross = consumed / (1 - rate) with consumed = -delta.amount0()) and, when realizedFee < feeFromBefore, return the excess to `sender` out of the ETH credit the hook holds (poolManager.take(ETH, sender, excess) or mint(sender, 0, excess)) before taking only realizedFee for the recipients; the hook's delta still nets to zero because the manager credits it feeFromBefore after afterSwap. Reverting on a partial fill of these two types is an acceptable stricter alternative. Either way the invariant to keep is fee <= rate * realizedGross / BPS, which is what the attached test asserts.","line":335,"path":"src/LaunchHook.sol","reproduction":"Setup: real PoolManager code, pool ETH/TEST at 1:1, token-only seed in [minUsableTick, 0], manager funded with 1000 ETH (mainnet-like), block.timestamp = launchedAt + 3600 so every swap owes 2%. Case A (exact-output sell beyond pool ETH): buyer swaps exact-input 10 ETH (9.8 ETH reaches the pool). Seller calls swap(zeroForOne=false, amountSpecified=+20e18, sqrtPriceLimit=MAX-1). Pool pays out 9.6775 ETH in total; hook keeps 20e18*200/9800 = 0.40816 ETH; seller receives 9.2693 ETH. Expected fee: 2% of 9.6775 = 0.19355 ETH. Actual: 0.40816 ETH (4.22%). Case B (seller loses ETH): buyer swaps exact-input 1 ETH (~0.98 ETH in pool). Seller holding 10 ETH calls swap(false, +100e18, MAX-1). Fee precomputed = 100e18*200/9800 = 2.0408 ETH > pool payout 0.98 ETH. Seller's ETH delta = -1.0731 ETH: balance goes 10 -> 8.9269 ETH and 0.98e18 TEST are taken from them; vault+dev receive 2.0608 ETH. Expected: a sell never costs the seller ETH and the fee is 2% of 0.98 ETH = 0.0196 ETH. Case C (exact-input buy, price limit): buyer calls swap(true, -10e18, limit = SQRT_PRICE_1_1 - SQRT_PRICE_1_1/1e9). Pool consumes 0.5063 ETH; buyer pays 0.7063 ETH in total; fee kept = 0.2 ETH (2% of the requested 10 ETH) = 28.3% of what the buyer actually paid. Expected fee: 0.01413 ETH. All three are test/scratch/PartialFillFee.t.sol (attached as proof); run forge test --match-path test/scratch/PartialFillFee.t.sol -vv: 3 failures on the current code.","severity":"high","snippet":"        if (exactInput == buy) {\n            // ETH was the specified currency: the fee was already taken in beforeSwap.\n            fee = feeFromBefore;\n            gross = buy ? uint256(-params.amountSpecified) : uint256(params.amountSpecified) + fee;","title":"Fee fixed in beforeSwap from the requested amount is never reconciled with the realized fill: partial fills overcharge, and an exact-output sell larger than the pool's ETH makes the seller pay ETH"},{"citation":"resolved","description":"_redeemClaims burns the hook's entire ERC-6909 balance for id 0 and takes that much ETH into the hook, but owed[] is only increased on the _collect claim path. Anyone can mint id-0 claims in the PoolManager (mint + settle{value}) and transfer them to the hook with PoolManager.transfer. redeemClaims() (callable by anyone) then converts them into hook ETH that is backed by no credit: withdraw() reverts NothingOwed for both recipients, redeemClaims' _flush pushes 0, receive() refuses outside ETH and there is no sweep. The README invariant 'the hook never keeps ETH for itself; its balance always equals the sum of credits not yet pulled' is broken. Impact is limited to whoever donates (no recipient loses what they are owed), so low. Fix: in _redeemClaims redeem only min(pendingClaims, owed[devWallet] + owed[researchVault] - address(this).balance), or credit any surplus redeemed ETH to the research vault.","line":481,"path":"src/LaunchHook.sol","reproduction":"1) Donor contract: poolManager.unlock -> in callback poolManager.mint(donor, 0, 1e18); poolManager.settle{value: 1e18}(). 2) donor calls poolManager.transfer(hook, 0, 1e18); hook.pendingClaims() == 1e18. 3) anyone calls hook.redeemClaims(). Result: address(hook).balance == 1e18, owed[vault] == 0, owed[dev] == 0; vm.prank(vault); hook.withdraw(vault) reverts NothingOwed. Expected: no ETH held by the hook without a matching credit. Verified in test/scratch/Extra.t.sol test_donatedClaims_strandEthInHook.","severity":"low","snippet":"        uint256 claims = pendingClaims();\n        if (claims == 0) return;\n        poolManager.unlock(abi.encode(claims));","title":"ERC-6909 ETH claims transferred to the hook by a third party are redeemed into ETH that no one can withdraw"},{"citation":"resolved","description":"Permitted by the brief ('the base fee can never go below the 0.5% dev share'), so not a defect, but the economic effect should be stated: with baseFeeBps = 50, _collect computes devAmount = gross*50/BPS == fee and vaultAmount = 0 for every sell and every post-window buy, and the launch decay ends at 0.5% all to the dev wallet. Since owner == devWallet (0x5Cf44fb6...7944), the party that benefits is the party holding the lever. Mitigation if the requester wants it: a floor for the base fee above the dev share (e.g. 100 bps) or a timelock on setBaseFee; both change the agreed design.","line":230,"path":"src/LaunchHook.sol","reproduction":"vm.prank(owner); hook.setBaseFee(50) succeeds. Then an exact-input sell paying out gross 10 ETH: fee = 0.05 ETH, devAmount = 0.05 ETH, vaultAmount = 0; FeeCharged(..., fee=5e16, devAmount=5e16, vaultAmount=0). Expected under the design: that is the allowed floor, documented here as the owner's power.","severity":"info","snippet":"        if (newBps < DEV_SHARE_BPS) revert FeeBelowDevShare();\n        baseFeeBps = newBps;","title":"Trust assumption: the owner (who is also the dev wallet) can set the base fee to the 0.5% dev share, which routes 100% of every base-rate fee to the dev wallet and 0 to the research vault"}],"hash":"7a05d0b61dc2d3ae255646efaf5ac724331bb6f894a7918e7f7b1b3f489f81c7","nodeId":"ecd3543a-c517-4ec8-888a-c09798e86e43","outcome":"completed","summary":"The review is complete. The findings file is at the repository root, no source files were modified, and the scratch tests live only under `test/scratch/`, which the repo ignores.\n\n**Outcome: one high, one low, one informational finding, with coverage rows for all 14 entry points plus five invariant rows.**\n\n**Finding 1, high.** The fee for exact-input buys and exact-output sells is fixed in `beforeSwap` from the requested amount and `afterSwap` never reconciles it with what the pool actually filled. Uniswap v4 fills partially instead of reverting, and this launch pool holds only the ETH buyers have brought, so any exact-output sell larger than that is a partial fill. The attached proof file reproduces three cases on the real PoolManager code:\n\n| Case | Realized ETH leg | Fee charged | Fee due at 2% |\n| --- | --- | --- | --- |\n| Exact-output sell for 20 ETH, pool holds 9.68 ETH | 9.68 ETH | 0.408 ETH | 0.194 ETH |\n| Exact-output sell for 100 ETH, pool holds 0.98 ETH | 0.98 ETH | 2.04 ETH | 0.0196 ETH |\n| Exact-input buy of 10 ETH with a price limit | 0.51 ETH swapped | 0.2 ETH | 0.014 ETH |\n\nIn the second case the seller's ETH balance drops by 1.07 ETH while they also give up tokens, and the vault and dev wallet receive ETH that was never part of a swap. The two swap types priced in `afterSwap` are correct because they read the realized delta. The proof is `test/scratch/PartialFillFee.t.sol`, and all three of its tests fail on the current code.\n\n**Finding 2, low.** `redeemClaims` burns every ERC-6909 ETH claim the hook holds, including claims a stranger transfers to it, and the resulting ETH is backed by no credit. It is stuck forever since `withdraw` reverts with nothing owed and there is no sweep. Verified in a scratch test.\n\n**Finding 3, info.** The owner, who is also the dev wallet, may lower the base fee to the 0.5% dev share, which sends every base-rate fee entirely to the dev wallet and nothing to the vault. The brief permits this, so it is recorded as a trust assumption rather than a defect.\n\n**What held.** Decay math gives 30%, 16% and 2% at the three checkpoints. Both setters are monotone downward and cannot raise any point of the buy curve. The dev and vault split conserves the fee exactly. The claim fallback on an ETH-poor manager, the pull path, and the one-pool binding all behaved as intended. The research vault's mainnet bytecode has a plain event-emitting receive, so the 100k gas push bound is sufficient for it.\n\n**Not reached.** I did not exercise the launch factory's zero-liquidity fee collection against this hook beyond confirming it has no remove-liquidity callbacks, and I did not run the mainnet-fork variant of the tests since the suite runs against locally deployed PoolManager code.","treeHash":null,"usage":{"cachedInputTokens":1558727,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":45537,"runtime":"claude","turns":31,"wallClockMs":669488}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"93c37f17670e4d98","findings":[],"hash":"8b39cc55395f7bf1acf6cb12a47e6e7ecdeb8feab1e417b63e2de0bd07ee28a1","nodeId":"3a5e9473-fb3e-4d9b-94d5-89f5d16cbf74","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":21}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"f0c4ae5b8ada215c","findings":[],"hash":"92c10a538b286f250c924cc520736efe6174420ffbb90358abb0b9b3d977a3db","nodeId":"61f13992-8b74-4bd1-b3d1-5be49fbff085","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":50}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"357c46e3781993d4","findings":[],"hash":"931c18cc6feadcaf28a005395b13f6d355a36cee181e20350926e4c05965db4a","nodeId":"61f13992-8b74-4bd1-b3d1-5be49fbff085","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":102}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"970da38f1d564250","findings":[{"citation":"resolved","description":"Failed pushes are credited to owed[recipient]. The only ways out are withdraw(), which pays owed[msg.sender] and so has to be called by the recipient contract itself, and redeemClaims()/_flush(), which retry with the same PAYOUT_GAS = 100_000 cap. The research vault is described only as 'a contract with a payable receive'. If its receive costs more than 100k gas (for example it does accounting or forwards the ETH) and it cannot make arbitrary outbound calls, every vault share is credited and never delivered: no third party can push it with full gas, and the vault cannot call withdraw. The brief says a failed recipient must be able to withdraw its credit later, and that cannot happen for such a vault.","line":240,"path":"src/LaunchHook.sol","reproduction":"Deploy the hook with researchVault = a contract whose receive() burns 150k gas (for example a loop of SSTOREs) and which has no other functions. Do an exact-input buy of 1 ETH at t=0: the vault's share of 0.295 ETH fails the 100k-gas push, so owed[vault] = 0.295 ETH. Call redeemClaims(): _flush retries with 100k gas, fails, and owed stays. No function lets anyone deliver it with enough gas. Expected: the credit can be delivered, for example by a permissionless withdrawFor(recipient) that forwards all gas to the fixed recipient. Actual: 0.295 ETH stays locked in the hook for good.","severity":"low","snippet":"    function withdraw(address to) external {\n        if (to == address(0)) revert ZeroAddress();\n        uint256 amount = owed[msg.sender];","title":"Credit owed to a contract recipient can only be pulled by that contract itself; a vault without call capability and a receive needing >100k gas keeps its fees locked forever"}],"hash":"9d438f934500fd0f1d1e70ccea8c0a4643ca3de7b035050f9d783ebdf0bdbfb6","nodeId":"1278a2d3-bd4a-424d-aa46-f9e58cdabe79","outcome":"completed","summary":"I found one low-severity defect in the permissions area of `LaunchHook`; everything else I traced holds. The report is in `.imd-findings.json`. I didn't change any other file, and I didn't write or run a Foundry proof: the finding is low severity, so none was required.\n\n**Finding 1 (low): a contract recipient's credit can get stuck for good** (`src/LaunchHook.sol:240`)\n- **How it happens:** when a payout to a recipient fails, the hook records it as `owed`. Only two things can move that credit:\n  - `withdraw()`, which pays only the caller's own credit, so the recipient contract must call it itself.\n  - `redeemClaims()`, which retries the payout with the same 100,000-gas limit that already failed.\n- **Failing case:** the research vault's `receive` costs 150k gas and the vault has no other functions.\n  1. An exact-input buy of 1 ETH at t=0 credits the vault 0.295 ETH.\n  2. Calling `redeemClaims()` fails again, and nothing else can deliver the ETH.\n- **Why it matters:** the brief requires that a recipient whose payout failed can withdraw its credit later. A vault like this can't.\n- **Possible fix:** a function anyone may call that pays a fixed recipient its credit and forwards all gas.\n- **Open question:** whether the real vault at `0x087C0CF0…` actually behaves this way. The brief only says it has a payable `receive`; I didn't check its code on-chain.\n\n**What holds:**\n- **Swap callbacks and `unlockCallback`:** all accept only the PoolManager. `receive` accepts ETH only from the PoolManager.\n- **Second initialization:** `afterInitialize` rejects it, and it only accepts the ETH/TEST pool.\n- **Fee setters:**\n  - Both are owner-only and can only lower a fee.\n  - The base fee can't go below the 0.5% dev share, and the launch fee can't go below the base fee.\n  - There is no function that moves liquidity, mints, pauses or takes a recipient's ETH.\n- **Ownership:** transfer and renounce use OpenZeppelin's `Ownable`, and transferring to the zero address reverts.\n- **Buy/sell asymmetry:** each of the four swap types charges the fee once, on the ETH side. The rate at t=1800 works out to 16%.\n- **Token:** `LaunchToken` is a plain ERC-20 with no owner or privileged functions.\n- **Static-analysis leads:** I checked them against the code and none became a finding.\n\n**Coverage:** all 14 entry points have a row: 12 hold, and `withdraw` and `redeemClaims` point to finding 1.\n\nI covered the access-control, trust-gap and asymmetry areas only, and didn't review delta accounting or gas in depth.","treeHash":null,"usage":{"cachedInputTokens":192451,"inputTokens":8,"model":"claude-opus-5-5","outputTokens":5565,"runtime":"claude","turns":5,"wallClockMs":60097}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b0b4e7bbc84f9d80","findings":[],"hash":"ac33645e6e5d590b43b55b34be228b31711a2ca2180837c394ebafab2e19c82a","nodeId":"57cf7f25-d88c-4b11-9d2e-e3ae6c4599c3","outcome":"completed","summary":"Both earlier findings are fixed, and I found nothing new to report. The findings file holds an empty list with full coverage.\n\n**Partial-fill fee (high) is fixed.** The revised afterSwap at `src/LaunchHook.sol:357-375` compares the pool's executed ETH leg with the amount the beforeSwap fee was sized for and reverts with a new PartialFill error on any difference. I copied my proof and the two specialist proofs under test/scratch and ran them against this tree. All five tests pass: the price-limited buys and sells and the sell on an ETH-less pool now revert with every balance unchanged, while the full-fill controls still trade. The two afterSwap-sized swap types still fill partially at exactly their rate.\n\n**Stranded recipient credit (low) is fixed.** The new withdrawFor at `src/LaunchHook.sol:262-270` lets anyone deliver a recipient's credit to that recipient itself with all remaining gas. The credit is zeroed before the call and restored by revert on failure. It cannot be redirected because credits only ever exist for the two fixed recipients. The gas-hungry vault scenario from my original reproduction is covered by the author's test and passes. Slither's and aderyn's \"sends ETH to arbitrary address\" lines on that function are false positives for the same reason.\n\n**Whole suite:** 86 tests pass with the fork suite excluded, including the author's eight new partial-fill tests and seven withdrawFor tests. The scratch directory was removed and the tree is clean.\n\n**Specialist leads I did not keep:**\n- Same-transaction payout deferred to a claim when the manager holds less ETH than the fee. That is the behaviour the task's own reference prescribes and the README documents it, so it is not a defect.\n- Third-party ERC-6909 claims donated to the hook are stranded. Only the donor loses, and no recipient credit is affected. I did not raise it last round and see no reason to open a revision for it now.\n- The owner setting the base fee to the dev share is a documented trust assumption, allowed by the brief.\n\nCoverage answers all fifteen entry points as holds, plus two invariant rows.","treeHash":null,"usage":{"cachedInputTokens":540225,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":11066,"runtime":"claude","turns":14,"wallClockMs":186447}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63c29c49a249ab7e","findings":[{"citation":"resolved","description":"Merges the matching findings from audit_math (both of its fee findings), audit_flow, write_foundry_tests and audit_economics, which share one root cause. When ETH is the specified currency, beforeSwap (lines 303-309) sizes the fee from the requested params.amountSpecified, `fee = uint256(-params.amountSpecified) * rate / BPS` for an exact-input buy and `fee = uint256(params.amountSpecified) * rate / (BPS - rate)` for an exact-output sell, and commits it as the specified-side BeforeSwapDelta. afterSwap (lines 335-338) collects feeFromBefore unchanged and derives gross from the request, never from `delta`. Uniswap v4 does not revert on a partial fill. A swap stops at sqrtPriceLimitX96 or when liquidity runs out. This pool is seeded with tokens only, so its ETH reserve is just what buyers have paid in. An exact-output sell asking for more than that reserve is therefore an ordinary partial fill, and so is any buy with a limit price. Effects: (1) the effective fee on the ETH actually swapped has no upper bound, which breaks the hardcoded 30% and 2% caps; (2) when the fixed fee is larger than the pool's ETH payout, the seller's ETH delta is negative, so the seller hands over tokens and also pays ETH; (3) the vault and dev wallet receive ETH that was never part of an executed swap. The exact-output buy and exact-input sell paths read delta.amount0() and are correct. Fix: in afterSwap, for these two swap types, compute the executed ETH (exact-input buy: -delta.amount0() + fee; exact-output sell: delta.amount0()) and either revert when it differs from what beforeSwap assumed, or recompute the fee on the executed amount and refund the excess to the swapper.","line":337,"path":"src/LaunchHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {LaunchHook} from \"src/LaunchHook.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\n\ncontract PartialFillRecipient { receive() external payable {} }\n\ncontract PartialFillActor is IUnlockCallback {\n    IPoolManager public immutable manager;\n    LaunchToken public immutable token;\n    constructor(IPoolManager manager_, LaunchToken token_) { manager = manager_; token = token_; }\n    receive() external payable {}\n    function seed(PoolKey memory key) external { manager.unlock(abi.encode(true, key, SwapParams(false, 0, 0))); }\n    function trade(PoolKey memory key, SwapParams memory params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(false, key, params)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (bool adding, PoolKey memory key, SwapParams memory params) = abi.decode(data, (bool, PoolKey, SwapParams));\n        BalanceDelta delta;\n        if (adding) (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(-887220, 0, 5e26, 0), \"\");\n        else delta = manager.swap(key, params, \"\");\n        if (delta.amount0() < 0) {\n            manager.sync(key.currency0);\n            manager.settle{value: uint256(-int256(delta.amount0()))}();\n        } else if (delta.amount0() > 0) manager.take(key.currency0, address(this), uint128(delta.amount0()));\n        if (delta.amount1() < 0) {\n            manager.sync(key.currency1);\n            token.transfer(address(manager), uint256(-int256(delta.amount1())));\n            manager.settle();\n        } else if (delta.amount1() > 0) manager.take(key.currency1, address(this), uint128(delta.amount1()));\n        return abi.encode(delta);\n    }\n}\n\ncontract PartialFillProof is Test {\n    using StateLibrary for IPoolManager;\n    uint160 constant PRICE = 79228162514264337593543950336;\n    IPoolManager manager;\n    LaunchHook hook;\n    LaunchToken token;\n    PartialFillActor actor;\n    PartialFillRecipient vault;\n    PartialFillRecipient dev;\n    PoolKey key;\n\n    function setUp() public {\n        vm.warp(1700000000);\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        token = new LaunchToken();\n        vault = new PartialFillRecipient();\n        dev = new PartialFillRecipient();\n        address at = address((uint160(0x1234) << 144) | uint160(0x10cc));\n        deployCodeTo(\"src/LaunchHook.sol:LaunchHook\", abi.encode(manager, token, address(this), vault, dev), at);\n        hook = LaunchHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12500, 60, IHooks(at));\n        manager.initialize(key, PRICE);\n        actor = new PartialFillActor(manager, token);\n        token.transfer(address(actor), 800_000_000 ether);\n        actor.seed(key);\n        vm.deal(address(actor), 100 ether);\n        vm.deal(address(manager), 100 ether);\n    }\n\n    function test_partialExactInputBuyRespectsThirtyPercentCap() public {\n        uint256 snapshot = vm.snapshotState();\n        assertGt(actor.trade(key, SwapParams(true, -1 ether, PRICE / 2)).amount1(), 0, \"ordinary buys must work\");\n        assertTrue(vm.revertToState(snapshot));\n        uint256 beforeEth = address(actor).balance;\n        BalanceDelta delta;\n        try actor.trade(key, SwapParams(true, -1 ether, PRICE - 1e12)) returns (BalanceDelta filled) {\n            delta = filled;\n        } catch {\n            // Rejecting an unsupported partial fill atomically is also a safe fix.\n            assertEq(address(actor).balance, beforeEth);\n            assertEq(address(vault).balance + address(dev).balance, 0);\n            return;\n        }\n        uint256 actualSpent = beforeEth - address(actor).balance;\n        uint256 fee = address(vault).balance + address(dev).balance;\n        assertGt(delta.amount1(), 0, \"the price-limited swap fills a positive amount\");\n        assertLt(actualSpent, 1 ether, \"the swap is genuinely partial\");\n        emit log_named_uint(\"actual ETH spent\", actualSpent);\n        emit log_named_uint(\"ETH fee charged\", fee);\n        // The specified fee cap applies to actual ETH spent, not an unused input budget.\n        assertLe(fee * 10000, actualSpent * 3000, \"partial fill charged above 30% of actual ETH\");\n    }\n\n    function test_partialExactOutputSellMustNotChargeTheSellerEth() public {\n        actor.trade(key, SwapParams(true, -10 ether, PRICE / 2));\n        (uint160 price,,,) = manager.getSlot0(key.toId());\n        uint256 snapshot = vm.snapshotState();\n        assertEq(actor.trade(key, SwapParams(false, 0.1 ether, PRICE)).amount0(), 0.1 ether, \"ordinary sells must work\");\n        assertTrue(vm.revertToState(snapshot));\n        uint256 beforeEth = address(actor).balance;\n        uint256 beforeToken = token.balanceOf(address(actor));\n        uint256 beforeFee = address(vault).balance + address(dev).balance;\n        BalanceDelta delta;\n        try actor.trade(key, SwapParams(false, 0.1 ether, price + 1e12)) returns (BalanceDelta filled) {\n            delta = filled;\n        } catch {\n            assertEq(address(actor).balance, beforeEth);\n            assertEq(token.balanceOf(address(actor)), beforeToken);\n            assertEq(address(vault).balance + address(dev).balance, beforeFee);\n            return;\n        }\n        assertLt(delta.amount1(), 0, \"a positive amount of TEST was sold\");\n        emit log_named_int(\"seller ETH delta\", delta.amount0());\n        assertGe(address(actor).balance, beforeEth, \"a sell may not debit ETH to pay a fee on unfilled output\");\n    }\n}","reproduction":"Reproduced locally (forge test --match-path test/scratch/PartialFillProof.t.sol -vv, both tests FAIL on the current tree). Setup: locally deployed real PoolManager; native ETH/TEST pool at sqrtPrice 2^96, LP fee 12500, tick spacing 60; token-only liquidity 5e26 in [-887220, 0]. (A) At t=0, swap(zeroForOne=true, amountSpecified=-1e18, sqrtPriceLimitX96=2^96-1e12). Expected: fee <= 30% of the ETH actually debited, or a revert. Actual: the hook takes fee = 300000000000000000 wei, while the buyer's total ETH debit is only 300000006390771891 wei, so about 100% of the debit is fee (assertion: 3000000000000000000000 > 900000019172315673000). (B) Buy with 10 ETH first, then an exact-output sell of 1e17 wei ETH with sqrtPriceLimitX96 = current price + 1e12. Expected: the seller's net ETH is >= 0. Actual: the seller's ETH delta is -2040810015643196 wei and TEST is also debited. Also reproduced with the specialists' proofs: an exact-output sell of 1 ETH on a pool before its first buy succeeds with amount0 = -20408163265306122 and amount1 = 0, so the seller pays 0.0204 ETH for nothing and the vault and dev wallet are paid; an exact-input buy of 100 ETH with a limit price reached after about 1.02 ETH costs 31.02 ETH, a 96.7% effective fee. Full-fill controls in the same test pass.","severity":"high","snippet":"            fee = feeFromBefore;\n            gross = buy ? uint256(-params.amountSpecified) : uint256(params.amountSpecified) + fee;","title":"Fee for exact-input buys and exact-output sells is fixed in beforeSwap from amountSpecified and never checked against the executed fill: partial fills pay far more than 30%/2%, and a sell can cost the"},{"citation":"resolved","description":"Merges the matching findings from audit_permissions and audit_flow. A failed push is credited to owed[recipient]. withdraw(to) pays only owed[msg.sender], so the recipient contract must make the call itself. redeemClaims()/_flush retries with the same PAYOUT_GAS = 100_000 cap (line 473) and keeps the credit if the push fails. The research vault is a contract. If its receive ever reverts or needs more than 100k gas, and it has no function that can call hook.withdraw, then its whole fee share is credited and never delivered: no third party can deliver it with full gas, and no one can redirect it. The brief requires that a recipient whose transfer fails can withdraw its credit later. Today this path is dormant: an eth_estimateGas against the mainnet vault (0x087C0CF0A103acA2c487c5017DbC121ad6C5afEE) sending 1 wei costs 22826 gas, so pushes succeed. Severity is low because the path is conditional. Fix that keeps the design: a permissionless withdrawFor(recipient) that sends owed[recipient] to that fixed recipient with all gas remaining. This would also cover a receive that needs more than 100k gas.","line":242,"path":"src/LaunchHook.sol","reproduction":"Deploy the hook with researchVault = a contract whose receive() reverts (or uses about 150k gas) and which has no function that calls out. At t=0, do an exact-input buy of 1 ETH against a funded manager. _push fails, so owed[vault] = 295000000000000000 and the hook's balance is 0.295 ETH. Then hook.withdraw(x) from any address other than the vault reverts NothingOwed. hook.redeemClaims() reaches _flush(researchVault), whose 100k-gas send fails again, so owed[vault] stays 0.295 ETH. No other function moves it. Expected: the recipient can still get its credit. Actual: the ETH stays in the hook until the vault itself calls withdraw, which this vault cannot do.","severity":"low","snippet":"        uint256 amount = owed[msg.sender];","title":"A recipient's credited ETH can only be pulled by the recipient itself, and redeemClaims retries with the same 100k-gas push, so a contract vault that cannot make calls has no way to recover its credit"}],"hash":"bdab4b8551c4269d617543f24fc7a09f7b8364964d370c24aedead0b78345c45","nodeId":"57cf7f25-d88c-4b11-9d2e-e3ae6c4599c3","outcome":"completed","summary":"I've written the review to `.imd-findings.json`: two findings, and coverage rows for all 14 entry points plus two invariants. I changed no repository files; the scratch tests I ran are deleted.\n\n**Finding 1 (high): swap fees are charged on the amount requested, not the amount actually swapped** (`src/LaunchHook.sol:337`). Four specialist areas reported this one problem (math reported it twice), so it is merged into one finding. For exact-input buys and exact-output sells, `beforeSwap` sets the fee from the amount the swapper asks for. `afterSwap` collects that fee without checking what the pool really filled. Uniswap v4 fills swaps partially when they hit a price limit or run out of liquidity, and this pool starts with tokens only, so partial fills are routine at launch.\n- I ran the specialists' proofs on this code and all six tests fail as described.\n- In one, a buyer paid about 0.3 ETH and almost all of it was fee, far above the 30% cap.\n- In another, a seller handed over tokens and still lost about 0.002 ETH.\n- In a third, a sell on a pool with no ETH yet received nothing and still paid a 0.0204 ETH fee to the vault and dev wallet.\n\nI attached the test-writer's proof, which covers the buy and sell cases and includes full-fill swaps that still pass.\n\n**Finding 2 (low): the research vault may never be able to collect a credited fee** (`src/LaunchHook.sol:242`). Two specialist findings, merged. Only the recipient itself can call `withdraw`, and `redeemClaims` retries with the same 100k gas limit. A vault contract that rejects ETH and has no way to make outgoing calls would leave its share stuck in the hook. This can't happen today: on mainnet, sending ETH to the real vault takes 22,826 gas, well under the limit, so payments go through. The suggested fix is a function anyone can call that sends a recipient's credit to that recipient with all remaining gas.\n\n**Dropped:**\n- **First swap on an empty PoolManager pays recipients later, not in the same transaction.** It reproduces, but the mainnet PoolManager holds plenty of ETH, the README documents the fallback, and the reference guidance recommends this design.\n- **Claims a third party gives the hook end up as ETH no one can withdraw.** Only the giver loses money.\n- **Rounding dust, and the owner's power to set the base fee to the 0.5% dev share.** The brief explicitly allows that minimum.\n\n**Checks I ran:**\n- The repository's own test suites all pass.\n- I ran the protected baseline tests with the hook built from `launch.json`'s constructor arguments and permission flags 4300; all 15 run tests pass and 1 is skipped. That covers the launch factory initializing the pool, collecting fees with the hook live, ownership, permission flags, runtime code with no escape hatch, and the token checks.\n- The remaining entry points do what they should. The fee setters can only lower fees, `afterInitialize` accepts only one pool, only the PoolManager can call the callbacks, and `withdraw` zeroes the balance before sending.","treeHash":null,"usage":{"cachedInputTokens":1037371,"inputTokens":28,"model":"claude-opus-5-5","outputTokens":14021,"runtime":"claude","turns":15,"wallClockMs":209519}},{"artifacts":[],"attempt":1,"bundleHash":"95bbb7c4474be2022a4429c6ac165ba66097d5289b8f76301cdc8aef1121a35d","device":"7b7c27166477ddd8","findings":[{"description":"When ETH is the specified currency, beforeSwap fixes the fee from the entire amountSpecified (lines 303-309). afterSwap then unconditionally collects that fee and derives gross from the requested amount rather than the actual AMM delta (lines 335-338). Uniswap v4 can stop early at a price limit or exhausted liquidity. A partially filled exact-input buy therefore pays a fee on unused input and can lose nearly all the ETH actually debited, exceeding the 30%/2% schedule. A partially filled exact-output sell can pay a fee greater than its actual ETH output, turning its ETH delta negative. The 0.5% dev split also uses the unfilled nominal amount. Settle fees on actual executed ETH, or reject unsupported partial fills atomically while continuing to support ordinary filled swaps.","line":335,"path":"src/LaunchHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {LaunchHook} from \"src/LaunchHook.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\n\ncontract PartialFillRecipient { receive() external payable {} }\n\ncontract PartialFillActor is IUnlockCallback {\n    IPoolManager public immutable manager;\n    LaunchToken public immutable token;\n    constructor(IPoolManager manager_, LaunchToken token_) { manager = manager_; token = token_; }\n    receive() external payable {}\n    function seed(PoolKey memory key) external { manager.unlock(abi.encode(true, key, SwapParams(false, 0, 0))); }\n    function trade(PoolKey memory key, SwapParams memory params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(false, key, params)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (bool adding, PoolKey memory key, SwapParams memory params) = abi.decode(data, (bool, PoolKey, SwapParams));\n        BalanceDelta delta;\n        if (adding) (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(-887220, 0, 5e26, 0), \"\");\n        else delta = manager.swap(key, params, \"\");\n        if (delta.amount0() < 0) {\n            manager.sync(key.currency0);\n            manager.settle{value: uint256(-int256(delta.amount0()))}();\n        } else if (delta.amount0() > 0) manager.take(key.currency0, address(this), uint128(delta.amount0()));\n        if (delta.amount1() < 0) {\n            manager.sync(key.currency1);\n            token.transfer(address(manager), uint256(-int256(delta.amount1())));\n            manager.settle();\n        } else if (delta.amount1() > 0) manager.take(key.currency1, address(this), uint128(delta.amount1()));\n        return abi.encode(delta);\n    }\n}\n\ncontract PartialFillProof is Test {\n    using StateLibrary for IPoolManager;\n    uint160 constant PRICE = 79228162514264337593543950336;\n    IPoolManager manager;\n    LaunchHook hook;\n    LaunchToken token;\n    PartialFillActor actor;\n    PartialFillRecipient vault;\n    PartialFillRecipient dev;\n    PoolKey key;\n\n    function setUp() public {\n        vm.warp(1700000000);\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        token = new LaunchToken();\n        vault = new PartialFillRecipient();\n        dev = new PartialFillRecipient();\n        address at = address((uint160(0x1234) << 144) | uint160(0x10cc));\n        deployCodeTo(\"src/LaunchHook.sol:LaunchHook\", abi.encode(manager, token, address(this), vault, dev), at);\n        hook = LaunchHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12500, 60, IHooks(at));\n        manager.initialize(key, PRICE);\n        actor = new PartialFillActor(manager, token);\n        token.transfer(address(actor), 800_000_000 ether);\n        actor.seed(key);\n        vm.deal(address(actor), 100 ether);\n        vm.deal(address(manager), 100 ether);\n    }\n\n    function test_partialExactInputBuyRespectsThirtyPercentCap() public {\n        uint256 snapshot = vm.snapshotState();\n        assertGt(actor.trade(key, SwapParams(true, -1 ether, PRICE / 2)).amount1(), 0, \"ordinary buys must work\");\n        assertTrue(vm.revertToState(snapshot));\n        uint256 beforeEth = address(actor).balance;\n        BalanceDelta delta;\n        try actor.trade(key, SwapParams(true, -1 ether, PRICE - 1e12)) returns (BalanceDelta filled) {\n            delta = filled;\n        } catch {\n            // Rejecting an unsupported partial fill atomically is also a safe fix.\n            assertEq(address(actor).balance, beforeEth);\n            assertEq(address(vault).balance + address(dev).balance, 0);\n            return;\n        }\n        uint256 actualSpent = beforeEth - address(actor).balance;\n        uint256 fee = address(vault).balance + address(dev).balance;\n        assertGt(delta.amount1(), 0, \"the price-limited swap fills a positive amount\");\n        assertLt(actualSpent, 1 ether, \"the swap is genuinely partial\");\n        emit log_named_uint(\"actual ETH spent\", actualSpent);\n        emit log_named_uint(\"ETH fee charged\", fee);\n        // The specified fee cap applies to actual ETH spent, not an unused input budget.\n        assertLe(fee * 10000, actualSpent * 3000, \"partial fill charged above 30% of actual ETH\");\n    }\n\n    function test_partialExactOutputSellMustNotChargeTheSellerEth() public {\n        actor.trade(key, SwapParams(true, -10 ether, PRICE / 2));\n        (uint160 price,,,) = manager.getSlot0(key.toId());\n        uint256 snapshot = vm.snapshotState();\n        assertEq(actor.trade(key, SwapParams(false, 0.1 ether, PRICE)).amount0(), 0.1 ether, \"ordinary sells must work\");\n        assertTrue(vm.revertToState(snapshot));\n        uint256 beforeEth = address(actor).balance;\n        uint256 beforeToken = token.balanceOf(address(actor));\n        uint256 beforeFee = address(vault).balance + address(dev).balance;\n        BalanceDelta delta;\n        try actor.trade(key, SwapParams(false, 0.1 ether, price + 1e12)) returns (BalanceDelta filled) {\n            delta = filled;\n        } catch {\n            assertEq(address(actor).balance, beforeEth);\n            assertEq(token.balanceOf(address(actor)), beforeToken);\n            assertEq(address(vault).balance + address(dev).balance, beforeFee);\n            return;\n        }\n        assertLt(delta.amount1(), 0, \"a positive amount of TEST was sold\");\n        emit log_named_int(\"seller ETH delta\", delta.amount0());\n        assertGe(address(actor).balance, beforeEth, \"a sell may not debit ETH to pay a fee on unfilled output\");\n    }\n}","reproduction":"Run the embedded self-contained test as test/scratch/PartialFillProof.t.sol with forge test --match-path test/scratch/PartialFillProof.t.sol -vv. A locally deployed real PoolManager, native ETH/TEST pool at 2^96, 12500 LP fee and token-only liquidity 5e26 in [-887220,0] are used. At t=0 swap zeroForOne=true, amountSpecified=-1e18, sqrtPriceLimitX96=2^96-1e12. Actual: a positive token fill, actual ETH debit 300000006390771891 wei, and fee 300000000000000000 wei (~100% of actual debit); expected at most 30% of actual ETH or an atomic rejection. A second test first buys with 10 ETH, then requests an exact-output sell of 1e17 wei ETH with sqrtPriceLimitX96=currentPrice+1e12. Actual seller ETH delta is -2040810015643196 wei while TEST is also debited; expected nonnegative net ETH and a fee proportional to filled output, or atomic rejection. Both assertions failed locally. Ordinary full-fill buy/sell controls succeed. The proofs allow atomic partial-fill rejection as a safe repair.","severity":"high","title":"Partial fills charge the fee on unexecuted ETH, bypassing the fee cap and debiting ETH on sells"},{"description":"When the PoolManager has less ETH than the fee during afterSwap, _collect mints an ERC-6909 claim and only increments owed. A normal router settles the buyer ETH after manager.swap returns; neither the hook nor the supplied test router flushes those credits before the transaction ends. Thus the first swap on a token-only, empty manager succeeds but neither accepting recipient is paid in the same transaction. Accounting and later redemption remain sound, and the pinned mainnet tests with existing manager ETH pass. This is a liveness/requirement gap against the brief’s unconditional same-transaction payouts, not evidence of lost backing. The implementation README documents the fallback, and the background reference permits claims on an empty manager, but that design does not itself fulfill the stricter brief. An integrated post-settlement payout or a changed requirement is needed.","line":435,"path":"src/LaunchHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {LaunchHook} from \"src/LaunchHook.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\n\ncontract FirstBuyRecipient { receive() external payable {} }\n\ncontract FirstBuyActor is IUnlockCallback {\n    IPoolManager public immutable manager;\n    LaunchToken public immutable token;\n    constructor(IPoolManager manager_, LaunchToken token_) { manager = manager_; token = token_; }\n    receive() external payable {}\n    function seed(PoolKey memory key) external { manager.unlock(abi.encode(true, key, SwapParams(false, 0, 0))); }\n    function trade(PoolKey memory key, SwapParams memory params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(false, key, params)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (bool adding, PoolKey memory key, SwapParams memory params) = abi.decode(data, (bool, PoolKey, SwapParams));\n        BalanceDelta delta;\n        if (adding) (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(-887220, 0, 5e26, 0), \"\");\n        else delta = manager.swap(key, params, \"\");\n        if (delta.amount0() < 0) {\n            manager.sync(key.currency0);\n            manager.settle{value: uint256(-int256(delta.amount0()))}();\n        } else if (delta.amount0() > 0) manager.take(key.currency0, address(this), uint128(delta.amount0()));\n        if (delta.amount1() < 0) {\n            manager.sync(key.currency1);\n            token.transfer(address(manager), uint256(-int256(delta.amount1())));\n            manager.settle();\n        } else if (delta.amount1() > 0) manager.take(key.currency1, address(this), uint128(delta.amount1()));\n        return abi.encode(delta);\n    }\n}\n\ncontract FirstBuyPayoutProof is Test {\n    using StateLibrary for IPoolManager;\n    uint160 constant PRICE = 79228162514264337593543950336;\n    IPoolManager manager;\n    LaunchHook hook;\n    LaunchToken token;\n    FirstBuyActor actor;\n    FirstBuyRecipient vault;\n    FirstBuyRecipient dev;\n    PoolKey key;\n\n    function setUp() public {\n        vm.warp(1700000000);\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        token = new LaunchToken();\n        vault = new FirstBuyRecipient();\n        dev = new FirstBuyRecipient();\n        address at = address((uint160(0x1234) << 144) | uint160(0x10cc));\n        deployCodeTo(\"src/LaunchHook.sol:LaunchHook\", abi.encode(manager, token, address(this), vault, dev), at);\n        hook = LaunchHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12500, 60, IHooks(at));\n        manager.initialize(key, PRICE);\n        actor = new FirstBuyActor(manager, token);\n        token.transfer(address(actor), 800_000_000 ether);\n        actor.seed(key);\n        vm.deal(address(actor), 100 ether);\n    }\n\n    function test_firstBuyPaysBothAcceptingRecipientsInTheSameTransaction() public {\n        assertEq(address(manager).balance, 0, \"token-only seed contributes no ETH\");\n        BalanceDelta delta = actor.trade(key, SwapParams(true, -1 ether, PRICE / 2));\n        assertEq(delta.amount0(), -1 ether);\n        assertGt(delta.amount1(), 0, \"the buy executed successfully\");\n        emit log_named_uint(\"dev ETH received\", address(dev).balance);\n        emit log_named_uint(\"vault ETH received\", address(vault).balance);\n        emit log_named_uint(\"claims deferred\", hook.pendingClaims());\n        assertEq(address(dev).balance, 0.005 ether, \"accepting dev must receive ETH in the swap transaction\");\n        assertEq(address(vault).balance, 0.295 ether, \"accepting research vault must receive ETH in the swap transaction\");\n    }\n}","reproduction":"Run the embedded test as test/scratch/FirstBuyPayoutProof.t.sol with forge test --match-path test/scratch/FirstBuyPayoutProof.t.sol -vv. Deploy the real PoolManager locally without ETH funding, initialize the hook pool at 2^96, seed only TEST, and make a 1 ETH exact-input buy at t=0 through one external router call that fully settles the swap. Both recipients have accepting payable receive functions. Expected end-of-transaction balances: dev +5000000000000000 wei, vault +295000000000000000 wei. Actual: both receive zero and pendingClaims() is 300000000000000000 wei. The first recipient-balance assertion failed locally. A separate future redeemClaims/withdraw call is currently required.","severity":"medium","title":"An empty PoolManager defers accepting recipients payments beyond the swap transaction"}],"hash":"c4313b831650251780d4a6ca37fe30bd27c6598b75c86f88c5f7ed23e720effa","nodeId":"c136adb4-e7dd-430c-af43-6305f4f010df","outcome":"completed","summary":"Added Foundry unit, fuzz, invariant, and pinned mainnet fork tests.\n\n`forge build` and `forge test` pass: **74 tests, zero failures or skips**.\n\nReported two defects with reproduced failing proofs in `.imd-findings.json`:\n\n- **High:** Partial fills overcharge fees and can debit ETH on sells.\n- **Medium:** First-buy payouts on an empty PoolManager require later redemption.\n\nProduction code and configuration remain unchanged. Coverage and reproduction details are in `test/TESTING.md`.","treeHash":"4d4ae913c7210d52576bd2eb43fad00d4d4b4ea8","usage":{"cachedInputTokens":4272256,"inputTokens":188577,"model":"gpt-6-astra","outputTokens":40018,"runtime":"codex","turns":9,"wallClockMs":1258156}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6e1819675bb7567a","findings":[],"hash":"c7afbca481d2aa4ec2351a708eeadeb3d038e2d57c378fbc4abf1dea3836f07a","nodeId":"3a5e9473-fb3e-4d9b-94d5-89f5d16cbf74","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":75}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"163f8bc7d28274e2","findings":[],"hash":"d157be13b51ed2f5ca5e493d3e237c0bc2b5f23038f2c4b519b7f53cd4816f12","nodeId":"c136adb4-e7dd-430c-af43-6305f4f010df","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":30}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"803eeae3d8b63de3","findings":[],"hash":"d965320bd8e2d97834cd333989085ea6dba1846c833e096b4f0d179e2f085c21","nodeId":"85191cfa-dc14-4e47-a6e2-f22d7aa7a257","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":89}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"dfa08759ca0424de","findings":[],"hash":"dd27159adb1b49673bace507baac4c22b3a2fd424b6b1e702e71e2c31fc84ffd","nodeId":"1278a2d3-bd4a-424d-aa46-f9e58cdabe79","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":35}}],"verification":[{"checks":[{"durationMs":3770,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.60s\nCompiler run successful!\n","passed":true},{"durationMs":181,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.34ms (852.89µs CPU time)\n\nRan 1 test for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForFreshManagerTest\n[PASS] test_withdrawFor_redeemsClaimsOnDemand() (gas: 447457)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.22ms (764.04µs CPU time)\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519320)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 520029)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 523962)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 403445)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.87ms (2.78ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389734)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197308)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130734)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 589311)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391909)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116468)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342458)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 348357)\n[PASS] test_transferOwnership() (gas: 110266)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 2.88ms (3.68ms CPU time)\n\nRan 5 tests for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForTest\n[PASS] test_withdrawFor_afterOwnPull_isEmpty() (gas: 430298)\n[PASS] test_withdrawFor_cannotRedirectCredit() (gas: 362763)\n[PASS] test_withdrawFor_deliversToRecipientWithFullGas() (gas: 1570491)\n[PASS] test_withdrawFor_nothingOwed_reverts() (gas: 61518)\n[PASS] test_withdrawFor_rejectingRecipient_revertsAndKeepsCredit() (gas: 482145)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.98ms (2.20ms CPU time)\n\nRan 8 tests for test/LaunchHookPartialFill.t.sol:LaunchHookPartialFillTest\n[PASS] test_exactOutputSellBeyondPoolEth_reverts() (gas: 655420)\n[PASS] test_exactOutputSellMoreThanPoolHolds_reverts() (gas: 976912)\n[PASS] test_fullFillWithPriceLimitStillWorks() (gas: 329349)\n[PASS] test_partialExactInputBuy_afterWindow_reverts() (gas: 326125)\n[PASS] test_partialExactInputBuy_reverts() (gas: 321777)\n[PASS] test_partialExactInputSell_paysFeeOnExecutedEth() (gas: 556061)\n[PASS] test_partialExactOutputBuy_paysFeeOnExecutedEth() (gas: 329583)\n[PASS] test_partialExactOutputSell_priceLimit_reverts() (gas: 603449)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 19.56ms (8.02ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 20350415)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 29.61ms (29.26ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344658, ~: 344562)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22597, ~: 22625)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 541200, ~: 541208)\n[PASS] test_buyAt1800_pays16Percent() (gas: 334215)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333855)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336729)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332283)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332540)\n[PASS] test_callbacksRefuseNonManager() (gas: 122808)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78683)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 233277)\n[PASS] test_feeSchedule_views() (gas: 91225)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1399222)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65834)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 190216)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 744079)\n[PASS] test_initializeWithoutEth_reverts() (gas: 744523)\n[PASS] test_permissionsMatchAddress() (gas: 12057)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365900)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535439)\n[PASS] test_secondInitialize_reverts() (gas: 86877)\n[PASS] test_sellExactInput_pays2Percent() (gas: 540716)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 541835)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 561753)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331752)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 34.87ms (70.23ms CPU time)\n\nRan 8 test suites in 37.32ms (96.33ms CPU time): 67 tests passed, 0 failed, 0 skipped (67 total tests)\n","passed":true},{"durationMs":71,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchHook.withdrawFor(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":217,\"foundry.toml\":24,\"launch.json\":33,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":535,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchHookPartialFill.t.sol\":288,\"test/LaunchToken.t.sol\":65,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":103},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2ec1669a815576e7201c87b04d5f23fa61cecf5e36c33b4fa89e2d963e7cd8af","verifiedTreeHash":"5f9e4a37da1b0ef1f65291a7a5090d7577f91587","verifierVersion":"0.1.0+3dc93dc1"},{"checks":[{"durationMs":3898,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.77s\nCompiler run successful!\n","passed":true},{"durationMs":203,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.21ms (1.04ms CPU time)\n\nRan 1 test for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForFreshManagerTest\n[PASS] test_withdrawFor_redeemsClaimsOnDemand() (gas: 447457)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.75ms (622.61µs CPU time)\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519320)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 520029)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 523962)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 403445)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.71ms (1.88ms CPU time)\n\nRan 5 tests for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForTest\n[PASS] test_withdrawFor_afterOwnPull_isEmpty() (gas: 430298)\n[PASS] test_withdrawFor_cannotRedirectCredit() (gas: 362763)\n[PASS] test_withdrawFor_deliversToRecipientWithFullGas() (gas: 1570491)\n[PASS] test_withdrawFor_nothingOwed_reverts() (gas: 61518)\n[PASS] test_withdrawFor_rejectingRecipient_revertsAndKeepsCredit() (gas: 482145)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 6.61ms (7.17ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389734)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197308)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130734)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 589311)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391909)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116468)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342458)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 348357)\n[PASS] test_transferOwnership() (gas: 110266)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 6.65ms (3.89ms CPU time)\n\nRan 8 tests for test/LaunchHookPartialFill.t.sol:LaunchHookPartialFillTest\n[PASS] test_exactOutputSellBeyondPoolEth_reverts() (gas: 655420)\n[PASS] test_exactOutputSellMoreThanPoolHolds_reverts() (gas: 976912)\n[PASS] test_fullFillWithPriceLimitStillWorks() (gas: 329349)\n[PASS] test_partialExactInputBuy_afterWindow_reverts() (gas: 326125)\n[PASS] test_partialExactInputBuy_reverts() (gas: 321777)\n[PASS] test_partialExactInputSell_paysFeeOnExecutedEth() (gas: 556061)\n[PASS] test_partialExactOutputBuy_paysFeeOnExecutedEth() (gas: 329583)\n[PASS] test_partialExactOutputSell_priceLimit_reverts() (gas: 603449)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 6.67ms (6.75ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 20350415)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 20.82ms (19.80ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344658, ~: 344562)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22597, ~: 22625)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 541200, ~: 541208)\n[PASS] test_buyAt1800_pays16Percent() (gas: 334215)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333855)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336729)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332283)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332540)\n[PASS] test_callbacksRefuseNonManager() (gas: 122808)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78683)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 233277)\n[PASS] test_feeSchedule_views() (gas: 91225)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1399222)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65834)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 190216)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 744079)\n[PASS] test_initializeWithoutEth_reverts() (gas: 744523)\n[PASS] test_permissionsMatchAddress() (gas: 12057)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365900)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535439)\n[PASS] test_secondInitialize_reverts() (gas: 86877)\n[PASS] test_sellExactInput_pays2Percent() (gas: 540716)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 541835)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 561753)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331752)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 42.82ms (86.55ms CPU time)\n\nRan 8 test suites in 43.63ms (91.25ms CPU time): 67 tests passed, 0 failed, 0 skipped (67 total tests)\n","passed":true},{"durationMs":75,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchHook.withdrawFor(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":217,\"foundry.toml\":24,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":535,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchHookPartialFill.t.sol\":288,\"test/LaunchToken.t.sol\":65,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":103},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1679,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/LaunchHook.sol:262: LaunchHook.withdrawFor(address) (src/LaunchHook.sol#262-270) sends eth to arbitrary user\n[medium/high] incorrect-equality at src/LaunchHook.sol:189: LaunchHook.buyFeeBpsAt(uint256) (src/LaunchHook.sol#189-197) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/LaunchHook.sol:323: LaunchHook.beforeSwap(address,PoolKey,SwapParams,bytes).fee (src/LaunchHook.sol#323) is a local variable never initialized\n[medium/medium] uninitialized-local at src/LaunchHook.sol:356: LaunchHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).hookDelta (src/LaunchHook.sol#356) is a local variable never initialized\n[medium/medium] unused-return at src/LaunchHook.sol:517: LaunchHook._redeemClaims() (src/LaunchHook.sol#517-522) ignores return value by poolManager.unlock(abi.encode(claims)) (src/LaunchHook.sol#520)\n[low/medium] missing-zero-check at src/LaunchHook.sol:262: LaunchHook.withdrawFor(address).recipient (src/LaunchHook.sol#262) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/LaunchHook.sol:462: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#462-482):\n[low/medium] reentrancy-benign at src/LaunchHook.sol:462: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#462-482):\n[low/medium] reentrancy-benign at src/LaunchHook.sol:273: Reentrancy in LaunchHook.redeemClaims() (src/LaunchHook.sol#273-277):\n[low/medium] reentrancy-events at src/LaunchHook.sol:262: Reentrancy in LaunchHook.withdrawFor(address) (src/LaunchHook.sol#262-270):\n[low/medium] reentrancy-events at src/LaunchHook.sol:517: Reentrancy in LaunchHook._redeemClaims() (src/LaunchHook.sol#517-522):\n[low/medium] reentrancy-events at src/LaunchHook.sol:462: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#462-482):\n[low/medium] reentrancy-events at src/LaunchHook.sol:462: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#462-482):\n[low/medium] reentrancy-events at src/LaunchHook.sol:273: Reentrancy in LaunchHook.redeemClaims() (src/LaunchHook.sol#273-277):\n[low/medium] reentrancy-events at src/LaunchHook.sol:248: Reentrancy in LaunchHook.withdraw(address) (src/LaunchHook.sol#248-257):\n[low/medium] timestamp at src/LaunchHook.sol:297: LaunchHook.afterInitialize(address,PoolKey,uint160,int24) (src/LaunchHook.sol#297-310) uses timestamp for comparisons\n[low/medium] timestamp at src/LaunchHook.sol:189: LaunchHook.buyFeeBpsAt(uint256) (src/LaunchHook.sol#189-197) uses timestamp for comparisons","passed":true},{"durationMs":451,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/LaunchHook.sol:262: ETH transferred without address checks\n[low] centralization-risk at src/LaunchHook.sol:47: Centralization Risk (3 places)\n[low] internal-function-used-once at src/HookFlags.sol:29: Internal Function Used Only Once\n[low] large-numeric-literal at src/LaunchHook.sol:56: Large Numeric Literal (3 places)\n[low] push-zero-opcode at src/HookFlags.sol:2: PUSH0 Opcode\n[low] unchecked-return at src/LaunchHook.sol:520: Unchecked Return\n[low] unused-public-function at src/LaunchHook.sol:205: Public Function Not Used Internally (2 places)","passed":true},{"durationMs":2938,"exitCode":0,"name":"proof 6d3a09a14997","output":"Compiling 80 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.13s\nCompiler run successful!\n\nRan 2 tests for test/imd-proof-d564a481/Proof_6d3a09a14997.t.sol:PartialFillProof\n[PASS] test_partialExactInputBuyRespectsThirtyPercentCap() (gas: 353356)\n[PASS] test_partialExactOutputSellMustNotChargeTheSellerEth() (gas: 522253)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.46ms (1.99ms CPU time)\n\nRan 1 test suite in 4.31ms (2.46ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"3864f5a8a4e4053237f9d24b8be0a55fa57ed6bb777d61d9a10142ae714984ec","verifiedTreeHash":"170ba2509e28fb144a34df3443cdd7fbee3f9860","verifierVersion":"0.1.0+3dc93dc1"},{"checks":[{"durationMs":4005,"exitCode":0,"name":"build","output":"Compiling 84 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.80s\nCompiler run successful!\n","passed":true},{"durationMs":262,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519088)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 519619)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 522786)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 402855)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 5.02ms (4.04ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 19242670)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 30.43ms (30.06ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389124)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197264)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130690)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 588435)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391321)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116446)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342048)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 347769)\n[PASS] test_transferOwnership() (gas: 110222)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 49.33ms (6.35ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344053, ~: 343974)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22557, ~: 22581)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 540367, ~: 540354)\n[PASS] test_buyAt1800_pays16Percent() (gas: 333627)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333267)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336141)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332017)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332274)\n[PASS] test_callbacksRefuseNonManager() (gas: 122764)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78168)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 230663)\n[PASS] test_feeSchedule_views() (gas: 91203)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1397845)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65746)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 189317)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 743180)\n[PASS] test_initializeWithoutEth_reverts() (gas: 743624)\n[PASS] test_permissionsMatchAddress() (gas: 12145)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365311)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535117)\n[PASS] test_secondInitialize_reverts() (gas: 86855)\n[PASS] test_sellExactInput_pays2Percent() (gas: 539862)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 540830)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 560899)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331253)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 49.41ms (89.59ms CPU time)\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 49.47ms (9.13ms CPU time)\n\nRan 5 test suites in 54.05ms (183.65ms CPU time): 53 tests passed, 0 failed, 0 skipped (53 total tests)\n","passed":true},{"durationMs":121,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":24,\"launch.json\":33,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":498,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchToken.t.sol\":65,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":96},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"39d8afd2fe9f991a7d9ea7d85d934ea0f123cbc286c0449d954e3acfa07cb803","verifiedTreeHash":"2bd3771980f7691a3546daec0a8d9522528889b0","verifierVersion":"0.1.0+3dc93dc1"},{"checks":[{"durationMs":2962,"exitCode":0,"name":"build","output":"Compiling 84 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.80s\nCompiler run successful!\n","passed":true},{"durationMs":212,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 955.05µs (913.86µs CPU time)\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519088)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 519619)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 522786)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 402855)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.49ms (2.02ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389124)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197264)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130690)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 588435)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391321)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116446)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342048)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 347769)\n[PASS] test_transferOwnership() (gas: 110222)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 3.14ms (3.79ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 19242670)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.45ms (15.18ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344053, ~: 343974)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22557, ~: 22581)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 540367, ~: 540354)\n[PASS] test_buyAt1800_pays16Percent() (gas: 333627)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333267)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336141)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332017)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332274)\n[PASS] test_callbacksRefuseNonManager() (gas: 122764)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78168)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 230663)\n[PASS] test_feeSchedule_views() (gas: 91203)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1397845)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65746)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 189317)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 743180)\n[PASS] test_initializeWithoutEth_reverts() (gas: 743624)\n[PASS] test_permissionsMatchAddress() (gas: 12145)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365311)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535117)\n[PASS] test_secondInitialize_reverts() (gas: 86855)\n[PASS] test_sellExactInput_pays2Percent() (gas: 539862)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 540830)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 560899)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331253)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 53.76ms (99.80ms CPU time)\n\nRan 5 test suites in 55.26ms (75.80ms CPU time): 53 tests passed, 0 failed, 0 skipped (53 total tests)\n","passed":true},{"durationMs":150,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":24,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":498,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchToken.t.sol\":65,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":96},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1412,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/LaunchHook.sol:181: LaunchHook.buyFeeBpsAt(uint256) (src/LaunchHook.sol#181-189) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/LaunchHook.sol:302: LaunchHook.beforeSwap(address,PoolKey,SwapParams,bytes).fee (src/LaunchHook.sol#302) is a local variable never initialized\n[medium/medium] uninitialized-local at src/LaunchHook.sol:334: LaunchHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).hookDelta (src/LaunchHook.sol#334) is a local variable never initialized\n[medium/medium] unused-return at src/LaunchHook.sol:480: LaunchHook._redeemClaims() (src/LaunchHook.sol#480-485) ignores return value by poolManager.unlock(abi.encode(claims)) (src/LaunchHook.sol#483)\n[low/medium] reentrancy-benign at src/LaunchHook.sol:425: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#425-445):\n[low/medium] reentrancy-benign at src/LaunchHook.sol:252: Reentrancy in LaunchHook.redeemClaims() (src/LaunchHook.sol#252-256):\n[low/medium] reentrancy-benign at src/LaunchHook.sol:425: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#425-445):\n[low/medium] reentrancy-events at src/LaunchHook.sol:425: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#425-445):\n[low/medium] reentrancy-events at src/LaunchHook.sol:480: Reentrancy in LaunchHook._redeemClaims() (src/LaunchHook.sol#480-485):\n[low/medium] reentrancy-events at src/LaunchHook.sol:252: Reentrancy in LaunchHook.redeemClaims() (src/LaunchHook.sol#252-256):\n[low/medium] reentrancy-events at src/LaunchHook.sol:425: Reentrancy in LaunchHook._collect(bool,bool,uint256,uint256) (src/LaunchHook.sol#425-445):\n[low/medium] reentrancy-events at src/LaunchHook.sol:240: Reentrancy in LaunchHook.withdraw(address) (src/LaunchHook.sol#240-249):\n[low/medium] timestamp at src/LaunchHook.sol:181: LaunchHook.buyFeeBpsAt(uint256) (src/LaunchHook.sol#181-189) uses timestamp for comparisons\n[low/medium] timestamp at src/LaunchHook.sol:276: LaunchHook.afterInitialize(address,PoolKey,uint160,int24) (src/LaunchHook.sol#276-289) uses timestamp for comparisons","passed":true},{"durationMs":429,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/LaunchHook.sol:42: Centralization Risk (3 places)\n[low] internal-function-used-once at src/HookFlags.sol:29: Internal Function Used Only Once\n[low] large-numeric-literal at src/LaunchHook.sol:51: Large Numeric Literal (3 places)\n[low] push-zero-opcode at src/HookFlags.sol:2: PUSH0 Opcode\n[low] unchecked-return at src/LaunchHook.sol:483: Unchecked Return\n[low] unused-public-function at src/LaunchHook.sol:197: Public Function Not Used Internally (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4bace423548b4f65cc74698feac577f4ab78839f8ed2108cd088dd41686848a3","verifiedTreeHash":"4a6502180eeb76ad8b17e53e8631d73543270a82","verifierVersion":"0.1.0+3dc93dc1"},{"checks":[{"durationMs":5152,"exitCode":0,"name":"build","output":"Compiling 95 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.03s\nCompiler run successful with warnings:\nWarning (5159): \"selfdestruct\" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created (see EIP-6780). Any use in newly deployed contracts is strongly discouraged even if the new behavior is taken into account. Future changes to the EVM might further reduce the functionality of the opcode.\n  --> test/LaunchHookInvariant.t.sol:23:9:\n   |\n23 |         selfdestruct(to);\n   |         ^^^^^^^^^^^^\n\n","passed":true},{"durationMs":6451,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 960.52µs (629.26µs CPU time)\n\nRan 1 test for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForFreshManagerTest\n[PASS] test_withdrawFor_redeemsClaimsOnDemand() (gas: 447457)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.00ms (527.25µs CPU time)\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519320)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 520029)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 523962)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 403445)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.33ms (1.76ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 20350415)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.97ms (15.65ms CPU time)\n\nRan 8 tests for test/LaunchHookPartialFill.t.sol:LaunchHookPartialFillTest\n[PASS] test_exactOutputSellBeyondPoolEth_reverts() (gas: 655420)\n[PASS] test_exactOutputSellMoreThanPoolHolds_reverts() (gas: 976912)\n[PASS] test_fullFillWithPriceLimitStillWorks() (gas: 329349)\n[PASS] test_partialExactInputBuy_afterWindow_reverts() (gas: 326125)\n[PASS] test_partialExactInputBuy_reverts() (gas: 321777)\n[PASS] test_partialExactInputSell_paysFeeOnExecutedEth() (gas: 556061)\n[PASS] test_partialExactOutputBuy_paysFeeOnExecutedEth() (gas: 329583)\n[PASS] test_partialExactOutputSell_priceLimit_reverts() (gas: 603449)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 24.02ms (15.19ms CPU time)\n\nRan 5 tests for test/LaunchHookPartialFill.t.sol:LaunchHookWithdrawForTest\n[PASS] test_withdrawFor_afterOwnPull_isEmpty() (gas: 430298)\n[PASS] test_withdrawFor_cannotRedirectCredit() (gas: 362763)\n[PASS] test_withdrawFor_deliversToRecipientWithFullGas() (gas: 1570491)\n[PASS] test_withdrawFor_nothingOwed_reverts() (gas: 61518)\n[PASS] test_withdrawFor_rejectingRecipient_revertsAndKeepsCredit() (gas: 482145)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 46.35ms (1.92ms CPU time)\n\nRan 7 tests for test/LaunchHookPayouts.t.sol:LaunchHookPayoutTest\n[PASS] testFuzz_splitFeeConservesAndHasBoundedRounding(uint128,uint128,uint16) (runs: 1000, μ: 26701, ~: 26989)\n[PASS] test_bothRejectingRecipientsAccumulateAndIndependentlyWithdraw() (gas: 824070)\n[PASS] test_failedClaimBackedPullRestoresClaimsAndBothCredits() (gas: 7698068)\n[PASS] test_payoutReentrancyCanOnlyFlushPreviouslyEarnedCredit() (gas: 3631577)\n[PASS] test_returnDataBombCannotBlockSwapOrLoseCredit() (gas: 3407156)\n[PASS] test_splitFeeZeroOneAndDevRoundingBoundaries() (gas: 24926)\n[PASS] test_withdrawReentrancyCannotSpendTheSameCreditTwice() (gas: 3435426)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 60.48ms (64.89ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389734)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197308)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130734)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 589311)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391909)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116468)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342458)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 348357)\n[PASS] test_transferOwnership() (gas: 110266)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 62.13ms (4.67ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344631, ~: 344562)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22657, ~: 22960)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 541206, ~: 541208)\n[PASS] test_buyAt1800_pays16Percent() (gas: 334215)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333855)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336729)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332283)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332540)\n[PASS] test_callbacksRefuseNonManager() (gas: 122808)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78683)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 233277)\n[PASS] test_feeSchedule_views() (gas: 91225)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1399222)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65834)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 190216)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 744079)\n[PASS] test_initializeWithoutEth_reverts() (gas: 744523)\n[PASS] test_permissionsMatchAddress() (gas: 12057)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365900)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535439)\n[PASS] test_secondInitialize_reverts() (gas: 86877)\n[PASS] test_sellExactInput_pays2Percent() (gas: 540716)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 541835)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 561753)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331752)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 83.36ms (131.87ms CPU time)\n\nRan 5 tests for test/LaunchHookIntegration.t.sol:LaunchHookIntegrationTest\n[PASS] testFuzz_exactOutputFeesUseActualEth(uint96,uint32,bool) (runs: 1000, μ: 603390, ~: 593886)\n[PASS] test_alternatingSwapTypesInOneUnlockHaveIndependentTransientFees() (gas: 1475408)\n[PASS] test_fourSwapTypesAtEveryScheduleBoundary() (gas: 8338583)\n[PASS] test_revertedSwapDoesNotContaminateLaterSwapInSameUnlock() (gas: 657247)\n[PASS] test_zeroSwapRevertsWithoutMovingMoney() (gas: 167942)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 83.28ms (97.68ms CPU time)\n\nRan 1 test for test/LaunchTokenInvariant.t.sol:LaunchTokenInvariantTest\n[PASS] invariant_supplyRemainsFixedAndEqualsEveryHolderBalance() (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------------+---------------------+-------+---------+----------╮\n| Contract           | Selector            | Calls | Reverts | Discards |\n+=======================================================================+\n| FixedSupplyHandler | approve             | 2739  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | attemptSupplyChange | 2711  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | overspend           | 2749  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | spend               | 2688  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | transfer            | 2753  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | unauthorizedSpend   | 2744  | 0       | 0        |\n╰--------------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.12s (1.12s CPU time)\n\nRan 1 test for test/LaunchHookInvariant.t.sol:LaunchHookInvariantTest\n[PASS]\nLaunchHookInvariantTest invariants:\n[PASS] invariant_feeCreditsAreFullyBackedAndConserved\n[PASS] invariant_feesStayLoweredAndThePoolRemainsBound\n[PASS] invariant_tokenSupplyAndEthBalancesAreConserved\n LaunchHookInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------------+------------------+-------+---------+----------╮\n| Contract             | Selector         | Calls | Reverts | Discards |\n+======================================================================+\n| FeeAccountingHandler | advanceTime      | 1325  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | attemptIncrease  | 1389  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | deliver          | 1306  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | donate           | 1425  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | limitedTrade     | 1309  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | lowerFees        | 1378  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | ownership        | 1411  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | pull             | 1376  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | redeem           | 1363  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | toggleRecipients | 1351  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | trade            | 1362  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | unauthorized     | 1389  | 0       | 0        |\n╰----------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.70s (3.70s CPU time)\n\nRan 10 tests for test/LaunchHookFork.t.sol:LaunchHookForkTest\n[PASS] testFork_adminCannotRaiseFeesAndStrangersCannotAdminister() (gas: 658382)\n[PASS] testFork_allFourSwapTypes_afterLaunch() (gas: 1552302)\n[PASS] testFork_allFourSwapTypes_at1800Seconds() (gas: 1552955)\n[PASS] testFork_allFourSwapTypes_at3600Seconds() (gas: 1552369)\n[PASS] testFork_allFourSwapTypes_atLaunch() (gas: 1553021)\n[PASS] testFork_factoryCanCollectAndRemoveLiquidityAfterRenunciation() (gas: 493749)\n[PASS] testFork_partialFills_afterWindow() (gas: 2693067)\n[PASS] testFork_partialFills_atLaunch() (gas: 2693990)\n[PASS] testFork_rejectingRecipientDoesNotBlockAnySwapTypeAndCanPull() (gas: 3869985)\n[PASS] testFork_withdrawForDeliversOnlyToTheRecipient() (gas: 3385537)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 6.29s (14.97s CPU time)\n\nRan 13 test suites in 6.30s (11.50s CPU time): 91 tests passed, 0 failed, 0 skipped (91 total tests)\n","passed":true},{"durationMs":75,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchHook.withdrawFor(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":217,\"foundry.toml\":24,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":535,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchHookFork.t.sol\":199,\"test/LaunchHookIntegration.t.sol\":92,\"test/LaunchHookInvariant.t.sol\":413,\"test/LaunchHookPartialFill.t.sol\":288,\"test/LaunchHookPayouts.t.sol\":216,\"test/LaunchToken.t.sol\":65,\"test/LaunchTokenInvariant.t.sol\":116,\"test/TESTING.md\":102,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":103,\"test/support/HookScenario.sol\":181,\"test/support/PartialFillRevert.sol\":33,\"test/support/SwapProbe.sol\":84},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4d0d0884bf201584d01884104a40bd85b1bd6747a734824e96afed20a2a4f658","verifiedTreeHash":"e14ee4c818691df0f5a3e3d382279825b0cb847e","verifierVersion":"0.1.0+3dc93dc1"},{"checks":[{"durationMs":4361,"exitCode":0,"name":"build","output":"Compiling 92 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.23s\nCompiler run successful with warnings:\nWarning (5159): \"selfdestruct\" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created (see EIP-6780). Any use in newly deployed contracts is strongly discouraged even if the new behavior is taken into account. Future changes to the EVM might further reduce the functionality of the opcode.\n  --> test/LaunchHookInvariant.t.sol:21:9:\n   |\n21 |         selfdestruct(to);\n   |         ^^^^^^^^^^^^\n\n","passed":true},{"durationMs":4716,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29219)\n[PASS] test_noMintPauseOrOwner() (gas: 153287)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 24700)\n[PASS] test_transferFromNeedsAllowance() (gas: 142183)\n[PASS] test_transferMovesExactly() (gas: 83527)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.59ms (1.24ms CPU time)\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployWithLaunchParams() (gas: 19242670)\n[PASS] test_launchParamsMatchBrief() (gas: 524)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 45.96ms (40.94ms CPU time)\n\nRan 13 tests for test/LaunchHookAdmin.t.sol:LaunchHookAdminTest\n[PASS] test_baseFeeCannotGoBelowDevShare() (gas: 389124)\n[PASS] test_launchFeeCannotGoBelowBaseFee() (gas: 80951)\n[PASS] test_loweringBaseBelowLaunchKeepsDecayValid() (gas: 97647)\n[PASS] test_noLiquidityOrMintOrPauseEntryPoints() (gas: 197264)\n[PASS] test_nonOwnerCannotSetFees() (gas: 130690)\n[PASS] test_ownerCanLowerBaseFee_andSwapsUseIt() (gas: 588435)\n[PASS] test_ownerCanLowerLaunchFee_andDecayStartsThere() (gas: 391321)\n[PASS] test_ownerCannotRaiseBaseFee() (gas: 116446)\n[PASS] test_ownerCannotRaiseLaunchFee() (gas: 113669)\n[PASS] test_ownerHasNoAccessToRecipientCredit() (gas: 342048)\n[PASS] test_recipientsAndFactoryAreNotOwners() (gas: 62379)\n[PASS] test_renounceOwnership_freezesFees() (gas: 347769)\n[PASS] test_transferOwnership() (gas: 110222)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 79.03ms (7.64ms CPU time)\n\nRan 28 tests for test/LaunchHook.t.sol:LaunchHookTest\n[PASS] testFuzz_buyExactInput_conservesEthAndSplitsCorrectly(uint256,uint256) (runs: 256, μ: 344039, ~: 343974)\n[PASS] testFuzz_buyFeeDecaysMonotonically(uint256,uint256) (runs: 256, μ: 22549, ~: 22581)\n[PASS] testFuzz_sellExactInput_conservesEthAndSplitsCorrectly(uint256) (runs: 256, μ: 540353, ~: 540354)\n[PASS] test_buyAt1800_pays16Percent() (gas: 333627)\n[PASS] test_buyAt3600_pays2Percent() (gas: 333267)\n[PASS] test_buyExactInput_t0_pays30Percent() (gas: 336141)\n[PASS] test_buyExactOutput_t0_pays30PercentOfGross() (gas: 332017)\n[PASS] test_buyLongAfterWindow_pays2Percent() (gas: 332274)\n[PASS] test_callbacksRefuseNonManager() (gas: 122764)\n[PASS] test_constructorRejectsWrongFlags() (gas: 78168)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 230663)\n[PASS] test_feeSchedule_views() (gas: 91203)\n[PASS] test_gasHungryVault_isCredited_andPullsWithFullGas() (gas: 1397845)\n[PASS] test_hookRefusesStrayEth() (gas: 33068)\n[PASS] test_immutablesAndDefaults() (gas: 65746)\n[PASS] test_initializeAcceptsAnyListedFeeAndSpacing() (gas: 189317)\n[PASS] test_initializeWithWrongToken_reverts() (gas: 743180)\n[PASS] test_initializeWithoutEth_reverts() (gas: 743624)\n[PASS] test_permissionsMatchAddress() (gas: 12145)\n[PASS] test_rejectingVault_canWithdrawToAnotherAddress() (gas: 365311)\n[PASS] test_rejectingVault_doesNotBlockSwap_andCanWithdraw() (gas: 535117)\n[PASS] test_secondInitialize_reverts() (gas: 86855)\n[PASS] test_sellExactInput_pays2Percent() (gas: 539862)\n[PASS] test_sellExactOutput_pays2PercentOfGross() (gas: 540830)\n[PASS] test_sellInsideLaunchWindow_stillPays2Percent() (gas: 560899)\n[PASS] test_strangerCannotTakeRecipientCredit() (gas: 331253)\n[PASS] test_withdraw_nothingOwed_reverts() (gas: 59764)\n[PASS] test_withdraw_toZero_reverts() (gas: 29788)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 79.94ms (132.00ms CPU time)\n\nRan 5 tests for test/LaunchHook.t.sol:LaunchHookFreshManagerTest\n[PASS] test_firstBuyOnFreshManager_mintsClaim_thenRedeems() (gas: 519088)\n[PASS] test_redeemClaims_keepsCreditForRejectingVault() (gas: 519619)\n[PASS] test_redeemClaims_withNothingPending_isNoop() (gas: 42937)\n[PASS] test_secondBuyOnFreshManager_paysDirectlyOnceEthIsIn() (gas: 522786)\n[PASS] test_withdrawRedeemsClaimsOnDemand() (gas: 402855)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 86.52ms (3.36ms CPU time)\n\nRan 7 tests for test/LaunchHookPayouts.t.sol:LaunchHookPayoutTest\n[PASS] testFuzz_splitFeeConservesAndHasBoundedRounding(uint128,uint128,uint16) (runs: 1000, μ: 26599, ~: 26498)\n[PASS] test_bothRejectingRecipientsAccumulateAndIndependentlyWithdraw() (gas: 823737)\n[PASS] test_failedClaimBackedPullRestoresClaimsAndBothCredits() (gas: 7694594)\n[PASS] test_payoutReentrancyCanOnlyFlushPreviouslyEarnedCredit() (gas: 3627760)\n[PASS] test_returnDataBombCannotBlockSwapOrLoseCredit() (gas: 3403593)\n[PASS] test_splitFeeZeroOneAndDevRoundingBoundaries() (gas: 24838)\n[PASS] test_withdrawReentrancyCannotSpendTheSameCreditTwice() (gas: 3431952)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 86.64ms (38.98ms CPU time)\n\nRan 5 tests for test/LaunchHookIntegration.t.sol:LaunchHookIntegrationTest\n[PASS] testFuzz_exactOutputFeesUseActualEth(uint96,uint32,bool) (runs: 1000, μ: 602895, ~: 593475)\n[PASS] test_alternatingSwapTypesInOneUnlockHaveIndependentTransientFees() (gas: 1473942)\n[PASS] test_fourSwapTypesAtEveryScheduleBoundary() (gas: 8332852)\n[PASS] test_revertedSwapDoesNotContaminateLaterSwapInSameUnlock() (gas: 657203)\n[PASS] test_zeroSwapRevertsWithoutMovingMoney() (gas: 167942)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 87.07ms (110.18ms CPU time)\n\nRan 1 test for test/LaunchTokenInvariant.t.sol:LaunchTokenInvariantTest\n[PASS] invariant_supplyRemainsFixedAndEqualsEveryHolderBalance() (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------------+---------------------+-------+---------+----------╮\n| Contract           | Selector            | Calls | Reverts | Discards |\n+=======================================================================+\n| FixedSupplyHandler | approve             | 2739  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | attemptSupplyChange | 2711  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | overspend           | 2749  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | spend               | 2688  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | transfer            | 2753  | 0       | 0        |\n|--------------------+---------------------+-------+---------+----------|\n| FixedSupplyHandler | unauthorizedSpend   | 2744  | 0       | 0        |\n╰--------------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.13s (1.13s CPU time)\n\nRan 1 test for test/LaunchHookInvariant.t.sol:LaunchHookInvariantTest\n[PASS]\nLaunchHookInvariantTest invariants:\n[PASS] invariant_feeCreditsAreFullyBackedAndConserved\n[PASS] invariant_feesStayLoweredAndThePoolRemainsBound\n[PASS] invariant_tokenSupplyAndEthBalancesAreConserved\n LaunchHookInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------------+------------------+-------+---------+----------╮\n| Contract             | Selector         | Calls | Reverts | Discards |\n+======================================================================+\n| FeeAccountingHandler | advanceTime      | 1601  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | attemptIncrease  | 1606  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | donate           | 1677  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | lowerFees        | 1656  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | ownership        | 1707  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | pull             | 1655  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | redeem           | 1594  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | toggleRecipients | 1702  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | trade            | 1628  | 0       | 0        |\n|----------------------+------------------+-------+---------+----------|\n| FeeAccountingHandler | unauthorized     | 1558  | 0       | 0        |\n╰----------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.36s (3.35s CPU time)\n\nRan 7 tests for test/LaunchHookFork.t.sol:LaunchHookForkTest\n[PASS] testFork_adminCannotRaiseFeesAndStrangersCannotAdminister() (gas: 657550)\n[PASS] testFork_allFourSwapTypes_afterLaunch() (gas: 1548791)\n[PASS] testFork_allFourSwapTypes_at1800Seconds() (gas: 1549533)\n[PASS] testFork_allFourSwapTypes_at3600Seconds() (gas: 1548836)\n[PASS] testFork_allFourSwapTypes_atLaunch() (gas: 1549488)\n[PASS] testFork_factoryCanCollectAndRemoveLiquidityAfterRenunciation() (gas: 493383)\n[PASS] testFork_rejectingRecipientDoesNotBlockAnySwapTypeAndCanPull() (gas: 3866314)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 4.54s (5.50s CPU time)\n\nRan 10 test suites in 4.55s (9.50s CPU time): 74 tests passed, 0 failed, 0 skipped (74 total tests)\n","passed":true},{"durationMs":105,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"LaunchHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LaunchHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LaunchHook.receive()\",\"LaunchHook.redeemClaims()\",\"LaunchHook.renounceOwnership()\",\"LaunchHook.setBaseFee(uint16)\",\"LaunchHook.setLaunchFee(uint16)\",\"LaunchHook.transferOwnership(address)\",\"LaunchHook.unlockCallback(bytes)\",\"LaunchHook.withdraw(address)\",\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":24,\"remappings.txt\":5,\"script/Deploy.s.sol\":69,\"src/HookFlags.sol\":55,\"src/LaunchHook.sol\":498,\"src/LaunchToken.sol\":17,\"test/Deploy.t.sol\":49,\"test/LaunchHook.t.sol\":487,\"test/LaunchHookAdmin.t.sol\":190,\"test/LaunchHookBase.sol\":118,\"test/LaunchHookFork.t.sol\":127,\"test/LaunchHookIntegration.t.sol\":92,\"test/LaunchHookInvariant.t.sol\":336,\"test/LaunchHookPayouts.t.sol\":216,\"test/LaunchToken.t.sol\":65,\"test/LaunchTokenInvariant.t.sol\":116,\"test/TESTING.md\":103,\"test/floor/OwnerChecks.sol\":24,\"test/mocks/MockERC20.sol\":15,\"test/mocks/Recipients.sol\":41,\"test/mocks/Swapper.sol\":96,\"test/support/HookScenario.sol\":146,\"test/support/SwapProbe.sol\":84},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c4313b831650251780d4a6ca37fe30bd27c6598b75c86f88c5f7ed23e720effa","verifiedTreeHash":"4d4ae913c7210d52576bd2eb43fad00d4d4b4ea8","verifierVersion":"0.1.0+3dc93dc1"}]}