{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"61b2582d-01da-4e15-8fd8-d397b3fac74d","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"4fdb02e0e97a21458d84743045653d98dac64da12698b71f49deb1494ac3dcdb","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":"55b8a01b3ad590a38d62ced741cd7d75fcc0ea36fffb8ccea2752036a30fd3ea","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":null,"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":"waiting"},{"acceptedSubmissionHash":"3048c5b7b4a32ab68adc32f94b31ad8b3ce181268c416f88fe159e3e7881b599","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":"62ddc11d9d1af12b03481b94b17b06a71a42432d4cd44a762e7ba7bbbfb27d21","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":"9fa16ee6665c34d66ff9c87fce3cbc58a0c821baa3ca6e904f668845c3a10f80","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"failed"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"ready"}],"objective":"{\n  \"objective\": \"Build Keel v2 smart contracts: permissionless memecoin launchpad on Uniswap v4. Foundry, Solidity 0.8.26, full tests. Do NOT deploy. v1 (github.com/forfun1111/keel-contracts) is style reference only. No IMD economics, basket, vouchers or build pulse.\\n\\n## Contracts\\n1. KeelV2Token: EIP-1167 clone. Name/symbol once. 1e9 supply, 18 decimals, minted to factory. Plain transfers, no restrictions, no owner, no mint, no tax. balanceAt(account, block) checkpoints on EVERY balance change (including PoolManager transfers during swaps). Test with swaps.\\n2. KeelV2Hook: one shared non-upgradeable v4 hook for all pools. Constructor(PoolManager, owner).\\n3. KeelV2Factory: permissionless launch in one tx.\\n4. KeelV2Takeover: community takeover of abandoned creator role.\\n\\n## Launch\\nlaunch(name, symbol, metadata, quote, startTick, feeBps):\\n- quote = ETH (address(0)) or ERC20 on owner allowlist. Fixed forever.\\n- startTick within owner bounds for that quote (price in quote units, no oracles).\\n- Clone token, create pool token/quote with hook, LP fee 0, add full supply as single-sided liquidity from startTick to max tick. Liquidity locked forever. Dust to 0x...dEaD.\\n- msg.sender = creator (gets no tokens).\\n- Emit Launched (metadata ≤1024 bytes).\\n- Owner can only pause new launches.\\n\\n## Fees (hook)\\n- Fee taken in QUOTE: from input on buys (beforeSwap + beforeSwapReturnDelta), from output on sells (afterSwap + afterSwapReturnDelta). Never sell meme inside swap, never convert.\\n- Exact-output reverts.\\n- Resting feeBps 100–500 (1–5%). Split 30% team / 70% creator. Fixed.\\n- Accrue in hook. claimCreator(token) / claimTeam(quote). No transfers inside swaps.\\n- Only factory may add liquidity. Nobody may remove it.\\n\\n## Fee changes\\n- Lower (min 100): immediate.\\n- Raise: scheduleRaise(newBps ≤ current+100 and ≤500). Applies after 24h via applyRaise. One pending. Cancelable. Events for site.\\n- No changes during decay.\\n\\n## Launch decay\\n- Starts at 9900 bps, falls linearly to feeBps over decaySeconds (owner-set, 30–600, default 120) from launch timestamp.\\n- Excess above feeBps → 100% team. Creator gets only 70% of resting part. No exemptions.\\n\\n## Creator\\n- Role = fee recipient + rate control. transferCreator by creator.\\n- lastActive updates on claimCreator, rate change, transferCreator, ping.\\n\\n## Takeover\\n- propose only if lastActive >30 days and proposer held ≥0.5% at previous block. One open proposal.\\n- Snapshot = proposal block-1. Vote 5 days. Weight = balanceAt(snapshot). Changeable.\\n- Circulating = totalSupply - PoolManager - 0x...dEaD at snapshot.\\n- Passes if yes>no AND yes≥10% circulating.\\n- 48h timelock, then execute moves creator role (old fees stay claimable by old creator).\\n- Any creator action during vote/timelock cancels. Failed/cancelled → 7 day cooldown.\\n- Only Takeover may call setCreatorByTakeover.\\n\\n## Owner powers (nothing more)\\n- Set team recipient; add/disable bases for NEW launches; set startTick bounds; set decaySeconds for NEW; pause launches.\\n- Cannot change existing token rate/split/creator/liquidity/fees. Two-step ownership.\\n- Allowlist rejects fee-on-transfer/rebasing (check received amounts). Support 6/8/18 decimals.\\n\\n## Security\\n- Hook flags match permissions. Mine salt in Deploy.s.sol (do not broadcast).\\n- Reentrancy-safe claims. CEI. Handle ETH + ERC20 via v4 settle/take.\\n- Solvency: for each quote, hook balance ≥ sum unclaimed fees.\\n- Funds leave hook ONLY via claimCreator/claimTeam (+swap settlement). No sweep/rescue/withdraw. No upgrade, selfdestruct, external delegatecall.\\n\\n## Tests (mainnet fork, real PoolManager)\\n- Launch ETH + 6-decimal base. Liquidity locked. Third-party add reverts.\\n- Fee math, 30/70, decay curve, excess to team.\\n- Rate lower/raise (limits, timing, cancel, apply). No changes during decay.\\n- Takeover: timing, balance, quorum, success, timelock, cancel, cooldown, post-snapshot votes ignored.\\n- Exact-output revert, pause, owner limits, claims, solvency fuzz.\\n\\n## Deliverables\\nsrc/, test/, script/Deploy.s.sol (dry-run only), README (architecture, params, events, owner powers, risks).\",\n  \"template\": \"impl_tests_review\",\n  \"references\": [\n    \"uniswap-v4-hooks\",\n    \"uniswap-v4-security\",\n    \"solidity-security-review\"\n  ],\n  \"github\": true\n}\n\nToken name: KKKK\nToken symbol: KKKKK","parentJobId":null,"planHash":"d91e93f747a84ec71db0179341e6ceed36dd1fc30faf649a369ea7b3747fd01b","previousHash":"6292a16fe8997a359154a58896a162b96dc3eb4b87112bb3fefeb5c3f5f6ea85","projectId":"61b2582d-01da-4e15-8fd8-d397b3fac74d","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2271-kkkk"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"54236","feedbackHash":"7631a0156826f38917af10420a131d8f274d8020750a2e3c82fe3cbae2d466a5","nodeKey":"audit_economics","submissionHash":"4fdb02e0e97a21458d84743045653d98dac64da12698b71f49deb1494ac3dcdb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"54234","feedbackHash":"44b3ec3c9c8fcd5c08f8ac5a5d85abe904e27ae27782881e685b20431f3250ab","nodeKey":"audit_flow","submissionHash":"55b8a01b3ad590a38d62ced741cd7d75fcc0ea36fffb8ccea2752036a30fd3ea","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51881","feedbackHash":"439462775c6bfe24cdf778f9bc882d729566d76e532e197a3242a0de2958a761","nodeKey":"audit_math","submissionHash":"3048c5b7b4a32ab68adc32f94b31ad8b3ce181268c416f88fe159e3e7881b599","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52135","feedbackHash":"6441cd0ed2e0c8b091dc856651d713341d676509355201f73a3eb517f43f9051","nodeKey":"audit_permissions","submissionHash":"62ddc11d9d1af12b03481b94b17b06a71a42432d4cd44a762e7ba7bbbfb27d21","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"89ef34a78208b0ebfbee12cabf8498f0c101fb605e4522aa4b5eaf908a710456","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1a7ecd03bd365366","findings":[{"citation":"resolved","description":"The factory validates the single-sided liquidity it is about to add only against int128.max (~1.7e38). The real bound is PoolManager's Pool.tickSpacingToMaxLiquidityPerTick(1) = type(uint128).max / 1774545 = 191757530477355301479181766273477 (~1.92e32), about 890,000x smaller. With the fixed 1e27 supply and an ETH (or any currency0) quote, L = supply * 2^96 / (sqrtPrice(-startTick) - sqrtPrice(MIN_TICK)) ~= 1e27 * 1.0001^(startTick/2); this exceeds the pool cap for startTick >= 243,292 while staying far below int128.max until startTick ~ 800,000. For every tick in that window, validateLaunch accepts the tick (if the owner's bounds include it), registerToken and initialize succeed, and the launch then reverts inside poolManager.modifyLiquidity with TickLiquidityOverflow(int24) (0xb8e3c385) instead of the factory's own InvalidLiquidity. README line 26 documents that such launches 'revert atomically', so there is no fund loss; the defect is that the factory's guard is set to a bound that cannot be the binding one and gives the creator / front-end an opaque PoolManager error for inputs the owner explicitly allowed. Minimal fix: compare liquidity against Pool.tickSpacingToMaxLiquidityPerTick(TICK_SPACING) (or a constant 191757530477355301479181766273477) in addition to int128.max, and/or have the owner bound check reject ticks whose liquidity exceeds the pool cap.","line":93,"path":"src/KeelV2Factory.sol","reproduction":"setUp: PoolManager, Deploy.dryRun(manager, admin); admin calls hook.configureBase(address(0), true, -887000, 887000). Then creator calls factory.launch('KKKK','KKKKK','', address(0), 243290, 300) -> succeeds. factory.launch(..., address(0), 243292, 300) -> reverts with 0xb8e3c385 (TickLiquidityOverflow) from PoolManager, not InvalidLiquidity; same for 243293, 243300, 245000, 300000. factory.launch(..., 800000, 300) reverts with 0x1fff9681 (InvalidLiquidity). Expected: a startTick inside the owner bounds either launches or is rejected by the factory's own guard; actual: int128.max guard passes and the pool rejects. Verified in test/scratch/Probe2.t.sol::testTickThreshold and Probe.t.sol::testHighTickLaunch.","severity":"low","snippet":"        if (liquidity == 0 || liquidity > uint256(uint128(type(int128).max))) revert InvalidLiquidity();","title":"Factory liquidity guard checks int128.max, not v4's per-tick liquidity cap, so owner-permitted start ticks above ~243,291 revert opaquely inside PoolManager"},{"citation":"resolved","description":"fee = floor(quoteAmount * bps / 10000) rounds down. Any buy whose gross input is below 10000/bps quote units pays zero fee: with the resting 100 bps, 99 units; during the first second of decay (9900 bps), a 1-wei buy pays 0 and receives exactly the same meme output as a 100-wei buy that paid 99 wei fee (both swap 1 wei through the pool). Zero-rounding per the Math Precision guide, confirmed with concrete numbers. Impact is nil in practice: each such swap costs >100k gas and yields ~4.8e8 meme wei on a 1e27 supply pool, so the fee avoided is orders of magnitude below gas. Reported for completeness; a round-up (mulDivRoundingUp) would close it at the cost of 1 wei per swap for the user.","line":301,"path":"src/KeelV2Hook.sol","reproduction":"Pool: ETH quote, startTick -200000, feeBps 300, same block as launch (currentFeeBps = 9900). alice buys with amountSpecified = -1 wei: hook.totalUnclaimed(address(0)) stays 0, alice receives 484680305 meme wei. alice then buys with -100 wei: fee 99 wei accrues, alice receives 484680305 meme wei again. Six-decimal quote at resting 100 bps: buy of 99 units accrues 0 fee and returns 23256584269034267303 meme wei. Verified in test/scratch/Probe.t.sol::testOneWeiBuyDuringDecay and testTinyBuyZeroFee.","severity":"info","snippet":"        fee = FullMath.mulDiv(quoteAmount, currentFeeBps(token), 10_000);","title":"Swap fee truncates to zero for inputs below 10_000/feeBps quote units (1-wei buys during the 99% decay pay nothing) - not economically exploitable"}],"hash":"3048c5b7b4a32ab68adc32f94b31ad8b3ce181268c416f88fe159e3e7881b599","nodeId":"40444a66-208b-4290-8aed-0a51f3a485ee","outcome":"completed","summary":"Review complete. The findings file is written and both snippets verify against the tree.\n\n**Outcome**\n\nThe assigned math area is clean of fund-affecting defects. Two reproducible edge notes were recorded in `.imd-findings.json`, neither of which loses funds:\n\n- **Low: factory liquidity guard checks the wrong bound.** The factory rejects liquidity only above int128.max, but PoolManager's per-tick cap for tick spacing 1 is about 890,000 times smaller. For an ETH quote, any start tick of 243,292 or higher that the owner's bounds allow passes validation, registers the token, initialises the pool, and then reverts inside PoolManager with TickLiquidityOverflow instead of the factory's own InvalidLiquidity. The README documents that such launches revert atomically, so this is a clarity defect, not a loss. Reproduced with ticks 243,290 (ok), 243,292 and 245,000 (pool revert), 800,000 (factory revert).\n- **Info: fee truncates to zero on dust inputs.** A 1 wei buy during the 99% decay pays no fee and receives exactly the same meme output as a 100 wei buy that paid 99 wei. Gas cost dwarfs the avoided fee by many orders of magnitude, so it is not exploitable.\n\n**What I traced and found to hold**\n\n- Decay curve: linear from 9900 bps to the resting rate, never below the resting rate, so team = fee minus creator share never underflows. Division by decaySeconds is safe since it is bounded 30 to 600 at registration.\n- Buy fee from input via beforeSwapReturnDelta and sell fee from output via afterSwapReturnDelta: signs and currency mapping verified through Hooks.beforeSwap and Hooks.afterSwap for both currency orderings. Fee is always strictly less than the input, so amountToSwap stays negative. The partial-fill check recomputes the identical fee at the same timestamp.\n- Launch liquidity: the formulas match getLiquidityForAmount0 and getLiquidityForAmount1. Rounding down on liquidity and rounding up on the pool's owed amount cannot exceed supply. Dust stays below 1e9 wei. Both tick-range orientations and the initial tick placement are correct.\n- Solvency: minted ERC-6909 equals accrued fee, fee equals creator plus team by construction, claims decrement both before redeeming. A sell past the liquidity edge, an int128.max-sized buy, and the fuzz suites all stayed solvent.\n- Takeover arithmetic: 0.5% threshold and 10% quorum are computed exactly with no rounding. Circulating supply cannot underflow. Snapshot weights are immutable, so vote changes subtract exactly. All timestamp boundaries are consistent between vote, finalize, execute, cooldown and the hook's decay gate.\n- Checkpoints: binary search returns the end-of-block balance, same-block writes coalesce, and the 1e27 supply fits the uint192 slot.\n\n**Coverage**\n\nAll 31 listed entry points have a row, plus two invariant rows. Two rows reference the findings above; the rest are marked holds with a one-line reason. Nothing was left unreached.\n\n**Not covered**\n\nThe mainnet fork suite is skipped in this environment because no fork URL is configured. Static-analysis leads (reentrancy-balance, unchecked-transfer, uninitialized-state) were checked and are false positives for this code: balance reads after external calls are the intended receipt checks, and the token's own transfer reverts on failure.","treeHash":null,"usage":{"cachedInputTokens":1592170,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":50503,"runtime":"claude","turns":31,"wallClockMs":917349}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1b657df861f7b08b","findings":[{"citation":"resolved","description":"Economic Security / Flow Gap (execution x first principles). The takeover is meant to let the community recover an abandoned creator role. The proposer is always the sole beneficiary (execute() hands the role to p.proposer), only one proposal may be open per token, and a failed proposal imposes a 7-day cooldown on the TOKEN (cooldownUntil[p.token] = voteEnd + COOLDOWN at line 134), not on the proposer. A holder G with the 0.5% proposal threshold can therefore re-propose themself the instant each cooldown expires, and a majority coalition that prefers candidate H must (a) win the ordering race for propose() at every cooldown expiry and (b) actively vote 'no' in every 5-day window forever. G only needs the majority to miss one window; the majority must win every round. When circulating supply is small (<= 5% of total supply), G's own 0.5%-of-supply holding already satisfies the 10% quorum, so a single missed window transfers 70% of all future resting fees to G. Even when G never wins, the role stays with the abandoned address and the creator share of every trade (0.7 x feeBps of volume) is permanently lost to all holders. Cost to G is gas only. This is within the letter of the brief (one open proposal; 7-day cooldown) but defeats its stated purpose of community recovery. Suggested fix preserving the design: make the cooldown apply to the proposer (or to proposer+token) rather than the token alone, and/or allow a proposal to name a candidate distinct from msg.sender so the community can run its own candidate in the same slot. Verification status: traced statically against propose()/finalize()/onCreatorActivity(); the sandbox could not compile the tree (via-IR build exceeds the 1.5 GB memory cgroup; non-IR hits stack-too-deep), so no Foundry run backs this sequence.","line":81,"path":"src/KeelV2Takeover.sol","reproduction":"State: token launched, creator inactive > 30 days. G holds 0.5% of supply (5e24) since block B-1; honest H holds 20% of circulating. (1) Block B, time T: G calls propose(token) -> id 1, voteEnd T+5d. (2) H votes no with more weight than G's yes; at T+5d finalize(1) returns false; cooldownUntil[token] = T+12d. (3) At T+12d both G and H submit propose(token) in the same block; G's tx is ordered first -> id 2 opens; H's reverts ProposalOpen (line 81). (4) Repeat steps 2-3 every 12 days. Expected: a majority coalition can install its candidate. Actual: H can never open a proposal while G keeps re-proposing; if H's coalition misses a single 5-day window on a low-circulation token (e.g. circulating 2e25 so G's 5e24 yes >= 10% quorum and no-votes < 5e24), finalize passes and execute() makes G the creator permanently.","severity":"low","snippet":"        if (openProposal[token] != 0) revert ProposalOpen();","title":"Single proposal slot plus per-token cooldown lets any 0.5% holder occupy takeover governance indefinitely"},{"citation":"resolved","description":"Economic Security note, recorded for the judge; it is the design the brief asked for (snapshot = proposal block - 1, weight = balanceAt(snapshot)) and README lists 'Concentrated holders may determine proposals'. Because both the proposer threshold and all voting weight are read at block N-1 and never re-checked, an actor can buy in block N-1, propose and vote yes in block N, and sell back in block N. The pool has LP fee 0 and symmetric concentrated liquidity, so a buy immediately reversed returns essentially the same quote minus the two hook fees; the only cost is ~2 x feeBps of the notional (2-10%) plus gas. For an abandoned token whose circulating supply is <= 5% of total supply, the 0.5%-of-supply proposer threshold alone satisfies the 10%-of-circulating quorum, so the attacker's own transient weight passes the vote unless remaining holders cast more 'no' weight within 5 days. After the 48h timelock execute() gives the attacker 70% of all future resting fees and rate control permanently. Mitigations that keep the design: require the proposer (not voters) to still hold the threshold at finalize/execute, or set snapshot a fixed number of blocks before the proposal.","line":83,"path":"src/KeelV2Takeover.sol","reproduction":"Token with circulating = 2e25 (2% of supply), creator inactive > 30 days, resting fee 300 bps. Block N-1: attacker A buys 5e24 tokens (0.5% of supply) from the pool for X quote, paying 3% fee. Block N: A calls propose(token) (balanceAt(A, N-1) = 5e24 >= supply/200 passes), vote(id, true) (weight 5e24), then sells 5e24 back receiving ~0.97 X. After 5 days with no-votes < 5e24: finalize -> passed (5e24 > no and 5e24*10 >= 2e25); 48h later execute(id) -> hook.setCreatorByTakeover(token, A). Expected by the brief's intent: a community holder recovers the role. Actual: an actor who held tokens for one block owns the creator role; total cost ~0.06 X.","severity":"info","snippet":"        uint256 snapshot = block.number - 1;","title":"Previous-block snapshot lets a two-block transient position propose and carry a takeover without holding tokens through the vote"}],"hash":"4fdb02e0e97a21458d84743045653d98dac64da12698b71f49deb1494ac3dcdb","nodeId":"15ab2806-c660-4b28-9bd6-b9b09c8c9803","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the results.\n\n**Outcome.** The economics of Keel v2 hold up under the Economic Security, Invariant and Flow Gap checklists. I found no fund-loss or accounting defect in the fee path, the claims, or the launch liquidity. Two takeover-governance issues are recorded, one low and one informational.\n\n**What I verified by tracing the code**\n\n- Buy fees come off the quote input through the before-swap delta, sell fees off the quote output through the after-swap delta, and the ERC-6909 claims minted inside each callback equal the hook delta Uniswap credits, so hook claims always back the unclaimed total.\n- The decay curve, the 70/30 split of the resting part, and the routing of all decay excess to the team match the brief, including rounding direction.\n- Liabilities are debited before any redeem, callbacks are one-use and hashed, and funds leave the hook only through the two claim functions.\n- Rate changes are blocked during decay, raises are capped at plus 100 bps per 24 hours, and pending raises are cleared on lowering or creator change.\n- Takeover timing, snapshot weights, quorum math, cancellation on creator activity, and cooldown behave as specified.\n\n**Findings reported**\n\n- **Low.** The single proposal slot plus a cooldown applied to the token lets any 0.5% holder re-propose themself at every cooldown expiry. The majority's candidate can never open a proposal and must vote no every round forever, while the griefer only needs one missed window on a low-circulation token.\n- **Info.** Because weight and the proposer threshold are read one block back and never rechecked, a two-block buy, propose, vote, sell sequence can carry a takeover for roughly two swap fees. This is the snapshot design the brief asked for and the README acknowledges it, so it is recorded as a note.\n\n**Coverage.** All 31 listed entry points have a row, plus four invariant rows. Two rows point at finding 1, the rest hold.\n\n**Limitation.** I could not execute any test in this sandbox. The via-IR build is killed at the 1.5 GB memory cgroup, and a non-IR build fails with stack-too-deep in the factory. The findings rest on static tracing, and the file says so.","treeHash":null,"usage":{"cachedInputTokens":2191720,"inputTokens":490,"model":"claude-fable-5-1","outputTokens":48572,"runtime":"claude","turns":34,"wallClockMs":2970148}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2042b7cb7b952e30","findings":[{"citation":"resolved","description":"propose() admits exactly one open proposal per token and the proposer is always the candidate (execute() hands the creator role to p.proposer). A failed or cancelled proposal imposes a 7-day cooldown that applies to every proposer, not only to the one whose proposal failed. A holder with >=0.5% of supply who does not want the creator role to move (for example the absent creator's own second wallet, or anyone profiting from fees staying unclaimed) can therefore occupy the slot forever: propose, vote no (or simply let quorum fail), wait 5 days, finalize (anyone may), wait out the 7-day cooldown, and propose again in the first block the cooldown ends, racing honest proposers. During the 5-day vote every other propose() reverts ProposalOpen; during the following 7 days it reverts Cooldown. Honest holders cannot nominate a different candidate on the open proposal because the candidate is fixed to the griefer, and voting 'yes' on it would install the griefer. The spec mandates one open proposal and a 7-day cooldown, so this is an emergent property of the requested design rather than a coding slip, but it defeats the stated purpose of KeelV2Takeover (recovering an abandoned creator role) at a cost of only 0.5% of supply held at one prior block. Mitigations that preserve the spec: scope the cooldown to the failed proposer (cooldownUntil[token][proposer]) or let voters elect a candidate other than the proposer.","line":81,"path":"src/KeelV2Takeover.sol","reproduction":"State: token launched, creator inactive for >30 days, griefer G holds >=0.5% of totalSupply at block N-1 (bought once), honest holder H holds 20% of circulating. Block N: G calls propose(token) -> id 1, voteEnd = now+5d. Same block or later: H calls propose(token) -> reverts ProposalOpen. G votes no (or nobody votes). At voteEnd anyone calls finalize(1) -> false, cooldownUntil[token] = voteEnd + 7d. H calls propose(token) before that time -> reverts Cooldown. At exactly cooldownUntil, G calls propose(token) again (front-running H in the same block) -> new open proposal; H's propose reverts ProposalOpen again. Expected: an honest 20% holder can put a candidate to a vote within a bounded time. Actual: H can never open a proposal while G repeats this 12-day cycle; the only vote H ever gets is for or against G. Reproduced locally (test/scratch/TakeoverGrief.t.sol on the offline KeelFixture: alice buys 1 ETH, bob 5 ETH; alice proposes; bob's propose reverts ProposalOpen; after 5 days finalize returns false; bob's propose reverts Cooldown; at cooldownUntil alice proposes again and bob's propose reverts ProposalOpen again).","severity":"low","snippet":"        if (openProposal[token] != 0) revert ProposalOpen();\n        if (block.timestamp < cooldownUntil[token]) revert Cooldown();","title":"Any 0.5% holder can indefinitely block community takeover by chaining self-proposals, failures and cooldowns"},{"citation":"resolved","description":"claimCreator() pays creatorFees[token][msg.sender] to msg.sender only; there is no recipient parameter and transferCreator() deliberately leaves already-accrued fees under the previous address. For ETH quotes _redeem -> unlockCallback -> poolManager.take(native, recipient, amount) performs a plain call to the recipient; for ERC20 quotes take() transfers to the recipient and the post-check requires the recipient's balance to grow by exactly amount. If the creator address is a contract without a payable receive/fallback (common for launches made from a contract wallet or a script), or an address the quote issuer blacklists (USDC), every claimCreator() call from that address reverts and the accrued amount stays in totalUnclaimed forever. transferCreator() does not help because the new creator only receives fees accrued after the transfer. The spec requires that old fees 'stay claimable by old creator', which this design implements, but it also makes the only exit path unusable for an entire class of legitimate creators. This is documented as a residual risk in REVIEW.md; it is reported so the author can decide whether a claimCreator(token, to) overload (still msg.sender-authorised) is in scope.","line":429,"path":"src/KeelV2Hook.sol","reproduction":"State: a contract C with no receive()/fallback (e.g. the Fees.t.sol ClaimReceiver with reject=true, or any plain contract) calls factory.launch(...) with quote=address(0), so tokens[token].creator == C. Alice buys 1 ETH of the token after decay: creatorFees[token][C] = 0.021 ETH. C calls hook.claimCreator(token) -> poolManager.take(ETH, C, 0.021e18) -> the native transfer to C reverts -> whole claim reverts (NativeTransferFailed). C calls hook.transferCreator(token, D) -> succeeds, but creatorFees[token][D] == 0 and creatorFees[token][C] is still 0.021 ETH; D.claimCreator(token) reverts NoFees. Expected: the creator role holder has some path to receive 0.021 ETH of fees it earned. Actual: 0.021 ETH remains in hook ERC-6909 claims indefinitely; totalUnclaimed(address(0)) stays >= 0.021e18 with no claimant able to redeem it.","severity":"low","snippet":"        if (amount != 0) _redeem(info.quote, msg.sender, amount);","title":"Creator fee accrual is bound to the creator address and can only be paid to that address, so a creator that cannot receive the quote loses its fees permanently"}],"hash":"55b8a01b3ad590a38d62ced741cd7d75fcc0ea36fffb8ccea2752036a30fd3ea","nodeId":"972702c1-e083-4650-baa3-8054df0f31ef","outcome":"completed","summary":"Review complete. The findings file is written and validated, and no tracked files were changed.\n\n**Outcome.** The control flow of Keel v2 holds on every value path I traced. The one shared v4 hook, the factory launch sequence, the authenticated unlock callbacks, the ERC-6909 fee custody, the fee-rate state machine and the takeover state machine all behave as the brief specifies. I found no defect that loses or misroutes funds. Two low-severity findings are in `.imd-findings.json`, both consequences of the requested design rather than coding slips:\n\n- **Takeover slot monopolisation.** One open proposal per token plus a token-wide 7-day cooldown lets any holder with 0.5% of supply at one prior block keep proposing themselves, letting the vote fail, and re-proposing the moment cooldown ends. Honest holders can never nominate another candidate. I reproduced the full cycle against the real PoolManager in an offline test. Suggested fix that preserves the spec: scope the cooldown to the failed proposer, or let voters elect a candidate other than the proposer.\n- **Creator fees bound to an unreachable address.** Claims pay only msg.sender and transferCreator leaves earlier accrual behind. A creator contract without a payable receive, or a USDC-blacklisted creator, loses its accrued fees permanently. REVIEW.md already lists this as a residual risk. A recipient-parameter overload would close it without changing authorisation.\n\n**Coverage.** All 31 listed entry points have a row, 29 marked holds and 2 marked finding, plus three invariant and periphery rows. The slither and aderyn leads on reentrancy, unchecked transfers, the encodePacked clone and the token history all checked out as false positives after tracing.\n\n**Verification limits.** This machine caps memory at 1.5 GB, so the via-IR build of the whole suite was killed. I compiled the sources and test files one at a time. Four of the eight existing test files ran and passed, 29 tests in total, and my reproduction ran. The Token, Launch, Fees and Mainnet test files did not finish compiling within the budget, so I did not re-execute them. The mainnet fork claims in TESTING.md are unverified here.","treeHash":null,"usage":{"cachedInputTokens":2881041,"inputTokens":772,"model":"claude-fable-5-1","outputTokens":50185,"runtime":"claude","turns":41,"wallClockMs":3627386}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b97527597dd934a0","findings":[{"citation":"resolved","description":"Trust gap (access x asymmetry). propose() is open to any holder of 0.5% of supply at the previous block, only one proposal may be open per token, and a failed proposal writes cooldownUntil[token] (per token, not per proposer). The proposer who causes a failure pays nothing and is not excluded, while every other holder is excluded for voteEnd + 7 days. A holder of 0.5% of supply (trivially cheap for an abandoned memecoin) can therefore propose first in the block the inactivity window or the previous cooldown opens, let the proposal fail for lack of votes, and repeat every 12 days (5 days voting + 7 days cooldown). Holders with far more than the 10% quorum never get to put their own candidate forward; their only option is to vote YES on the griefer and hand the creator role (70% of resting fees forever) to the griefer. The creator stays inactive throughout, so the abandonment-recovery mechanism the takeover exists for is defeated by the smallest eligible holder. Because propose() is first-come in a block, the same asymmetry also makes the candidate selection a pure mempool race at every cooldown expiry. The brief says 'Failed/cancelled -> 7 day cooldown' but does not say the cooldown should bind proposers other than the one who failed; a per-proposer cooldown (or a cooldown only on cancelled proposals, which the creator causes) preserves the intended behavior while removing the lockout.","line":132,"path":"src/KeelV2Takeover.sol","reproduction":"State: token launched, creator never acts for 30 days. alice holds ~30% of circulating, bob ~15%, carol exactly totalSupply()/200 (0.5%) at block N-1. Sequence (verified in test/scratch/Griefing.t.sol, which passes on the current code): (1) at lastActive+30d+1, carol calls takeover.propose(token) -> id 1; (2) alice calls takeover.propose(token) -> reverts ProposalOpen; (3) nobody votes; at voteEnd anyone calls finalize(1) -> returns false, status Failed, cooldownUntil[token] = voteEnd + 7 days; (4) alice calls propose(token) -> reverts Cooldown; (5) at cooldownUntil, carol proposes again in the same block, before alice; (6) repeat. After three cycles (36 days) hook.tokenInfo(token).creator is still the inactive creator and alice has never been able to open a proposal. Expected: a holder above quorum can put a candidate forward once the creator is inactive; a failed proposal should at most penalize its own proposer. Actual: one 0.5% holder blocks all takeovers for as long as they care to keep proposing.","severity":"low","snippet":"            p.status = Status.Failed;\n            openProposal[p.token] = 0;\n            cooldownUntil[p.token] = uint256(p.voteEnd) + COOLDOWN;","title":"Failed takeover proposal puts the whole token in cooldown, so any 0.5% holder can lock out every other proposer indefinitely"},{"citation":"resolved","description":"Deployment/configuration gap, not a code defect. The constructor validates its own address against the seven hook flags, as Uniswap v4 requires, and the flags are correct (REQUIRED_FLAGS == 0x2acc matches getHookPermissions). The dry-run script mines a salt for its own simulation-only CREATE2 executor, but the real deployer is the project factory, whose address and salt differ, and the manifest schema has no salt field. Unless the launch service mines a CREATE2 salt for the real factory address and the exact creation bytes (hook creation code ++ abi.encode(poolManager, owner)) such that the resulting address has low 14 bits 0x2acc, the constructor reverts with Hooks.HookAddressNotValid and the whole launch transaction fails. Factory and Takeover are created inside the hook constructor, so all four application contracts are lost together. Evidence needed from the service: the salt it will use for KeelV2Hook and the predicted address (Deploy.mine(realFactory, manager, owner, 0, N) computes both without deploying).","line":107,"path":"src/KeelV2Hook.sol","reproduction":"In any test: `new KeelV2Hook(IPoolManager(makeAddr(\"manager\")), makeAddr(\"owner\"))` from a plain CREATE reverts (test/Deployment.t.sol testBadFlagAddressFailsConstructor already shows this). Same bytes deployed via CREATE2 with a salt from Deploy.mine(deployer, manager, owner, 0, 1_000_000) for the actual deployer address succeed. Expected: the launch service's salt yields an address with (uint160(addr) & 0x3fff) == 0x2acc. Actual if it does not: constructor reverts, nothing is deployed.","severity":"info","snippet":"        Hooks.validateHookPermissions(IHooks(address(this)), getHookPermissions());","title":"KeelV2Hook can only be constructed at an address whose low 14 bits equal 0x2acc; deployment through a factory that does not mine that salt reverts and parks the launch"}],"hash":"62ddc11d9d1af12b03481b94b17b06a71a42432d4cd44a762e7ba7bbbfb27d21","nodeId":"6ce34f84-3dfe-4ca7-a7a4-5989155da592","outcome":"completed","summary":"The review is complete. The findings file is valid JSON, the quoted snippets match the tree, and no repository file was changed (the scratch test lives in the gitignored `test/scratch/`).\n\n**Outcome**\n\nThe permission model of Keel v2 holds under every guard trace in my area. I report one low finding and one informational deployment gap. No critical, high or medium defect was substantiated, so no proof test files were attached.\n\n**What I covered**\n\n- **Access Control:** mapped every role (owner, pending owner, factory, PoolManager, creator, takeover contract) and every modifier or inline check across the four contracts. Every storage variable has consistent guards on all writers. Clone initialization is locked on the implementation and single-use on clones. Callbacks are authenticated by caller plus a one-use hash. No confused-deputy path lets a third party spend the owner's factory approval or make the factory add liquidity.\n- **Asymmetry:** compared buy/sell fee branches, ETH/ERC20 claim branches, `transferCreator` against `setCreatorByTakeover` (shared `_changeCreator`), `lowerFee` against `scheduleRaise`/`applyRaise`, and the failed-versus-cancelled proposal paths. The one asymmetry with consequence is the per-token cooldown below.\n- **Trust Gap:** checked each admin setter against in-flight value. `setTeamRecipient` redirects unclaimed team fees, which is the requested owner power and already documented. Creator changes keep old accrual with the old address. Reentrancy from an ETH claim recipient into swaps, claims, liquidity and takeover execution is blocked by the hook guard, the PoolManager lock and the factory-only check.\n\n**Findings**\n\n1. **Low, KeelV2Takeover.finalize/propose:** a failed proposal writes `cooldownUntil[token]`, binding every proposer, while any 0.5% holder may propose first in the block a window opens. A scratch test confirms one such holder locks out a 30% holder through three consecutive 12-day cycles with the creator still inactive. The judge should weigh that the brief literally asks for a 7-day cooldown on failure.\n2. **Info, KeelV2Hook constructor:** the hook only constructs at an address whose low 14 bits equal `0x2acc`. The launch service must mine the CREATE2 salt for the real project factory, or the constructor reverts and all four contracts fail to deploy.\n\n**Coverage record**\n\nAll 31 listed entry points have a row. Two are marked `finding` (the takeover `propose` and `finalize`), the rest `holds`, plus three invariant rows for fund exits, owner limits and locked liquidity. Nothing was left `unreached`.\n\n**Not reached in depth**\n\nFee arithmetic, liquidity math and the v4 delta accounting were read for severity triage only, since they belong to the other specialists. The mainnet fork test was not rerun here.","treeHash":null,"usage":{"cachedInputTokens":1494438,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":47001,"runtime":"claude","turns":34,"wallClockMs":905000}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7b7c27166477ddd8","findings":[],"hash":"71dd285fb44874a60dfa793c2f6527b1b5a7eab2d0ebc2f80a8a5697eb43c303","nodeId":"03ae4f40-78e8-48cf-8efb-2fe181cdef9c","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/… before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":10,"wallClockMs":1390063}},{"artifacts":[],"attempt":1,"bundleHash":"286825765a1b2bdec56863294b93f0c641886e64931f7672dac2f58a0cc239d4","device":"c7b1db4d25939bd5","findings":[],"hash":"9fa16ee6665c34d66ff9c87fce3cbc58a0c821baa3ca6e904f668845c3a10f80","nodeId":"bd708cba-28f6-449b-b780-c90c2e69ff25","outcome":"completed","summary":"Implemented all four contracts, vendored dependencies, tests, documentation, and dry-run deployment.\n\n- Clean build and formatting checks pass.\n- Offline: 54 tests pass; one fork-only test skipped.\n- Mainnet fork: all 55 tests pass.\n- Dry-run script passes. Nothing deployed or broadcast.\n\nDetails: [README](README.md) and [verification results](TESTING.md).","treeHash":"2774dd03352f54c5b987ae2fec8e2858289181b8","usage":{"cachedInputTokens":8430208,"inputTokens":181742,"model":"gpt-6-astra","outputTokens":81750,"runtime":"codex","turns":16,"wallClockMs":2360223}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6960837f8e61d811","findings":[],"hash":"cfb5c494f381649863c9d4bcf94514154895d4896a16e49c224a2b97e9b4bdc1","nodeId":"af7b860d-df10-49ea-a536-ca7869cebc36","outcome":"failed","summary":"needs input: The accepted implementation cannot be integrated through launch.json alone. KeelV2Hook validates that its address has low 14 bits 0x2acc, but the evm_contracts manifest cannot select a mined CREATE2 salt. None of the accepted application contracts can deploy the hook later. KeelV2DryRunDeployer is a simulation helper whose controller is constructor msg.sender, so deploying it through the project factory would leave the owner unable to use it. A production application deployer with an explicit owner argument and a function accepting an off-chain-mined salt requires a source change outside this assignment's launch.json-only scope. The existing manifest and accepted source have been left unchanged. — Can this task return to implementation to add and test a production hook deployer controlled by the explicit owner, then resume the evm_contracts manifest assignment?","treeHash":null,"usage":{"cachedInputTokens":999168,"inputTokens":93087,"model":"gpt-6-astra","outputTokens":8360,"runtime":"codex","turns":8,"wallClockMs":418622}}],"verification":[{"checks":[{"durationMs":70274,"exitCode":0,"name":"build","output":"Compiling 84 files with Solc 0.8.26\nSolc 0.8.26 finished in 70.18s\nCompiler run successful!\n","passed":true},{"durationMs":534,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Mainnet.t.sol:MainnetTest\n[SKIP] testMainnetUSDCLaunchSwapCheckpointAndClaims() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 13.46ms (28.98µs CPU time)\n\nRan 11 tests for test/Takeover.t.sol:TakeoverTest\n[PASS] testClaimCancelsTimelock() (gas: 1752726)\n[PASS] testCreatorPingCancelsVotingAndStartsCooldown() (gas: 1731990)\n[PASS] testFormerCreatorClaimDoesNotCancelNewCreatorTakeover() (gas: 1101166)\n[PASS] testFuzzCreatorActionsCancel(uint8,bool) (runs: 256, μ: 1576435, ~: 1505483)\nLogs:\n  Bound result 1\n\n[PASS] testNobodyElseCanCancelOrReplaceCreator() (gas: 1511344)\n[PASS] testPreviousBlockHoldingRequiredAndExactThresholdAllowed() (gas: 1551165)\n[PASS] testQuorumFailureAndSevenDayCooldown() (gas: 2055114)\n[PASS] testSuccessfulTakeoverTimelockAndOldFeesRemainClaimable() (gas: 2430828)\n[PASS] testTiedVoteFailsEvenWithQuorum() (gas: 1791227)\n[PASS] testTimingHoldingAndOnlyOneOpenProposal() (gas: 1534809)\n[PASS] testVotesChangeWithoutDoubleCountingAndIgnorePostSnapshotTransfers() (gas: 2449484)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 131.71ms (143.44ms CPU time)\n\nRan 7 tests for test/CreatorAndOwner.t.sol:CreatorAndOwnerTest\n[PASS] testActivityUpdatesOnAllCreatorActions() (gas: 467976)\n[PASS] testLowerImmediateAndRaiseLimitsTimingApplyCancel() (gas: 822476)\n[PASS] testMaximumRaiseAndPendingInvalidation() (gas: 624814)\n[PASS] testNoRateChangesDuringDecay() (gas: 213296)\n[PASS] testOnlyCreatorMayControlRoleAndRate() (gas: 347507)\n[PASS] testOwnerAccessBoundsAndTwoStepOwnership() (gas: 472827)\n[PASS] testOwnerChangesOnlyNewLaunchConfiguration() (gas: 2753052)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 163.72ms (7.19ms CPU time)\n\nRan 4 tests for test/Token.t.sol:TokenTest\n[PASS] testERC20FailureCasesAndHistoricalLookupBoundaries() (gas: 134844)\n[PASS] testEverySwapUpdatesManagerAndTraderCheckpoints() (gas: 1143486)\n[PASS] testFuzzHistoricalBalances(uint256) (runs: 256, μ: 3692171, ~: 3691246)\n[PASS] testTransferTransferFromSelfZeroAndSameBlockCoalescing() (gas: 1124778)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 163.69ms (138.15ms CPU time)\n\nRan 7 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testCallbackAuthenticationAndNoClaimOperatorApproval() (gas: 646497)\n[PASS] testFuzzERC20SolvencyAcrossRatesAndClaims(uint256) (runs: 256, μ: 6534364, ~: 6550355)\n[PASS] testFuzzFullRoundTripNeverMintsOrLosesCheckpoints(uint96) (runs: 256, μ: 2180649, ~: 760575)\nLogs:\n  Bound result 10309\n\n[PASS] testLaunchAndFeeEventsContainIndexableData() (gas: 1131513)\n[PASS] testOneOpenProposalPerTokenNotGlobally() (gas: 2586572)\n[PASS] testOwnerCreatorAndTeamHaveNoFeeExemption() (gas: 692518)\n[PASS] testQuorumExactlyTenPercentPassesAndOneUnitBelowFails() (gas: 1713232)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 320.04ms (527.59ms CPU time)\n\nRan 12 tests for test/Fees.t.sol:FeesTest\n[PASS] testClaimsAndMultiplePoolsSameQuote() (gas: 2323132)\n[PASS] testCreatorTransferPreservesOldAccrual() (gas: 1385147)\n[PASS] testERC20ClaimsCheckReceiptAndRollbackWhenTokenChangesBehavior() (gas: 1889814)\n[PASS] testExactOutputRevertsBothDirections() (gas: 714880)\n[PASS] testFuzzDecayAndFeeRounding(uint16,uint16,uint96) (runs: 256, μ: 1750778, ~: 1210051)\nLogs:\n  Bound result 444\n  Bound result 400\n  Bound result 99999999999999996863\n\n[PASS] testFuzzSolvencySequence(uint256) (runs: 256, μ: 8987639, ~: 8984323)\n[PASS] testLaunchDecayCurveAndExcessTeamOnly() (gas: 1266682)\n[PASS] testNoFeeTransferInsideSwapCallbacks() (gas: 1301514)\n[PASS] testPartialBuyRevertsWithoutAccruingFee() (gas: 313954)\n[PASS] testReentrantClaimCannotDoubleSpendAndFailedPaymentRetainsClaim() (gas: 1252733)\n[PASS] testRestingBuyAndSellQuoteFeeAndSplit() (gas: 1246579)\n[PASS] testSellDuringDecayExcessGoesToTeam() (gas: 720967)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 439.08ms (595.69ms CPU time)\n\nRan 4 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] testBadFlagAddressFailsConstructor() (gas: 7141)\n[PASS] testDryRunMinesAndBindsComponentsWithEmptyExternalChain() (gas: 31641587)\n[PASS] testMissingAddressesFailConstructors() (gas: 4111)\n[PASS] testSaltMiningLongSearchUsesBoundedMemory() (gas: 14329633)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 439.14ms (61.74ms CPU time)\n\nRan 9 tests for test/Launch.t.sol:LaunchTest\n[PASS] testAllowlistRejectsTaxRebaseFalseAndUnsupportedDecimals() (gas: 3205377)\n[PASS] testBothSortedCurrencyOrientationsAndDecimalSupport() (gas: 7206041)\n[PASS] testCannotInitializeUnregisteredPoolOrCallCallbacksDirectly() (gas: 129033)\n[PASS] testImplementationAndCloneCannotReinitialize() (gas: 83929)\n[PASS] testLaunchNativeIsSingleSidedAndLocked() (gas: 179190)\n[PASS] testLaunchSixDecimalQuoteAndSwapBothWays() (gas: 1688940)\n[PASS] testMetadataFeeQuoteAndTickValidation() (gas: 1274207)\n[PASS] testNonReturningERC20IsSupported() (gas: 1753239)\n[PASS] testThirdPartyLiquidityAndAllRemovalRevert() (gas: 161309)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 439.16ms (10.00ms CPU time)\n\nRan 8 test suites in 440.79ms (2.11s CPU time): 54 tests passed, 0 failed, 1 skipped (55 total tests)\n","passed":true},{"durationMs":50,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelV2Factory.launch(string,string,bytes,address,int24,uint16)\",\"KeelV2Factory.probeQuote(address,address,uint256)\",\"KeelV2Factory.unlockCallback(bytes)\",\"KeelV2Hook.acceptOwnership()\",\"KeelV2Hook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"KeelV2Hook.applyRaise(address)\",\"KeelV2Hook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"KeelV2Hook.cancelRaise(address)\",\"KeelV2Hook.claimCreator(address)\",\"KeelV2Hook.claimTeam(address)\",\"KeelV2Hook.configureBase(address,bool,int24,int24)\",\"KeelV2Hook.lowerFee(address,uint16)\",\"KeelV2Hook.ping(address)\",\"KeelV2Hook.registerToken(address,address,address,uint16,(address,address,uint24,int24,address))\",\"KeelV2Hook.scheduleRaise(address,uint16)\",\"KeelV2Hook.setCreatorByTakeover(address,address)\",\"KeelV2Hook.setDecaySeconds(uint16)\",\"KeelV2Hook.setLaunchesPaused(bool)\",\"KeelV2Hook.setTeamRecipient(address)\",\"KeelV2Hook.transferCreator(address,address)\",\"KeelV2Hook.transferOwnership(address)\",\"KeelV2Hook.unlockCallback(bytes)\",\"KeelV2Takeover.execute(uint256)\",\"KeelV2Takeover.finalize(uint256)\",\"KeelV2Takeover.onCreatorActivity(address)\",\"KeelV2Takeover.propose(address)\",\"KeelV2Takeover.vote(uint256,bool)\",\"KeelV2Token.approve(address,uint256)\",\"KeelV2Token.initialize(string,string)\",\"KeelV2Token.transfer(address,uint256)\",\"KeelV2Token.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"DEPENDENCIES.md\":7,\"README.md\":138,\"REVIEW.md\":44,\"TESTING.md\":57,\"foundry.toml\":18,\"remappings.txt\":3,\"script/Deploy.s.sol\":80,\"src/KeelV2Factory.sol\":182,\"src/KeelV2Hook.sol\":478,\"src/KeelV2Takeover.sol\":161,\"src/KeelV2Token.sol\":116,\"src/interfaces/IKeelV2.sol\":36,\"src/libraries/ExactTransfer.sol\":36,\"src/libraries/Guard.sol\":14,\"test/CreatorAndOwner.t.sol\":188,\"test/Deployment.t.sol\":82,\"test/EdgeCases.t.sol\":150,\"test/Fees.t.sol\":250,\"test/Launch.t.sol\":169,\"test/Mainnet.t.sol\":56,\"test/Takeover.t.sol\":273,\"test/Token.t.sol\":95,\"test/helpers/KeelFixture.sol\":159,\"test/helpers/MockQuote.sol\":64,\"test/helpers/Router.sol\":67},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5171,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/KeelV2Hook.sol:456: Reentrancy in KeelV2Hook.unlockCallback(bytes) (src/KeelV2Hook.sol#456-477):\n[high/medium] reentrancy-balance at src/libraries/ExactTransfer.sol:24: Reentrancy in ExactTransfer.transfer(address,address,uint256) (src/libraries/ExactTransfer.sol#24-35):\n[high/medium] reentrancy-balance at src/libraries/ExactTransfer.sol:24: Reentrancy in ExactTransfer.transfer(address,address,uint256) (src/libraries/ExactTransfer.sol#24-35):\n[high/medium] reentrancy-balance at src/libraries/ExactTransfer.sol:11: Reentrancy in ExactTransfer.transferFrom(address,address,address,uint256) (src/libraries/ExactTransfer.sol#11-22):\n[high/medium] reentrancy-balance at src/KeelV2Hook.sol:456: Reentrancy in KeelV2Hook.unlockCallback(bytes) (src/KeelV2Hook.sol#456-477):\n[high/medium] reentrancy-balance at src/libraries/ExactTransfer.sol:11: Reentrancy in ExactTransfer.transferFrom(address,address,address,uint256) (src/libraries/ExactTransfer.sol#11-22):\n[high/medium] unchecked-transfer at src/KeelV2Factory.sol:145: KeelV2Factory.unlockCallback(bytes) (src/KeelV2Factory.sol#145-181) ignores return value by KeelV2Token(token).transfer(address(poolManager),owed) (src/KeelV2Factory.sol#173)\n[high/medium] unchecked-transfer at src/KeelV2Factory.sol:65: KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116) ignores return value by KeelV2Token(token).transfer(DEAD,dust) (src/KeelV2Factory.sol#102)\n[high/high] uninitialized-state at src/KeelV2Token.sol:88: KeelV2Token.history (src/KeelV2Token.sol#30) is never initialized. It is used in:\n[medium/high] incorrect-equality at src/KeelV2Token.sol:88: KeelV2Token._checkpoint(address) (src/KeelV2Token.sol#88-97) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/KeelV2Hook.sol:172: Reentrancy in KeelV2Hook.configureBase(address,bool,int24,int24) (src/KeelV2Hook.sol#172-193):\n[medium/medium] uninitialized-local at src/KeelV2Token.sol:103: KeelV2Token.balanceAt(address,uint256).low (src/KeelV2Token.sol#103) is a local variable never initialized\n[medium/medium] unused-return at src/KeelV2Factory.sol:65: KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116) ignores return value by poolManager.initialize(key,sqrtUpper) (src/KeelV2Factory.sol#96)\n[medium/medium] unused-return at src/KeelV2Factory.sol:145: KeelV2Factory.unlockCallback(bytes) (src/KeelV2Factory.sol#145-181) ignores return value by (delta,None) = poolManager.modifyLiquidity(info.key,IPoolManager.ModifyLiquidityParams({tickLower:info.tickLower,tickUpper:info.tickUpper,liquidityDelta:int256(uint256(info.liquidity)),salt:bytes32(0)}),) (src/KeelV2Factory.sol#155-164)\n[medium/medium] unused-return at src/KeelV2Hook.sol:449: KeelV2Hook._redeem(address,address,uint256) (src/KeelV2Hook.sol#449-454) ignores return value by poolManager.unlock(data) (src/KeelV2Hook.sol#452)\n[medium/medium] unused-return at src/KeelV2Factory.sol:65: KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116) ignores return value by hook.validateLaunch(quote,startTick,feeBps) (src/KeelV2Factory.sol#74)\n[medium/medium] unused-return at src/KeelV2Factory.sol:65: KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116) ignores return value by poolManager.initialize(key,sqrtLower) (src/KeelV2Factory.sol#96)\n[medium/medium] unused-return at src/KeelV2Factory.sol:65: KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116) ignores return value by poolManager.unlock(data) (src/KeelV2Factory.sol#99)\n[low/medium] reentrancy-benign at src/KeelV2Factory.sol:65: Reentrancy in KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116):\n[low/medium] reentrancy-benign at src/KeelV2Factory.sol:65: Reentrancy in KeelV2Factory.launch(string,string,bytes,address,int24,uint16) (src/KeelV2Factory.sol#65-116):\n[low/medium] reentrancy-events at src/KeelV2Takeover.sol:139: Reentrancy in KeelV2Takeover.execute(uint256) (src/KeelV2Takeover.sol#139-148):\n[low/medium] timestamp at src/KeelV2Hook.sol:277: KeelV2Hook.afterSwap(address,PoolKey,IPoolManager.SwapParams,BalanceDelta,bytes) (src/KeelV2Hook.sol#277-297) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Hook.sol:299: KeelV2Hook._accrue(address,uint256) (src/KeelV2Hook.sol#299-310) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Hook.sol:219: KeelV2Hook.currentFeeBps(address) (src/KeelV2Hook.sol#219-225) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Takeover.sol:139: KeelV2Takeover.execute(uint256) (src/KeelV2Takeover.sol#139-148) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Takeover.sol:106: KeelV2Takeover.vote(uint256,bool) (src/KeelV2Takeover.sol#106-120) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Takeover.sol:123: KeelV2Takeover.finalize(uint256) (src/KeelV2Takeover.sol#123-137) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Hook.sol:404: KeelV2Hook._rateAuthority(address) (src/KeelV2Hook.sol#404-407) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Hook.sol:204: KeelV2Hook.registerToken(address,address,address,uint16,PoolKey) (src/KeelV2Hook.sol#204-213) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Hook.sol:339: KeelV2Hook.applyRaise(address) (src/KeelV2Hook.sol#339-350) uses timestamp for comparisons\n[low/medium] timestamp at src/KeelV2Takeover.sol:77: KeelV2Takeover.propose(address) (src/KeelV2Takeover.sol#77-104) uses timestamp for comparisons","passed":true},{"durationMs":412,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/KeelV2Factory.sol:126: `abi.encodePacked()` Hash Collision\n[high] reentrancy-state-change at src/KeelV2Factory.sol:74: Reentrancy: State change after external call (13 places)\n[low] centralization-risk at src/KeelV2Hook.sol:136: Centralization Risk (5 places)\n[low] large-numeric-literal at src/KeelV2Hook.sol:290: Large Numeric Literal (4 places)\n[low] literal-instead-of-constant at src/KeelV2Factory.sol:91: Literal Instead of Constant (9 places)\n[low] missing-inheritance at src/KeelV2Takeover.sol:8: Missing Inheritance (2 places)\n[low] non-reentrant-not-first at src/KeelV2Hook.sol:150: `nonReentrant` is Not the First Modifier (4 places)\n[low] state-change-without-event at src/KeelV2Factory.sol:119: State Change Without Event (4 places)\n[low] unchecked-return at src/KeelV2Factory.sol:74: Unchecked Return (7 places)\n[low] unsafe-erc20-operation at src/KeelV2Factory.sol:102: Unsafe ERC20 Operation (5 places)\n[low] unused-public-function at src/KeelV2Hook.sol:415: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9fa16ee6665c34d66ff9c87fce3cbc58a0c821baa3ca6e904f668845c3a10f80","verifiedTreeHash":"2774dd03352f54c5b987ae2fec8e2858289181b8","verifierVersion":"0.1.0+6a374fcd"}]}