{"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":"98dbc132-2f46-4991-ba25-abdd397da90f","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"2db0d6c3a654de3c02707837c6c8dd92b60b205c8c23adb07c1afaa4e7307beb","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":"671b0a5f0e520b454298392d3772c489efef4f6590cda67c712c12d569071186","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":"13f0f5f76f512a38a19d0a2c30004e28335d3d5f1b09ff4deb7ee13301f27078","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":"82b79b8400a5022c5e73cec547b62e0991405a60b5201acc4896ad0a06f0964b","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":"ebb0adf569b5bbfba619a784687ad468abcfb924a1f8129faed399a4fb7bd54a","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":"f358a0b4c45be9e2e2a38af5ec644a289abbd9bf6da9db1da8bec4be2b64edd6","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"failed"},{"acceptedSubmissionHash":"a9d70288472dc27bd38703d8d71fa729d7c6d446b813a3a5c7387434fc31778f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"waiting"},{"acceptedSubmissionHash":"84421ef9cbf9951add31d11ba0b80a0ec56fcb7f2abc3c85b2406ffff1e1bd4b","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"waiting"}],"objective":"Build and deploy UNLOCK (UNLK) as a TWO-STAGE Sepolia-only experiment: chainId 11155111, univ4_hook, ETH pair, poolBps 8700. No mainnet deployment or website. Use accepted code/tests from job 6a7439e9-e9aa-469b-bdb5-633bd6c7f423; baseline https://github.com/identity-md-launches/launch-1309-unlk commit eb37eb6d1985670dfd2dc691c34a0c879c885b5a. Resolve/pin the latest accepted artifacts; do not silently discard later fixes or rebuild from scratch.\n\nFIX: Remove router_, rewards_ and oracle_ from the hook constructor and launch manifest. Accept only supported launch inputs and verified static settings. Do not use dummy addresses. Adding activate() while still requiring these addresses at construction does not fix the blocker. Companion addresses are dependencies, not trader exemptions.\n\nBEFORE DEPLOYING: Build/test both stages. Verify Sepolia factory, registry, distributor, PoolManager, feed and IMD policy. Identify the exact supported service and authorized actor for companion deployment AND the final setup transaction. Do not assume IMD supports companion slots or post-deployment calls. Rehearse end to end on a recorded-block fork; local impersonation needs no private key. If stage 2 has no executable route, do not deploy stage 1: deliver code and identify the missing platform operation, without asking for nonexistent addresses.\n\nSTAGE 1: The authorized factory creates token, hook and native reward distributor and seeds the official pool with this hook in its PoolKey from initialization. Authenticate PoolManager/factory, bind the pool once and permit required initial liquidity operations. Initial state is UNCONFIGURED: swaps, credit synchronization and milestone progression revert. Ordinary token transfers and eligible native claims remain available. Never add or replace the pool hook later.\n\nSTAGE 2: Using actual stage-1 identities, deploy UnlkSwapRouter, MerkleClaimEvidence and UnlkOracle. Bind them immutably to this hook, or use authenticated one-time reciprocal setup in a tested order. An immutable setup authority then calls configureAndActivate(router,rewards,oracle,launchNumber), or an equivalent atomic operation. Verify reviewed implementations, deployed code, matching manager/token/pool/quote, factory registry record, distributor, oracle settings and reciprocal bindings. Prove the initial allocation recipient from authoritative launch evidence, not current balance alone. Reject unauthorized, wrong-contract, replayed and cross-launch setup. Failed activation changes no hook state. Successful activation seals all configuration permanently. No upgrades, later pausing, address replacement, counter resets, arbitrary calls, trader exemptions or admin withdrawal.\n\nWhen the hook acts: authenticate initialization; before every swap require activation and the authenticated router/trader; after every swap enforce quotas and record actual token deltas atomically. Enforce principal protection on liquidity operations. Charge no extra hook fee. Preserve the official 1.25% pool fee, split 1% payer and 0.25% IMD.\n\nPreserve UNLOCK/UNLK, 1 billion tokens, 18 decimals, minted once to the factory; final allocation 10% swarm, 87% pool, 3% payer. Transfers have no token-level tax/quota/blacklist/pause. Preserve verified factory principal custody.\n\nPer wallet E counts authenticated official-pool buys, proven initial allocation and actual native claims; S counts executed sales. Before full unlock, Q=max(0,floor(E*p/10000)-S); sales cannot exceed Q. Use overflow-safe executed-delta/fee accounting; failed swaps change nothing. Transfers/donations/liquidity operations create no rights, and wallets cannot consume others' quotas. Verify reward proofs, exact delivered amounts and deduplication, including claims made before activation. Confirm one leaf per recipient per round against the actual tree if required by the adapter. Native claims retain existing eligibility/release rules and never depend on credit-sync success.\n\nRates: initially 25%; verified USD FDV $100K:35%, $250K:45%, $500K:55%, $1M:70%, $2.5M:85%. At $5M permanently bypass quotas for everyone, including transfer-only holders; retain fees/authentication. Price falls never relock. Preserve reviewed time-weighted token pricing and Chainlink ETH/USD validation; invalid data blocks new tiers only. Do not silently change thresholds or oracle settings.\n\nValidate deployment without companions, inactive swaps, native claims/transfers before activation, secure setup/sealing, claim/sync/sale, payer attribution, buy/sell limits, bypass attempts, fees, liquidity and milestones. Reproduce existing audit findings, including sustained-price manipulation and the 10 ETH depth-floor/first-tier conflict; report unresolved issues and respect platform review requirements. Test against actual protected admission tests, without weakening them. Correct obsolete deployment instructions.\n\nDeliver pinned source, both stages' manifests/scripts, exact test results, actual Sepolia addresses/transactions, PoolKey/PoolId, ABIs, verification status, sealed-setup evidence and repeatable trade/claim tests. Unexecuted work must be labeled. Stage 1 alone means \"pool deployed, trading inactive\", not a successful launch. No fabricated addresses, mainnet transactions or production-safety claims.","parentJobId":null,"planHash":"3a74d2f643cfaf9de0f20d54209f135f3fa6dfaf7b435c8295cdfe6daba964ce","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"98dbc132-2f46-4991-ba25-abdd397da90f","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1401-build-deploy-unlock-unlk"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52468","feedbackHash":"27566b68f3efb338d4d15c87a785b7689d7dd4d02ab8742fa468034ad02ad6c7","nodeKey":"audit_economics","submissionHash":"2db0d6c3a654de3c02707837c6c8dd92b60b205c8c23adb07c1afaa4e7307beb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52290","feedbackHash":"c199e11737acdb3788ced13891957677aadac8157b0c849728a8ad7f35d8372b","nodeKey":"audit_flow","submissionHash":"671b0a5f0e520b454298392d3772c489efef4f6590cda67c712c12d569071186","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52678","feedbackHash":"f0b3731ebf5b4a4bf1d54dbd0dbc025a82b1fc0599190f62d951c6803b13a360","nodeKey":"audit_judge","submissionHash":"13f0f5f76f512a38a19d0a2c30004e28335d3d5f1b09ff4deb7ee13301f27078","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52481","feedbackHash":"7e58adc20e45123a49e26f9003535c30d0eaa36922558ed98d5caf9e51c60009","nodeKey":"audit_judge","submissionHash":"6a5d29f30617958cb8a576c4858ef1193a1919f0652b128a143ab85477edde88","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51485","feedbackHash":"87662cb67e0106dd22c065333cd74ba3a378fbb83f6be46e191c47afd36b686f","nodeKey":"audit_math","submissionHash":"82b79b8400a5022c5e73cec547b62e0991405a60b5201acc4896ad0a06f0964b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51382","feedbackHash":"ac30fc3d0634b604cb172e1c55d492cbbb41ea7ebd3880757f9d031e059948c8","nodeKey":"audit_permissions","submissionHash":"ebb0adf569b5bbfba619a784687ad468abcfb924a1f8129faed399a4fb7bd54a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52220","feedbackHash":"a4fe16c1c731553e33031f3cd5c3a0e3bb58e89a8f0dbea279a61a35847ec329","nodeKey":"build_contract_project","submissionHash":"cfb09656835a244bd3a8c89b115ad830eac808e56e7543bf94fb78e88132d14f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52128","feedbackHash":"93d43a0ed384447149bebf9a88bb608f9d258fe129d8d3a43e9b0d3c255a7900","nodeKey":"build_contract_project","submissionHash":"f358a0b4c45be9e2e2a38af5ec644a289abbd9bf6da9db1da8bec4be2b64edd6","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51225","feedbackHash":"419d2b6793423712aa57db420bd9215f0d543077e417886cd3a5617a67c4b45d","nodeKey":"manifest","submissionHash":"a9d70288472dc27bd38703d8d71fa729d7c6d446b813a3a5c7387434fc31778f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52552","feedbackHash":"ecf97faea955dd4122ab407bb13ba3e06e248dba6d7d22287b6fa16d9702f98d","nodeKey":"manifest","submissionHash":"93d0f85cf9aca9dccebe5ab30e7a40dd0d5e31a932f68746bd0c688595efaa7c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51494","feedbackHash":"5391048bf0d06e96f17c82b404c46d87f83734f9dfd527dd2772922c99e8d1b6","nodeKey":"write_foundry_tests","submissionHash":"9abb951f77a35640dfabcf7a8b0de9abd3e791d10c47afe6a2aa730ae2238ec8","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52323","feedbackHash":"c32088e21f4c13ba73e5552c94735825f9669755ab223768bfaa607f7b899ec6","nodeKey":"write_foundry_tests","submissionHash":"84421ef9cbf9951add31d11ba0b80a0ec56fcb7f2abc3c85b2406ffff1e1bd4b","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"55f55f7ec72b14a37b4b44d953186b4ab985e672647392a9f531a2a6968c7d05","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"03332db135fe4c15","findings":[{"citation":"resolved","description":"Settled from round 1 (id a52eab30): the author reproduced it, changed no code, and documented it as README known issue 1 with the gap quantified at $2,000 and $2,555 per ETH, the ETH price above which the conflict exists (about $1,460) and three one-constant resolutions. I agree with the author's dispute that the choice is the requester's and that the brief forbids changing thresholds or oracle settings silently; the finding is kept because the promised guarantee (35% at a verified $100K FDV) still cannot hold at today's ETH price, and the launch should not proceed until the requester picks an option. UnlkOracle.checkpoint marks an observation liquid only if the ETH principal in the seed range is >= MIN_QUOTE_DEPTH (src/UnlkOracle.sol:102 `bool liquid = sqrtPrice != 0 && liquidity >= MIN_LIQUIDITY && depth >= MIN_QUOTE_DEPTH`), twapTick reverts on an illiquid last observation or any illiquid second in the window (src/UnlkOracle.sol:129, 143), and UnlkHook._updateMilestone then takes its catch branch and returns false. With the factory's 870M token-only seed at normalized tick -174960 and ETH/USD $2,000, about 9.5 ETH of buys lifts spot FDV above $100K while the seed holds under 10 ETH, so the first tier is unobservable for all FDV between $100K and roughly $137K. Not a loss of funds; a promised guarantee that cannot hold. Resolution options (requester): lower MIN_QUOTE_DEPTH, raise the first threshold, or a USD-denominated floor, each with the matching pin in UnlkHook._verifyOracle (src/UnlkHook.sol:286).","line":21,"path":"src/UnlkOracle.sol","reproduction":"Existing test test/TwoStage.t.sol::testDepthFloorBlocksFirstTierAtSpotAbove100k (passes in the suite, demonstrating the defect): activate, _buy(bob, 9.5 ether); assert spot FDV >= 100_000e18 and seed depth < 10 ether; run two full 90-minute confirmation cycles (_confirm() twice: updateMilestone every 5 minutes). Expected per brief: tier() == 1, p == 3500. Actual: tier() == 0 and oracle.fdvUsd() reverts InvalidOracle. Re-run here on this code: PASS (defect present). My scratch test (test/scratch/DipNearFloor.t.sol) also logs depth 10.27 ETH and FDV $108,738 after a 10.4 ETH buy at $2,000/ETH, i.e. the floor is only crossed well above the first threshold.","severity":"medium","snippet":"    uint256 public constant MIN_QUOTE_DEPTH = 10 ether;","title":"The 10 ETH quote-depth floor conflicts with the $100K first tier: with the token-only seed, FDV passes $100K while the seed holds under 10 ETH, so every observation is illiquid, fdvUsd reverts and tie"},{"citation":"resolved","description":"Settled from round 1 (id 18a69b14): the author reproduced it, changed no code, and documented it as README known issue 2 with the measured cost (about 206 ETH deployed, about 37 ETH of quota-limited counter-sales absorbed, about 203 ETH recovered, roughly 40 ETH net), why arbitrage cannot cap it, and three mitigations with their effect on honest paths. I agree that each mitigation changes the unlock schedule for honest price discovery and so needs the requester's decision; the finding is kept because the quota scheme the brief centres on remains removable by one funded actor at a cost that does not scale with the manipulated price. Mechanism, unchanged: every tier above the current one gets its qualifyingSince in the same _updateMilestone call (src/UnlkHook.sol:499-508), so a 30-minute TWAP window plus one 60-minute confirmation with calls <= 5 minutes apart confirms tiers 1..6 together and sets fullyUnlocked permanently (line 512; 'price falls never relock'). Only wallets with eligible credit can sell into the spike, each at most p = 25% of E (src/UnlkHook.sol:338), and transfer-only holders cannot sell at all, so counter-selling is bounded by the sum of existing quotas, smallest right after activation. The fix to the catch branch (round-1 finding 9b6dd839) does not change this cost: the attacker never needed an invalid-data path.","line":512,"path":"src/UnlkHook.sol","reproduction":"Existing test test/Oracle.t.sol::testSustainedManipulationRequiresWindowAndConfirmation (passes, demonstrating): activated system with >= 10 ETH depth, _buy(alice, 500 ether); _advance(30 minutes); updateMilestone() -> tierForFdv(oracle.fdvUsd()) == 6, fullyUnlocked() false; then 11 x (_advance(5 minutes); updateMilestone()) -> still false; one more -> fullyUnlocked() == true, remainingQuota(any) == type(uint256).max, alice may sell all 500 ETH worth of tokens back. Expected per brief: a permanent unlock should not be purchasable by one funded actor at the cost of LP fees plus other holders' capped quotas. Re-run here on this code: PASS (defect present).","severity":"medium","snippet":"            if (tier == 6) fullyUnlocked = true;","title":"Sustained-price manipulation buys a permanent quota bypass for every holder; the arbitrage that is supposed to make it costly is itself capped by the quota rule (unresolved from round 1; scope decisio"},{"citation":"resolved","description":"The round-1 fix holds for what it claims: the catch branch in UnlkHook._updateMilestone now only returns false (src/UnlkHook.sol:491-495) and the failing calls neither erase qualifyingSince nor touch lastConfirmation (verified: five keeper calls during the blackout left both unchanged). But the price-based trigger I merged into that finding is only partly closed. twapTick requires zero illiquid seconds inside the 30-minute window (line 143) and an observation is illiquid when seed depth < MIN_QUOTE_DEPTH (src/UnlkOracle.sol:102). While depth sits just above 10 ETH, which is the normal state while the first tier is being confirmed (finding 1), any holder with quota can sell a small amount that pulls depth under 10 ETH, which afterSwap's checkpoint records as liquid == false, then buy back 12 seconds later. That single illiquid block makes fdvUsd() revert for exactly 30 minutes. Because a failed read is not an observation, lastConfirmation stays at the last good call, the 30-minute gap exceeds MAX_CONFIRMATION_GAP = 5 minutes (src/UnlkHook.sol:497 `if (block.timestamp - lastConfirmation > MAX_CONFIRMATION_GAP) delete qualifyingSince;`), and the first valid call restarts every tier's hour. Cost to the griefer: two 1.25% LP fees on about 0.3 ETH of round trip (under 0.01 ETH) per 90 minutes of delay; it needs pre-existing eligibility of roughly four times the depth margin above the floor, so it is limited to the near-floor regime of finding 1 and to actors who already bought. Not a loss of funds, and the brief's 'invalid data blocks new tiers only' is arguably met for the failing call itself, but the hour restarting because of one block of bad data is the same outcome the round-1 finding described, reached through the gap rule instead of the catch branch. Mitigation inside the design: tolerate a bounded number of illiquid seconds per window (for example reject only if endBad - startBad exceeds a small fraction of WINDOW), or do not count an invalid stretch shorter than MAX_CONFIRMATION_GAP plus WINDOW as unobserved time. Either is a change to a reviewed oracle rule and needs the requester's sign-off; it is reported here so it is decided together with finding 1 rather than discovered after launch.","line":143,"path":"src/UnlkOracle.sol","reproduction":"SystemFixture (test/fixtures/SystemFixture.sol), activated, feed $2,000. alice buys 10.4 ETH (depth 10.27 ETH >= 10, spot FDV $108,738). _advance(30 minutes); oracle.checkpoint(); hook.updateMilestone() at t0 -> qualifyingSince(0) == t0. _advance(5 minutes); updateMilestone() -> unchanged. alice sells 3% of her tokens (within her 25% quota): depth 9.83 ETH (< 10). _advance(12); alice withdrawNative() and buys the proceeds back: depth 10.26 ETH. Then updateMilestone() every 5 minutes for 25 minutes: each call leaves qualifyingSince(0) == t0 and lastConfirmation unchanged (fix 1 holds). At t0 + 5 min + 12 s + 30 min (first valid TWAP) updateMilestone(): lastConfirmation advances, qualifyingSince(0) == 1800003912 > t0 == 1800001800 (restarted), tier() == 0. Expected per brief: a one-block dip delays at most one observation; actual: 30 minutes without milestone data and the hour restarts. Run: test/scratch/DipNearFloor.t.sol::testOneBlockDipStillRestartsConfirmationViaGapRule (a demonstration test: it passes on this code, showing the restart; source summarized above, built on SystemFixture with _buy/_sell/_advance helpers).","severity":"low","snippet":"        if (endBad != startBad) revert InvalidOracle();","title":"A one-block depth dip near the 10 ETH floor still restarts a pending confirmation: one illiquid second invalidates the TWAP for the next 30 minutes, longer than MAX_CONFIRMATION_GAP, so the first vali"},{"citation":"resolved","description":"Reproduced the tester's finding (write_foundry_tests ac2017e7) with its attached proof on this code: FAIL 'the adapter credits more than the distributor delivered: 500000000000000000000 != 100000000000000000000'. attest proves a claim by the distributor's boolean claimed[round][account] flag (line 51) plus a Merkle proof of (account, amount) against the round's root (lines 52-54). The verified MerkleDistributor stores no amount, so when a round's tree holds two leaves for one account the wallet can natively claim the smaller leaf (setting the flag) and attest the larger one; hook.syncRewards then adds the larger amount to E. The adapter cannot distinguish the two leaves on chain; the distributor's Round.claimed total is not per account. So there is no code fix within the design, which is why this is recorded as information: the brief's 'confirm one leaf per recipient per round against the actual tree if required by the adapter' is exactly the off-chain check the operator must run on IMD's tree before stage 2, and README known issue 4 states the assumption. Over-credit is bounded by the tree's largest duplicate leaf and the SWARM_ALLOCATION cap in pendingRewardCredit (src/UnlkHook.sol:448).","line":56,"path":"src/MerkleClaimEvidence.sol","reproduction":"Open a round whose tree is [(alice, 100e18), (alice, 500e18)]. distributor.claim(0, alice, 100e18, [leaf(alice,500e18)]): alice receives 100e18, claimed[0][alice] = true. evidence.attest(0, alice, 500e18, [leaf(alice,100e18)]). Expected: refused or not counted, evidence.claimed(alice) == 100e18. Actual: accepted, evidence.claimed(alice) == 500e18 while alice holds 100e18; hook.syncRewards(alice) would credit 500e18 of E. Run: copy .imd/reads/proofs/Proof_ac2017e77bf9.t.sol to test/scratch/ and forge test --match-path test/scratch/Proof_ac2017e77bf9.t.sol -> FAIL as quoted (run here).","severity":"info","snippet":"        claimed[account] += amount;","title":"Claim evidence accepts a second leaf for the same wallet in one round, crediting more than the distributor delivered (unenforceable on chain; operational check on IMD's tree required, as the brief say"},{"citation":"resolved","description":"Settled from round 1 (id 2142eaf0): the author reproduced it, kept the fail-closed design, and recorded it as README known issue 8 with the exact failing state, what keeps working (transfers, native claims), the alternative (tolerate protocolFee != 0 while pinning lpFee) and its cost, plus the Sepolia check that protocolFeeController() == address(0) at block 11898691. beforeSwap refuses every swap once getSlot0(poolId).protocolFee != 0, and UnlkOracle.checkpoint (src/UnlkOracle.sol:102-103) marks every observation illiquid under the same condition; the hook is sealed with no admin. The trust assumption is on Uniswap's protocol-fee controller, an external actor. Recorded as information so the dependency stays visible; no code change requested.","line":310,"path":"src/UnlkHook.sol","reproduction":"Existing test test/Adversarial.t.sol::testProtocolFeeChangeCannotSilentlyAlterTradingEconomics (passes): SystemFixture after activation; manager.setProtocolFeeController(address(this)); manager.setProtocolFee(key, 500). A router buy reverts UnlkHook.InvalidPool from beforeSwap; eligible stays 0; an ERC-20 transfer still succeeds. Expected per brief: sale within quota succeeds at the 1.25% LP fee; actual: no official-pool trade is possible from then on and no function can restore it.","severity":"info","snippet":"        if (protocolFee != 0 || lpFee != FEE) revert InvalidPool();","title":"A PoolManager protocol fee on the official pool halts every swap permanently, with no recovery path (accepted fail-closed dependency, now documented; unchanged from round 1)"}],"hash":"13f0f5f76f512a38a19d0a2c30004e28335d3d5f1b09ff4deb7ee13301f27078","nodeId":"30ed11ed-2e12-4673-be39-7f4fe03d3f90","outcome":"completed","summary":"The review is complete and `.imd-findings.json` is written. No tracked file was changed.\n\n**Settled from round 1**\n\n- **Catch-branch reset (high):** fixed. The catch now only returns false, and the feed age is 90 minutes in both the oracle and the hook pin. I confirmed an 89-minute-old round is accepted and a 91-minute one refused, and that failing calls leave both the pending confirmation and the last observation untouched.\n- **Payer credit followed the fee-share requester (medium):** fixed. Activation now also requires the recorded requester to hold the full 30M allocation. My attached proof passes by its refusal branch, and the author's two new tests cover the reassignment and moved-balance cases.\n- **Obsolete Sepolia factory table (low):** fixed in the README and a new Stage 0 section of the deployment doc.\n- **Companion identity info:** the missing checkpoint-age pin was added; the rest stays a documented one-time trust assumption on the setup authority.\n\n**Kept, unchanged by the author's own account**\n\n- **Depth floor vs first tier (medium)** and **sustained manipulation bypass (medium).** Both reproduce with the repository's own tests. The author's position that each needs a requester decision is correct, and I say so in the findings. They stay open so the requester decides before any deployment.\n- **Protocol-fee halt:** downgraded to info, now explicitly documented as an accepted fail-closed dependency.\n\n**New this round**\n\n- **One low finding.** Near the 10 ETH floor, a one-block depth dip still restarts a pending confirmation. The erasure is gone, but one illiquid second invalidates the TWAP for 30 minutes, which exceeds the 5-minute gap, so the first valid call restarts the hour. Demonstrated with a scratch test on the system fixture.\n- **One info finding** carrying the tester's duplicate-leaf proof, which fails on this code. It is unenforceable on chain and matches the operational tree check the brief already requires.\n\n**Coverage:** all 23 entry points answered, plus rows for the manifest and each settled round-1 finding. Full suite result on this tree: 125 of 126 pass, the single failure being the tester's duplicate-leaf proof copied into scratch.","treeHash":null,"usage":{"cachedInputTokens":2473495,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":34252,"runtime":"claude","turns":37,"wallClockMs":592707}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c9743cd9807c4c3a","findings":[{"citation":"resolved","description":"UnlkOracle._ethUsd rejects a feed round older than MAX_FEED_AGE = 3600 s (src/UnlkOracle.sol:24,173). The hook's _updateMilestone wraps oracle.fdvUsd() in try/catch and, on ANY revert, executes `delete qualifyingSince; lastConfirmation = 0;` (src/UnlkHook.sol:480-481), erasing all pending confirmation progress instead of merely skipping this observation. On Sepolia the published ETH/USD proxy 0x694AA1769357215DE4FAC081bf1f309aDC325306 (aggregator 0x719E22E3D4b690E5d96cCb40619180B5427F14AE) lands a new round every 3624-3636 s (rounds 36698-36737, read 2026-10-12: updatedAt deltas alternate 3624/3636, one 2760), i.e. heartbeat 3600 s plus 24-36 s of transmission latency. So for 2-3 Sepolia blocks of every hour `block.timestamp - updatedAt > 3600` holds and fdvUsd() reverts. Any unprivileged caller who sends hook.updateMilestone() in that window (or any ordinary swap landing there, since afterSwap also calls _updateMilestone) wipes qualifyingSince for all tiers. Because a tier needs 60 minutes of continuous qualification with <=5 minute gaps and the stale window recurs every ~60.5 minutes, one transaction per hour is enough to hold every holder at p = 2500 forever and to make the $5M permanent bypass unreachable, whatever the real FDV is. The brief says 'invalid data blocks new tiers only'; this implementation turns a transient 30-second data gap into a full restart, which an attacker can trigger on schedule at the cost of gas. The same reset is reachable without an attacker: a keeper on a 5-minute cadence has ~10% chance per hour of landing a call in the window. The existing test testInvalidDataResetsUnfinishedConfirmation asserts the reset as intended behaviour and would need to change with the fix. Fix options that keep the design: (a) in the catch branch only return false (do not touch qualifyingSince or lastConfirmation), letting the <=5 minute gap rule on the next valid call decide whether progress survives; or (b) set MAX_FEED_AGE above the feed's real cadence (e.g. 2 hours, or heartbeat + grace) so a healthy feed is never classed stale. The proof passes with either.","line":477,"path":"src/UnlkHook.sol","reproduction":"State: activated hook, pool with >=10 ETH depth, feed answer $2000 (FDV >= $100K observed), oracle warmed 30 min. Honest keeper calls updateMilestone() at t0 and every 5 min up to t0+55min: qualifyingSince[0] == t0, tier == 0. At t0+55min+1s the latest feed round is 3630 s old (the normal Sepolia state once an hour); attacker (any EOA) calls hook.updateMilestone(). Expected: tier progress untouched (data is merely unavailable for ~30 s). Actual: fdvUsd() reverts with InvalidOracle, catch runs `delete qualifyingSince; lastConfirmation = 0`, qualifyingSince[0] becomes 0. The heartbeat round lands 20 s later; the keeper calls updateMilestone() at t0+60min. Expected tier == 1, p == 3500. Actual tier == 0, p == 2500 (qualifyingSince[0] restarted at t0+60min). Repeat once per hour: no tier ever confirms. Run: forge test --match-path test/scratch/StaleWindowProof.t.sol  -> FAIL '0 != 1' on current code; PASS when the catch branch no longer deletes progress.","severity":"medium","snippet":"        try oracle.fdvUsd() returns (uint256 value) {\n            fdv = value;\n        } catch {\n            delete qualifyingSince;\n            lastConfirmation = 0;\n            return false;\n        }","title":"Hourly Chainlink stale window plus the catch-branch reset lets anyone keep every unlock tier from ever confirming"},{"citation":"resolved","description":"Reproduces and quantifies README known issue 2 in my area (Economic Security). The README states the manipulation cost is 'capital at risk against arbitrage'. But the only wallets that can sell into a spike are those with eligible credit, and each may sell at most p = 25% of E; transfer-only holders cannot sell at all (src/UnlkHook.sol:326). So the arbitrage that is meant to make a 90-minute excursion expensive is capped by the sum of existing quotas, which is smallest right after activation (payer 7.5M tokens, plus 25% of whatever has been bought; contributor claims are still time-locked). With the real seed (870M UNLK at tick 174960, no ETH depth beyond buyers' ETH) a single actor needs ~206 ETH to lift spot FDV to ~$5.3M at $2000/ETH and then holds 74.8% of the supply. Every tier's qualifyingSince is set in the same call, so after 30 min (TWAP window) + 60 min (CONFIRMATION, line 492) tiers 1..6 confirm in one updateMilestone call and fullyUnlocked becomes true permanently (line 500). The actor then sells everything back. Measured in test: spent 206 ETH + 37 ETH absorbing the two quota holders' sales, recovered 203 ETH, net cost 40.1 ETH (3 ETH is LP fees, 37 ETH went to alice, who turned a 1 ETH pre-spike buy into 22.3 ETH by selling 25% of it, and to the payer, 14.9 ETH for 7.5M tokens). Afterwards remainingQuota(anyone) == type(uint256).max and transfer-only holders can dump at will. The protocol's gradual-unlock guarantee is removable by one funded actor at a cost that does not scale with the manipulated price but only with other holders' quotas. This is a design limitation the requester must decide on (longer confirmation, capital-weighted confirmation, or a cap on how many tiers may confirm per call); no code fix is proposed here beyond flagging that 'arbitrage' is structurally capped by the quota rule itself.","line":492,"path":"src/UnlkHook.sol","reproduction":"test/scratch/EconomicsProbe.t.sol::testSustainedExcursionCostIsBoundedByOtherHoldersQuotas (demonstration, passes). Setup: real PoolManager, token-only seed 870M at tick 174960, activated, feed $2000. alice buys 1 ETH; syncPayer(). whale buys ETH computed by SqrtPriceMath.getAmount0Delta(sqrtPriceAtTick(128400), sqrtNow, seedLiquidity, true)/0.9875 = 205.77 ETH -> 748.1M tokens (7480 bps of supply), spot FDV $5,355,116. advance 30 min, updateMilestone: tierForFdv(fdvUsd()) == 6. alice sells 25% of her tokens -> 22.31 ETH; payer sells 7.5M -> 14.93 ETH; whale re-buys 37.24 ETH same block. 12 x (advance 5 min, updateMilestone). Expected per brief: a 90-minute self-funded excursion should be costly; actual: fullyUnlocked == true permanently, whale sells all and recovers 202.92 ETH, net cost 40.09 ETH, remainingQuota(attacker) == type(uint256).max.","severity":"medium","snippet":"            } else if (block.timestamp - qualifyingSince[i] >= CONFIRMATION) {\n                tier = i + 1;\n                changed = true;","title":"Sustained-price manipulation (known, unresolved): quota-capped sellers bound the manipulator's risk, so ~40 ETH net buys a permanent unlock for all holders"},{"citation":"resolved","description":"Reproduces README known issue 1 with the actual Sepolia ETH/USD answer (2555.33 USD, round 36737). The oracle marks an observation liquid only if the ETH principal in the seed range is >= MIN_QUOTE_DEPTH = 10 ether (src/UnlkOracle.sol:21,100). With the 870M token-only seed at tick 174960, spot FDV crosses $100K after only 5.5 ETH of buys (seed depth then 5.43 ETH), so for every FDV between $100K and ~$137.6K the TWAP is rejected (InvalidOracle) and tier 1 cannot be observed; depth reaches 10 ETH only after ~10.25 ETH of buys, at spot FDV $137,627. Not a loss of funds, but the first milestone the brief promises ('verified USD FDV $100K: 35%') is silently shifted to ~$137K at today's price, and any ETH price above ~$2,000 widens the gap. Either the threshold or the floor must change; the README correctly says that is a requester decision. Reported so the judge has concrete numbers at the live price.","line":100,"path":"src/UnlkOracle.sol","reproduction":"test/scratch/EconomicsProbe.t.sol::testDepthFloorAtCurrentSepoliaEthPrice (passes, logs). feed.answer = 2555e8. Buy 0.25 ETH repeatedly until spot FDV >= $100K: 5.5 ETH spent, depth 5.431 ETH (< 10). advance 30 min, checkpoint, oracle.fdvUsd() reverts InvalidOracle (expected: a valid ~$100K+ reading). Keep buying until depth >= 10 ETH: 10.25 ETH spent, spot FDV $137,627. Expected per brief: tier 1 observable from $100K; actual: unobservable below ~$137.6K.","severity":"low","snippet":"        bool liquid = sqrtPrice != 0 && liquidity >= MIN_LIQUIDITY && depth >= MIN_QUOTE_DEPTH\n            && protocolFee == 0 && lpFee == 12500;","title":"10 ETH depth floor vs first tier (known, unresolved): at the current Sepolia ETH price the $100K tier is unobservable until FDV is ~$137K"},{"citation":"resolved","description":"twapTick requires zero illiquid seconds inside the 30-minute window (src/UnlkOracle.sol:141). An observation is illiquid when seed depth < 10 ETH (line 100). While depth sits just above 10 ETH (the normal state while the first tier is being confirmed, see the depth-floor finding), any holder with quota can sell a small amount to pull depth under 10 ETH, which afterSwap's checkpoint records as liquid == false, then buy back in the next block. That single 12-second illiquid interval makes fdvUsd() revert for the following 30 minutes, and each updateMilestone()/swap during that time takes the catch branch (src/UnlkHook.sol:480) and deletes qualifyingSince. Cost to the griefer: two 1.25% LP fees on ~0.2 ETH. Secondary to finding 1 (same catch-branch root cause) but with a different, price-based trigger that exists on any chain; fixing the catch branch removes the reset, and a hysteresis or a time-weighted depth rule would remove the 30-minute blackout.","line":141,"path":"src/UnlkOracle.sol","reproduction":"test/scratch/EconomicsProbe.t.sol::testOneBlockIlliquidityNearFloorResetsProgress (passes, demonstrates). alice buys 10.4 ETH (depth >= 10). warm 30 min; updateMilestone at t0 and t0+5min: qualifyingSince[0] == t0. alice sells 2% of her tokens (within her 25% quota): depth 9.975 ETH (< 10). 12 s later alice buys back the ETH she received. t0+10min: updateMilestone() -> fdvUsd reverts (endBad != startBad), catch deletes qualifyingSince: qualifyingSince[0] == 0. t0+15min: oracle.fdvUsd() still reverts. Expected: a one-block dip should at most delay one observation; actual: 30 minutes without milestone data and the hour restarts.","severity":"low","snippet":"        if (endBad != startBad) revert InvalidOracle();","title":"One quota-sized sale near the depth floor makes a single block illiquid, which invalidates the TWAP for 30 minutes and erases confirmation progress"},{"citation":"resolved","description":"The brief requires the initial-allocation recipient to be proven from authoritative launch evidence. configureAndActivate takes `requester` from LaunchFactory.positionOf (src/UnlkHook.sol:261) and syncPayer credits that address with PAYER_ALLOCATION (30M E, lines 422-425). In the verified factory the 3% remainder is sent to LaunchParams.remainderTo (test/imd/LaunchFactory.sol:225-231) while `requester` is a separate LaunchParams field used only for the LP-fee split (line 183, PoolFees._openPosition), and PoolFees.setRequester lets the current requester hand it to any address at any time (test/imd/PoolFees.sol:81-87). Nothing in the hook ties the credited wallet to the wallet that received the tokens. Failing states: (a) a launch whose deployer passes remainderTo = A and requester = B (both valid inputs to launch()) credits B with 30M sellable-quota tokens it never received (B can then sell up to 7.5M tokens acquired by plain transfer, i.e. a quota backed by nothing) while A holds 30M unsellable tokens; (b) requester calls setRequester(1309, C) before the owner activates: C gets the 30M credit, the token holder gets none. The README lists this as an assumption (issue 3). A cheap on-chain guard that preserves the design: require token.balanceOf(requester) >= PAYER_ALLOCATION at activation in addition to the registry check (balance alone is not proof, but combined with the factory record it refuses case (a) and the hostile variant of (b)).","line":261,"path":"src/UnlkHook.sol","reproduction":"Using the fixture factory (test/imd/LaunchFactory.sol): in _launchParams set params.remainderTo = payer and params.requester = bob (both accepted by launch()). After stage 2 activation call hook.syncPayer(). Expected: the 30M credit goes to the wallet that received the 3% (payer). Actual: hook.payer() == bob, eligible(bob) == 30_000_000e18, eligible(payer) == 0; payer transfers 7.5M tokens to bob; bob sells 7.5M via router (quota 30M*25%) although bob never bought, claimed or received an allocation. Variant: with requester == remainderTo == payer, before activation payer calls factory.setRequester(1309, bob); activation then credits bob.","severity":"low","snippet":"        (recorded, lower, upper, requester) = ILaunchFactoryView(launchFactory).positionOf(launchNumber_);","title":"Payer credit follows PoolFees.requester, which is a fee-share address separable from remainderTo (the actual 3% recipient) and reassignable before activation"},{"citation":"resolved","description":"Documented as a privileged-power risk, not a permission bypass. configureAndActivate verifies companions only by reciprocal view fields and constants (src/UnlkHook.sol:204-217, 269-279), never by code. The brief asks to 'verify reviewed implementations, deployed code'. A contract that returns hook()/poolManager() correctly but reports context() = (attackerWallet, expectedHash) for every swap, or an 'oracle' with the right constants and feed whose fdvUsd() returns 5_000_000e18, passes every check. The owner (also the launch payer and 30M-token holder) therefore holds a one-shot ability to (a) credit E to itself from other traders' buys or consume their quotas, or (b) have fullyUnlocked set after one hour regardless of price. Preconditions: the owner acts against documented intent at the single activation call; no unprivileged amplifier exists afterwards because setup is sealed. README issue 6 asks operators to compare bytecode manually; that comparison is the only control and it is off-chain. Mitigation within the design: the hook can compute the expected runtime code of each companion at activation (immutables are the hook and manager addresses it knows) and compare extcodehash, or the IMD review can pin companion creation-code hashes in the attestation.","line":213,"path":"src/UnlkHook.sol","reproduction":"Deploy FakeRouter { hook() returns realHook; poolManager() returns realManager; context() returns (owner, OperationHash.hash(key, params, owner, nonce)) } and route swaps through it with activeTrader forced to owner. Owner calls configureAndActivate(fakeRouter, rewards, oracle, 1309): passes (code present, distinct, reciprocal fields match). Alice buys 1 ETH through fakeRouter: afterSwap credits eligible[owner] += output (expected: eligible[alice]). Owner can then sell tokens acquired by transfer against that credit. Local-only; not executable by any unprivileged actor.","severity":"low","snippet":"        if (\n            IUnlkRouterView(router_).hook() != address(this)\n                || address(IUnlkRouterView(router_).poolManager()) != address(poolManager)\n        ) revert InvalidConfig();","title":"Trust assumption: setupAuthority can activate look-alike companions (no deployed-code check), which would bypass quotas or force the $5M unlock"},{"citation":"resolved","description":"beforeSwap refuses every swap once slot0.protocolFee != 0 (src/UnlkHook.sol:297-298) and the oracle marks the pool illiquid (src/UnlkOracle.sol:100-101). Only the official router may swap, and the hook has no admin. If Uniswap governance's ProtocolFeeController sets a protocol fee on this pool, every holder with sellable quota is locked out of the official route forever (tokens remain transferable, so value is not lost, but the quota mechanism and the fee split the brief promises stop working permanently). The existing test testProtocolFeeChangeCannotSilentlyAlterTradingEconomics shows the revert and treats it as the intended fail-closed choice. Reported as a dependency-failure risk the requester should accept explicitly; an alternative that preserves the promise would be to keep trading open and only block new tiers (the oracle already does) rather than freeze sales.","line":298,"path":"src/UnlkHook.sol","reproduction":"Fixture: manager.setProtocolFeeController(address(this)); manager.setProtocolFee(key, 500). alice (eligible > 0, within quota) calls router.swap to sell: reverts InvalidPool from beforeSwap. Expected per brief: 'eligible native claims remain available' and sales within quota remain possible; actual: no sale is possible on the official pool from then on and no actor can restore it.","severity":"low","snippet":"        if (protocolFee != 0 || lpFee != FEE) revert InvalidPool();","title":"A PoolManager protocol-fee change permanently halts the only trading route, with no recovery path"}],"hash":"2db0d6c3a654de3c02707837c6c8dd92b60b205c8c23adb07c1afaa4e7307beb","nodeId":"8b25e463-a8d3-4b02-aaf4-adc99e63d8b3","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds 7 findings and 26 coverage rows (all 23 listed entry points plus 3 invariants, none unreached). Every snippet was extracted from the tree programmatically and re-verified against its line.\n\n**What I found, in my area (Economic Security, Invariant, Flow Gap):**\n\n1. **Medium, with proof.** The oracle rejects a Chainlink round older than 3600 s, but the real Sepolia ETH/USD feed lands rounds every 3624–3636 s (I read rounds 36698–36737 from the aggregator). So for 2–3 blocks each hour `fdvUsd()` reverts, and the hook's catch branch at `src/UnlkHook.sol:480` erases all pending tier progress. One unprivileged `updateMilestone()` per hour keeps every holder at 25% forever and makes the $5M bypass unreachable. The proof test fails on the current code (tier 0 instead of 1) and passes in a throwaway copy where the catch branch only returns false.\n2. **Medium, known issue quantified.** Sustained manipulation: because all other sellers are quota-capped, a single actor committing ~243 ETH for 90 minutes obtains a permanent unlock for everyone at a measured net cost of ~40 ETH, of which 37 ETH is what the two existing quota holders extracted (a 1 ETH pre-spike buy became 22 ETH).\n3. **Low, known issue at the live price.** At $2555/ETH the $100K spot FDV is reached after 5.5 ETH of buys with only 5.4 ETH depth, so tier 1 is unobservable until FDV is about $137.6K.\n4. **Low.** One quota-sized sale near the floor makes a single block illiquid, poisoning the 30-minute window and resetting progress for two 1.25% fees.\n5. **Low.** The 30M payer credit follows the fee-share `requester`, which the factory records separately from `remainderTo` and which `setRequester` can move before activation.\n6. **Low, trust assumption.** Companions are verified by reciprocal fields only, so the one-shot setup authority could activate look-alikes that bypass quotas or force the unlock.\n7. **Low.** A PoolManager protocol-fee change halts the only trading route permanently with no recovery.\n\n**Verified clean:** router sourcing and settlement, quota and credit accounting from actual deltas, overflow safety, transfer/donation/liquidity creating no rights, principal lock, claim-evidence dedup, one-shot sealing, and the baseline suite (87 passed, 0 failed).\n\n**Not reached:** I did not independently audit `script/Stage2.s.sol`, `CheckReleaseConfig.s.sol`, or the Sepolia factory identity question beyond what the README states; those are outside the assigned area and not among the listed entry points. Scratch tests live under `test/scratch/` and nothing else in the tree was changed.","treeHash":null,"usage":{"cachedInputTokens":3216212,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":68013,"runtime":"claude","turns":47,"wallClockMs":982496}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"60c045240bb09c33","findings":[{"citation":"resolved","description":"The deployment documentation that stage 2 depends on is wrong in a way that matters for executability. `UnlkHook.configureAndActivate` (src/UnlkHook.sol:200-201, 233-267) can only succeed if the Sepolia `$factory` exposes `registry()`, `registry().artifactsOf(uint64)` returning `(bytes32 role, bytes32 name, address addr)[]`, and `positionOf(uint64)` returning `(PoolKey, int24, int24, address requester)`. README known issue 5 says this could not be checked because none of the five candidate contracts answered `registry()`. Those five (deployer nonces 7, 10, 14, 17, 20) are not LaunchFactory instances: all five revert on `registry()`, `deployer()` and `positionOf(uint64)`. Scanning all 424 nonces of the deployer `0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7` on Sepolia finds 14 contracts that do answer `registry()`; the most recent one, nonce 394 = `0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A`, has `deployer() == 0xcECc29B037f5064fCdF45a5C318F132ef76aA551` (identical to the mainnet LaunchFactory's `deployer()`), `SWARM_BPS() == 1000`, `distributorCreationCodeHash() == 0xf1c21108…30b9` (identical to mainnet), `fees() == 0x428Ff16D1bb25d6674463881ed6a62BD09e30584` whose `poolFee() == 12500` and `REQUESTER_BPS() == 8000`, and a registry `0x90aaeDd35073e2A31861276e58081251a601aaD9` with `count() == 18` and `writer()` equal to the factory. On that registry, launch 1296 (`kind` = \"evm_contracts\", recorded 2026-10-11) decodes correctly through the fixture ABI for `isRecorded`, `launchOf`, `artifactsOf` (three `other` artifacts) and `positionOf` (all-zero, as expected for a non-pool launch). So the ABI shape the hook relies on is present on Sepolia. Two gaps remain that the README should state instead of the current text: (a) `0x84cf…8F7A` is NOT verified on Sourcify (`match: null`) and its runtime code is larger than the Sourcify-verified mainnet factory (46,693 vs 34,641 hex chars), so it is a different build whose `launch()`/`predictHook` behaviour is unconfirmed (`predictHook(bytes,bytes32)` reverted on an empty creation-code probe where the mainnet version is a pure view); (b) it was created after Sepolia block 11,850,000, i.e. days before this delivery, so the README's 'none found' conclusion is stale. The fork rehearsal that docs/DEPLOYMENT.md prescribes cannot be run until this address is confirmed as IMD's configured Sepolia factory; stage-1 deployment before that confirmation risks a permanently inactive pool holding 87% of supply if `positionOf`/`artifactsOf` differ on the real factory, because the hook has no second activation path.","line":94,"path":"README.md","reproduction":"RPC https://ethereum-sepolia-rpc.publicnode.com. (1) For each of the README's five candidates, e.g. `cast call 0x4a4d0F305B9D462532930E41e7B84F133E070Ed5 'registry()(address)'` -> execution reverted; `cast call <same> 'positionOf(uint64)((address,address,uint24,int24,address),int24,int24,address)' 1` -> execution reverted. Expected per README: at least one is the LaunchFactory. Actual: none is. (2) `cast compute-address 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7 --nonce 394` -> 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A; `cast call 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A 'deployer()(address)'` -> 0xcECc29B037f5064fCdF45a5C318F132ef76aA551 (same as `cast call 0x12c63b581d07093f6126bc02263c58f7eadaa96f 'deployer()(address)' --rpc-url <mainnet>`); `'registry()(address)'` -> 0x90aaeDd35073e2A31861276e58081251a601aaD9; `cast call 0x90aaeDd35073e2A31861276e58081251a601aaD9 'artifactsOf(uint64)((bytes32,bytes32,address)[])' 1296` -> three artifacts; `cast call 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A 'positionOf(uint64)((address,address,uint24,int24,address),int24,int24,address)' 1296` -> zeroed tuple (decodes, does not revert). (3) `curl https://sourcify.dev/server/v2/contract/11155111/0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A` -> {\"match\":null,...}; `cast code 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A | wc -c` -> 46693 vs mainnet 34641. Expected deliverable: README/DEPLOYMENT name this factory as the candidate, state it is unverified, and make stage-1 deployment conditional on IMD confirming it and on a fork rehearsal of `configureAndActivate` against it.","severity":"low","snippet":"| IMD LaunchFactory on Sepolia | **Not authenticated.** The mainnet factory deployer `0x047F…54B7` has CREATE contracts on Sepolia that return this PoolManager from `poolManager()` (nonces 7, 10, 14, 17, 20: `0x4a4d0F30…0Ed5`, `0x3E4f5Dd1…b408`, `0x7Dec6dc7…fB11`, `0xe92d8DD6…55F9`, `0xe29166a0…3CB1`). Which one is current is a fact only IMD's network config holds, and none was supplied. The `registry()` call shape on these did not return through `cast` |","title":"README Sepolia factory table is obsolete: it names five non-factory contracts and misses the live LaunchFactory the stage-2 activation path must be verified against"},{"citation":"resolved","description":"beforeSwap refuses every swap once `getSlot0(poolId).protocolFee != 0`, and `UnlkOracle.checkpoint` (src/UnlkOracle.sol:100-101) marks every observation illiquid under the same condition. The PoolManager owner (Uniswap governance, via `setProtocolFeeController`/`setProtocolFee`) can set a protocol fee on any pool key at any time; nothing in this project can react, because the hook has no configuration path after `configureAndActivate`. The consequence is that every quota holder loses the only authorized sell route for UNLK forever (the hook rejects all other routers), while the 87% principal stays locked in the factory position. The repository tests this behaviour as intended (test/Adversarial.t.sol `testProtocolFeeChangeCannotSilentlyAlterTradingEconomics`), and the Sepolia PoolManager `0xE03A1074c86CFeDd5C142C4F04F1a1536e203543` currently has `protocolFeeController() == address(0)`, so the trigger is not armed today. Recorded as a trust assumption with a concrete trigger rather than a code defect: the brief asks to preserve the 1.25% fee, and halting is the chosen enforcement. The README should state this dependency explicitly next to known issue 7.","line":298,"path":"src/UnlkHook.sol","reproduction":"Local, SystemFixture after activation: `manager.setProtocolFeeController(address(this)); manager.setProtocolFee(key, uint24(500));` then any `router.swap{value: 1 ether}(request)` reverts with UnlkHook.InvalidPool and `oracle.fdvUsd()` reverts InvalidOracle 30 minutes later for lack of liquid seconds. Expected by holders: ability to sell within quota; actual: no sell path exists for the remaining life of the token. On Sepolia: `cast call 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543 'protocolFeeController()(address)' --rpc-url https://ethereum-sepolia-rpc.publicnode.com` -> 0x0 (not armed).","severity":"info","snippet":"        if (protocolFee != 0 || lpFee != FEE) revert InvalidPool();","title":"Trading halts permanently and irrecoverably if Uniswap v4 governance ever sets a protocol fee on the official pool (design choice, external trusted actor)"}],"hash":"671b0a5f0e520b454298392d3772c489efef4f6590cda67c712c12d569071186","nodeId":"1cbc57b5-c0ff-4977-8013-5da71567a0d1","outcome":"completed","summary":"The findings file is in place (excluded from git by the harness's own `.git/info/exclude`, as intended), the working tree is otherwise untouched, and my scratch copies are removed. The review is complete.\n\n## Outcome\n\n`.imd-findings.json` holds 2 findings and 28 coverage rows. No code defect of critical, high or medium severity was substantiated in the Execution Trace, Periphery or First Principles areas. Build and all 87 project tests pass, and I re-ran the unmodified protected admission tests with probe values: 11 passed, matching the delivered results.\n\n**Findings written**\n\n1. **Low, README.md line 94.** The Sepolia factory table is obsolete. The five candidate contracts it names are not LaunchFactory instances; all revert on `registry()`, `deployer()` and `positionOf`. A scan of all 424 deployer nonces found the live factory at nonce 394, `0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A`. It shares the mainnet factory's `deployer()`, SWARM_BPS and distributor code hash, has a registry with 18 recorded launches, and its LaunchFees reports fee 12500 with 80% to the requester. Every call `configureAndActivate` makes decodes correctly against recorded launch 1296. Two gaps remain and should replace the current text: the contract is not Sourcify-verified and is a larger build than mainnet, so stage 1 should wait for IMD to confirm this address and for a fork rehearsal of activation against it.\n\n2. **Info, src/UnlkHook.sol line 298.** Any Uniswap v4 protocol fee on the pool halts trading permanently with no recovery path. This is the design's chosen enforcement of the 1.25% fee and is tested as intended. Sepolia's PoolManager currently has no protocol fee controller, so the trigger is unarmed.\n\n**What I traced and found sound**\n\n- Swap authentication across router, PoolManager and hook: nonce plus operation hash, pending-trader lifecycle, exact balance reconciliation, batch ETH accounting, pull-only native proceeds.\n- Activation: all checks precede all writes, cross-launch and replay rejection, reciprocal bindings, sealing.\n- Oracle: cumulative tick and bad-seconds arithmetic, same-block replacement, binary-search window, FDV math and sign handling, Chainlink validation.\n- Milestones: multi-tier jumps, gap reset, monotone tier, permanent unlock.\n- Periphery: OperationHash encoding parity, HookFlags, interface structs against the verified mainnet factory ABI, HookDeployer init code.\n\n**Not reached:** quantifying the sustained-manipulation economics beyond what the repository's own test reproduces. Quotas also throttle arbitrage against a manipulator, which the economic specialist should weigh.","treeHash":null,"usage":{"cachedInputTokens":3747737,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":65976,"runtime":"claude","turns":57,"wallClockMs":1074946}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"26f033b775c04047","findings":[],"hash":"6748888d0f0db93edf9f0589b6ca8821244a9fc1d7d4f6eba289953588da6e19","nodeId":"706cda53-04c9-4256-9b90-862649f8e62f","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/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":7,"wallClockMs":1468778}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a4e285247c6f8aed","findings":[{"citation":"resolved","description":"UnlkHook._updateMilestone (src/UnlkHook.sol:477-483) wraps oracle.fdvUsd() in try/catch and on ANY revert executes `delete qualifyingSince; lastConfirmation = 0;` before returning. UnlkOracle._ethUsd (src/UnlkOracle.sol:173) rejects a round older than exactly MAX_FEED_AGE = 1 hours (src/UnlkOracle.sol:24), and configureAndActivate pins both that constant (ORACLE_MAX_FEED_AGE) and, on chain 11155111, the published proxy 0x694AA1769357215DE4FAC081bf1f309aDC325306. I read that proxy on 2026-10-12 (latestRound 36738, phase 1): the last 15 rounds' updatedAt values are 1791803208, 1791799584, 1791795948, 1791792324, 1791788688, ... i.e. gaps alternate 3624/3636 s, every one above 3600 s. So for the last 24-36 s of every round fdvUsd() reverts InvalidOracle on a perfectly healthy feed, and any call that reaches _updateMilestone in that window, updateMilestone() from any address or afterSwap from any trade, wipes the pending confirmation of every tier and resets lastConfirmation. A tier needs 60 minutes of continuous qualification with <= 5-minute gaps and the window recurs every ~60.5 minutes, so one cheap transaction per hour keeps tier at 0 and p at 2500 forever and makes the $5M permanent bypass unreachable, whatever the real FDV is; under ordinary volume the same happens by accident. The brief says invalid data blocks new tiers only. The same catch branch is also reachable by a price trigger on any chain: while seed depth sits just above the 10 ETH floor, one quota-sized sell that dips depth below 10 ETH for a single block makes twapTick revert (endBad != startBad, src/UnlkOracle.sol:141) for the next 30 minutes and every updateMilestone()/swap in that time deletes qualifyingSince. Merged from audit_permissions (high), audit_economics (stale window, medium) and audit_economics (one-block illiquidity, low): one root cause, one fix. Fix that preserves the design: in the catch branch only `return false` (the existing 5-minute gap rule already restarts an unobserved confirmation), and/or raise MAX_FEED_AGE and ORACLE_MAX_FEED_AGE above the feed's real cadence (heartbeat plus grace). The attached proof fails on the current code and passes with the catch-branch change (verified locally by patching and restoring the file).","line":480,"path":"src/UnlkHook.sol","reproduction":"State: stage 2 activated against a feed that updates every 3624 s (the measured Sepolia cadence), ETH/USD $2000, one 15 ETH buy so the seed range holds >= 10 ETH and TWAP FDV >= $100K; oracle warmed 30 minutes. A keeper calls hook.updateMilestone() every 300 s for 5 hours. Once per feed round, at a block where block.timestamp == updatedAt + 3601 (inside the 24 s window before the next round), an unprivileged address also calls hook.updateMilestone(). Expected: tier() >= 1 within ~65 minutes (invalid data only blocks the tier for that call). Actual: oracle.fdvUsd() reverts InvalidOracle in that call, the catch runs `delete qualifyingSince; lastConfirmation = 0`, qualifyingSince[0] restarts, and after 5 hours hook.tier() == 0, p == 2500. Run: forge test --match-path test/scratch/FeedStaleResetProof.t.sol -> FAIL 'first tier never confirmed ... 0 < 1'. On-chain evidence: cast call 0x694AA1769357215DE4FAC081bf1f309aDC325306 'getRoundData(uint80)' <(1<<64)+36738-k> for k = 0..14 on https://ethereum-sepolia-rpc.publicnode.com gives updatedAt gaps of 3624 and 3636 s.","severity":"high","snippet":"            delete qualifyingSince;","title":"Any oracle revert in _updateMilestone's catch branch erases all confirmation progress; with the real Sepolia ETH/USD cadence (3624-3636 s rounds) the feed is classed stale for 24-36 s every hour, so o"},{"citation":"resolved","description":"configureAndActivate stores payer = positionOf(launchNumber).requester (src/UnlkHook.sol:201, 225) and syncPayer credits that wallet PAYER_ALLOCATION = 30,000,000 UNLK of eligibility (lines 420-427). In the verified factory source carried in test/imd, LaunchParams.requester and LaunchParams.remainderTo are independent inputs: the 3% remainder is sent to remainderTo (test/imd/LaunchFactory.sol:225-231) while requester is only the PoolFees fee-share holder (line 183), and the current holder may reassign it at any time through PoolFees.setRequester (test/imd/PoolFees.sol:81-87). The hook never ties the credited wallet to the wallet that received the tokens, so the brief's 'prove the initial allocation recipient from authoritative launch evidence' is not met. Outcomes: (a) a launch with requester != remainderTo credits the fee-share wallet 30M E (it can then sell up to 7.5M UNLK it acquires by plain transfer) while the actual recipient holds 30M unsellable tokens; (b) the requester hands its fee share to another address before stage 2 and the credit follows. README issue 3 records the assumption; the code does not enforce it. Merged from audit_economics (low), write_foundry_tests (medium) and audit_permissions (medium). Minimal fix within the design: at activation require token.balanceOf(requester) >= PAYER_ALLOCATION in addition to the registry check, or have the setup authority pass the recipient and verify it against both the launch record and the balance.","line":261,"path":"src/UnlkHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.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 {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {UnlkHook} from \"src/UnlkHook.sol\";\nimport {UnlkToken} from \"src/UnlkToken.sol\";\nimport {UnlkOracle} from \"src/UnlkOracle.sol\";\nimport {UnlkSwapRouter} from \"src/UnlkSwapRouter.sol\";\nimport {MerkleClaimEvidence} from \"src/MerkleClaimEvidence.sol\";\nimport {\n    IUnlkHookView,\n    IMerkleDistributor,\n    IChainlinkEthUsd,\n    ILaunchRegistryView\n} from \"src/interfaces/IIntegration.sol\";\n\n/// @dev The subset of IMD's factory the hook reads at activation, with the policy split the real factory performs:\n/// 10% to the distributor, the seed into the pool, the remainder (3%) to `remainderTo`, and `requester` recorded\n/// separately as the fee-share wallet. The two are distinct fields in the verified factory ABI.\ncontract MinimalFactory is IUnlockCallback {\n    struct Artifact {\n        bytes32 role;\n        bytes32 name;\n        address addr;\n    }\n\n    IPoolManager public immutable poolManager;\n    MinimalRegistry public immutable registry;\n    PoolKey internal key;\n    int24 internal lower;\n    int24 internal upper;\n    address internal requesterAddr;\n    uint128 internal liquidity;\n    UnlkToken internal token;\n\n    constructor(IPoolManager manager_) {\n        poolManager = manager_;\n        registry = new MinimalRegistry();\n    }\n\n    function deployToken() external returns (UnlkToken t) {\n        t = new UnlkToken();\n        token = t;\n    }\n\n    function deployHook(bytes memory initCode, bytes32 salt) external returns (address hook) {\n        assembly (\"memory-safe\") {\n            hook := create2(0, add(initCode, 0x20), mload(initCode), salt)\n        }\n        require(hook != address(0), \"hook deploy failed\");\n    }\n\n    function launch(address hook, address distributor, address remainderTo, address requester_) external {\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12500, 60, IHooks(hook));\n        lower = -887220;\n        upper = 174960;\n        requesterAddr = requester_;\n        uint160 sqrt = TickMath.getSqrtPriceAtTick(upper);\n        liquidity = uint128(uint256(870_000_000 ether) * (1 << 96) / (sqrt - TickMath.getSqrtPriceAtTick(lower)));\n        poolManager.initialize(key, sqrt);\n        poolManager.unlock(\"\");\n        token.transfer(distributor, 100_000_000 ether);\n        token.transfer(remainderTo, token.balanceOf(address(this)));\n        registry.record(address(token), hook, distributor);\n    }\n\n    function unlockCallback(bytes calldata) external returns (bytes memory) {\n        (BalanceDelta delta,) = poolManager.modifyLiquidity(\n            key, ModifyLiquidityParams(lower, upper, int256(uint256(liquidity)), bytes32(0)), \"\"\n        );\n        uint256 due = uint256(uint128(-delta.amount1()));\n        poolManager.sync(key.currency1);\n        token.transfer(address(poolManager), due);\n        poolManager.settle();\n        return \"\";\n    }\n\n    function positionOf(uint64) external view returns (PoolKey memory, int24, int24, address) {\n        return (key, lower, upper, requesterAddr);\n    }\n}\n\ncontract MinimalRegistry {\n    ILaunchRegistryView.Artifact[] internal arts;\n\n    function record(address token, address hook, address distributor) external {\n        arts.push(ILaunchRegistryView.Artifact(\"token\", \"token\", token));\n        arts.push(ILaunchRegistryView.Artifact(\"hook\", \"hook\", hook));\n        arts.push(ILaunchRegistryView.Artifact(\"distributor\", \"distributor\", distributor));\n    }\n\n    function artifactsOf(uint64) external view returns (ILaunchRegistryView.Artifact[] memory) {\n        return arts;\n    }\n}\n\ncontract MinimalDistributor {\n    address public token;\n    address public opener;\n\n    constructor(address token_, address opener_) {\n        token = token_;\n        opener = opener_;\n    }\n\n    function claimed(uint256, address) external pure returns (bool) {\n        return false;\n    }\n\n    function roundOf(uint256) external pure returns (IMerkleDistributor.Round memory r) {\n        return r;\n    }\n}\n\ncontract MinimalFeed is IChainlinkEthUsd {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @title The 3% \"payer\" sale rights follow the factory's fee-share `requester`, not the wallet that received the\n/// allocation.\n/// @notice The factory records `remainderTo` (who receives the 3%) and `requester` (who shares LP fees) as two\n/// independent inputs. `UnlkHook.configureAndActivate` takes `positionOf(launchNumber).requester` as the payer\n/// and `syncPayer` credits it 30,000,000 UNLK of eligibility. When the two differ, a wallet holding no allocation\n/// gains 30M of sale rights while the wallet holding the 30M tokens has none.\ncontract PayerAttributionProof is Test {\n    address constant OWNER = address(0xA11CE);\n    address constant RECIPIENT = address(0xB0B);\n    address constant FEE_SHARER = address(0xCA201);\n\n    function testPayerCreditGoesToTheWalletThatReceivedTheAllocation() public {\n        vm.warp(1_800_000_000);\n        PoolManager manager = new PoolManager(address(this));\n        MinimalFactory factory = new MinimalFactory(manager);\n        UnlkToken token = factory.deployToken();\n        bytes memory initCode =\n            bytes.concat(type(UnlkHook).creationCode, abi.encode(manager, token, address(factory), OWNER));\n        bytes32 codeHash = keccak256(initCode);\n        bytes32 salt;\n        for (uint256 i;; ++i) {\n            salt = bytes32(i);\n            address predicted = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, codeHash))))\n            );\n            if (uint160(predicted) & 0x3fff == 0x3fc0) break;\n        }\n        UnlkHook hook = UnlkHook(factory.deployHook(initCode, salt));\n        MinimalDistributor distributor = new MinimalDistributor(address(token), address(factory));\n        // The launch: RECIPIENT receives the 3% allocation; FEE_SHARER is recorded as the requester.\n        factory.launch(address(hook), address(distributor), RECIPIENT, FEE_SHARER);\n        assertGe(token.balanceOf(RECIPIENT), 30_000_000 ether, \"the allocation went to RECIPIENT\");\n        assertEq(token.balanceOf(FEE_SHARER), 0);\n\n        MerkleClaimEvidence rewards =\n            new MerkleClaimEvidence(IMerkleDistributor(address(distributor)), address(token));\n        UnlkSwapRouter router = new UnlkSwapRouter(manager, IUnlkHookView(address(hook)));\n        UnlkOracle oracle = new UnlkOracle(manager, new MinimalFeed(), address(hook));\n        vm.prank(OWNER);\n        try hook.configureAndActivate(address(router), rewards, oracle, 1) {}\n        catch {\n            // Refusing to activate when the recorded requester does not hold the allocation is an acceptable fix.\n            return;\n        }\n        hook.syncPayer();\n        assertEq(hook.eligible(FEE_SHARER), 0, \"a wallet that received no allocation was credited 30M of sale rights\");\n        assertEq(\n            hook.eligible(RECIPIENT),\n            token.PAYER_ALLOCATION(),\n            \"the wallet holding the 3% allocation has no sale rights\"\n        );\n    }\n}","reproduction":"Local system with the fixture factory (test/fixtures/SystemFixture.sol): after stage 1, token.balanceOf(payer) >= 30_000_000e18 and factory.positionOf(1309).requester == payer. Before stage 2: vm.prank(payer); factory.setRequester(1309, bob). Owner calls hook.configureAndActivate(router, rewards, oracle, 1309); anyone calls hook.syncPayer(). Expected: eligible(payer) == 30_000_000e18. Actual: hook.payer() == bob, eligible(bob) == 30_000_000e18 with token.balanceOf(bob) == 0, eligible(payer) == 0. Variant with a minimal factory implementing exactly the views the hook reads and launching with remainderTo = RECIPIENT, requester = FEE_SHARER: forge test --match-path test/scratch/PayerProof.t.sol -> FAIL 'a wallet that received no allocation was credited 30M of sale rights: 30000000000000000000000000 != 0' (run here on this code).","severity":"medium","snippet":"        (recorded, lower, upper, requester) = ILaunchFactoryView(launchFactory).positionOf(launchNumber_);","title":"The 3% payer credit follows the factory's mutable LP-fee-share requester (positionOf().requester), not the wallet that received the allocation (remainderTo); a pre-activation setRequester or requester"},{"citation":"resolved","description":"UnlkOracle.checkpoint marks an observation liquid only if the ETH principal in the seed range is >= MIN_QUOTE_DEPTH = 10 ether (src/UnlkOracle.sol:21, 95-101). With the factory's 870M token-only seed at normalized tick -174960 (opening FDV about 25 ETH) and ETH/USD $2000, about 9.5 ETH of buys lifts spot FDV above $100K while the seed holds under 10 ETH. Every checkpoint is then liquid = false, twapTick reverts (!last.liquid, endBad != startBad), _updateMilestone takes the catch branch and the first tier the brief promises is unobservable for all FDV between $100K and roughly $137K at today's price (audit_economics measured 5.5 ETH of buys to $100K vs 10.25 ETH to reach the floor at $2555/ETH); a higher ETH price widens the gap. Not a loss of funds, but a promised guarantee that cannot hold; either the threshold or the floor must change and that is a requester decision the README records. Merged from audit_math (medium), audit_economics (low) and write_foundry_tests (medium).","line":100,"path":"src/UnlkOracle.sol","reproduction":"Existing test test/TwoStage.t.sol::testDepthFloorBlocksFirstTierAtSpotAbove100k (passes in the suite, demonstrating the defect): activate, _buy(bob, 9.5 ether); spot FDV >= 100_000e18 and seed depth < 10 ether; run two full 90-minute confirmation cycles (_confirm() twice, updateMilestone every 5 minutes). Expected per brief: tier() == 1, p == 3500. Actual: tier() == 0 and oracle.fdvUsd() reverts InvalidOracle.","severity":"medium","snippet":"        bool liquid = sqrtPrice != 0 && liquidity >= MIN_LIQUIDITY && depth >= MIN_QUOTE_DEPTH","title":"The 10 ETH quote-depth floor conflicts with the $100K first tier: with the token-only seed, spot/TWAP FDV passes $100K while the seed holds under 10 ETH, so every observation is illiquid, fdvUsd rever"},{"citation":"resolved","description":"Milestones come from a 30-minute TWAP of the pool's own tick plus a one-hour confirmation with <= 5-minute gaps (src/UnlkHook.sol:485-495, CONFIRMATION at line 43). Every tier's qualifyingSince is set in the same call, so a single actor who lifts price past $5M FDV and holds it for 90 minutes has tiers 1..6 confirm in one updateMilestone call and fullyUnlocked becomes true permanently (line 500; 'price falls never relock'). Nothing bounds the capital or makes the move costly beyond LP fees: the only wallets that can sell into the spike are those with eligible credit, each at most p = 25% of E, and transfer-only holders cannot sell at all (line 326), so counter-selling is capped by the sum of existing quotas, smallest right after activation. audit_economics measured on the real seed: about 206 ETH of buys reaches $5.35M FDV at $2000/ETH, the attacker absorbs 37 ETH of quota-limited sales, then sells everything back and recovers about 203 ETH; net cost about 40 ETH for a permanent bypass of the quota scheme for all wallets. The brief asks to reproduce this; it is unmitigated and documented as README issue 2. Merged from audit_math (medium), audit_economics (medium) and write_foundry_tests (low). Mitigations need a scope decision (longer or capital-weighted confirmation, one tier per confirmation period, or a cap on tiers confirmed per call).","line":492,"path":"src/UnlkHook.sol","reproduction":"Existing test test/Oracle.t.sol::testSustainedManipulationRequiresWindowAndConfirmation (passes, demonstrating): activated system with >= 10 ETH depth, _buy(alice, 500 ether); _advance(30 minutes); updateMilestone() -> tierForFdv(fdvUsd()) == 6; then 12 x (_advance(5 minutes); updateMilestone()). Actual: fullyUnlocked() == true after 90 minutes, remainingQuota(any) == type(uint256).max, alice can sell all 500 ETH worth of tokens back. Expected per brief: a permanent unlock should not be purchasable by one funded actor at a cost of LP fees plus other holders' capped quotas.","severity":"medium","snippet":"            } else if (block.timestamp - qualifyingSince[i] >= CONFIRMATION) {","title":"Sustained-price manipulation buys a permanent quota bypass for every holder; the arbitrage that is supposed to make it costly is itself capped by the quota rule (unresolved audit finding, reproduced)"},{"citation":"resolved","description":"beforeSwap refuses every swap once getSlot0(poolId).protocolFee != 0, and UnlkOracle.checkpoint (src/UnlkOracle.sol:100-101) marks every observation illiquid under the same condition. Uniswap's protocol-fee controller can set a protocol fee on any pool at any time; the hook is sealed with no admin, so if that happens all quota holders lose the only authorized sell route for the life of the token while 87% of supply stays locked in the factory position (transfers and native claims keep working). The repository treats this as intended (test/Adversarial.t.sol testProtocolFeeChangeCannotSilentlyAlterTradingEconomics). Sepolia's PoolManager 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543 has protocolFeeController() == address(0) today, so the trigger is not armed. Reported as a trust assumption with an exact failing state, merged from audit_economics, audit_flow, write_foundry_tests and audit_permissions. Alternative that keeps the fee promise: tolerate a non-zero protocol fee while enforcing lpFee == FEE, or document the dependency explicitly as accepted.","line":298,"path":"src/UnlkHook.sol","reproduction":"SystemFixture after activation: manager.setProtocolFeeController(address(this)); manager.setProtocolFee(key, 500). alice (eligible > 0, within quota) calls router.swap to sell 1e18 tokens. Expected per brief: sale within quota succeeds at the 1.25% LP fee. Actual: revert UnlkHook.InvalidPool from beforeSwap for every trader from then on; oracle.fdvUsd() reverts InvalidOracle 30 minutes later; no function on the hook or companions can restore trading.","severity":"low","snippet":"        if (protocolFee != 0 || lpFee != FEE) revert InvalidPool();","title":"A PoolManager protocol fee on the official pool halts every swap permanently, with no recovery path (fail-closed design choice depending on an external actor)"},{"citation":"resolved","description":"The stage-2 route depends on the Sepolia $factory exposing registry().artifactsOf(uint64) and positionOf(uint64) in the mainnet ABI shape (src/UnlkHook.sol:233-267). README known issue 5 and this table say none of the five candidates answered registry() and that the factory could not be found. I re-ran the checks on 2026-10-12 against https://ethereum-sepolia-rpc.publicnode.com: the first listed candidate 0x4a4d0F305B9D462532930E41e7B84F133E070Ed5 reverts on registry(); the contract at nonce 394 of the same deployer, 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A, returns registry() = 0x90aaeDd35073e2A31861276e58081251a601aaD9, deployer() = 0xcECc29B037f5064fCdF45a5C318F132ef76aA551 and poolManager() = 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543 (the Sepolia PoolManager the README names). audit_flow further found it unverified on Sourcify with a larger runtime than the verified mainnet factory, so its launch()/positionOf behaviour is unconfirmed. The brief requires correcting obsolete deployment instructions and verifying the Sepolia factory before stage 1; the current text sends an operator to the wrong contracts and omits the one candidate the activation path must be rehearsed against. Documentation only, no code impact; reproduced from audit_flow (low).","line":94,"path":"README.md","reproduction":"RPC=https://ethereum-sepolia-rpc.publicnode.com. cast call 0x4a4d0F305B9D462532930E41e7B84F133E070Ed5 'registry()(address)' --rpc-url $RPC -> execution reverted (README says this is a factory candidate). cast call 0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A 'registry()(address)' -> 0x90aaeDd35073e2A31861276e58081251a601aaD9; 'deployer()(address)' -> 0xcECc29B037f5064fCdF45a5C318F132ef76aA551; 'poolManager()(address)' -> 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543. Expected: README/DEPLOYMENT name this address as the unverified candidate and make stage 1 conditional on IMD confirming it plus a fork rehearsal of configureAndActivate against it. Actual: the table lists five non-factories and 'none found'.","severity":"low","snippet":"| IMD LaunchFactory on Sepolia | **Not authenticated.** The mainnet factory deployer `0x047F…54B7` has CREATE contracts on Sepolia that return this PoolManager from `poolManager()` (nonces 7, 10, 14, 17, 20: `0x4a4d0F30…0Ed5`, `0x3E4f5Dd1…b408`, `0x7Dec6dc7…fB11`, `0xe92d8DD6…55F9`, `0xe29166a0…3CB1`). Which one is current is a fact only IMD's network config holds, and none was supplied. The `registry()` call shape on these did not return through `cast` |","title":"README's Sepolia factory table is obsolete: its five candidates are not LaunchFactory instances, while a factory that answers registry()/deployer()/poolManager() exists at 0x84cf2Bc1Dc2af577dba5d62e26"},{"citation":"resolved","description":"configureAndActivate accepts router_, rewards_ and oracle_ if they have code, are distinct, and return the expected hook(), poolManager(), token(), distributor() and oracle constants (src/UnlkHook.sol:194-217, 269-279); it never checks code identity (immutables vary the runtime), and the oracle check list omits MAX_CHECKPOINT_AGE. A router whose context() names an arbitrary trader would let that wallet be credited E for every buy and consume others' quotas; an evidence contract whose claimed() returns any value up to SWARM_ALLOCATION would mint eligibility; an oracle whose fdvUsd() returns 5_000_000e18 would set fullyUnlocked after one hour. Only the immutable setupAuthority (0x48e4e74fe1d0c14404ceb57b9fec5b21329c6ec9), once, can do this, so it is a documented trust assumption on that key; the brief's 'verify reviewed implementations, deployed code' must be done off chain before it signs (docs/DEPLOYMENT.md says to compare bytecode). Merged from audit_economics (low), write_foundry_tests (info) and audit_permissions (info).","line":214,"path":"src/UnlkHook.sol","reproduction":"Owner deploys FakeEvidence { token() == token; distributor() == <registry distributor>; claimed(x) == 100_000_000e18 for all x } and calls configureAndActivate(router, FakeEvidence, oracle, 1309). Expected per the brief: reviewed implementation verified. Actual: activation succeeds (checks at lines 204-206 pass); syncRewards(anyWallet) then credits 100M E to any wallet. Requires the setup authority's key; no unprivileged actor can reach it.","severity":"info","snippet":"            IUnlkRouterView(router_).hook() != address(this)","title":"Activation verifies companions by reciprocal getters and constants only; the setup authority can seal look-alike contracts that would consume other wallets' quotas or force the $5M unlock (one-time tr"}],"hash":"6a5d29f30617958cb8a576c4858ef1193a1919f0652b128a143ab85477edde88","nodeId":"30ed11ed-2e12-4673-be39-7f4fe03d3f90","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the tree is unchanged, and the build is clean.\n\n**Outcome.** I kept 7 findings out of the 22 reported by the four specialists, merged duplicates, and answered all 23 entry points in the coverage record. Nothing in the repository was changed. Scratch proofs live under `test/scratch/`, which is git-ignored.\n\n**Findings reported in `.imd-findings.json`:**\n\n1. **High, with proof.** The milestone catch branch in `UnlkHook._updateMilestone` deletes all confirmation progress on any oracle revert. I read the published Sepolia ETH/USD proxy on chain today. Its rounds land 3624 or 3636 seconds apart, so a feed bound of exactly one hour classes a healthy feed stale for 24 to 36 seconds every round. Any swap or any unprivileged `updateMilestone()` call in that window restarts every tier's hour. My proof `test/scratch/FeedStaleResetProof.t.sol` fails on the current code and passes when the catch branch only returns false. I verified that by patching and restoring the file. This merges three specialist reports, including the one-block illiquidity trigger.\n2. **Medium, with proof.** The 3% payer credit follows the factory's mutable fee-share requester rather than the wallet that received the allocation. The specialist proof fails on this code for the stated reason, so I attached it.\n3. **Medium.** The 10 ETH depth floor conflicts with the $100K first tier. Reproduced by the existing test `testDepthFloorBlocksFirstTierAtSpotAbove100k`.\n4. **Medium.** Sustained-price manipulation buys a permanent unlock, and quota-capped counter-selling bounds the manipulator's risk. Reproduced by the existing sustained-manipulation test.\n5. **Low.** A protocol fee set by Uniswap governance halts trading forever. Fail-closed design choice, reported as a trust assumption.\n6. **Low.** The README's Sepolia factory table is obsolete. I confirmed by RPC that its listed candidate reverts on `registry()` while the unlisted contract at the deployer's nonce 394 answers `registry()`, `deployer()` and `poolManager()`.\n7. **Info.** Companion identity is checked by reciprocal getters only. One-time trust in the setup authority.\n\n**Dropped.** The invariant-handler note was already fixed, and the deployment-status item labels unexecuted work rather than a defect.\n\n**Verification status.** Full suite: 111 tests passed. Contract sizes are within the CREATE2 limits. The manifest's constructor order, permissions, fee and notes length check out. No Sepolia deployment exists, so no on-chain stage-1 or stage-2 evidence could be judged.","treeHash":null,"usage":{"cachedInputTokens":1594414,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":36542,"runtime":"claude","turns":35,"wallClockMs":528322}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"234a4a00ca903cb9","findings":[{"citation":"resolved","description":"depth is the ETH principal in the seed range (getAmount0Delta(clamped, rangeUpper, L)). With the factory's token-only seed (opening tick -174960 normalized, 870M tokens) and ETH/USD $2,000, about 9.5 ETH of buys moves FDV past $100K while depth < 10 ETH. Every checkpoint is then liquid=false, twapTick reverts (!last.liquid / endBad != startBad), _updateMilestone's catch deletes qualifyingSince, and the 35% tier cannot be reached although the brief's $100K threshold is met. The threshold and the floor conflict; one must change (documented, needs a scope decision).","line":100,"path":"src/UnlkOracle.sol","reproduction":"Existing test test/TwoStage.t.sol testDepthFloorBlocksFirstTierAtSpotAbove100k: activate, _buy(bob, 9.5 ether) -> spot FDV >= 100_000e18, depth < 10 ether; after two 1h confirmation passes hook.tier() == 0 and oracle.fdvUsd() reverts InvalidOracle. Expected per brief: tier 1 (p=3500) after sustained FDV >= $100K.","severity":"medium","snippet":"        bool liquid = sqrtPrice != 0 && liquidity >= MIN_LIQUIDITY && depth >= MIN_QUOTE_DEPTH\n            && protocolFee == 0 && lpFee == 12500;","title":"10 ETH quote-depth floor can exceed the ETH needed to reach the $100K first tier, so tier 1 cannot confirm while spot/TWAP FDV is above it"},{"citation":"resolved","description":"The milestone input is a 30-minute TWAP of the pool's own tick plus a 1-hour confirmation (CONFIRMATION). Nothing bounds the capital or makes the move costly beyond pool fees: with a single factory token-only position (L ~ 1.375e5 for 870M tokens from sqrtP ~1.58e-4 ETH^0.5/token), raising price from the ~$50K opening FDV to $5M (100x) costs ~L*(sqrt(p1)-sqrt(p0)) ~ 196 ETH of buys. The buyer is credited E for all bought tokens, and once tier 6 sets fullyUnlocked (irreversible, 'price falls never relock') they sell everything back, losing ~1.25% each way (~5 ETH) plus price-impact loss to any counter-traders in the 90 min. The quota scheme for all holders is permanently disabled by one well-funded actor. Brief asks to reproduce this sustained-price manipulation; it is unmitigated.","line":500,"path":"src/UnlkHook.sol","reproduction":"Fork/local fixture: activate; attacker buys ~200 ETH via UnlkSwapRouter (FDV >= $5M at ETH $2,000); call hook.updateMilestone() every <=5 minutes for 90 minutes (30m TWAP fill + 60m confirmation) -> tier()==6, fullyUnlocked()==true; attacker sells all bought tokens (E = bought, quota bypassed), recovering ~195 ETH. Expected: unlock should not be purchasable at a cost of a few ETH; actual: permanent bypass for every wallet.","severity":"medium","snippet":"            if (tier == 6) fullyUnlocked = true;","title":"Permanent full unlock for everyone can be bought for ~2.5% round-trip fees by holding price at $5M FDV for 90 minutes"}],"hash":"82b79b8400a5022c5e73cec547b62e0991405a60b5201acc4896ad0a06f0964b","nodeId":"a03c0b2b-d295-4c2e-8cb3-73078f29ae3c","outcome":"completed","summary":"I wrote `.imd-findings.json` with two medium findings and 24 coverage rows: all 23 listed entry points plus one invariant row. I changed no repository files and did not run any new tests.\n\n**Findings**\n1. **The 10 ETH depth floor blocks the first tier** (`src/UnlkOracle.sol:100`). This is the conflict the brief asked me to reproduce. With ETH at $2,000, a 9.5 ETH buy takes FDV past $100K while the seed position still holds less than 10 ETH. The oracle then reverts, and the hook clears the qualifying timers each time, so the $100K tier can never confirm at that point. The existing test `testDepthFloorBlocksFirstTierAtSpotAbove100k` in `test/TwoStage.t.sol` shows this, and it is still unresolved. Fixing it means changing either the floor or the threshold, which is a scope decision for you.\n2. **Anyone with enough ETH can buy a permanent full unlock** (`src/UnlkHook.sol:500`). The milestone check uses only the pool's own 30-minute average price plus a one-hour confirmation. By my rough estimate from the test setup's seed (I did not run it), about 196 ETH of buys lifts FDV from about $50K at launch to $5M. The buyer then calls `updateMilestone()` at least every 5 minutes for 90 minutes, and quotas are bypassed for every wallet. The buyer can sell everything back, since the unlock never relocks, losing roughly 2.5% in fees, about 5 ETH. This is the sustained-price manipulation the brief asked about; nothing in the code prevents it.\n\n**What I checked that holds:**\n- The quota check rounds down correctly and uses the actual token amount from each swap, and a failed swap changes nothing.\n- The time-weighted price rounds in the conservative direction for this token's position in the pair.\n- The FDV calculation and the Chainlink ETH/USD checks are correct.\n- Tiers can step up several at once and never go back down.\n- The 3% payer credit happens once, and reward credits count only newly claimed amounts, capped at the swarm allocation.\n\n**Not reviewed:** the six router entry points and `MerkleClaimEvidence.attest` are outside the math scope and marked `unreached`. Neither finding has a Foundry proof file, since proofs were only required for critical or high findings.","treeHash":null,"usage":{"cachedInputTokens":414069,"inputTokens":14,"model":"claude-opus-5-5","outputTokens":9667,"runtime":"claude","turns":8,"wallClockMs":105492}},{"artifacts":[],"attempt":1,"bundleHash":"0dcbb533b92aca62ac0ac07e8dd827a7fdb68988b0fa974bbd6394383de697ee","device":"3a720671678111bc","findings":[{"description":"MerkleClaimEvidence.attest proves a claim by (1) the distributor's boolean claimed[round][account] flag and (2) a Merkle proof of (account, amount) against the round's root. The verified MerkleDistributor stores no amount, so when a round's tree contains two leaves for one account (OpenZeppelin's StandardMerkleTree does not forbid duplicates, and IMD's builder is not verifiable from here), the wallet can natively claim the smaller leaf (which sets the flag) and then attest the larger one. hook.syncRewards then adds the larger amount to E, so the wallet gains sale quota on tokens it never received. The over-credit is bounded by the tree's largest duplicate leaf and requires IMD's tree to be malformed; the adapter cannot distinguish the two leaves on chain. README already records this as an unenforceable assumption (known issue 4); this finding gives it a concrete failing input. Possible fixes: the adapter records only the first attested amount per account and refuses any amount larger than the round's per-account maximum visible to it (not available on chain), or the credit is reconciled against the distributor's roundOf(round).claimed delta observed between two attestations (only exact when one claim happens between them), or IMD's tree builder guarantees one leaf per account per round and states so in the launch record.","line":50,"path":"src/MerkleClaimEvidence.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MerkleProof} from \"@openzeppelin/contracts/utils/cryptography/MerkleProof.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {UnlkToken} from \"src/UnlkToken.sol\";\nimport {MerkleClaimEvidence} from \"src/MerkleClaimEvidence.sol\";\nimport {IMerkleDistributor} from \"src/interfaces/IIntegration.sol\";\n\n/// @dev The claim path of IMD's verified `MerkleDistributor`, reduced to what the adapter reads: a per-round,\n/// per-account `claimed` flag (no amount), the round's root, and a transfer of exactly the proven leaf amount.\ncontract DistributorStandIn {\n    struct Round {\n        bytes32 root;\n        uint256 funded;\n        uint256 claimed;\n        uint64 unlocksAt;\n    }\n\n    IERC20 public immutable token;\n    address public immutable opener;\n    Round[] private rounds;\n    mapping(uint256 => mapping(address => bool)) public claimed;\n\n    constructor(IERC20 token_) {\n        token = token_;\n        opener = msg.sender;\n    }\n\n    function openRound(bytes32 root, uint256 funded) external {\n        rounds.push(Round(root, funded, 0, uint64(block.timestamp)));\n    }\n\n    function claim(uint256 round, address account, uint256 amount, bytes32[] calldata proof) external {\n        Round storage current = rounds[round];\n        require(!claimed[round][account], \"AlreadyClaimed\");\n        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));\n        require(MerkleProof.verifyCalldata(proof, current.root, leaf), \"InvalidProof\");\n        claimed[round][account] = true;\n        current.claimed += amount;\n        require(token.transfer(account, amount));\n    }\n\n    function roundOf(uint256 round) external view returns (Round memory) {\n        return rounds[round];\n    }\n\n    function roundCount() external view returns (uint256) {\n        return rounds.length;\n    }\n\n    function swept(uint256) external pure returns (bool) {\n        return false;\n    }\n}\n\n/// @notice A round whose tree holds two leaves for one wallet (OpenZeppelin's StandardMerkleTree does not\n/// forbid it). The distributor lets the wallet claim exactly one of them; the adapter then accepts the OTHER\n/// leaf as evidence, because it only sees the boolean flag and a valid proof. Credit exceeds delivery.\ncontract Proof_DuplicateLeafOverCredit is Test {\n    address alice = address(0xA11CE);\n\n    function _leaf(address account, uint256 amount) internal pure returns (bytes32) {\n        return keccak256(bytes.concat(keccak256(abi.encode(account, amount))));\n    }\n\n    function _pair(bytes32 a, bytes32 b) internal pure returns (bytes32) {\n        return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));\n    }\n\n    function testEvidenceNeverExceedsWhatTheDistributorDelivered() public {\n        UnlkToken token = new UnlkToken();\n        DistributorStandIn distributor = new DistributorStandIn(IERC20(address(token)));\n        token.transfer(address(distributor), 600 ether);\n        bytes32 small = _leaf(alice, 100 ether);\n        bytes32 large = _leaf(alice, 500 ether);\n        distributor.openRound(_pair(small, large), 600 ether);\n        MerkleClaimEvidence evidence =\n            new MerkleClaimEvidence(IMerkleDistributor(address(distributor)), address(token));\n\n        bytes32[] memory proofSmall = new bytes32[](1);\n        proofSmall[0] = large;\n        distributor.claim(0, alice, 100 ether, proofSmall);\n        assertEq(token.balanceOf(alice), 100 ether, \"the distributor delivered the small leaf\");\n\n        bytes32[] memory proofLarge = new bytes32[](1);\n        proofLarge[0] = small;\n        // Expected: evidence for a leaf the distributor never paid is refused, or at least never counted.\n        try evidence.attest(0, alice, 500 ether, proofLarge) {} catch {}\n        assertEq(\n            evidence.claimed(alice),\n            token.balanceOf(alice),\n            \"the adapter credits more than the distributor delivered\"\n        );\n    }\n}","reproduction":"Open a round whose tree is [(alice, 100e18), (alice, 500e18)]. Call distributor.claim(0, alice, 100e18, [leaf(alice,500e18)]): alice receives 100e18 and claimed[0][alice] = true. Call evidence.attest(0, alice, 500e18, [leaf(alice,100e18)]). Expected: refused or not counted, evidence.claimed(alice) == 100e18 == alice's balance. Actual: accepted, evidence.claimed(alice) == 500e18 while alice holds 100e18; hook.syncRewards(alice) then credits 500e18 of E.","severity":"low","title":"Claim evidence accepts a second leaf for the same wallet in one round, crediting more than the distributor delivered"},{"description":"UnlkOracle marks an observation liquid only when the seed position holds at least MIN_QUOTE_DEPTH = 10 ETH of actual quote principal. With the 870M token-only seed opening at about 25 ETH FDV and ETH at $2,000, about 9.5 ETH of buys lifts spot FDV past $100K while the seed holds under 10 ETH, so fdvUsd() reverts and tier 1 cannot confirm until roughly $137K FDV. At higher ETH prices the gap widens. The conflict is structural between a fixed ETH floor and a fixed USD threshold; the brief forbids changing either silently. Options are the requester's: lower MIN_QUOTE_DEPTH, raise the first threshold, or express the floor in USD through the same feed. Each is a one-constant change in UnlkOracle with a matching pin in UnlkHook._verifyOracle.","line":21,"path":"src/UnlkOracle.sol","reproduction":"test/TwoStage.t.sol testDepthFloorBlocksFirstTierAtSpotAbove100k: after activation, bob buys 9.5 ETH; spot FDV at $2,000/ETH >= 100,000e18 and the seed's quote depth < 10 ether; two full confirmation cycles later hook.tier() is still 0 and oracle.fdvUsd() reverts InvalidOracle. Expected by the brief: the $100K tier confirms once the TWAP holds above it for an hour.","severity":"medium","title":"Fixed 10 ETH quote-depth floor can make the first $100K tier unreachable (unresolved audit finding, requester decision required)"},{"description":"_updateMilestone starts qualifyingSince for every tier above the current one in the same call, so one actor who lifts the 30-minute TWAP past $5M FDV and holds it for a further hour with calls at most 5 minutes apart has tiers 1-6 confirm together and fullyUnlocked becomes true permanently, including for transfer-only holders. Counter-selling cannot discipline this because sales are themselves capped by the quota rule until the bypass. The reviewer measured about 206 ETH of buys at $2,000/ETH absorbing about 37 ETH of quota-limited counter-sales and about 203 ETH recovered on exit: roughly 40 ETH net for a permanent bypass. Mitigations (one tier per confirmation period, a longer confirmation for the bypass tier, or depth-weighted confirmation) change honest paths too and were not made unilaterally.","line":499,"path":"src/UnlkHook.sol","reproduction":"test/Oracle.t.sol testSustainedManipulationRequiresWindowAndConfirmation: after the warm-up, alice buys 500 ETH, 30 minutes pass, hook.tierForFdv(oracle.fdvUsd()) == 6; after twelve updateMilestone calls 5 minutes apart hook.fullyUnlocked() is true. Expected by the brief's intent: a single actor's temporary capital should not permanently remove every wallet's quota.","severity":"medium","title":"Sustained price manipulation for 90 minutes buys a permanent quota bypass for every wallet (unresolved audit finding, scope decision required)"},{"description":"pendingRewardCredit reverts InvalidEvidence when rewards.claimed(recipient) > token.SWARM_ALLOCATION(). The cap is a sanity bound, but it is all-or-nothing: once exceeded (only possible if the treasury funds later rounds beyond the 100M the factory placed in the distributor), syncRewards reverts permanently for that wallet, including the part under the cap that was never credited. Fail-closed and bounded, recorded so operators know the treasury must not pay any single wallet more than 100M UNLK across rounds if that wallet is to keep sale rights.","line":448,"path":"src/UnlkHook.sol","reproduction":"test/EvidenceAndOracleProperties.t.sol testEvidenceBeyondTheSwarmAllocationIsRefusedNotTruncated: with rewards.claimed(alice) written to 100,000,000e18 + 1, hook.pendingRewardCredit(alice) and hook.syncRewards(alice) revert InvalidEvidence and eligible(alice) stays 0; at exactly 100,000,000e18 the full amount is credited.","severity":"info","title":"A wallet whose cumulative native claims exceed the 10% swarm allocation can never synchronize any reward credit"},{"description":"beforeSwap refuses every swap while getSlot0 reports protocolFee != 0 or lpFee != 12500, and the oracle marks such observations illiquid. If Uniswap governance ever sets a protocol fee on this pool, the only authorized sell route is gone for the life of the token with no admin to restore it; transfers and native claims keep working. This is the price of the sealed 1.25% (1%/0.25%) fee promise. Sepolia's PoolManager has protocolFeeController() == address(0) today.","line":310,"path":"src/UnlkHook.sol","reproduction":"test/Adversarial.t.sol testProtocolFeeChangeCannotSilentlyAlterTradingEconomics: after manager.setProtocolFee(key, 500) a router buy reverts and eligible stays 0, while an ERC-20 transfer succeeds.","severity":"info","title":"A PoolManager protocol fee on this pool halts all router trading permanently (accepted fail-closed dependency)"}],"hash":"84421ef9cbf9951add31d11ba0b80a0ec56fcb7f2abc3c85b2406ffff1e1bd4b","nodeId":"315b34f1-d462-4521-9422-a1c76c9dd372","outcome":"completed","summary":"Done. The delivered suite is green and the findings file is written.\n\n**What I added** (one new file, `test/EvidenceAndOracleProperties.t.sol`, nine tests; nothing in the accepted suites was touched):\n\n- **Real four-leaf Merkle tree** through the verified distributor and the adapter. Every earlier test used a one-leaf tree with an empty proof, so the sibling-hashing path never ran. Each wallet's credit now equals exactly what the distributor delivered to it, proven with its own path. A sibling's path, a sibling's amount, a truncated path, a reordered path, an unclaimed leaf and a nonexistent round are all refused and leave no credit. One claim is made before activation and credited after it.\n- **Swept rounds** freeze evidence: unclaimed leaves can never be attested, earlier claims still can, and the treasury's sweep creates no rights.\n- **Swarm-cap fail-closed path**: evidence above 100M UNLK is refused outright, not truncated; exactly 100M is accepted.\n- **Brute-force TWAP reference**: random buy, sell and checkpoint sequences with random gaps, then the oracle's binary-search result is compared with a direct integration of its own observation log, including the floor rounding and the illiquid-seconds rule. A scratch split check showed 15 illiquid and 25 valid runs across 40 seeds, so both branches are exercised.\n- **Feed monotonicity** fuzz, the $1M/ETH ceiling boundary, same-second checkpoint replacement, and the router's inclusive deadline edge.\n\n**Verification**\n\n| Run | Result |\n| --- | --- |\n| `forge test` excluding scratch | 124 passed, 0 failed |\n| Protected admission tests, unmodified, current creation code | 11 passed |\n\n**Findings reported** in `.imd-findings.json` (five entries). One is new: a round whose tree holds two leaves for one wallet lets the wallet claim the smaller leaf natively and attest the larger one, because the distributor stores only a boolean flag. The scratch proof fails on the current adapter with credit 500e18 against 100e18 delivered. It is rated low since it requires a malformed tree from IMD's builder and is bounded by that tree. The other four restate the unresolved audit items so the judge sees them in this channel: the 10 ETH depth floor against the first tier, the 90-minute sustained-manipulation bypass, the swarm-cap lockout, and the protocol-fee fail-closed halt.\n\n**Not done**: no Sepolia deployment, address, transaction or fork rehearsal exists, as before. The findings file is excluded from git by the harness and the docs directory is outside my write scope, so the results above are recorded here rather than in `docs/TEST_RESULTS.txt`.","treeHash":"2b921f9d5c6528268739b7cd5e951c0d3ad54171","usage":{"cachedInputTokens":2335327,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":45938,"runtime":"claude","turns":39,"wallClockMs":726950}},{"artifacts":[],"attempt":1,"bundleHash":"78c0c5a763e6e3756025abf77c994b507144620e6096cb193ef28fa84ca6f4cf","device":"809ae05b6282abad","findings":[],"hash":"93d0f85cf9aca9dccebe5ab30e7a40dd0d5e31a932f68746bd0c688595efaa7c","nodeId":"f825d7ca-ecfd-4712-9218-adab59a161a0","outcome":"completed","summary":"Created [launch.json](/root/.identitymd/work/98dbc132-2f46-4991-ba25-abdd397da90f/f825d7ca-ecfd-4712-9218-adab59a161a0/launch.json) with matching constructor inputs, eight permissions, and required pool settings. Only the manifest is changed.\n\nValidation: build succeeded; 87 repository tests and 11 unchanged protected tests passed.\n\nNotes document unresolved deployment blockers. No deployment was performed.","treeHash":"5e158045dce0aee376bf545bf1b81dfb7b09ae9c","usage":{"cachedInputTokens":606976,"inputTokens":64321,"model":"gpt-6-astra","outputTokens":7494,"runtime":"codex","turns":4,"wallClockMs":262917}},{"artifacts":[],"attempt":1,"bundleHash":"de8fcb70b33289f35154c9297ce766766d1c58b9b499f74bf10e6718d2361288","device":"3a40eaafbd83a6bc","findings":[{"description":"configureAndActivate takes positionOf(launchNumber).requester as `payer`, and syncPayer credits that wallet PAYER_ALLOCATION (30,000,000 UNLK) of eligibility. In the verified IMD LaunchFactory, `requester` (who shares LP fees; changeable by its holder through setRequester at any time before activation) and `remainderTo` (who actually receives the 3% remainder) are two independent launch inputs. The hook never checks that the recorded requester received the allocation. When they differ, a wallet holding none of the allocation gains 30M of sale rights while the wallet holding the 30M tokens has none, and the brief's requirement to prove the initial allocation recipient from launch evidence is not met. The README documents this as an assumption (requester == remainderTo); it is not enforced on chain and the setRequester path lets the requester move the rights before activation.","line":261,"path":"src/UnlkHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.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 {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {UnlkHook} from \"src/UnlkHook.sol\";\nimport {UnlkToken} from \"src/UnlkToken.sol\";\nimport {UnlkOracle} from \"src/UnlkOracle.sol\";\nimport {UnlkSwapRouter} from \"src/UnlkSwapRouter.sol\";\nimport {MerkleClaimEvidence} from \"src/MerkleClaimEvidence.sol\";\nimport {\n    IUnlkHookView,\n    IMerkleDistributor,\n    IChainlinkEthUsd,\n    ILaunchRegistryView\n} from \"src/interfaces/IIntegration.sol\";\n\n/// @dev The subset of IMD's factory the hook reads at activation, with the policy split the real factory performs:\n/// 10% to the distributor, the seed into the pool, the remainder (3%) to `remainderTo`, and `requester` recorded\n/// separately as the fee-share wallet. The two are distinct fields in the verified factory ABI.\ncontract MinimalFactory is IUnlockCallback {\n    struct Artifact {\n        bytes32 role;\n        bytes32 name;\n        address addr;\n    }\n\n    IPoolManager public immutable poolManager;\n    MinimalRegistry public immutable registry;\n    PoolKey internal key;\n    int24 internal lower;\n    int24 internal upper;\n    address internal requesterAddr;\n    uint128 internal liquidity;\n    UnlkToken internal token;\n\n    constructor(IPoolManager manager_) {\n        poolManager = manager_;\n        registry = new MinimalRegistry();\n    }\n\n    function deployToken() external returns (UnlkToken t) {\n        t = new UnlkToken();\n        token = t;\n    }\n\n    function deployHook(bytes memory initCode, bytes32 salt) external returns (address hook) {\n        assembly (\"memory-safe\") {\n            hook := create2(0, add(initCode, 0x20), mload(initCode), salt)\n        }\n        require(hook != address(0), \"hook deploy failed\");\n    }\n\n    function launch(address hook, address distributor, address remainderTo, address requester_) external {\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12500, 60, IHooks(hook));\n        lower = -887220;\n        upper = 174960;\n        requesterAddr = requester_;\n        uint160 sqrt = TickMath.getSqrtPriceAtTick(upper);\n        liquidity = uint128(uint256(870_000_000 ether) * (1 << 96) / (sqrt - TickMath.getSqrtPriceAtTick(lower)));\n        poolManager.initialize(key, sqrt);\n        poolManager.unlock(\"\");\n        token.transfer(distributor, 100_000_000 ether);\n        token.transfer(remainderTo, token.balanceOf(address(this)));\n        registry.record(address(token), hook, distributor);\n    }\n\n    function unlockCallback(bytes calldata) external returns (bytes memory) {\n        (BalanceDelta delta,) = poolManager.modifyLiquidity(\n            key, ModifyLiquidityParams(lower, upper, int256(uint256(liquidity)), bytes32(0)), \"\"\n        );\n        uint256 due = uint256(uint128(-delta.amount1()));\n        poolManager.sync(key.currency1);\n        token.transfer(address(poolManager), due);\n        poolManager.settle();\n        return \"\";\n    }\n\n    function positionOf(uint64) external view returns (PoolKey memory, int24, int24, address) {\n        return (key, lower, upper, requesterAddr);\n    }\n}\n\ncontract MinimalRegistry {\n    ILaunchRegistryView.Artifact[] internal arts;\n\n    function record(address token, address hook, address distributor) external {\n        arts.push(ILaunchRegistryView.Artifact(\"token\", \"token\", token));\n        arts.push(ILaunchRegistryView.Artifact(\"hook\", \"hook\", hook));\n        arts.push(ILaunchRegistryView.Artifact(\"distributor\", \"distributor\", distributor));\n    }\n\n    function artifactsOf(uint64) external view returns (ILaunchRegistryView.Artifact[] memory) {\n        return arts;\n    }\n}\n\ncontract MinimalDistributor {\n    address public token;\n    address public opener;\n\n    constructor(address token_, address opener_) {\n        token = token_;\n        opener = opener_;\n    }\n\n    function claimed(uint256, address) external pure returns (bool) {\n        return false;\n    }\n\n    function roundOf(uint256) external pure returns (IMerkleDistributor.Round memory r) {\n        return r;\n    }\n}\n\ncontract MinimalFeed is IChainlinkEthUsd {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @title The 3% \"payer\" sale rights follow the factory's fee-share `requester`, not the wallet that received the\n/// allocation.\n/// @notice The factory records `remainderTo` (who receives the 3%) and `requester` (who shares LP fees) as two\n/// independent inputs. `UnlkHook.configureAndActivate` takes `positionOf(launchNumber).requester` as the payer\n/// and `syncPayer` credits it 30,000,000 UNLK of eligibility. When the two differ, a wallet holding no allocation\n/// gains 30M of sale rights while the wallet holding the 30M tokens has none.\ncontract PayerAttributionProof is Test {\n    address constant OWNER = address(0xA11CE);\n    address constant RECIPIENT = address(0xB0B);\n    address constant FEE_SHARER = address(0xCA201);\n\n    function testPayerCreditGoesToTheWalletThatReceivedTheAllocation() public {\n        vm.warp(1_800_000_000);\n        PoolManager manager = new PoolManager(address(this));\n        MinimalFactory factory = new MinimalFactory(manager);\n        UnlkToken token = factory.deployToken();\n        bytes memory initCode =\n            bytes.concat(type(UnlkHook).creationCode, abi.encode(manager, token, address(factory), OWNER));\n        bytes32 codeHash = keccak256(initCode);\n        bytes32 salt;\n        for (uint256 i;; ++i) {\n            salt = bytes32(i);\n            address predicted = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, codeHash))))\n            );\n            if (uint160(predicted) & 0x3fff == 0x3fc0) break;\n        }\n        UnlkHook hook = UnlkHook(factory.deployHook(initCode, salt));\n        MinimalDistributor distributor = new MinimalDistributor(address(token), address(factory));\n        // The launch: RECIPIENT receives the 3% allocation; FEE_SHARER is recorded as the requester.\n        factory.launch(address(hook), address(distributor), RECIPIENT, FEE_SHARER);\n        assertGe(token.balanceOf(RECIPIENT), 30_000_000 ether, \"the allocation went to RECIPIENT\");\n        assertEq(token.balanceOf(FEE_SHARER), 0);\n\n        MerkleClaimEvidence rewards =\n            new MerkleClaimEvidence(IMerkleDistributor(address(distributor)), address(token));\n        UnlkSwapRouter router = new UnlkSwapRouter(manager, IUnlkHookView(address(hook)));\n        UnlkOracle oracle = new UnlkOracle(manager, new MinimalFeed(), address(hook));\n        vm.prank(OWNER);\n        try hook.configureAndActivate(address(router), rewards, oracle, 1) {}\n        catch {\n            // Refusing to activate when the recorded requester does not hold the allocation is an acceptable fix.\n            return;\n        }\n        hook.syncPayer();\n        assertEq(hook.eligible(FEE_SHARER), 0, \"a wallet that received no allocation was credited 30M of sale rights\");\n        assertEq(\n            hook.eligible(RECIPIENT),\n            token.PAYER_ALLOCATION(),\n            \"the wallet holding the 3% allocation has no sale rights\"\n        );\n    }\n}","reproduction":"Launch through the factory with remainderTo = X and requester = Y (X != Y). Deploy companions, owner calls configureAndActivate(router, rewards, oracle, launchNumber), then anyone calls syncPayer(). Expected: X (holding >= 30,000,000 UNLK from the launch) has eligible == 30,000,000e18, Y has 0, or activation refuses the mismatched record. Actual: hook.payer() == Y, eligible(Y) == 30,000,000e18, eligible(X) == 0. The scratch proof below reproduces it with a minimal factory implementing the exact views the hook reads.","severity":"medium","title":"3% payer sale rights follow the factory's fee-share requester, not the wallet that received the allocation"},{"description":"UnlkOracle marks an observation illiquid unless the seed position holds at least MIN_QUOTE_DEPTH = 10 ETH of actual quote principal. At $2,000/ETH a 9.5 ETH buy already pushes spot FDV above $100,000 while the seed holds < 10 ETH, so the 35% tier cannot confirm and fdvUsd() reverts (test/TwoStage.t.sol testDepthFloorBlocksFirstTierAtSpotAbove100k). Whether the floor or the first threshold should move is a requester decision; neither was changed silently. Reported, not asserted correct.","line":21,"path":"src/UnlkOracle.sol","reproduction":"Activated system, ETH/USD = $2,000: _buy(bob, 9.5 ether); spot FDV >= $100K; seed depth < 10 ETH; run two full 90-minute confirmation cycles with 5-minute checkpoints. Expected by the brief's tier table: tier 1 (35%). Actual: tier 0, oracle.fdvUsd() reverts InvalidOracle.","severity":"medium","title":"Audit finding reproduced and unresolved: 10 ETH quote-depth floor can block the first $100K tier"},{"description":"An actor holding the pool price above a threshold for the 30-minute TWAP window plus the 1-hour confirmation, with checkpoints at most 5 minutes apart, confirms the tier (test/Oracle.t.sol testSustainedManipulationRequiresWindowAndConfirmation reaches full unlock from a 500 ETH buy held 90 minutes). Only capital at risk against arbitrage deters it; an on-chain TWAP cannot distinguish it from a lasting market price. Same-block and single-block spikes are correctly weightless.","line":122,"path":"src/UnlkOracle.sol","reproduction":"Activated system with >= 10 ETH depth: _buy(alice, 500 ether); advance 30 min + 12 x 5 min calling updateMilestone each time. Actual: fullyUnlocked() == true after 90 minutes of sustained price.","severity":"low","title":"Audit finding reproduced and unresolved: sustained price manipulation reaches tiers"},{"description":"beforeSwap reverts InvalidPool when protocolFee != 0 so the promised 1.25% total fee cannot change silently. The hook is deliberately immutable with no admin, so if Uniswap governance (or on Sepolia, the PoolManager owner) ever sets a protocol fee on this pool, every swap reverts forever and 87% of supply stays locked in the seed position. Transfers and native claims keep working. This is a liveness trade-off the requester should accept explicitly (test/Adversarial.t.sol testProtocolFeeChangeCannotSilentlyAlterTradingEconomics shows the halt).","line":298,"path":"src/UnlkHook.sol","reproduction":"manager.setProtocolFeeController(controller); manager.setProtocolFee(key, 500); any router.swap reverts InvalidPool; no function on the hook or companions can restore trading.","severity":"low","title":"A PoolManager protocol fee on this pool halts trading permanently with no recovery path"},{"description":"configureAndActivate checks router.hook()/router.poolManager(), oracle fields and constants, and the evidence contract's token/distributor, but cannot check that the deployed bytecode is the reviewed UnlkSwapRouter/UnlkOracle/MerkleClaimEvidence (immutables vary the runtime, so no codehash constant exists). A setup authority that installs a router whose context() reports an arbitrary trader could consume other wallets' quotas, and an evidence contract returning arbitrary claimed() would mint eligibility. This is a one-time trust in the setup authority; the brief's 'verify reviewed implementations, deployed code' must therefore be done off chain (Etherscan/Sourcify match of the three companion addresses) before the owner signs configureAndActivate. The stage-2 script deploys them itself, which bounds the risk, but the hook cannot prove it.","line":212,"path":"src/UnlkHook.sol","reproduction":"Design observation; no failing input against the reviewed companions. A malicious router passing hook()/poolManager() checks is accepted by the hook.","severity":"info","title":"Activation verifies companions by reciprocal bindings and constants, not by code identity"},{"description":"ActionHandler.buy called router.swap without msg.value on a native-ETH pool and derived tokenIs0 from the WETH fixture address, so every buy reverted inside try/catch and the ledger invariants were checked only against zero buys. Fixed in place (value attached, tokenIs0 = false); test/QuotaInvariants.t.sol adds a handler whose smoke test asserts its counters are nonzero so this cannot regress silently.","line":65,"path":"test/Invariants.t.sol","reproduction":"Before the fix: handler.successfulBuys() == 0 after any sequence; ghostBuys all zero.","severity":"info","title":"Pre-existing invariant handler never executed a buy (test quality; fixed in this submission)"},{"description":"No Sepolia address, transaction, PoolKey/PoolId or verification exists; the local fork rehearsal runs against the verified mainnet factory sources, not an authenticated Sepolia factory (README table). Stage 2 requires three owner deployments plus configureAndActivate from the hook's immutable setupAuthority; no IMD service performs post-launch calls, so if the owner cannot send them, stage 1 must not be deployed. README/docs are outside this task's write scope (test/** only) and were not changed; test/FailurePaths.t.sol testSepoliaActivatesWithThePublishedFeedAndTrades rehearses the Sepolia chain-id path with the published Chainlink proxy address.","line":72,"path":"README.md","reproduction":"Not applicable: labels unexecuted work.","severity":"info","title":"Deployment status: nothing executed on Sepolia; stage 2 depends on an owner-signed transaction IMD does not provide"}],"hash":"9abb951f77a35640dfabcf7a8b0de9abd3e791d10c47afe6a2aa730ae2238ec8","nodeId":"315b34f1-d462-4521-9422-a1c76c9dd372","outcome":"completed","summary":"The findings file is on disk at the repository root; the workspace's own `.git/info/exclude` keeps it out of git by design, so the platform reads it from disk. Everything is complete.\n\n## Summary\n\n**Result: 111 tests pass, 0 fail**, up from 87. All changes are under `test/`, plus `.imd-findings.json` at the root with 7 findings (1 with a self-contained proof). The implementation in `src/` was not touched.\n\n**What I found and fixed in the existing tests**\n\nThe prior invariant handler in `test/Invariants.t.sol` never executed a single buy. It called the router without `msg.value` on a native-ETH pool and derived the swap direction from the WETH fixture's address, so every buy reverted inside a `try/catch` and the ledger invariants were only ever checked against zero buys. I repaired it in place with a two-line change. The invariants still hold with real activity.\n\n**New test files**\n\n- `test/QuotaInvariants.t.sol`: a handler with 15 actions on the activated system, including exact-input and exact-output native buys, in-quota and over-quota sales, exact-output sales, refund withdrawal and theft attempts, transfers, native claim plus attestation, credit syncs, time advance with milestone checks, a moving ETH/USD feed, factory fee claims, donations, direct manager swaps and re-activation attempts. Seven invariants cover quota enforcement, ledger equals executed deltas, supply conservation and no custody, router ETH solvency, principal locked with fee terms fixed, milestone monotonicity and sealed configuration. A smoke test asserts the handler's counters are nonzero so the suite cannot regress into the vacuous state the old one had.\n- `test/StageOneInvariants.t.sol`: stage 1 alone under random pressure. Random callers, companions and launch numbers attempt activation. Invariants prove the hook activates exactly when the owner sent the one correct call, is inert before that (price frozen, no rights, no syncs, no foreign liquidity), and that transfers and claims work throughout.\n- `test/FailurePaths.t.sol`: 21 tests. Router degenerate inputs (zero, `int256.min`, `int128.max + 1`, zero min output, past deadline, wrong value), batch bounds and atomic rollback, slippage refusals leave no trace, partial fill at a price limit charges only executed input, exact output cannot partially fill, hook callback malformed data and unknown senders, full unlock keeps fees and router authentication, quota widens correctly on tier change, the confirmation boundary is exactly one hour, requester change after activation does not move the payer, second-round claims accumulate once per round, a real second launch from the same factory is refused in every cross-launch direction, the Sepolia chain-id path activates with the published Chainlink proxy address and refuses the local feed, companion constructors reject missing inputs, an unpaid launch credits nobody, and four fuzz properties (quota formula via storage writes, tier monotonicity, operation hash binding to trader, nonce and chain, exact transfers).\n\n**Findings reported rather than tested around** (`.imd-findings.json`)\n\n1. **Medium, with proof**: the 3% payer sale rights follow the factory's fee-share `requester`, not the wallet that received the allocation. The factory records these as independent inputs and the hook never checks the requester holds the tokens. The proof uses a minimal in-file factory implementing the exact views the hook reads and fails on current code.\n2. **Medium**: the 10 ETH depth floor can block the first $100K tier (existing audit finding, reproduced, unresolved).\n3. **Low**: sustained 90-minute price manipulation reaches tiers (existing audit finding, reproduced).\n4. **Low**: a PoolManager protocol fee on this pool halts trading permanently with no recovery.\n5. **Info**: activation verifies companions by reciprocal bindings only, so code identity must be verified off chain before the owner signs.\n6. **Info**: the pre-existing handler defect above.\n7. **Info**: deployment s","treeHash":"ebdc7187d4c16693b32fe84dc310f015f732ee45","usage":{"cachedInputTokens":4255769,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":81270,"runtime":"claude","turns":61,"wallClockMs":1307979}},{"artifacts":[],"attempt":1,"bundleHash":"b89b110f9d9d6236d2570c90bf61be79d183e8827ffa15b7500438e6721176c1","device":"4dd74fd7c315da80","findings":[],"hash":"a9d70288472dc27bd38703d8d71fa729d7c6d446b813a3a5c7387434fc31778f","nodeId":"f825d7ca-ecfd-4712-9218-adab59a161a0","outcome":"completed","summary":"The manifest is done and verified. Only launch.json changed in the working tree.\n\n**What I found.** The existing manifest already had the correct shape: the four-argument constructor (`$poolManager`, `$token`, `$factory`, `$owner`) and the eight permissions that match the hook source. But its notes were stale against the latest accepted commit. They pinned an older commit, described the Chainlink feed age as one hour when the accepted code now uses 90 minutes, reported 87 tests when the suite has 91, and omitted the later fixes the other contributor landed.\n\n**What I changed.** I rewrote only the notes field, leaving every structural field untouched:\n\n- Pinned the current tree at commit 20a99d3 over the launch-1309-unlk baseline, with no rebuild.\n- Recorded the later accepted fixes: 90-minute feed age, five-minute checkpoint age re-checked at activation, invalid oracle data no longer erasing a pending confirmation, and activation refusing a requester that no longer holds the 3% allocation.\n- Named the one Sepolia contract that answers the factory ABI, marked as unverified source, and the Stage 0 confirmation IMD must give before stage 1.\n- Kept the blocked-deployment statement, the unresolved audit findings, and the \"unexecuted\" labels for all on-chain evidence.\n- Trimmed the notes to fit the 4000-character cap.\n\n**Verification at this commit.**\n\n| Check | Result |\n| --- | --- |\n| forge build | passed, warnings only |\n| forge test | 91 passed, 0 failed, 0 skipped |\n| Protected admission tests, run unmodified from test/scratch with manifest probes | 11 passed, 0 failed, 0 skipped |\n| Checked-in ABIs vs fresh build | all six match |\n| Manifest JSON | parses, first key is `kind`, notes 3872 characters |\n\nThe hook's flag constant 0x3fc0 equals the eight declared permissions, which the protected permissions test confirmed. Deployment remains blocked as the notes state: no Sepolia addresses or transactions exist, and the stage-2 activation route still needs an authenticated IMD operation.","treeHash":"442f4a4af79147a88457da3b9e84f88dd86ccf5f","usage":{"cachedInputTokens":1058294,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":14235,"runtime":"claude","turns":18,"wallClockMs":205105}},{"artifacts":[],"attempt":2,"bundleHash":"c0138e1fa001579c889a6f790da454a2fea3844e42e2c6d7a4a2088e2aeab9e2","device":"4ca9ed4f0937da89","findings":[],"hash":"cfb09656835a244bd3a8c89b115ad830eac808e56e7543bf94fb78e88132d14f","nodeId":"706cda53-04c9-4256-9b90-862649f8e62f","outcome":"completed","summary":"I've rebuilt UNLOCK (UNLK) as two stages from the baseline commit `eb37eb6`, but **nothing is deployed**. There are no Sepolia addresses, transactions, PoolKey/PoolId or verification results for this launch. The baseline commit is the latest on every branch of the launch repo, so I carried its code forward rather than starting over. `forge build` and `forge fmt --check` pass, and `forge test` gives 87 passed, 0 failed (saved in `docs/TEST_RESULTS.txt`).\n\n**The blocker fix**\n- **Constructor:** the hook now takes only the four supported launch inputs: `$poolManager`, `$token`, `$factory` and `$owner`. Router, rewards, oracle and tick spacing are gone; fee, tick spacing, hook flags and oracle settings are constants.\n- **Stage 1:**\n  - Only the factory can open the pool, once, on the official key (native ETH / UNLK, 1.25% fee, tick spacing 60, this hook). The pool's opening callback reads no other contract, so it works before any companion exists.\n  - The factory's seed liquidity is accepted and its range recorded; removing principal is refused.\n  - Until activation, swaps, credit syncing and milestone updates all revert. Token transfers and native reward claims keep working.\n- **Stage 2:**\n  - The router and oracle are fixed to the real hook when they are deployed; the old one-time setters are removed. The claim adapter reads the real distributor.\n  - The owner (`0x48e4…6ec9`) then calls `configureAndActivate(router, rewards, oracle, launchNumber)` once.\n  - It refuses the wrong caller, a different launch number, wrong or codeless contracts, a non-Chainlink feed on Sepolia, and a second call. Any failure reverts with no state change. Success seals everything permanently.\n- **Payer:** the 3% recipient is taken from the factory's own record for this launch (`positionOf(launchNumber).requester`), not from a balance.\n\n**Tests**\n- New suites cover a launch with no companions, every refused activation, sealing, and a claim made before activation being credited afterwards. A local rehearsal runs both stages end to end through the stage-2 script.\n- The platform's protected admission tests ran unmodified and passed: 11 of 11, using stand-in environment values (`docs/PROTECTED_TEST_RESULTS.txt`).\n- To make them compile I upgraded the bundled `v4-core` and `forge-std` libraries (pinned in `dependencies.lock.json`) and added `src/HookFlags.sol` and `test/mocks/MockERC20.sol`.\n- Because the pool is now native ETH, I removed the baseline tests that only make sense with WETH or with the token sorting first. That includes a flash-loan test that depended on a WETH lender. The ETH equivalents of the router cases remain.\n\n**What blocks deployment**\n- **Sepolia factory:** I could not confirm which IMD factory is current on Sepolia. The PoolManager and the Chainlink ETH/USD feed checked out read-only. Five contracts from IMD's deployer point at that PoolManager, but picking the right one needs IMD's network config. The registry, distributor and fee policy are unverified for the same reason.\n- **No fork rehearsal:** with no stage-1 launch to fork from, I could not rehearse on a recorded Sepolia block. The rehearsal is local only.\n- **Stage-2 actor:** I found no IMD service that deploys companion contracts or sends calls after launch. Stage 2 therefore depends on the owner sending four Sepolia transactions from their own wallet (three deployments, then activation). If they can't or won't, stage 2 has no route and stage 1 should not be deployed. The missing platform operation would be \"deploy listed companions and send one owner-signed setup call after `launch()`\".\n\n**Open issues, reproduced but not fixed**\n- **10 ETH depth floor vs first tier:** at $2,000 per ETH, a 9.5 ETH buy pushes the spot valuation past $100K while the pool's seed position still holds under 10 ETH, so the 35% tier can't confirm. I didn't change the thresholds or the floor; that is the requester's call.\n- **Sustained manipulation:** holding the price above a threshold for the 30-mi","treeHash":"cba31c391189f6139333fab3e95eae5bf7043e5e","usage":{"cachedInputTokens":7545683,"inputTokens":114,"model":"claude-opus-5-5","outputTokens":59527,"runtime":"claude","turns":62,"wallClockMs":804856}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"85f29f4c09dba910","findings":[{"citation":"resolved","description":"`UnlkOracle._ethUsd` (src/UnlkOracle.sol:173, `block.timestamp - updatedAt > MAX_FEED_AGE`) rejects any answer older than exactly 3600 s (src/UnlkOracle.sol:24, `MAX_FEED_AGE = 1 hours`), and `UnlkHook._verifyOracle` (src/UnlkHook.sol:272) pins that constant and the published Sepolia feed 0x694AA1769357215DE4FAC081bf1f309aDC325306. Read on chain today (Sepolia, aggregator rounds 36697-36737 of phase 1): 39 of the last 40 rounds are 3624 or 3636 s apart (one is 2760 s). The proxy's heartbeat is 3600 s and its rounds land a few seconds late, so for the last 24-36 s of almost every round `fdvUsd()` reverts InvalidOracle. `_updateMilestone` is reachable by anyone through `updateMilestone()` and by every swap through `afterSwap`; when `fdvUsd()` reverts its catch branch does `delete qualifyingSince; lastConfirmation = 0`, discarding the whole one-hour confirmation of every tier. One unprivileged `updateMilestone()` call timed into that window each round therefore keeps `tier` at 0 and `p` at 2500 forever, at the cost of one cheap transaction per hour; no privilege, no capital and no price movement is needed. Without an attacker the same thing happens by accident whenever any trade or keeper call lands in the window (about 0.8% of wall-clock time), so under real volume the first tier is unlikely to ever confirm. This contradicts the brief ('invalid data blocks new tiers only'; price-independent, unprivileged counter resets are excluded) and defeats the only path to the 35-85% tiers and the $5M permanent bypass. Trust-gap seam: permissionless `updateMilestone` (access) x periphery timing (feed heartbeat) x the asymmetric reset (a transient read failure is treated like a price fall, but worse: a fall only clears timers for tiers the price left). Fix options that keep the reviewed design: on a feed/oracle revert return false without touching `qualifyingSince`/`lastConfirmation` (the 5-minute gap rule already restarts an unobserved confirmation), and/or give the age bound the slack Chainlink recommends over the heartbeat (which also requires changing ORACLE_MAX_FEED_AGE in the hook, since activation pins the oracle constant). The proof passes under either.","line":479,"path":"src/UnlkHook.sol","reproduction":"State: stage 2 activated on Sepolia with the published feed; one 15 ETH buy so the seed range holds >= 10 ETH; ETH/USD such that TWAP FDV >= $100K (any tier). Sequence: (1) keeper calls `updateMilestone()` every 300 s as docs/DEPLOYMENT.md instructs; `qualifyingSince[0]` is set at the first qualifying call T0. (2) Feed round k has updatedAt = U_k; at any block with U_k + 3600 < timestamp < U_{k+1} (the real feed gives such a window of 24-36 s every round) an attacker sends `hook.updateMilestone()` (or any 1-wei swap). `oracle.fdvUsd()` reverts InvalidOracle (src/UnlkOracle.sol:170-174), the catch at src/UnlkHook.sol:479-483 runs. Expected: a transient feed-staleness blocks a new tier for that call only. Actual: `qualifyingSince` is deleted and `lastConfirmation = 0`; `hook.tier()` stays 0 and `p` stays 2500 after 5 hours of FDV above $100K (proof: test/scratch/FeedHeartbeatResetsConfirmation.t.sol, fails with 'first tier never confirmed although FDV stayed above $100K for 5 hours: 0 < 1'; the same schedule without the attacker's call confirms tier 1, and the proof passes with either fix). On-chain evidence: `cast call 0x694AA1769357215DE4FAC081bf1f309aDC325306 'getRoundData(uint80)' <(1<<64)+36737-k>` for k = 0..40 gives consecutive updatedAt gaps [3624, 3636, 3624, 3636, ... , 2760, 3636, ...], max 3636, 39 of 40 > 3600.","severity":"high","snippet":"        } catch {\n            delete qualifyingSince;\n            lastConfirmation = 0;\n            return false;\n        }","title":"Any caller can erase milestone confirmation progress once per Chainlink round: MAX_FEED_AGE equals the Sepolia ETH/USD heartbeat, so the feed is 'stale' 24-36 s before every update and the catch branc"},{"citation":"resolved","description":"`configureAndActivate` stores `payer = requester` (src/UnlkHook.sol:225) and `syncPayer` later credits `PAYER_ALLOCATION` (30,000,000 UNLK of E) to that address. In the verified factory, `requester` is the PoolFees fee-share holder: `LaunchParams.requester` is a separate field from `LaunchParams.remainderTo` (the wallet that actually receives the 3% + rounding in `_seedRequesterPool`), and the current holder may reassign it at any time with `PoolFees.setRequester(launchNumber, next)` (test/imd/PoolFees.sol:81-87, 'Hands the requester's share of a launch's future fees to another address'). Nothing in the hook ties the credit to the wallet that holds or received the allocation, and the brief asks to 'prove the initial allocation recipient from authoritative launch evidence'. Trust-gap seam (access x asymmetry): a right the factory lets its holder transfer independently (fee share) silently carries a second right the hook invented (unlock rights over 30M tokens), while the token recipient keeps the tokens and loses the rights. Outcomes: (a) requester hands its fee share to B before stage 2 (a sale, a treasury wallet, a mistake): B gets 30M E with zero tokens and can sell up to 7.5M UNLK acquired by transfer or OTC, while the 3% holder can sell nothing before $5M; (b) the deployer sets `requester != remainderTo` or `requester = 0` for a paid launch: no one who holds the allocation is credited, or the credit goes to the wrong wallet. README issue 3 records the assumption but the code does not enforce or verify it. Fix: resolve the recipient from launch-time evidence (the `PositionOpened(launchNumber, requester, ...)` requester at launch is still only the fee-share holder; the authoritative evidence is the `remainderTo` transfer in the launch transaction), or require at activation that the recorded requester holds at least PAYER_ALLOCATION and credit that wallet, or have the setup authority pass the recipient and verify it against both the launch record and the balance.","line":261,"path":"src/UnlkHook.sol","reproduction":"Local system (test/fixtures/SystemFixture.sol, verified factory sources): after stage 1 `token.balanceOf(payer) >= 30_000_000e18` and `factory.positionOf(1309).requester == payer`. Before stage 2: `vm.prank(payer); factory.setRequester(1309, bob)`. Then `owner` calls `hook.configureAndActivate(router, rewards, oracle, 1309)` and anyone calls `hook.syncPayer()`. Expected: the wallet that received the 3% allocation (payer) is credited 30M E. Actual: `hook.payer() == bob`, `hook.eligible(bob) == 30_000_000e18` while `token.balanceOf(bob) == 0`, and `hook.eligible(payer) == 0`, `hook.remainingQuota(payer) == 0` while payer holds the 30M tokens. Reproduced with test/scratch/PayerFollowsFeeShare.t.sol (passes on current code, i.e. the mismatch occurs).","severity":"medium","snippet":"        (recorded, lower, upper, requester) = ILaunchFactoryView(launchFactory).positionOf(launchNumber_);","title":"The 3% 'proven initial allocation' credit is taken from the factory's mutable LP-fee-share holder (positionOf().requester), not from the allocation recipient (remainderTo): a pre-activation setRequest"},{"citation":"resolved","description":"`beforeSwap` reverts whenever `getSlot0(poolId).protocolFee != 0`. Uniswap's protocol-fee controller (a third party the launch does not control; on Sepolia a Uniswap deployer key) can call `PoolManager.setProtocolFee(key, fee)` on any pool at any time. After that, every `UnlkSwapRouter.swap` reverts with InvalidPool, `UnlkOracle.checkpoint` marks the pool illiquid, no tier can ever confirm, and because the hook is sealed with no admin function there is no way to resume trading; 87% of the supply stays locked in the pool and all quota rights become unusable until the fee is removed by the same third party. The repository's own test `testProtocolFeeChangeCannotSilentlyAlterTradingEconomics` treats this as intended ('refuse rather than change the split'). It is reported here as a trust assumption with an exact failing state, because the brief promises 'no later pausing' and the pause here is in the hands of an external actor. Alternatives that preserve the fee promise: tolerate a non-zero protocol fee while enforcing `lpFee == FEE` (the LP split stays 80/20 of the LP fee), or document the dependency as an accepted platform risk.","line":298,"path":"src/UnlkHook.sol","reproduction":"Fork or local: `manager.setProtocolFeeController(c); c.setProtocolFee(key, 500)` (any non-zero value within v4's limits). Then `router.swap{value: 1 ether}(SwapRequest(true, -1 ether, MIN_SQRT_PRICE+1, 1 ether, 1, deadline))` from any wallet. Expected per the brief: trading continues at the official LP fee. Actual: revert InvalidPool from src/UnlkHook.sol:298, for every trader, permanently, with `hook.eligible` unchanged; `oracle.checkpoint()` records `liquid = false` (src/UnlkOracle.sol:100-101).","severity":"low","snippet":"        if (protocolFee != 0 || lpFee != FEE) revert InvalidPool();","title":"A protocol fee set by the PoolManager's fee controller halts every swap on the official pool forever (no recovery path exists by design)"},{"citation":"resolved","description":"`configureAndActivate` accepts `router_`, `rewards_` and `oracle_` if they have code and return the expected `hook()`, `poolManager()`, `token()`, `distributor()` and oracle constants. It does not check code hashes (immutables vary the runtime, as README issue 6 says), does not re-deploy the companions itself, and the oracle check list omits `MAX_CHECKPOINT_AGE`. A router whose `context()` names an arbitrary trader, or a claim-evidence contract whose `claimed()` returns any value up to SWARM_ALLOCATION, would pass and would let the chosen wallets sell without bound. Only the immutable `setupAuthority` (0x48e4...6ec9 per the README) can do this, once; it is therefore a documented trust assumption on that one key rather than a bypass, and is listed so the judge records it with the actor and precondition. Not a defect in isolation; the operational control (compare deployed bytecode with this build before activation) is stated in docs/DEPLOYMENT.md.","line":214,"path":"src/UnlkHook.sol","reproduction":"Owner deploys `FakeEvidence` with `token() == token`, `distributor() == <registry distributor>` and `claimed(x) == 100_000_000e18 for all x`, then calls `configureAndActivate(router, FakeEvidence, oracle, 1309)`. Expected per the brief: 'verify reviewed implementations, deployed code'. Actual: activation succeeds; `syncRewards(anyWallet)` credits 100M E to any wallet. Requires the setup authority's key: trust assumption, not an unprivileged exploit.","severity":"info","snippet":"            IUnlkRouterView(router_).hook() != address(this)\n                || address(IUnlkRouterView(router_).poolManager()) != address(poolManager)","title":"Companion 'reviewed implementation' is verified only through reciprocal getters and a subset of constants; the setup authority can seal any contract that answers them (trust assumption), and MAX_CHECK"}],"hash":"ebb0adf569b5bbfba619a784687ad468abcfb924a1f8129faed399a4fb7bd54a","nodeId":"829e043d-c2f5-44a8-a4a2-d7a1777e6927","outcome":"completed","summary":"The review is complete. The findings file is in place and no tracked file was changed.\n\n**Outcome.** The permission model itself holds: every hook callback is PoolManager-only, initialization and liquidity are factory-only with principal locked, swaps are authenticated through the sealed router with per-operation nonces, activation is single-use by the immutable setup authority with checks before writes, and the token is a plain ERC-20. The defects I found live at the seams between access, periphery timing and the allocation proof.\n\n**Findings written to `.imd-findings.json`** (4 findings, 25 coverage rows, 1 proof):\n\n1. **High. Anyone can erase milestone confirmation once per Chainlink round.** The oracle rejects feed answers older than exactly 3600 s, but the real Sepolia ETH/USD proxy updates every 3624 to 3636 s (39 of the last 40 rounds read on chain). So the feed is \"stale\" for 24 to 36 s before every update. In that window any `updateMilestone()` call or any swap makes `fdvUsd()` revert, and the catch branch deletes `qualifyingSince`, wiping the one-hour confirmation of every tier. One cheap unprivileged call per hour keeps `p` at 25% forever, and ordinary volume hits the window by accident. Proof: `test/scratch/FeedHeartbeatResetsConfirmation.t.sol` fails on current code (tier stays 0 after 5 hours above $100K), passes without the attacker's call, and passes under either fix (no reset on a transient feed failure, or an age bound with slack over the heartbeat, which also needs the hook's pinned constant changed).\n2. **Medium. The 3% payer credit follows the factory's mutable fee-share holder, not the allocation recipient.** `positionOf().requester` can be reassigned with `setRequester` before stage 2, and is a separate launch parameter from `remainderTo`. Reproduced: after reassignment, the new holder gets 30M E with zero tokens and the actual 3% holder has no sell rights.\n3. **Low, design.** A protocol fee set by Uniswap's fee controller halts every swap permanently, since the sealed hook has no recovery path. Reported as a third-party trust assumption with the exact failing state.\n4. **Info.** Companion identity at activation is verified by getters and a subset of constants only; the setup authority could seal any conforming contract. Trust assumption on that one key.\n\n**Coverage.** All 23 listed entry points have a row, plus two invariants. Nothing was left unreached. Not examined in depth, as outside my area: the TWAP arithmetic and the sustained-manipulation and depth-floor economics, which the repository already reproduces in its own tests.\n\n**Verification.** The project builds and its 87 tests pass. Sources were temporarily patched only to confirm the proof passes under a fix and were restored; the tracked diff is zero lines.","treeHash":null,"usage":{"cachedInputTokens":2715502,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":70167,"runtime":"claude","turns":53,"wallClockMs":1073009}},{"artifacts":[],"attempt":1,"bundleHash":"de24b879a275d4a097aa481f9ea84955adc07a6d41ba1728cbea1a07c77bf731","device":"9c6767b941fcfedc","findings":[],"hash":"f358a0b4c45be9e2e2a38af5ec644a289abbd9bf6da9db1da8bec4be2b64edd6","nodeId":"706cda53-04c9-4256-9b90-862649f8e62f","outcome":"completed","summary":"The revision is complete and verified. Summary:\n\n**Outcome**\n\n- `forge build`, `forge fmt --check` and `forge test` pass: 91 tests, 0 failures (was 87). The reviewer's proof `Proof_5cc5f97ca5a4.t.sol` now passes. The protected admission tests were re-run unmodified against the new bytecode with probe values: 11 of 11 pass (`docs/PROTECTED_TEST_RESULTS.txt`).\n- `.imd-responses.json` answers all seven findings. Nothing has been deployed; stage 1 remains conditional as before.\n\n**Fixed (code)**\n\n- Catch branch in `_updateMilestone` no longer deletes confirmation progress. Invalid oracle data now blocks new tiers for that call only. Three new tests cover a mid-confirmation feed failure, the gap rule, and the measured Sepolia feed cadence with a hostile call each round.\n- Feed staleness limit raised from exactly one hour to 90 minutes in both the oracle and the hook's activation pin. I verified the cadence myself: two Sepolia reads 3624 s apart. This is documented in the README as a deliberate oracle-setting change with evidence, not a silent one.\n- Activation now requires the recorded requester to hold the full 30M UNLK allocation, so a reassigned fee-share requester or a wallet that moved its tokens is refused with `InvalidEvidence`. Two new tests plus the proof cover both outcomes. Operational rule added to the deployment doc: the payer must not move tokens or call `setRequester` before stage 2.\n- Activation also pins the oracle's `MAX_CHECKPOINT_AGE`, the constant the info finding noted was missing.\n\n**Disputed with reasoning, code unchanged**\n\n- Depth floor vs first tier, and sustained-price manipulation. Both reproduce. The brief names both as audit findings to reproduce and report and forbids silently changing thresholds or oracle settings. The reviewer also called each a requester or scope decision. The README now quantifies each and lists the concrete one-constant or one-function options with trade-offs for the requester to pick.\n- Protocol-fee fail-closed behaviour is kept as the accepted design that protects the 1.25% fee promise. It is now recorded explicitly as known issue 8, with the Sepolia controller confirmed unset.\n\n**Documentation corrected**\n\n- The Sepolia factory table now says the five earlier candidates are not factories and names `0x84cf2Bc1Dc2af577dba5d62e26F90D041CA38F7A` as the unverified candidate, with the registry, fees, deployer and PoolManager it returns. The deployment doc gained a blocking \"Stage 0\" step: IMD confirms the factory and stage 2 is rehearsed on a fork against it before stage 1 is deployed.\n\nFiles are left uncommitted in the working tree, as in the previous round.","treeHash":"56beac671e339d4c9847a72ca1d5a9503e7ad1f9","usage":{"cachedInputTokens":2227553,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":47069,"runtime":"claude","turns":57,"wallClockMs":816170}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e02bec63aa9dcaa5","findings":[],"hash":"f40df3e40c4445e919e14993caf1d9cb8c8328827f6c2664e67c6f4a1b4d6d7f","nodeId":"706cda53-04c9-4256-9b90-862649f8e62f","outcome":"failed","summary":"needs input: Two reopened economic findings (independent review ids 08f7e7e8 and 3589008c) each need a design decision that is permanently sealed into UnlkHook/UnlkOracle at activation and that the brief forbids changing silently: (1) the 10 ETH quote-depth floor in UnlkOracle.MIN_QUOTE_DEPTH makes the $100K first tier unobservable whenever ETH trades above about $1,460 (at $2,000/ETH the first tier cannot confirm for any FDV between $100K and about $137K; reproduced by test/TwoStage.t.sol::testDepthFloorBlocksFirstTierAtSpotAbove100k); because the official pool's only liquidity is the factory's single locked seed, that ETH floor is just a price floor of roughly 68 ETH FDV and adds no manipulation resistance beyond the tier thresholds themselves. (2) All tiers confirm in the same 60-minute confirmation window, so one funded actor who holds the 30-minute TWAP above $5M FDV for 90 minutes sets the permanent quota bypass for every holder at a measured net cost of about 40 ETH, and the counter-selling ","treeHash":null,"usage":{"cachedInputTokens":579190,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":18815,"runtime":"claude","turns":12,"wallClockMs":343901}}],"verification":[{"checks":[{"durationMs":11019,"exitCode":0,"name":"build","output":"Compiling 120 files with Solc 0.8.26\nSolc 0.8.26 finished in 10.91s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:175:5:\n    |\n175 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":2734,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/EvidenceAndOracleProperties.t.sol:RouterDeadlineTest\n[PASS] testDeadlineIsInclusive() (gas: 408564)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.94ms (325.94µs CPU time)\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43150)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.54ms (265.32µs CPU time)\n\nRan 1 test for test/FailurePaths.t.sol:UnpaidLaunchTest\n[PASS] testNoRequesterMeansNoPayerCreditAnywhere() (gas: 1186945)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.60ms (681.40µs CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338542)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.81ms (313.92µs CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5064001)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52930)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.11ms (1.21ms CPU time)\n\nRan 5 tests for test/EvidenceAndOracleProperties.t.sol:MultiLeafEvidenceTest\n[PASS] testEvidenceBeyondTheSwarmAllocationIsRefusedNotTruncated() (gas: 834438)\n[PASS] testProofsFromTheActualTreeCreditExactlyWhatWasDelivered() (gas: 2193364)\n[PASS] testSiblingCannotAttestAnotherWalletsClaim() (gas: 679475)\n[PASS] testSweptRoundFreezesEvidence() (gas: 1043066)\n[PASS] testTheTreeIsWellFormed() (gas: 40929)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 5.55ms (2.75ms CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301510)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87230)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211425)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49882)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353100)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239235)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 8.71ms (6.66ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 58148837)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.59ms (8.59ms CPU time)\n\nRan 15 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2254696)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858550)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16283631)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5049573)\n[PASS] testInvalidDataBlocksNewTiersWithoutErasingConfirmation() (gas: 3125336)\n[PASS] testInvalidDataLongerThanGapRestartsOnNextValidCall() (gas: 973217)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741696)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721871)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73530)\n[PASS] testRealFeedCadenceCannotBeUsedToResetConfirmation() (gas: 11277961)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738159)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1262391)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117104)\n[PASS] testThresholdMappingBoundaries() (gas: 70568)\n[PASS] testWarmupAndExactWindow() (gas: 384567)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 19.89ms (30.43ms CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320365)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190935)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1962176)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66412)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137496)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80489)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 73.95ms (1.80ms CPU time)\n\nRan 13 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationRefusesRequesterWhoDoesNotHoldTheAllocation() (gas: 929087)\n[PASS] testActivationRefusesRequesterWhoMovedTheAllocationAway() (gas: 885987)\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 775828)\n[PASS] testCrossLaunchNumberRejected() (gas: 333003)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5284058)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37337)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122671)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 716290)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 392537)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811916)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1068862)\n[PASS] testUnauthorizedActivationRejected() (gas: 337873)\n[PASS] testWrongContractsRejected() (gas: 1281831)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 73.93ms (9.00ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 457220)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640074)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 480272)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 704539)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 769422)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 992070, ~: 992639)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 410318)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1021217)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 794929)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 74.02ms (74.08ms CPU time)\n\nRan 3 tests for test/EvidenceAndOracleProperties.t.sol:OracleReferenceTest\n[PASS] testCheckpointReplacementRewritesOnlyTheCurrentSecond() (gas: 963203)\n[PASS] testFuzzFdvIsMonotonicInTheEthUsdAnswer(uint64,uint64) (runs: 256, μ: 728674, ~: 728847)\n[PASS] testFuzzTwapMatchesBruteForceReference(uint256) (runs: 160, μ: 2785636, ~: 2836044)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 156.28ms (191.21ms CPU time)\n\nRan 3 tests for test/FailurePaths.t.sol:ActivationFailurePathsTest\n[PASS] testCompanionConstructorsRejectMissingInputs() (gas: 6041)\n[PASS] testCrossLaunchSetupIsRefusedInEveryDirection() (gas: 67833035)\n[PASS] testSepoliaActivatesWithThePublishedFeedAndTrades() (gas: 23203036)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 365.00ms (62.65ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336841)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089293)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437553)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92013)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62530)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 364.99ms (9.87ms CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336842)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089304)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437642)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 590028)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 91991)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 980507)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62508)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 445543)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1058829)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 397.52ms (8.14ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575313)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 627678)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 851179)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1299363)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82464)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574820)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1149824)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227492)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 40040)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 397.61ms (4.37ms CPU time)\n\nRan 17 tests for test/FailurePaths.t.sol:RouterAndHookFailurePathsTest\n[PASS] testBatchBoundsAndValueMustMatch() (gas: 186819)\n[PASS] testBatchIsAtomicWhenALaterStepFails() (gas: 811381)\n[PASS] testConfirmationBoundaryIsExactlyOneHour() (gas: 3133905)\n[PASS] testFullUnlockKeepsFeesAndRouterAuthentication() (gas: 4231093)\n[PASS] testFuzzOperationHashBindsTraderNonceAndChain(address,uint256,uint64) (runs: 256, μ: 17656, ~: 17656)\n[PASS] testFuzzRemainingQuotaFormula(uint128,uint128,uint8) (runs: 512, μ: 7303944, ~: 7303790)\n[PASS] testFuzzTierIsMonotonicInFdv(uint256,uint256) (runs: 512, μ: 17181, ~: 17133)\n[PASS] testFuzzTransfersMoveExactlyTheAmount(address,uint96) (runs: 256, μ: 120269, ~: 120201)\n[PASS] testHookRejectsMalformedRouteDataAndUnknownSenders() (gas: 230757)\n[PASS] testPartialFillAtPriceLimitChargesOnlyExecutedInput() (gas: 1165665)\n[PASS] testQuotaWidensWithTierAndNeverDoubleCounts() (gas: 4631569)\n[PASS] testRequesterChangeAfterActivationDoesNotMoveThePayer() (gas: 146456)\n[PASS] testRouterCallbackNeedsALiveSwap() (gas: 73839)\n[PASS] testRouterRefusesDegenerateRequests() (gas: 500499)\n[PASS] testSecondRoundClaimsAccumulateOncePerRound() (gas: 925993)\n[PASS] testSlippageRefusalsLeaveNoTrace() (gas: 864470)\n[PASS] testSyncForStrangerAndZeroAddressCreatesNothing() (gas: 114887)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 397.68ms (463.76ms CPU time)\n\nRan 1 test for test/StageOneInvariants.t.sol:StageOneInvariantTest\n[PASS]\nStageOneInvariantTest invariants:\n[PASS] invariant_onlyTheAuthorizedCallActivates\n[PASS] invariant_tokenAndClaimsIndependentOfTheHook\n[PASS] invariant_unconfiguredPoolIsInert\n StageOneInvariantTest invariants (runs: 96, calls: 3840, reverts: 0)\n\n╭-----------------+------------------+-------+---------+----------╮\n| Contract        | Selector         | Calls | Reverts | Discards |\n+=================================================================+\n| StageOneHandler | advance          | 505   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | attemptActivate  | 455   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | claimAndAttest   | 487   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | directSwap       | 471   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | foreignLiquidity | 471   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | routerSwap       | 522   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | syncAndMilestone | 483   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | transfer         | 446   | 0       | 0        |\n╰-----------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 657.87ms (651.60ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 858   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 856   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 840   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 866   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 869   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 831   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.49s (1.47s CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 858   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 856   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 840   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 866   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 869   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 831   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.57s (1.56s CPU time)\n\nRan 2 tests for test/QuotaInvariants.t.sol:QuotaInvariantTest\n[PASS]\nQuotaInvariantTest invariants:\n[PASS] invariant_configurationSealedAndRouteEnforced\n[PASS] invariant_ledgerMatchesExecutedDeltas\n[PASS] invariant_milestonesMonotonicAndConsistent\n[PASS] invariant_principalLockedAndFeeTermsFixed\n[PASS] invariant_routerNativeSolvency\n[PASS] invariant_salesNeverExceedQuota\n[PASS] invariant_supplyConservedAndNoCustody\n QuotaInvariantTest invariants (runs: 96, calls: 5760, reverts: 0)\n\n╭--------------+-----------------+-------+---------+----------╮\n| Contract     | Selector        | Calls | Reverts | Discards |\n+=============================================================+\n| QuotaHandler | advance         | 356   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | buyExactIn      | 345   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | buyExactOut     | 337   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | claimAndAttest  | 360   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | claimFees       | 389   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | directSwap      | 339   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | donate          | 339   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | foreignActivate | 327   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellExactOut    | 348   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellOverQuota   | 374   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellWithinQuota | 701   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | setFeed         | 395   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sync            | 391   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | transfer        | 371   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | withdrawNative  | 388   | 0       | 0        |\n╰--------------+-----------------+-------+---------+----------╯\n\n[PASS] testHandlerActionsActuallyExecute() (gas: 7527555)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.62s (2.61s CPU time)\n\nRan 22 test suites in 2.62s (8.70s CPU time): 124 tests passed, 0 failed, 0 skipped (124 total tests)\n","passed":true},{"durationMs":81,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":229,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":71,\"docs/PROTECTED_TEST_RESULTS.txt\":18,\"docs/TEST_RESULTS.txt\":163,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1750,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":525,\"src/UnlkOracle.sol\":192,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/EvidenceAndOracleProperties.t.sol\":337,\"test/FailurePaths.t.sol\":586,\"test/Invariants.t.sol\":220,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":378,\"test/QuotaInvariants.t.sol\":529,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/StageOneInvariants.t.sol\":281,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":314,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"84421ef9cbf9951add31d11ba0b80a0ec56fcb7f2abc3c85b2406ffff1e1bd4b","verifiedTreeHash":"2b921f9d5c6528268739b7cd5e951c0d3ad54171","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":17896,"exitCode":0,"name":"build","output":"Compiling 116 files with Solc 0.8.26\nSolc 0.8.26 finished in 17.64s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:171:5:\n    |\n171 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":2966,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43138)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 22.40ms (633.51µs CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338544)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 27.12ms (524.64µs CPU time)\n\nRan 11 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 771576)\n[PASS] testCrossLaunchNumberRejected() (gas: 333024)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5457983)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37227)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122629)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 712006)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 388364)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811991)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1033653)\n[PASS] testUnauthorizedActivationRejected() (gas: 337916)\n[PASS] testWrongContractsRejected() (gas: 1253055)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 26.77ms (17.62ms CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5070705)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52984)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 30.05ms (2.86ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575243)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 632952)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 856524)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1304749)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82462)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574797)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1160631)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227366)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 39994)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 32.96ms (8.27ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342583)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100088)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 442944)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92101)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62465)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 34.29ms (11.77ms CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301620)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87240)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211374)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49892)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353175)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239161)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 39.98ms (12.39ms CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342584)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100099)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 443033)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 595373)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92079)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 985898)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62443)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 450934)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1064197)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 50.01ms (16.36ms CPU time)\n\nRan 13 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2327834)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858835)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16285698)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5050123)\n[PASS] testInvalidDataResetsUnfinishedConfirmation() (gas: 3158037)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741784)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721936)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73506)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738246)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1274595)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117478)\n[PASS] testThresholdMappingBoundaries() (gas: 70590)\n[PASS] testWarmupAndExactWindow() (gas: 384644)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 52.74ms (40.84ms CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320246)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190913)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1918919)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66388)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137494)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80465)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 130.45ms (2.87ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 81192281)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 149.46ms (76.48ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 462699)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640071)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 485640)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 715321)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 774770)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 1002892, ~: 1003531)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 415697)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1032109)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 800361)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 149.89ms (127.88ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 782   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 854   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 884   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 841   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 888   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 871   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.65s (2.60s CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 782   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 854   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 884   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 841   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 888   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 871   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.65s (2.61s CPU time)\n\nRan 14 test suites in 2.66s (6.05s CPU time): 87 tests passed, 0 failed, 0 skipped (87 total tests)\n","passed":true},{"durationMs":184,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":153,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":49,\"docs/PROTECTED_TEST_RESULTS.txt\":15,\"docs/TEST_RESULTS.txt\":125,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1737,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"launch.json\":35,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":513,\"src/UnlkOracle.sol\":190,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/Invariants.t.sol\":215,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":302,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":274,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"93d0f85cf9aca9dccebe5ab30e7a40dd0d5e31a932f68746bd0c688595efaa7c","verifiedTreeHash":"5e158045dce0aee376bf545bf1b81dfb7b09ae9c","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":25479,"exitCode":0,"name":"build","output":"Compiling 119 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.25s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:171:5:\n    |\n171 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":6290,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43138)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 18.22ms (514.93µs CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5070705)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52984)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 28.69ms (2.58ms CPU time)\n\nRan 1 test for test/FailurePaths.t.sol:UnpaidLaunchTest\n[PASS] testNoRequesterMeansNoPayerCreditAnywhere() (gas: 1192310)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.72ms (1.87ms CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338544)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 36.34ms (889.26µs CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301620)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87240)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211374)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49892)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353175)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239161)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 45.27ms (16.70ms CPU time)\n\nRan 13 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2327834)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858835)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16285698)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5050123)\n[PASS] testInvalidDataResetsUnfinishedConfirmation() (gas: 3158037)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741784)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721936)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73506)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738246)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1274595)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117478)\n[PASS] testThresholdMappingBoundaries() (gas: 70590)\n[PASS] testWarmupAndExactWindow() (gas: 384644)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 138.69ms (38.56ms CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342584)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100099)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 443033)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 595373)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92079)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 985898)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62443)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 450934)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1064197)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 141.88ms (14.26ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 81192281)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 142.05ms (68.08ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342583)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100088)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 442944)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92101)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62465)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 141.96ms (11.24ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 462699)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640071)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 485640)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 715321)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 774770)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 1003002, ~: 1003531)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 415697)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1032109)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 800361)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 141.73ms (114.21ms CPU time)\n\nRan 3 tests for test/FailurePaths.t.sol:ActivationFailurePathsTest\n[PASS] testCompanionConstructorsRejectMissingInputs() (gas: 6041)\n[PASS] testCrossLaunchSetupIsRefusedInEveryDirection() (gas: 41485367)\n[PASS] testSepoliaActivatesWithThePublishedFeedAndTrades() (gas: 19341581)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 861.55ms (94.99ms CPU time)\n\nRan 11 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 771576)\n[PASS] testCrossLaunchNumberRejected() (gas: 333024)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5457983)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37227)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122629)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 712006)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 388364)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811991)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1033653)\n[PASS] testUnauthorizedActivationRejected() (gas: 337916)\n[PASS] testWrongContractsRejected() (gas: 1253055)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 861.48ms (14.28ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575243)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 632952)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 856524)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1304749)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82462)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574797)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1160631)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227366)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 39994)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 939.71ms (8.39ms CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320246)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190913)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1918919)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66388)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137494)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80465)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 940.53ms (3.99ms CPU time)\n\nRan 17 tests for test/FailurePaths.t.sol:RouterAndHookFailurePathsTest\n[PASS] testBatchBoundsAndValueMustMatch() (gas: 186884)\n[PASS] testBatchIsAtomicWhenALaterStepFails() (gas: 818142)\n[PASS] testConfirmationBoundaryIsExactlyOneHour() (gas: 3139692)\n[PASS] testFullUnlockKeepsFeesAndRouterAuthentication() (gas: 4236838)\n[PASS] testFuzzOperationHashBindsTraderNonceAndChain(address,uint256,uint64) (runs: 256, μ: 17656, ~: 17656)\n[PASS] testFuzzRemainingQuotaFormula(uint128,uint128,uint8) (runs: 512, μ: 7303854, ~: 7303698)\n[PASS] testFuzzTierIsMonotonicInFdv(uint256,uint256) (runs: 512, μ: 17184, ~: 17143)\n[PASS] testFuzzTransfersMoveExactlyTheAmount(address,uint96) (runs: 256, μ: 119878, ~: 120172)\n[PASS] testHookRejectsMalformedRouteDataAndUnknownSenders() (gas: 230683)\n[PASS] testPartialFillAtPriceLimitChargesOnlyExecutedInput() (gas: 1183033)\n[PASS] testQuotaWidensWithTierAndNeverDoubleCounts() (gas: 4648381)\n[PASS] testRequesterChangeAfterActivationDoesNotMoveThePayer() (gas: 146409)\n[PASS] testRouterCallbackNeedsALiveSwap() (gas: 73827)\n[PASS] testRouterRefusesDegenerateRequests() (gas: 500763)\n[PASS] testSecondRoundClaimsAccumulateOncePerRound() (gas: 925947)\n[PASS] testSlippageRefusalsLeaveNoTrace() (gas: 877772)\n[PASS] testSyncForStrangerAndZeroAddressCreatesNothing() (gas: 114864)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 940.57ms (1.18s CPU time)\n\nRan 1 test for test/StageOneInvariants.t.sol:StageOneInvariantTest\n[PASS]\nStageOneInvariantTest invariants:\n[PASS] invariant_onlyTheAuthorizedCallActivates\n[PASS] invariant_tokenAndClaimsIndependentOfTheHook\n[PASS] invariant_unconfiguredPoolIsInert\n StageOneInvariantTest invariants (runs: 96, calls: 3840, reverts: 0)\n\n╭-----------------+------------------+-------+---------+----------╮\n| Contract        | Selector         | Calls | Reverts | Discards |\n+=================================================================+\n| StageOneHandler | advance          | 478   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | attemptActivate  | 484   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | claimAndAttest   | 467   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | directSwap       | 459   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | foreignLiquidity | 479   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | routerSwap       | 474   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | syncAndMilestone | 495   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| StageOneHandler | transfer         | 504   | 0       | 0        |\n╰-----------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.42s (1.38s CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 837   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 839   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 857   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 830   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 899   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 858   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.56s (3.52s CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 837   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 839   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 857   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 830   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 899   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 858   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.73s (3.69s CPU time)\n\nRan 2 tests for test/QuotaInvariants.t.sol:QuotaInvariantTest\n[PASS]\nQuotaInvariantTest invariants:\n[PASS] invariant_configurationSealedAndRouteEnforced\n[PASS] invariant_ledgerMatchesExecutedDeltas\n[PASS] invariant_milestonesMonotonicAndConsistent\n[PASS] invariant_principalLockedAndFeeTermsFixed\n[PASS] invariant_routerNativeSolvency\n[PASS] invariant_salesNeverExceedQuota\n[PASS] invariant_supplyConservedAndNoCustody\n QuotaInvariantTest invariants (runs: 96, calls: 5760, reverts: 0)\n\n╭--------------+-----------------+-------+---------+----------╮\n| Contract     | Selector        | Calls | Reverts | Discards |\n+=============================================================+\n| QuotaHandler | advance         | 355   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | buyExactIn      | 350   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | buyExactOut     | 387   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | claimAndAttest  | 385   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | claimFees       | 355   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | directSwap      | 348   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | donate          | 333   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | foreignActivate | 363   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellExactOut    | 360   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellOverQuota   | 351   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sellWithinQuota | 728   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | setFeed         | 355   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | sync            | 378   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | transfer        | 357   | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| QuotaHandler | withdrawNative  | 355   | 0       | 0        |\n╰--------------+-----------------+-------+---------+----------╯\n\n[PASS] testHandlerActionsActuallyExecute() (gas: 7582896)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.02s (5.99s CPU time)\n\nRan 19 test suites in 6.03s (20.14s CPU time): 111 tests passed, 0 failed, 0 skipped (111 total tests)\n","passed":true},{"durationMs":230,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":153,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":49,\"docs/PROTECTED_TEST_RESULTS.txt\":15,\"docs/TEST_RESULTS.txt\":125,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1737,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":513,\"src/UnlkOracle.sol\":190,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/FailurePaths.t.sol\":586,\"test/Invariants.t.sol\":220,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":302,\"test/QuotaInvariants.t.sol\":529,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/StageOneInvariants.t.sol\":281,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":274,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9abb951f77a35640dfabcf7a8b0de9abd3e791d10c47afe6a2aa730ae2238ec8","verifiedTreeHash":"ebdc7187d4c16693b32fe84dc310f015f732ee45","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":20262,"exitCode":0,"name":"build","output":"Compiling 116 files with Solc 0.8.26\nSolc 0.8.26 finished in 19.93s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:175:5:\n    |\n175 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":2958,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43150)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.75ms (277.57µs CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5064001)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52930)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.07ms (2.08ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336841)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089293)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437553)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92013)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62530)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 12.91ms (10.53ms CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320365)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190935)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1962176)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66412)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137496)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80489)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 17.92ms (3.51ms CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338542)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 17.99ms (791.03µs CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301510)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87230)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211425)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49882)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353100)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239235)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 18.47ms (11.83ms CPU time)\n\nRan 13 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationRefusesRequesterWhoDoesNotHoldTheAllocation() (gas: 929087)\n[PASS] testActivationRefusesRequesterWhoMovedTheAllocationAway() (gas: 885987)\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 775828)\n[PASS] testCrossLaunchNumberRejected() (gas: 333003)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5284058)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37337)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122671)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 716290)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 392537)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811916)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1068862)\n[PASS] testUnauthorizedActivationRejected() (gas: 337873)\n[PASS] testWrongContractsRejected() (gas: 1281831)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 25.39ms (15.35ms CPU time)\n\nRan 15 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2254696)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858550)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16283631)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5049573)\n[PASS] testInvalidDataBlocksNewTiersWithoutErasingConfirmation() (gas: 3125336)\n[PASS] testInvalidDataLongerThanGapRestartsOnNextValidCall() (gas: 973217)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741696)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721871)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73530)\n[PASS] testRealFeedCadenceCannotBeUsedToResetConfirmation() (gas: 11277961)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738159)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1262391)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117104)\n[PASS] testThresholdMappingBoundaries() (gas: 70568)\n[PASS] testWarmupAndExactWindow() (gas: 384567)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 25.50ms (51.67ms CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336842)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089304)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437642)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 590028)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 91991)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 980507)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62508)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 445543)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1058829)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 93.52ms (12.00ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575313)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 627678)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 851179)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1299363)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82464)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574820)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1149824)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227492)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 40040)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 94.74ms (7.47ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 58148837)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 102.21ms (14.39ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 457220)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640074)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 480272)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 704539)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 769422)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 992124, ~: 992639)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 410318)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1021217)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 794929)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 102.23ms (104.22ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 941   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 834   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 858   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 830   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 804   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 853   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.52s (2.50s CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 941   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 834   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 858   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 830   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 804   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 853   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.72s (2.71s CPU time)\n\nRan 14 test suites in 2.72s (5.77s CPU time): 91 tests passed, 0 failed, 0 skipped (91 total tests)\n","passed":true},{"durationMs":208,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":229,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":71,\"docs/PROTECTED_TEST_RESULTS.txt\":18,\"docs/TEST_RESULTS.txt\":163,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1750,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"launch.json\":35,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":525,\"src/UnlkOracle.sol\":192,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/Invariants.t.sol\":215,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":378,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":314,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a9d70288472dc27bd38703d8d71fa729d7c6d446b813a3a5c7387434fc31778f","verifiedTreeHash":"442f4a4af79147a88457da3b9e84f88dd86ccf5f","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":7760,"exitCode":0,"name":"build","output":"Compiling 116 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.65s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:171:5:\n    |\n171 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1385,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43138)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.75ms (179.74µs CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5070705)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52984)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 14.34ms (1.52ms CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320246)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190913)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1918919)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66388)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137494)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80465)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 14.49ms (1.67ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575243)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 632952)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 856524)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1304749)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82462)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574797)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1160631)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227366)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 39994)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 14.76ms (4.44ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342583)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100088)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 442944)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92101)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62465)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 17.20ms (5.72ms CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338544)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 18.92ms (350.83µs CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3342584)\n[PASS] testNativeBatchSettlement() (gas: 850559)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1100099)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 443033)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 595373)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92079)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 985898)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62443)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 450934)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1064197)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4862498)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 26.27ms (9.33ms CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301620)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87240)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211374)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49892)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353175)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239161)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 31.61ms (8.39ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 81192281)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 37.50ms (37.27ms CPU time)\n\nRan 13 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2327834)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858835)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16285698)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5050123)\n[PASS] testInvalidDataResetsUnfinishedConfirmation() (gas: 3158037)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741784)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721936)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73506)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738246)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1274595)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117478)\n[PASS] testThresholdMappingBoundaries() (gas: 70590)\n[PASS] testWarmupAndExactWindow() (gas: 384644)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 63.51ms (23.36ms CPU time)\n\nRan 11 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 771576)\n[PASS] testCrossLaunchNumberRejected() (gas: 333024)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5457983)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37227)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122629)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 712006)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 388364)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811991)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1033653)\n[PASS] testUnauthorizedActivationRejected() (gas: 337916)\n[PASS] testWrongContractsRejected() (gas: 1253055)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 63.45ms (7.31ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 462699)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640071)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 485640)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 715321)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 774770)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 1002951, ~: 1003531)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 415697)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1032109)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 800361)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 63.89ms (56.64ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 806   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 881   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 875   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 855   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 863   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 840   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.18s (1.16s CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 806   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 881   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 875   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 855   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 863   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 840   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.27s (1.25s CPU time)\n\nRan 14 test suites in 1.27s (2.83s CPU time): 87 tests passed, 0 failed, 0 skipped (87 total tests)\n","passed":true},{"durationMs":97,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":153,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":49,\"docs/PROTECTED_TEST_RESULTS.txt\":15,\"docs/TEST_RESULTS.txt\":125,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1737,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":513,\"src/UnlkOracle.sol\":190,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/Invariants.t.sol\":215,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":302,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":274,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3538,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[high/medium] reentrancy-balance at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[medium/medium] divide-before-multiply at src/UnlkOracle.sol:122: UnlkOracle.twapTick() (src/UnlkOracle.sol#122-147) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:122: UnlkOracle.twapTick() (src/UnlkOracle.sol#122-147) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:86: UnlkOracle.checkpoint() (src/UnlkOracle.sol#86-115) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:86: UnlkOracle.checkpoint() (src/UnlkOracle.sol#86-115) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/UnlkHook.sol:305: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#305-336):\n[medium/medium] uninitialized-local at src/UnlkHook.sol:237: UnlkHook._verifyLaunchRecord(uint64).hookSeen (src/UnlkHook.sol#237) is a local variable never initialized\n[medium/medium] uninitialized-local at src/UnlkHook.sol:236: UnlkHook._verifyLaunchRecord(uint64).tokenSeen (src/UnlkHook.sol#236) is a local variable never initialized\n[medium/medium] uninitialized-local at src/UnlkSwapRouter.sol:114: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256).state (src/UnlkSwapRouter.sol#114) is a local variable never initialized\n[medium/medium] unused-return at src/UnlkHook.sol:283: UnlkHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/UnlkHook.sol#283-303) ignores return value by (None,None,protocolFee,lpFee) = poolManager.getSlot0(poolId) (src/UnlkHook.sol#297)\n[low/medium] events-maths at src/UnlkHook.sol:340: UnlkHook.beforeAddLiquidity(address,PoolKey,ModifyLiquidityParams,bytes) (src/UnlkHook.sol#340-356) should emit an event for: \n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: outputBefore = state.outputToken.balanceOf(msg.sender) (src/UnlkSwapRouter.sol#155)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: key = hook.poolKey() (src/UnlkSwapRouter.sol#113)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.baseInput = state.inputToken.balanceOf(address(this)) (src/UnlkSwapRouter.sol#120-122)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.outputToken.balanceOf(msg.sender) != outputBefore + output (src/UnlkSwapRouter.sol#157)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: ! hook.activated() || block.timestamp > request.deadline || request.amountSpecified == 0 || request.amountSpecified == type()(int256).min || request.maxInput == 0 || request.maxInput > uint256(uint128(type()(int128).max)) || request.minOutput == 0 (src/UnlkSwapRouter.sol#109-111)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: result = poolManager.unlock(abi.encode(key,params,operationNonce,request.maxInput)) (src/UnlkSwapRouter.sol#139)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.baseOutput = state.outputToken.balanceOf(address(this)) (src/UnlkSwapRouter.sol#123-124)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.inputToken.balanceOf(address(this)) != state.baseInput + request.maxInput (src/UnlkSwapRouter.sol#129)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:172: UnlkSwapRouter._balance(IERC20) (src/UnlkSwapRouter.sol#172-174) has external calls inside a loop: currency.balanceOf(address(this)) (src/UnlkSwapRouter.sol#173)\n[low/medium] reentrancy-benign at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[low/medium] reentrancy-benign at src/UnlkHook.sol:305: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#305-336):\n[low/medium] reentrancy-benign at src/UnlkHook.sol:458: Reentrancy in UnlkHook.updateMilestone() (src/UnlkHook.sol#458-462):\n[low/medium] reentrancy-events at src/UnlkHook.sol:185: Reentrancy in UnlkHook.configureAndActivate(address,IClaimEvidence,UnlkOracle,uint64) (src/UnlkHook.sol#185-229):\n[low/medium] reentrancy-events at src/UnlkHook.sol:458: Reentrancy in UnlkHook.updateMilestone() (src/UnlkHook.sol#458-462):\n[low/medium] reentrancy-events at src/UnlkHook.sol:305: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#305-336):\n[low/medium] timestamp at src/UnlkOracle.sol:122: UnlkOracle.twapTick() (src/UnlkOracle.sol#122-147) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:86: UnlkOracle.checkpoint() (src/UnlkOracle.sol#86-115) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkHook.sol:474: UnlkHook._updateMilestone() (src/UnlkHook.sol#474-503) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:166: UnlkOracle._ethUsd() (src/UnlkOracle.sol#166-179) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:150: UnlkOracle.fdvUsd() (src/UnlkOracle.sol#150-164) uses timestamp for comparisons","passed":true},{"durationMs":694,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/UnlkSwapRouter.sol:184: ETH transferred without address checks\n[high] reentrancy-state-change at src/MerkleClaimEvidence.sol:51: Reentrancy: State change after external call (6 places)\n[high] unprotected-initializer at src/HookDeployer.sol:19: Unprotected initializer (2 places)\n[low] costly-loop at src/UnlkHook.sol:487: Costly operations inside loop (2 places)\n[low] internal-function-used-once at src/HookFlags.sol:35: Internal Function Used Only Once\n[low] large-numeric-literal at src/UnlkHook.sol:326: Large Numeric Literal (15 places)\n[low] literal-instead-of-constant at src/UnlkHook.sol:290: Literal Instead of Constant (12 places)\n[low] missing-inheritance at src/UnlkOracle.sol:15: Missing Inheritance\n[low] require-revert-in-loop at src/UnlkHook.sol:238: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/UnlkHook.sol:283: State Change Without Event (4 places)\n[low] unchecked-return at src/UnlkHook.sol:334: Unchecked Return\n[low] uninitialized-local-variable at src/UnlkHook.sol:238: Uninitialized Local Variable\n[low] unused-import at src/libraries/OperationHash.sol:4: Unused Import","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cfb09656835a244bd3a8c89b115ad830eac808e56e7543bf94fb78e88132d14f","verifiedTreeHash":"cba31c391189f6139333fab3e95eae5bf7043e5e","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":8336,"exitCode":0,"name":"build","output":"Compiling 116 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.22s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:175:5:\n    |\n175 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1404,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/TwoStage.t.sol:StageOneConstructorTest\n[PASS] testConstructorRejectsMissingOrConflatedInputs() (gas: 8175)\n[PASS] testOnlyFactoryCanInitializeTheHooksPool() (gas: 43150)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.58ms (220.39µs CPU time)\n\nRan 8 tests for test/Launch.t.sol:LaunchTest\n[PASS] testCompanionsAreImmutablyBound() (gas: 37775)\n[PASS] testFactoryHoldsPrincipalAndCannotRemoveIt() (gas: 320365)\n[PASS] testLaunchAllocationAndLockedPrincipal() (gas: 190935)\n[PASS] testLaunchIsOnceAndDeployerOnly() (gas: 1962176)\n[PASS] testOnlyCanonicalManagerCanCallHook() (gas: 66412)\n[PASS] testPayerCreditExactDeduplicated() (gas: 137496)\n[PASS] testUnauthorizedFactoryAndWrongPoolRejected() (gas: 80489)\n[PASS] testUnconfiguredComponentsFailClosed() (gas: 42290)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 4.98ms (1.04ms CPU time)\n\nRan 2 tests for test/ReleaseConfig.t.sol:ReleaseConfigTest\n[PASS] testReadOnlyReleaseChecksOnLocalFixture() (gas: 1338542)\n[PASS] testReadOnlyReleaseChecksRejectWrongChainOrHash() (gas: 1273471)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.89ms (345.05µs CPU time)\n\nRan 2 tests for test/Stage2Script.t.sol:Stage2ScriptTest\n[PASS] testStage2DeploysFromActualIdentitiesAndOwnerActivates() (gas: 5064001)\n[PASS] testStage2RefusesUnknownLaunchNumber() (gas: 52930)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.98ms (2.37ms CPU time)\n\nRan 7 tests for test/NativeLaunch.t.sol:NativeLaunchTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336841)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089293)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437553)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 92013)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62530)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 8.32ms (8.77ms CPU time)\n\nRan 8 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test100PercentAloneIsNotFullUnlock() (gas: 7301510)\n[PASS] testArbitraryHookDataAndMultihopPayloadCannotSelectVictim() (gas: 87230)\n[PASS] testCallbacksAllRequireCanonicalManager() (gas: 211425)\n[PASS] testForeignPoolKeyRejectedEvenWithManagerPrank() (gas: 49882)\n[PASS] testNoMintPauseBlacklistUpgradeOrCounterResetEntrypoint() (gas: 114703)\n[PASS] testProtocolFeeChangeCannotSilentlyAlterTradingEconomics() (gas: 353100)\n[PASS] testQuotaMultiplicationDoesNotOverflowAtMaxEligible() (gas: 239235)\n[PASS] testTokenMintsWholeSupplyToDeployerOnly() (gas: 32786)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 9.48ms (6.30ms CPU time)\n\nRan 11 tests for test/NativeLaunch.t.sol:NativeWithdrawalTest\n[PASS] testDeepNativePoolWarmsUpAndCanUnlock() (gas: 3336842)\n[PASS] testNativeBatchSettlement() (gas: 844258)\n[PASS] testNativeBuySellAndFeeDistribution() (gas: 1089304)\n[PASS] testNativeExactOutputRefundToAuthenticatedCaller() (gas: 437642)\n[PASS] testNativeSettlementResetsPriorSyncCurrency() (gas: 590028)\n[PASS] testNativeValueMismatchAndUnsolicitedSendRejected() (gas: 91991)\n[PASS] testNativeWithdrawalUsesCEIAndCannotDoubleWithdraw() (gas: 980507)\n[PASS] testOneSidedOpeningLocks87PercentWithoutEthFunding() (gas: 62508)\n[PASS] testOtherWalletCannotWithdrawNativeRefund() (gas: 445543)\n[PASS] testRejectingNativeWalletCanTradeAndRetryWithdrawal() (gas: 1058829)\n[PASS] testThinLaunchCannotUnlockFromInitialSpotOrTinyBuy() (gas: 4684181)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 10.97ms (10.77ms CPU time)\n\nRan 2 tests for test/LocalScript.t.sol:LocalScriptTest\n[PASS] testLocalScriptRefusesPublicChainId() (gas: 13946406)\n[PASS] testLocalSimulationFunctionDoesNotDependOnCaller() (gas: 58148837)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 13.14ms (12.73ms CPU time)\n\nRan 13 tests for test/TwoStage.t.sol:StageOneTest\n[PASS] testActivationRefusesRequesterWhoDoesNotHoldTheAllocation() (gas: 929087)\n[PASS] testActivationRefusesRequesterWhoMovedTheAllocationAway() (gas: 885987)\n[PASS] testActivationSealsAndCannotBeReplayed() (gas: 775828)\n[PASS] testCrossLaunchNumberRejected() (gas: 333003)\n[PASS] testDepthFloorBlocksFirstTierAtSpotAbove100k() (gas: 5284058)\n[PASS] testFactoryLiquidityAllowedBeforeActivationButPrincipalStaysLocked() (gas: 37337)\n[PASS] testLaunchSucceedsWithoutCompanions() (gas: 122671)\n[PASS] testPayerCreditOnlyAfterActivationAndFromRecordedRequester() (gas: 716290)\n[PASS] testSepoliaRequiresPublishedChainlinkFeed() (gas: 392537)\n[PASS] testSwapsSyncAndMilestonesRevertBeforeActivation() (gas: 811916)\n[PASS] testTransfersAndNativeClaimsWorkBeforeActivationAndClaimIsCreditedLater() (gas: 1068862)\n[PASS] testUnauthorizedActivationRejected() (gas: 337873)\n[PASS] testWrongContractsRejected() (gas: 1281831)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 25.52ms (11.50ms CPU time)\n\nRan 10 tests for test/CreditsAndFees.t.sol:CreditsAndFeesTest\n[PASS] testAttestationRequiresActualClaimAndExactLeaf() (gas: 431528)\n[PASS] testClaimTransferAndPayerTransferDoNotDuplicateCredit() (gas: 575313)\n[PASS] testFeeCollectionDoesNotReducePrincipal() (gas: 627678)\n[PASS] testNativeClaimNeverDependsOnAdapterOrFeed() (gas: 851179)\n[PASS] testNativeClaimThenAttestThenSyncThenSale() (gas: 1299363)\n[PASS] testNegativeLiquidityForbiddenEvenForFactory() (gas: 82464)\n[PASS] testProofReleaseAndDedupRemainNativeRules() (gas: 574820)\n[PASS] testSaleFeesCountGrossInputAndNeverCreditFeeRecipients() (gas: 1149824)\n[PASS] testUnauthorizedLiquidityAndDonationNoCredit() (gas: 227492)\n[PASS] testUnclaimedRewardsNotEligible() (gas: 40040)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 53.45ms (5.51ms CPU time)\n\nRan 15 tests for test/Oracle.t.sol:OracleTest\n[PASS] testBadChainlinkRoundsAndAnswersFailClosed() (gas: 2254696)\n[PASS] testConfirmationGapRestartsRatherThanCountingUnobservedTime() (gas: 2858550)\n[PASS] testEveryTierAndPermanentUnlock() (gas: 16283631)\n[PASS] testFallingPricesNeverLowerReachedTier() (gas: 5049573)\n[PASS] testInvalidDataBlocksNewTiersWithoutErasingConfirmation() (gas: 3125336)\n[PASS] testInvalidDataLongerThanGapRestartsOnNextValidCall() (gas: 973217)\n[PASS] testNegativeTickAveragesRoundConservativelyForBothOrders() (gas: 741696)\n[PASS] testOneBlockSpikeCannotConfirm() (gas: 721871)\n[PASS] testOracleCannotBeReboundOrConfiguredByAnyone() (gas: 73530)\n[PASS] testRealFeedCadenceCannotBeUsedToResetConfirmation() (gas: 11277961)\n[PASS] testSameTimestampManipulationHasNoTimeWeight() (gas: 738159)\n[PASS] testStaleCheckpointAndFeedPreventNewTierButNotTrades() (gas: 1262391)\n[PASS] testSustainedManipulationRequiresWindowAndConfirmation() (gas: 3117104)\n[PASS] testThresholdMappingBoundaries() (gas: 70568)\n[PASS] testWarmupAndExactWindow() (gas: 384567)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 53.49ms (40.93ms CPU time)\n\nRan 9 tests for test/Trading.t.sol:TradingTest\n[PASS] testAnotherWalletCannotConsumeVictimsAllowanceOrQuota() (gas: 457220)\n[PASS] testDirectManagerAndForeignRouterSpoofingRejected() (gas: 640074)\n[PASS] testDonatedRouterBalancesCannotCreateCreditOrBeSwept() (gas: 480272)\n[PASS] testExactOutputSaleAccountsActualInput() (gas: 704539)\n[PASS] testExcessSaleRollsBackPoolBalancesOracleAndNonce() (gas: 769422)\n[PASS] testFuzzQuotaBoundary(uint96) (runs: 256, μ: 992112, ~: 992639)\n[PASS] testReplayOldCallbackHasNoLiveContext() (gas: 410318)\n[PASS] testSaleBoundaryExecutedInputIncludesPoolFee() (gas: 1021217)\n[PASS] testTransfersDoNotMoveRightsOrAllowChainCredit() (gas: 794929)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 65.23ms (65.73ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FullyUnlockedInvariantTest\n[PASS]\nFullyUnlockedInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n[PASS] invariantUnlockRemainsPermanentDespiteInvalidFeed\n FullyUnlockedInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 887   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 837   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 900   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 805   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 844   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 847   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.24s (1.23s CPU time)\n\nRan 1 test for test/Invariants.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariantEligibleEqualsOnlyExecutedBuysAndSynchronizedProofs\n[PASS] invariantNoActorExceedsItsQuotaBeforeUnlock\n[PASS] invariantPrincipalLiquidityNeverDecreases\n[PASS] invariantSupplyConservedAndRouterHasNoCustody\n SystemInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| ActionHandler | advance  | 887   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | buy      | 837   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | claim    | 900   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sell     | 805   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | sync     | 844   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| ActionHandler | transfer | 847   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.26s (1.25s CPU time)\n\nRan 14 test suites in 1.26s (2.76s CPU time): 91 tests passed, 0 failed, 0 skipped (91 total tests)\n","passed":true},{"durationMs":83,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MerkleClaimEvidence.attest(uint256,address,uint256,bytes32[])\",\"UnlkHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"UnlkHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"UnlkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"UnlkHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"UnlkHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"UnlkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"UnlkHook.configureAndActivate(address,address,address,uint64)\",\"UnlkHook.syncPayer()\",\"UnlkHook.syncRewards(address)\",\"UnlkHook.updateMilestone()\",\"UnlkOracle.bind((address,address,uint24,int24,address),address,int24,int24)\",\"UnlkOracle.checkpoint()\",\"UnlkSwapRouter.receive()\",\"UnlkSwapRouter.swap((bool,int256,uint160,uint256,uint256,uint256))\",\"UnlkSwapRouter.swapBatch((bool,int256,uint160,uint256,uint256,uint256)[])\",\"UnlkSwapRouter.unlockCallback(bytes)\",\"UnlkSwapRouter.withdrawNative()\",\"UnlkToken.approve(address,uint256)\",\"UnlkToken.transfer(address,uint256)\",\"UnlkToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":25,\"README.md\":229,\"dependencies.lock.json\":24,\"docs/DEPLOYMENT.md\":71,\"docs/PROTECTED_TEST_RESULTS.txt\":18,\"docs/TEST_RESULTS.txt\":163,\"docs/abi/HookDeployer.json\":84,\"docs/abi/MerkleClaimEvidence.json\":179,\"docs/abi/UnlkHook.json\":1750,\"docs/abi/UnlkOracle.json\":430,\"docs/abi/UnlkSwapRouter.json\":359,\"docs/abi/UnlkToken.json\":367,\"foundry.toml\":31,\"remappings.txt\":4,\"script/CheckReleaseConfig.s.sol\":61,\"script/LocalDemo.s.sol\":41,\"script/Stage2.s.sol\":76,\"scripts/export_abis.py\":26,\"scripts/mine-hook.sh\":9,\"src/HookDeployer.sol\":29,\"src/HookFlags.sol\":45,\"src/MerkleClaimEvidence.sol\":60,\"src/UnlkHook.sol\":525,\"src/UnlkOracle.sol\":192,\"src/UnlkSwapRouter.sol\":225,\"src/UnlkToken.sol\":22,\"src/interfaces/IIntegration.sol\":82,\"src/libraries/OperationHash.sol\":15,\"test/Adversarial.t.sol\":117,\"test/CreditsAndFees.t.sol\":176,\"test/Invariants.t.sol\":215,\"test/Launch.t.sol\":112,\"test/LocalScript.t.sol\":28,\"test/NativeLaunch.t.sol\":236,\"test/Oracle.t.sol\":378,\"test/ReleaseConfig.t.sol\":24,\"test/Stage2Script.t.sol\":57,\"test/Trading.t.sol\":171,\"test/TwoStage.t.sol\":314,\"test/fixtures/LocalFixtures.sol\":59,\"test/fixtures/SystemFixture.sol\":341,\"test/imd/HookFlags.sol\":45,\"test/imd/LaunchFactory.sol\":375,\"test/imd/LaunchFees.sol\":73,\"test/imd/LaunchRegistry.sol\":109,\"test/imd/MerkleDistributor.sol\":181,\"test/imd/PoolFees.sol\":220,\"test/imd/README.md\":23,\"test/mocks/MockERC20.sol\":11},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3785,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[high/medium] reentrancy-balance at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[medium/medium] divide-before-multiply at src/UnlkOracle.sol:124: UnlkOracle.twapTick() (src/UnlkOracle.sol#124-149) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:124: UnlkOracle.twapTick() (src/UnlkOracle.sol#124-149) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:88: UnlkOracle.checkpoint() (src/UnlkOracle.sol#88-117) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/UnlkOracle.sol:88: UnlkOracle.checkpoint() (src/UnlkOracle.sol#88-117) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/UnlkHook.sol:317: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#317-348):\n[medium/medium] uninitialized-local at src/UnlkHook.sol:248: UnlkHook._verifyLaunchRecord(uint64).hookSeen (src/UnlkHook.sol#248) is a local variable never initialized\n[medium/medium] uninitialized-local at src/UnlkHook.sol:247: UnlkHook._verifyLaunchRecord(uint64).tokenSeen (src/UnlkHook.sol#247) is a local variable never initialized\n[medium/medium] uninitialized-local at src/UnlkSwapRouter.sol:114: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256).state (src/UnlkSwapRouter.sol#114) is a local variable never initialized\n[medium/medium] unused-return at src/UnlkHook.sol:295: UnlkHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/UnlkHook.sol#295-315) ignores return value by (None,None,protocolFee,lpFee) = poolManager.getSlot0(poolId) (src/UnlkHook.sol#309)\n[low/medium] events-maths at src/UnlkHook.sol:352: UnlkHook.beforeAddLiquidity(address,PoolKey,ModifyLiquidityParams,bytes) (src/UnlkHook.sol#352-368) should emit an event for: \n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: outputBefore = state.outputToken.balanceOf(msg.sender) (src/UnlkSwapRouter.sol#155)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: key = hook.poolKey() (src/UnlkSwapRouter.sol#113)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.baseInput = state.inputToken.balanceOf(address(this)) (src/UnlkSwapRouter.sol#120-122)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.outputToken.balanceOf(msg.sender) != outputBefore + output (src/UnlkSwapRouter.sol#157)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: ! hook.activated() || block.timestamp > request.deadline || request.amountSpecified == 0 || request.amountSpecified == type()(int256).min || request.maxInput == 0 || request.maxInput > uint256(uint128(type()(int128).max)) || request.minOutput == 0 (src/UnlkSwapRouter.sol#109-111)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: result = poolManager.unlock(abi.encode(key,params,operationNonce,request.maxInput)) (src/UnlkSwapRouter.sol#139)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.baseOutput = state.outputToken.balanceOf(address(this)) (src/UnlkSwapRouter.sol#123-124)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) has external calls inside a loop: state.inputToken.balanceOf(address(this)) != state.baseInput + request.maxInput (src/UnlkSwapRouter.sol#129)\n[low/medium] calls-loop at src/UnlkSwapRouter.sol:172: UnlkSwapRouter._balance(IERC20) (src/UnlkSwapRouter.sol#172-174) has external calls inside a loop: currency.balanceOf(address(this)) (src/UnlkSwapRouter.sol#173)\n[low/medium] reentrancy-benign at src/UnlkHook.sol:470: Reentrancy in UnlkHook.updateMilestone() (src/UnlkHook.sol#470-474):\n[low/medium] reentrancy-benign at src/UnlkSwapRouter.sol:104: Reentrancy in UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166):\n[low/medium] reentrancy-benign at src/UnlkHook.sol:317: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#317-348):\n[low/medium] reentrancy-events at src/UnlkHook.sol:470: Reentrancy in UnlkHook.updateMilestone() (src/UnlkHook.sol#470-474):\n[low/medium] reentrancy-events at src/UnlkHook.sol:189: Reentrancy in UnlkHook.configureAndActivate(address,IClaimEvidence,UnlkOracle,uint64) (src/UnlkHook.sol#189-240):\n[low/medium] reentrancy-events at src/UnlkHook.sol:317: Reentrancy in UnlkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/UnlkHook.sol#317-348):\n[low/medium] timestamp at src/UnlkSwapRouter.sol:104: UnlkSwapRouter._swap(UnlkSwapRouter.SwapRequest,uint256) (src/UnlkSwapRouter.sol#104-166) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:88: UnlkOracle.checkpoint() (src/UnlkOracle.sol#88-117) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:152: UnlkOracle.fdvUsd() (src/UnlkOracle.sol#152-166) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:124: UnlkOracle.twapTick() (src/UnlkOracle.sol#124-149) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkHook.sol:486: UnlkHook._updateMilestone() (src/UnlkHook.sol#486-515) uses timestamp for comparisons\n[low/medium] timestamp at src/UnlkOracle.sol:168: UnlkOracle._ethUsd() (src/UnlkOracle.sol#168-181) uses timestamp for comparisons","passed":true},{"durationMs":733,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/UnlkSwapRouter.sol:184: ETH transferred without address checks\n[high] reentrancy-state-change at src/MerkleClaimEvidence.sol:51: Reentrancy: State change after external call (7 places)\n[high] unprotected-initializer at src/HookDeployer.sol:19: Unprotected initializer (2 places)\n[low] costly-loop at src/UnlkHook.sol:499: Costly operations inside loop (2 places)\n[low] internal-function-used-once at src/HookFlags.sol:35: Internal Function Used Only Once\n[low] large-numeric-literal at src/UnlkHook.sol:338: Large Numeric Literal (15 places)\n[low] literal-instead-of-constant at src/UnlkHook.sol:302: Literal Instead of Constant (12 places)\n[low] missing-inheritance at src/UnlkOracle.sol:15: Missing Inheritance\n[low] require-revert-in-loop at src/UnlkHook.sol:249: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/UnlkHook.sol:295: State Change Without Event (4 places)\n[low] unchecked-return at src/UnlkHook.sol:346: Unchecked Return\n[low] uninitialized-local-variable at src/UnlkHook.sol:249: Uninitialized Local Variable\n[low] unused-import at src/libraries/OperationHash.sol:4: Unused Import","passed":true},{"durationMs":3096,"exitCode":0,"name":"proof 5cc5f97ca5a4","output":"Compiling 102 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.29s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> src/UnlkHook.sol:175:5:\n    |\n175 |     function afterInitialize(address, PoolKey calldata key, uint160, int24)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 1 test for test/imd-proof-e2f279d2/Proof_5cc5f97ca5a4.t.sol:PayerAttributionProof\n[PASS] testPayerCreditGoesToTheWalletThatReceivedTheAllocation() (gas: 114771794)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 44.55ms (44.08ms CPU time)\n\nRan 1 test suite in 45.34ms (44.55ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f358a0b4c45be9e2e2a38af5ec644a289abbd9bf6da9db1da8bec4be2b64edd6","verifiedTreeHash":"56beac671e339d4c9847a72ca1d5a9503e7ad1f9","verifierVersion":"0.1.0+fb8278c5"}]}