{"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":"48844890-e197-4e13-b271-6b39a72a2b51","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"8640e38dbe3088015ee71b6dd539afd8ec64a178ff7fff6c4afabc8425711b3f","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":"bb43b6e881caf27ac0d591f9be79ce9ad2d93f556e55603152993733bd1528f3","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":"273c998b066d3ca3426d2a79e7f1cbc701a4557672a6994d7381c11d9a9dbdf0","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"021ffb5f59ecf3249137fdd9cbf6691da43a7dbe38d02eec1313d21a4fcae1cb","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":"b6a8f4bfc5ffca8c259d0c0bc86bce158b6889867d011414e6704cd9fdf3cf64","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":"44e0a9b44f099096470d42d1e3b95a1e59147baeb81becf48ecd0f12482b9a3b","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":"accepted"},{"acceptedSubmissionHash":"584e078657265ff13bab839be73376d5446c46f0226ce2c4a41974c4f1c229bc","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"e044168e6bfb0001046654c9cf2c68551f2918ebd7980a2860a67dc277ebd602","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Launch kind univ4_hook, paired with native ETH; token, hook, treasury and fee router deploy in the launch. No outside services or oracles. Nobody can change the rules after deployment: no owner, setters, minting, upgrades or pause.\nFACTORIES: LaunchFactory 0x12C63b581d07093F6126bc02263c58f7EadaA96F (hooks) and ProjectFactory 0xfF03410d0Fe5fa8f7F59F743de35E333D9857120 share one fee API and pay native ETH: treasury and router need receive(); send ETH with call. INTAKE 0xa43e6F75ee006411F79Ac1C84120606C2330DE82. IMD/ETH POOL: ETH, IMD, fee 10000, tickSpacing 200, no hook.\n\nTOKEN. Token name: Medici. Token symbol: MEDICI. The standard launch token; poolBps 9000, no team allocation. Burn = send to 0x000000000000000000000000000000000000dEaD; treasury.totalBurned() counts its burns.\n\nHOOK FEE: 2% of every buy and sell in ETH, on settled deltas, exact-in and exact-out: 1.5% to the treasury, 0.5% accrued for TEAM, paid out by claimTeam() (anyone). The treasury's own swaps pay none. First 15 min the buy fee decays 50% to 2%, surplus to the treasury. The hook keeps a TWAP accumulator.\n\nFEE ROUTER: its launch number is unknown at deploy, so no constructor takes it: setLaunchNumber(n), anyone, once, needs LaunchFactory.positionOf(n).key.hooks == hook. Then the paying wallet calls setRequester(n, feeRouter) on LaunchFactory. harvest(), anyone, reverts before: claimFees, withdraw both currencies; ETH to TEAM, MEDICI burned.\n\nROLES: TEAM and KEEPER live in the treasury; each can only hand itself over (setTeam, setKeeper).\n\nTREASURY (holds ETH and IMD; anyone may send IMD):\n- 7-day epochs from pool open; currentEpoch() moves on once the previous end (with extension) passes; ended epochs stay status 0 until settle(epoch).\n- price(): ETH per MEDICI (1e18), 24h TWAP, or since open if younger.\n- propose(title, brief): title max 80 bytes; brief (JSON launch.open body, max 12,000 bytes) emitted in Proposed; store keccak256(brief) and the block. Burns MEDICI worth 0.002 ETH x 1.15^n at price() (n = proposals so far this epoch), clamped 1,000-50,000,000; counts as the proposer's vote. Max 20 per epoch, before the original 7-day end.\n- vote(epoch, id, amount): epoch == currentEpoch(); burns amount, recorded per voter.\n- A vote in the last hour extends the end to 10 min after it, max +24h.\n- flag(epoch, id, note): KEEPER; note max 120 bytes.\n- settle(epoch): anyone, after the end. Most burned wins (tie: earlier); runner-up recorded. Winner's burns under 0.01 ETH at the epoch TWAP: no winner (status 4).\n- Bounty escrowed at settle: min(10% of free treasury ETH, 1 ETH, 50% of the winner's burns at the epoch TWAP), paid to the proposer at recordCommission; kept if the transfer fails.\n- commission(epoch, brief): keccak256(brief) == winner's briefHash; KEEPER at once, anyone 24h after the winner was set; one live request per epoch. If IMD < 0.5, first buys exactly 0.5 IMD in the IMD/ETH POOL for at most 0.03 ETH. Then approves exactly 0.5 IMD and calls INTAKE.requestWithFailure(\"launch.open@launch-1\", brief, (this, onResult), onFailure, IMD, 0.5e18).\n- onResult(bytes32 requestId, bytes32 resultHash, string uri) stores them (uri cut to 96 bytes); onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes) promotes the runner-up. Only from INTAKE for the live request, under 200k gas.\n- lapse(epoch): anyone, 14 days after the winner was set with no recorded launch: runner-up wins with a fresh 14 days; after a second lapse the bounty returns. skip(epoch, reason): KEEPER, before commission; runner-up now. A promoted runner-up's quorum and bounty use its own burns.\n- recordCommission(epoch, launchNumber, jobId): KEEPER, after commission; each launch once; positionOf(launchNumber).requester == treasury (either factory); treasury holds at least 5% of the launch token's supply (the pool currency other than ETH/IMD). Stores both, pays the bounty, reserves that balance as the backer share.\n- claimBacker(epoch): the winner's voters claim it pro rata to their burn for 90 days; then burnUnclaimed(epoch), anyone.\n- Buyback: at settle, 40% of the epoch's ETH income (not returned escrow) unlocks evenly over 24h; unspent carries over. buyback(maxEth), anyone: spends min(unlocked, maxEth) on MEDICI here, burned; caller gets 0.5% of it; min-out from the TWAP.\n- keeperWithdraw(amount): KEEPER, max 0.01 ETH per epoch.\n- harvest(launchNumber), anyone, recorded launches: claimFees, withdraw; launch tokens above the unclaimed reserve burned; IMD kept for launches.\n\nADDRESSES: TEAM 0x01670a5CfAEEaEE388cD38085C4ADf1Bad45a1c2, KEEPER 0x16578E0dfAFa219f2B0f52095D8206b5c02F8250, IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7.\n\nINTERFACE (exact; a page and keeper use it). epoch, id, amount: uint256; launchNumber, blockNumber, times: uint64; title, note, reason, jobId: string; brief: bytes; briefHash: bytes32; proposer, voter, token: address. hook: treasury(), feeRouter(), teamOwed(), claimTeam(). router: launchNumber(), setLaunchNumber(n), harvest(). Treasury views:\ntoken(); price(); totalBurned(); currentEpoch(); epochEnd(e); proposeDeadline(e); proposeCost(); proposalCount(e); votesOf(e,id,voter); claimable(e,voter); totalBuybackBurned();\nproposalOf(e,id) -> (proposer, title, briefHash, blockNumber, burned, flag);\nepochResult(e) -> (uint8 status, winnerId, runnerUpId, bounty, launchNumber, deadline, jobId): 0 open, 1 awaiting launch, 2 launched, 3 lapsed, 4 no winner;\nrequestOf(e) -> (bytes32 requestId, openToAllAt), requestId 0 when none is live; backerShare(e) -> (token, amount, claimed, claimUntil); buybackChunk() -> (unlocked eth, tip).\nWrites as named above. Event Proposed(epoch, id, proposer, title, brief), plus one event per action.","parentJobId":null,"planHash":"5fea98f9d5a74c72e1fe201b1295eae7f5d00c65d5abc46226c3df56c4901f94","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"48844890-e197-4e13-b271-6b39a72a2b51","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1785-launch-kind-univ4-hook-paired"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52253","feedbackHash":"b3f091781440d37f09eda1c25a17a862318c71262bb45def0dbcb7f1dbaf803f","nodeKey":"audit_economics","submissionHash":"8640e38dbe3088015ee71b6dd539afd8ec64a178ff7fff6c4afabc8425711b3f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52618","feedbackHash":"79876c8b066947b10fdd376a27cc8b51ab5d527bb8d36f7058f0eb2da13e7f28","nodeKey":"audit_flow","submissionHash":"bb43b6e881caf27ac0d591f9be79ce9ad2d93f556e55603152993733bd1528f3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51166","feedbackHash":"3f2bf17fbf9f06750c81a91bd66693fa4ddb4d7c19d0e7a8fc7647d7d86b62a1","nodeKey":"audit_judge","submissionHash":"273c998b066d3ca3426d2a79e7f1cbc701a4557672a6994d7381c11d9a9dbdf0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52669","feedbackHash":"24d24bcb1756593e68fa2a6722293946dd9860c41118768eb421d87a0669c54c","nodeKey":"audit_judge","submissionHash":"08f66ac5d7caec56301126d3ee94c00fe5dd412b1d5b77bfe3be258f102a7108","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52615","feedbackHash":"2bbdd75a01db905caed0763d70ed40fc5f2de2ca5e210e509a4f5919f9a69f3f","nodeKey":"audit_math","submissionHash":"021ffb5f59ecf3249137fdd9cbf6691da43a7dbe38d02eec1313d21a4fcae1cb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52114","feedbackHash":"929853b8b1d01cdfc0e656ec750df4343a58a7b837b67748bbfcdac72e7dd3c6","nodeKey":"audit_permissions","submissionHash":"b6a8f4bfc5ffca8c259d0c0bc86bce158b6889867d011414e6704cd9fdf3cf64","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51382","feedbackHash":"432ef7756be7a1106ca7eb45f7aaf9f81acfa1ff1efa678ecda73d95f1d9505e","nodeKey":"build_contract_project","submissionHash":"44e0a9b44f099096470d42d1e3b95a1e59147baeb81becf48ecd0f12482b9a3b","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52422","feedbackHash":"360d2a3bf12eb6cdd6ea19d5ba3401c65eb2450890785cebaf0389d671d3eecf","nodeKey":"build_contract_project","submissionHash":"024ce20ec6059c6ca9c0847cfa710452e0e59d57a003486187442ee2086989dd","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52715","feedbackHash":"9296a04b1a805a5c1d33aa50dca45a1efa4582e9be9286eb2301d770263ce4d1","nodeKey":"manifest","submissionHash":"3efcdc6ad6155887218890653d7aa1d4f9e1023e929ce285030ed1922438aa85","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51043","feedbackHash":"f1fee8759dcd808b78054918dc62d4cc51f9da1b7b40dc8bc30ac9e16fce7898","nodeKey":"manifest","submissionHash":"584e078657265ff13bab839be73376d5446c46f0226ce2c4a41974c4f1c229bc","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52250","feedbackHash":"6e9ca091c588ae8a318368b11ca5e01fd200ebdcb7e04c8f43b2dc0fd8f4e5d8","nodeKey":"write_foundry_tests","submissionHash":"d8ba53585a663e569be9480708f2943a9ae6aa0fd1499f7788db701937f1f8b5","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52268","feedbackHash":"39dbbc463476d9f683b5718b60917c79b33374f0bb808e4b596ebb380e490b61","nodeKey":"write_foundry_tests","submissionHash":"e044168e6bfb0001046654c9cf2c68551f2918ebd7980a2860a67dc277ebd602","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"c6db45e5501656f4328a6bf3c5d7ba32ec1e3784093253ca51a467e4e4655550","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b8fd8054ea57c5d8","findings":[{"citation":"resolved","description":"beforeSwap derives the exact-in buy fee as fee = specified - floor(specified*10000/(10000+rate)) (src/MediciHook.sol:121-124), while afterSwap recomputes exact = floor(ethBase*rate/10000) on the pool's settled ETH delta and requires fee <= exact + 1. Algebraically, with m = (specified*10000) mod (10000+rate), fee - exact = ceil(m/10000), which is 2 whenever m > 10000. m can exceed 10000 whenever rate does not divide 10000; during the decay the rate takes 897 of its 901 per-second values in that set (gcd(10000,rate) < rate), so at e.g. rate 4989 bps (two seconds after open) 4988/14989 = 33% of all exact-in amounts fail, at 4680 bps (one minute) 31%, at 1800 bps (ten minutes) 14%. At the steady 200 bps rate m is a multiple of 200 and the gap is at most 1, which is why the existing suite (fees checked at 1 day, decay only tested with a price-limited partial fill where fee = quoted*rate/10000 exactly) never hits it. Impact: a large share of ordinary buys from routers and wallets revert for the whole launch window, with the revert surfacing inside afterSwap after the pool math ran. Fix: make the two sides agree, e.g. have afterSwap accept fee <= exact + 2 when the buy rate is in decay, or compute the beforeSwap fee as floor(target*rate/10000) + (remainder != 0) from the same formula afterSwap uses, keeping fee = specified - target only as the upper bound.","line":193,"path":"src/MediciHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciHook} from \"src/MediciHook.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\ncontract Swapper {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager m) {\n        manager = m;\n    }\n\n    function modify(PoolKey memory k, int24 lower, int24 upper, int256 liquidity) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(0), k, abi.encode(ModifyLiquidityParams(lower, upper, liquidity, 0)))),\n            (BalanceDelta)\n        );\n    }\n\n    function swap(PoolKey memory k, bool zeroForOne, int256 amount, uint160 limit) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(1), k, abi.encode(SwapParams(zeroForOne, amount, limit)))), (BalanceDelta)\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory k, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) (d,) = manager.modifyLiquidity(k, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        else d = manager.swap(k, abi.decode(args, (SwapParams)), \"\");\n        _settle(k.currency0, d.amount0());\n        _settle(k.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) internal {\n        if (d < 0) {\n            uint256 amount = uint256(-int256(d));\n            manager.sync(c);\n            if (c.isAddressZero()) {\n                manager.settle{value: amount}();\n            } else {\n                require(IERC20(Currency.unwrap(c)).transfer(address(manager), amount));\n                manager.settle();\n            }\n        } else if (d > 0) {\n            manager.take(c, address(this), uint128(d));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @notice During the 15-minute buy-fee decay, beforeSwap's exact-in fee (specified - floor(specified*10000/(10000+rate)))\n/// can exceed afterSwap's recomputed bound floor(target*rate/10000) + 1 by one more wei, so the swap reverts InvalidQuote.\ncontract BuyDecayRoundingTest is Test {\n    uint64 internal constant OPEN = 1_800_000_000;\n    uint160 internal constant Q96 = 79228162514264337593543950336;\n    PoolManager internal manager;\n    MediciToken internal token;\n    MediciHook internal hook;\n    PoolKey internal key;\n    Swapper internal actor;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 1_000 ether);\n        manager = new PoolManager(address(this));\n        token = new MediciToken();\n        bytes memory code = abi.encodePacked(type(MediciHook).creationCode, abi.encode(manager, address(token)));\n        bytes32 hash = keccak256(code);\n        address at;\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (!HookFlags.matches(predicted, HookFlags.MEDICI)) continue;\n            assembly (\"memory-safe\") {\n                at := create2(0, add(code, 32), mload(code), salt)\n            }\n            break;\n        }\n        require(at != address(0), \"hook deployment failed\");\n        hook = MediciHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        manager.initialize(key, Q96 * 10_000);\n        actor = new Swapper(manager);\n        vm.deal(address(actor), 100 ether);\n        token.transfer(address(actor), 600_000_000 ether);\n        actor.modify(key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e22);\n    }\n\n    /// Two seconds after open the buy rate is 4989 bps. For specified = 1 ether + 1 wei:\n    /// target = floor(specified * 10000 / 14989) = 667155914337180599, fee = specified - target = 332844085662819402,\n    /// but afterSwap recomputes exact = target * 4989 / 10000 = 332844085662819400 and requires fee <= exact + 1.\n    /// About a third of all exact-in buy amounts hit this during the decay window (whenever specified*10000 mod 14989 > 10000).\n    function test_exactInBuyDuringDecayDoesNotRevertOnTwoWeiRoundingGap() public {\n        vm.warp(OPEN + 2);\n        assertEq(hook.buyFeeBps(), 4989);\n        // Control: an amount whose remainder is below 10000 settles fine.\n        BalanceDelta ok = actor.swap(key, true, -int256(1 ether), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(ok.amount0(), -int128(int256(1 ether)));\n        // One wei more must be a valid buy as well; today it reverts with InvalidQuote inside afterSwap.\n        uint256 specified = 1 ether + 1;\n        BalanceDelta d = actor.swap(key, true, -int256(specified), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(specified)));\n        assertGt(d.amount1(), 0);\n    }\n}","reproduction":"Pool initialized at sqrtPrice 10000*2^96 with 1e22 full-range liquidity (same as test/helpers/Fixtures.sol). Warp to openedAt + 2 (buyFeeBps() == 4989). Swap zeroForOne exact-in with amountSpecified = -(1 ether + 1), sqrtPriceLimit MIN_SQRT_PRICE + 1. beforeSwap: target = floor((1e18+1)*10000/14989) = 667155914337180599, fee = 332844085662819402; full fill so quoted == target. afterSwap: ethBase = 667155914337180599, exact = 667155914337180599*4989/10000 = 332844085662819400, fee == exact + 2 -> revert InvalidQuote (selector 0xf8618030, wrapped by PoolManager as WrappedError(hook, afterSwap selector, 0xf8618030)). Expected: the buy settles with amount0 == -(1 ether + 1) and the hook accrues ~49.9% of 667155914337180599 as fee. The same swap with amountSpecified = -1 ether (remainder 1589 < 10000) succeeds, showing it is the amount, not the state, that breaks. Run: forge test --match-path test/scratch/BuyDecayRounding.t.sol","severity":"medium","snippet":"            // Specified-side integer budgets may round the fee up by one wei.\n            uint256 exact = ethBase * rate / 10_000;\n            if (fee < exact || fee > exact + 1) revert InvalidQuote();","title":"Exact-in ETH buys during the 15-minute fee decay revert with InvalidQuote for roughly a third of all input amounts"},{"citation":"resolved","description":"price() sets start = openedAt when the pool is younger than a day and end = block.timestamp, then calls twap(start, end), which reverts NotOpen when end <= start. In the block whose timestamp equals openedAt (the factory initializes the pool and the first trades can land in that same block) every consumer of price() reverts: MediciTreasury.price(), proposeCost(), propose() (line 200 calls proposeCost) and buyback() (line 571). The brief says price() is the 24h TWAP 'or since open if younger'; an age of zero has a defined value (the opening spot stored in observations[0]) but the code reverts instead. Boundary only, no funds at risk. Fix: in price(), return observations[observations.length-1].spot when end == start, or let twap treat end == start as the latest spot.","line":295,"path":"src/MediciHook.sol","reproduction":"Deploy the hook and initialize the pool at timestamp T (vm.warp(T); manager.initialize(key, 10000*2^96)). Without advancing time call hook.price(): reverts NotOpen (0x... selector of NotOpen()). treasury.proposeCost() and treasury.propose(\"t\", \"{}\") (with a MEDICI approval) revert with the same error bubbled from the hook. Warp to T + 1: hook.price() returns 1e10 as expected. Verified with test/scratch/PriceAtOpen.t.sol (expectRevert version passes on current code).","severity":"low","snippet":"    function twap(uint64 start, uint64 end) public view returns (uint256) {\n        if (end <= start) revert NotOpen();\n        return (cumulativeAt(end) - cumulativeAt(start)) / (end - start);\n    }","title":"price() reverts in the pool's opening second, so propose, proposeCost and buyback revert instead of using the opening price"}],"hash":"021ffb5f59ecf3249137fdd9cbf6691da43a7dbe38d02eec1313d21a4fcae1cb","nodeId":"10e4aca3-f9a3-48f4-919b-9c940416d624","outcome":"completed","summary":"The review is complete. The findings file is written, the proof fails on the current code for the stated reason, and no repository file was changed (git status is clean apart from the gitignored scratch tests and the findings file).\n\n**Outcome**\n\nI audited the math of the Medici hook, treasury and fee router against the Math Precision, Boundary and Numerical Gap guides. Two defects, written to `.imd-findings.json` with coverage rows for all 35 entry points plus four invariants.\n\n**Finding 1, medium, with proof.** Exact-in ETH buys during the 15-minute fee decay revert for about a third of all input amounts. The fee that `beforeSwap` charges is the input minus the rounded-down pool target, while `afterSwap` recomputes the fee from the settled delta and allows at most one wei of difference. The difference is the ceiling of a remainder over 10000 and reaches two wei whenever the decaying rate does not divide 10000, which is true for 897 of the 901 per-second rates in the window. Concrete input: two seconds after open, a buy of one ether plus one wei reverts with InvalidQuote while one ether settles. The existing suite misses it because its decay test uses a price-limited partial fill, where both sides use the same formula. The proof is `test/scratch/BuyDecayRounding.t.sol`.\n\n**Finding 2, low.** In the pool's opening second the price view reverts because the TWAP window has zero length, so proposeCost, propose and buyback revert too, instead of returning the opening spot the brief implies for \"since open if younger.\"\n\n**Coverage.** Every entry point has a row. I traced the fee split, claims versus take paths, the TWAP accumulator bounds, propose cost clamping, epoch and extension boundaries, tie-breaking, quorum and bounty caps, the buyback vesting stream and its reserve bookkeeping, pro-rata backer claims, and the two swap callbacks. Three invariants hold: escrow plus buyback reserve never exceeds the treasury balance, the reserve equals the streamed 40 percent minus spend, and the hook balance equals team plus treasury owed after redemption.\n\n**Caveat.** The 1.5 GB memory cap kills solc under the project's via-IR setting, so I compiled and ran everything with via-IR disabled through an environment override. The arithmetic is unaffected, and the verifier's own build should reproduce the failure unchanged.","treeHash":null,"usage":{"cachedInputTokens":1636532,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":56127,"runtime":"claude","turns":34,"wallClockMs":964477}},{"artifacts":[],"attempt":2,"bundleHash":"e46a36677c785e926144a03fb28a7554a9836250c4d1eecb48040f14832a8539","device":"e35a3abed04af1ac","findings":[],"hash":"024ce20ec6059c6ca9c0847cfa710452e0e59d57a003486187442ee2086989dd","nodeId":"c9c2c07b-9a29-4b62-8fd5-b54f3966ae28","outcome":"completed","summary":"Implemented all four contracts and documented deployment in [README.md](/home/imd6/.identitymd/work/48844890-e197-4e13-b271-6b39a72a2b51/c9c2c07b-9a29-4b62-8fd5-b54f3966ae28/README.md).\n\n- Build, tests, and formatting checks pass.\n- 47 tests passed; 8,192 invariant calls completed without reverts.\n- Offline dependencies included.\n- Source deliverable: **1.24 MB**, below 8 MiB. Generated build files removed.","treeHash":"06c4f915f88aed3cc092a6dc36198e8a5d3b977c","usage":{"cachedInputTokens":7416320,"inputTokens":178344,"model":"gpt-6-astra","outputTokens":76164,"runtime":"codex","turns":11,"wallClockMs":1711173}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7cb215928f3c9473","findings":[{"citation":"resolved","description":"Merged from audit_permissions, audit_math, audit_flow and write_foundry_tests (same root cause). For an exact-in buy beforeSwap charges fee = S - floor(S*10000/(10000+rate)) (lines 121-124), while afterSwap recomputes exact = floor(ethBase*rate/10000) with ethBase == target and requires fee <= exact + 1. With m = (S*10000) mod (10000+rate), fee - exact can be 2 whenever m > 10000, which is possible for any rate whose 10000+rate does not divide evenly. At the steady 200 bps it never happens, but in the decay window (rate 5000 -> 200 bps) roughly 14-33% of ordinary input amounts revert inside afterSwap. The swap the pool would fill is rejected, so in the launch window, when exact-in ETH buys are the main trade, many buys fail with an opaque HookCallFailed. No funds are lost, but the spec guarantee (a fee on every buy, exact-in included) breaks during launch. Fix: accept fee <= exact + 2, or compute the fee in beforeSwap with the same formula afterSwap uses (floor(target*rate/10000), rounded up at most 1).","line":193,"path":"src/MediciHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciHook} from \"src/MediciHook.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\ncontract Swapper {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey memory k, int24 lower, int24 upper, int256 liquidity) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(0), k, abi.encode(ModifyLiquidityParams(lower, upper, liquidity, 0)))),\n            (BalanceDelta)\n        );\n    }\n\n    function swap(PoolKey memory k, bool zeroForOne, int256 amount, uint160 limit) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(1), k, abi.encode(SwapParams(zeroForOne, amount, limit)))), (BalanceDelta)\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory k, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) (d,) = manager.modifyLiquidity(k, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        else d = manager.swap(k, abi.decode(args, (SwapParams)), \"\");\n        _settle(k.currency0, d.amount0());\n        _settle(k.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) internal {\n        if (d < 0) {\n            uint256 amount = uint256(-int256(d));\n            manager.sync(c);\n            if (c.isAddressZero()) {\n                manager.settle{value: amount}();\n            } else {\n                require(IERC20(Currency.unwrap(c)).transfer(address(manager), amount));\n                manager.settle();\n            }\n        } else if (d > 0) {\n            manager.take(c, address(this), uint128(d));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @notice Exact-in ETH buys during the launch decay revert in afterSwap because the\n/// specified-side fee computed in beforeSwap (specified - target) can exceed\n/// floor(target * rate / 10_000) by two wei, while afterSwap allows at most one.\ncontract FeeBoundTest is Test {\n    PoolManager internal manager;\n    MediciToken internal token;\n    MediciHook internal hook;\n    PoolKey internal key;\n    Swapper internal actor;\n    uint64 internal constant OPEN = 1_800_000_000;\n    uint160 internal constant Q96 = 79228162514264337593543950336;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 1_000 ether);\n        manager = new PoolManager(address(this));\n        token = new MediciToken();\n        bytes memory code = abi.encodePacked(type(MediciHook).creationCode, abi.encode(manager, address(token)));\n        bytes32 hash = keccak256(code);\n        address at;\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (!HookFlags.matches(predicted, HookFlags.MEDICI)) continue;\n            assembly (\"memory-safe\") {\n                at := create2(0, add(code, 32), mload(code), salt)\n            }\n            break;\n        }\n        require(at != address(0), \"CREATE2 mining/deployment failed\");\n        hook = MediciHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        manager.initialize(key, Q96 * 10_000);\n        actor = new Swapper(manager);\n        vm.deal(address(actor), 100 ether);\n        token.transfer(address(actor), 600_000_000 ether);\n        actor.modify(key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e22);\n    }\n\n    /// 60 seconds after open the buy rate is 4680 bps. For a 0.01 ETH exact-in buy:\n    /// target = 1e16 * 10000 / 14680 = 6811989100817438, fee = 3188010899182562,\n    /// but floor(target * 4680 / 10000) = 3188010899182560, so fee == exact + 2 and\n    /// afterSwap reverts with InvalidQuote. The trade itself is perfectly valid.\n    function test_exactInBuyOfRoundAmountDuringDecayMustNotRevert() public {\n        vm.warp(OPEN + 60);\n        assertEq(hook.buyFeeBps(), 4680);\n        uint256 amount = 0.01 ether;\n        uint256 target = amount * 10_000 / (10_000 + 4680);\n        assertEq(amount - target, target * 4680 / 10_000 + 2, \"fee is two wei above the rounded-down rate\");\n        BalanceDelta d = actor.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(amount)), \"the swapper pays exactly the specified ETH\");\n        assertGt(d.amount1(), 0, \"and receives MEDICI\");\n        assertEq(hook.teamOwed(), target * 50 / 10_000, \"team share accrued on the pool ETH delta\");\n    }\n\n    /// The same bound is reached at many other decay rates; one second after open (4994 bps)\n    /// with 0.01 ETH + 2 wei.\n    function test_exactInBuyOneSecondAfterOpen() public {\n        vm.warp(OPEN + 1);\n        assertEq(hook.buyFeeBps(), 4994);\n        uint256 amount = 10000000000000002;\n        BalanceDelta d = actor.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(amount)));\n        assertGt(d.amount1(), 0);\n    }\n}","reproduction":"Ran .imd/reads/proofs/Proof_4d1958ed063b.t.sol (copied to test/scratch): fresh PoolManager, MediciHook mined for 0x20cc, ETH/MEDICI pool at 10000*2^96 with 1e22 full-range liquidity. At openedAt+60 (buyFeeBps()==4680), swap zeroForOne with amountSpecified = -1e16 and limit MIN_SQRT_PRICE+1. beforeSwap: target = 6811989100817438, fee = 3188010899182562. afterSwap: ethBase = 6811989100817438, exact = 3188010899182560, so fee == exact+2 and it reverts InvalidQuote. Observed: FAIL WrappedError(hook, 0xb47b2fb1, 0xf8618030, 0xa9e35b2f). Same at openedAt+1 with 10000000000000002 wei. Expected: the swap settles and the 46.8% fee accrues. The other three specialist proofs (Proof_701e09d9815d: 12 of 40 consecutive amounts fail; Proof_c01dd3e25509) fail the same way.","severity":"medium","snippet":"            if (fee < exact || fee > exact + 1) revert InvalidQuote();","title":"Exact-in ETH buys revert with InvalidQuote for a large share of amounts during the 15-minute buy-fee decay (afterSwap tolerance one wei too tight)"},{"citation":"resolved","description":"Merged from audit_economics, audit_permissions and audit_flow. The two factories number their positions independently, but launchFactory[n]/launchToken[n] (lines 469-470) and harvest(n) (line 623) use n alone, and _findLaunch (lines 440-443) always tries LAUNCH_FACTORY first. Case 1: launch n on one factory is recorded for epoch A. A later epoch B whose commissioned launch is position n on the other factory then reverts WrongState in recordCommission forever, even though the treasury is the requester and holds >= 5% of supply. Epoch B can only lapse: the proposer never gets the bounty, the voters never get the backer share, the 5% allocation of that token sits in the treasury with no function that can move or burn it, and the treasury can never harvest that position's fees. Case 2: if both factories' position n name the treasury and neither is recorded, the ProjectFactory launch cannot be selected, and the LaunchFactory token is reserved instead. This breaks the spec's 'each launch once; requester == treasury (either factory)'. Fix: key the bookkeeping by (factory, n). In recordCommission, probe both factories, skip the one already recorded, and revert if both are eligible and ambiguous. harvest needs the same key.","line":459,"path":"src/MediciTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciTreasury} from \"src/MediciTreasury.sol\";\nimport {Protocol, IIntake, IMediciHook} from \"src/Protocol.sol\";\n\ncontract CollisionMockToken is ERC20 {\n    constructor() ERC20(\"Launch asset\", \"ASSET\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Stands in for either factory. Each factory instance keeps its own numbering.\ncontract CollisionMockFactory {\n    struct Position {\n        PoolKey key;\n        int24 lower;\n        int24 upper;\n        address requester;\n    }\n    mapping(uint64 => Position) private positions;\n    mapping(uint64 => uint256) public pending1;\n\n    function register(uint64 n, PoolKey memory key, address requester) external {\n        positions[n] = Position(key, -60, 60, requester);\n    }\n\n    function positionOf(uint64 n) external view returns (PoolKey memory, int24, int24, address) {\n        Position storage p = positions[n];\n        return (p.key, p.lower, p.upper, p.requester);\n    }\n\n    function claimFees(uint64 n) external returns (uint256 a0, uint256 a1) {\n        Position storage p = positions[n];\n        require(p.requester != address(0));\n        a1 = pending1[n];\n        delete pending1[n];\n        if (a1 != 0) require(IERC20(Currency.unwrap(p.key.currency1)).transfer(p.requester, a1));\n    }\n\n    function withdraw(address, Currency) external {}\n    receive() external payable {}\n}\n\ncontract CollisionMockIntake is IIntake {\n    uint256 public nonce;\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(msg.sender, ++nonce));\n    }\n}\n\ncontract CollisionMockHook is IMediciHook {\n    uint64 public openedAt;\n    uint256 public price = 1e10;\n\n    constructor(uint64 at) {\n        openedAt = at;\n    }\n\n    function twap(uint64, uint64) external view returns (uint256) {\n        return price;\n    }\n\n    function poolKey() external pure returns (PoolKey memory) {\n        revert();\n    }\n}\n\n/// @notice Two different launches, one per factory, that happen to carry the same launch number.\n/// The treasury keys recorded launches by number alone, so the second one can never be recorded,\n/// its backers never get a share, its proposer never gets the bounty, and the launch tokens the\n/// treasury already holds for it can never be claimed, burned or harvested.\ncontract LaunchNumberCollisionTest is Test {\n    MediciToken token;\n    MediciTreasury treasury;\n    CollisionMockHook oracle;\n    CollisionMockFactory launchFactory;\n    CollisionMockFactory projectFactory;\n    CollisionMockToken imd;\n    address alice = address(0xA11CE);\n    uint64 constant OPEN = 1_800_000_000;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 100 ether);\n        token = new MediciToken();\n        oracle = new CollisionMockHook(OPEN);\n        treasury = new MediciTreasury(IPoolManager(address(this)), address(token), IMediciHook(address(oracle)));\n        token.transfer(alice, 300_000_000 ether);\n        vm.prank(alice);\n        token.approve(address(treasury), type(uint256).max);\n\n        CollisionMockFactory f = new CollisionMockFactory();\n        vm.etch(Protocol.LAUNCH_FACTORY, address(f).code);\n        vm.etch(Protocol.PROJECT_FACTORY, address(f).code);\n        launchFactory = CollisionMockFactory(payable(Protocol.LAUNCH_FACTORY));\n        projectFactory = CollisionMockFactory(payable(Protocol.PROJECT_FACTORY));\n        CollisionMockIntake i = new CollisionMockIntake();\n        vm.etch(Protocol.INTAKE, address(i).code);\n        CollisionMockToken t = new CollisionMockToken();\n        vm.etch(Protocol.IMD, address(t).code);\n        imd = CollisionMockToken(Protocol.IMD);\n    }\n\n    function _winEpoch(uint256 e, bytes memory brief) internal {\n        vm.prank(alice);\n        uint256 id = treasury.propose(\"A commissioned launch\", brief);\n        vm.prank(alice);\n        treasury.vote(e, id, 3_000_000 ether);\n        (bool ok,) = address(treasury).call{value: 1 ether}(\"\");\n        require(ok);\n        vm.warp(treasury.epochEnd(e));\n        treasury.settle(e);\n        imd.mint(address(treasury), 0.5 ether);\n        vm.prank(Protocol.KEEPER);\n        treasury.commission(e, brief);\n    }\n\n    function _position(CollisionMockFactory f, uint64 n, CollisionMockToken asset) internal {\n        PoolKey memory k =\n            PoolKey(Currency.wrap(address(0)), Currency.wrap(address(asset)), 12_500, 60, IHooks(address(0)));\n        f.register(n, k, address(treasury));\n        asset.mint(address(this), 950 ether);\n        asset.mint(address(treasury), 50 ether);\n    }\n\n    function test_sameLaunchNumberOnTheOtherFactoryCanStillBeRecorded() public {\n        // Epoch 1: a hook launch, number 77 on LaunchFactory, is recorded.\n        _winEpoch(1, bytes(\"{\\\"idea\\\":\\\"one\\\"}\"));\n        CollisionMockToken assetA = new CollisionMockToken();\n        _position(launchFactory, 77, assetA);\n        vm.prank(Protocol.KEEPER);\n        treasury.recordCommission(1, 77, \"job-1\");\n        (uint8 status,,,,,,) = treasury.epochResult(1);\n        assertEq(status, 2);\n\n        // Epoch 2: a project launch, number 77 on ProjectFactory. The treasury is its requester\n        // and holds 5% of its supply, exactly what recordCommission demands.\n        vm.warp(treasury.epochEnd(1) + 1);\n        _winEpoch(2, bytes(\"{\\\"idea\\\":\\\"two\\\"}\"));\n        CollisionMockToken assetB = new CollisionMockToken();\n        _position(projectFactory, 77, assetB);\n        vm.prank(Protocol.KEEPER);\n        treasury.recordCommission(2, 77, \"job-2\");\n\n        (status,,,,,,) = treasury.epochResult(2);\n        assertEq(status, 2, \"the second launch is recordable\");\n        assertEq(treasury.claimable(2, alice), 50 ether, \"its backers can claim their share\");\n    }\n}","reproduction":"Ran .imd/reads/proofs/Proof_4b32e422eff6.t.sol (copied to test/scratch). Epoch 1: winner commissioned. LaunchFactory.positionOf(77) = (ETH/assetA, requester treasury), treasury holds 50/1000 assetA, and recordCommission(1,77,'job-1') succeeds. Epoch 2: winner commissioned. ProjectFactory.positionOf(77) = (ETH/assetB, requester treasury), treasury holds 50/1000 assetB. KEEPER calls recordCommission(2,77,'job-2'). Expected: status 2 with backer share assetB. Actual: revert WrongState() (observed: [FAIL: WrongState()] test_sameLaunchNumberOnTheOtherFactoryCanStillBeRecorded).","severity":"medium","snippet":"        if (x.status != 1 || !x.commissioned || launchFactory[n] != address(0) || block.timestamp >= x.deadline) {","title":"Recorded launches are keyed by launch number alone, so the same number on LaunchFactory and ProjectFactory collides: the second launch can never be recorded, or the wrong one is recorded"},{"citation":"resolved","description":"The spec says the bounty is 'kept if the transfer fails'. In _payBounty a failure re-adds the amount to escrowed (lines 489-492). The only exit is claimBounty(e), which retries the same fixed proposer, with no expiry and no redirect. A proposer contract with no receive() (or one that reverts) therefore locks up to 1 ETH per epoch out of freeETH() for good. That lowers later bounties (10% of free ETH), the IMD-purchase ceiling in commission() and keeperWithdraw.","line":488,"path":"src/MediciTreasury.sol","reproduction":"Proposer P is a contract whose receive() reverts. P wins epoch 1 with burns >= 0.02 ETH at the epoch TWAP, and the treasury has 10 ETH free, so settle(1) escrows 1 ETH. commission and then recordCommission(1,n,job) emit BountyPaid(1,P,1e18,false), and escrowed stays 1e18. Every later claimBounty(1) emits success=false. freeETH() is 1 ETH lower forever. Expected per spec: the treasury keeps the bounty (escrow released to free ETH).","severity":"low","snippet":"        (bool ok,) = proposer.call{value: amount}(\"\");","title":"A failed bounty transfer stays escrowed forever instead of being kept by the treasury, permanently reducing freeETH"},{"citation":"resolved","description":"harvest() claims both currencies, burns MEDICI through treasury.burnDeposited(), then sends the ETH to treasury.team() and reverts if the call fails (line 54). That undoes the burn too, so as long as any ETH fee is owed, both currencies stay in the factory until TEAM hands itself over. The hook's claimTeam isolates the same failure from trading. The router does not isolate it from the burn. Only TEAM can fix it.","line":53,"path":"src/MediciFeeRouter.sol","reproduction":"TEAM calls treasury.setTeam(C), where C has no receive/fallback. The position accrues 1 ETH and 1000 MEDICI of fees. Anyone calls router.harvest(): MEDICI is moved and burned, then C.call fails, TransferFailed is raised and the whole call reverts. totalBurned is unchanged. Expected: MEDICI burned, with ETH accrued for TEAM or sent later.","severity":"low","snippet":"            (bool ok,) = treasury.team().call{value: eth}(\"\");","title":"FeeRouter.harvest reverts entirely when TEAM cannot receive ETH, so the MEDICI fees are not burned either"},{"citation":"resolved","description":"When block.timestamp == openedAt, price() calls twap(openedAt, openedAt), which reverts NotOpen. Every consumer reverts in that block: treasury.price(), proposeCost(), propose() and buyback(). The spec defines price() as the TWAP since open when the pool is younger than 24h. At age 0 a value exists (observations[0].spot), but the code reverts. Boundary only, no funds at risk.","line":295,"path":"src/MediciHook.sol","reproduction":"vm.warp(T); initialize the pool through the PoolManager (openedAt = T). Without advancing time, hook.price() reverts NotOpen(), and treasury.proposeCost() and propose('t','{}') bubble the same error. At T+1, price() returns the opening spot (1e10 at sqrtPrice 10000*2^96). Expected: the opening spot at T.","severity":"low","snippet":"        if (end <= start) revert NotOpen();","title":"price() reverts in the pool's opening second, so propose/proposeCost/buyback revert instead of using the opening spot"}],"hash":"08f66ac5d7caec56301126d3ee94c00fe5dd412b1d5b77bfe3be258f102a7108","nodeId":"f2fd5194-ee24-4f4d-a010-f5e99e44b69d","outcome":"completed","summary":"The review is finished: 5 findings and a coverage row for all 35 entry points are in `.imd-findings.json`. Nothing is critical or high. Two findings are medium, one of which I reproduced by running its proof and the other partly, and three are low. I ran the four specialist proof tests, then deleted `test/scratch/`. No source files were changed.\n\n**Medium**\n1. **Exact-input ETH buys fail during the 15-minute fee decay** (`src/MediciHook.sol:193`). Four specialists reported this; it is merged into one finding. `beforeSwap` works out the fee one way and `afterSwap` checks it with a slightly different formula that allows only one wei of rounding. While the buy fee is decaying, the difference can be two wei, so the swap reverts with `InvalidQuote`. Roughly 14–33% of ordinary amounts fail, for example 0.01 ETH at 60 seconds after open. All four specialist proofs fail on the current code with this error, and one is attached. The fix is to allow two wei, or use the same formula on both sides.\n2. **The same launch number on the two factories collides** (`src/MediciTreasury.sol:459`). Three specialists reported this; it is merged. Recorded launches and `harvest` are keyed by the launch number alone, and lookups always try LaunchFactory first. Two consequences:\n   - Once launch `n` from one factory is recorded, launch `n` from the other one can never be recorded (`WrongState`). That epoch can only lapse: the proposer gets no bounty, the voters get no backer share, and the treasury's 5% of that token is stuck.\n   - If launch `n` on both factories names the treasury as requester and neither is recorded yet, the ProjectFactory launch can't be selected and the LaunchFactory token is reserved instead. I traced this from the code; the proof doesn't cover it.\n\n   The attached proof fails as described. The fix is to key the records by factory and number.\n\n**Low**\n3. **A failed bounty is never released** (`MediciTreasury.sol:488`). If the proposer can't receive ETH, the bounty stays escrowed forever and can only be retried to the same address. The spec says the treasury keeps it. Instead it permanently lowers the treasury's free ETH.\n4. **Fee router harvest reverts when TEAM can't receive ETH** (`MediciFeeRouter.sol:53`). Because the ETH payment and the MEDICI burn are in the same call, the burn is undone too. Only TEAM can fix it, by handing the role to a new address with `setTeam`.\n5. **`price()` reverts in the pool's opening second** (`MediciHook.sol:295`). In that block, `propose`, `proposeCost` and `buyback` all revert instead of using the opening price.\n\n**Dropped:** the specialist note about ETH sent directly to the hook. It is unclaimable but harms no one's funds.\n\n**My own pass found nothing new.** It covered:\n- **Deployment:** hook creation code is 31,020 bytes and runtime 8,344, both under the limits. `launch.json` matches the constructor and the hook permissions (address bits 0x20cc).\n- **Hook:** the quote simulation and the treasury's fee exemption behave as intended.\n- **Treasury:** epoch and extension timing, buyback vesting and reserve accounting, and the Intake call shape and callbacks are as intended.","treeHash":null,"usage":{"cachedInputTokens":996963,"inputTokens":24,"model":"claude-opus-5-5","outputTokens":23655,"runtime":"claude","turns":17,"wallClockMs":519420}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"45aa937328087de3","findings":[{"citation":"resolved","description":"Merged from write_foundry_tests (b6d29c1099c7). The fix for the opening-second revert returns the latest observation's spot when block.timestamp == openedAt. _observe (lines 277-279) overwrites that same observation's spot on every swap in the opening second, so within the launch block price() is whatever the last swap left, not an average, and the factory's opening price is lost from the accumulator (observations[0].spot is replaced). MediciTreasury.proposeCost() divides 0.002 ETH by that price, so a proposal landing in the launch block pays a cost set by the previous swap in that block. buyback() is not exposed: no stream exists before the first settle, a week later. The exposure is one block and is economically irrational to exploit: the buy that moves the price pays the 50% launch-rate hook fee (0.25 ETH on 0.5 ETH below) to save under 0.001 ETH of MEDICI on one proposal. From openedAt+1 the since-open TWAP applies. No change is required; if the author prefers a manipulation-free opening value, return observations[0].spot captured at beforeInitialize (kept in a separate immutable field so _observe cannot overwrite it) instead of the latest spot.","line":307,"path":"src/MediciHook.sol","reproduction":"Real-hook fixture (test/helpers/Fixtures.sol _deployReal(true), pool at sqrtPrice 10000*2^96, 1e22 full-range liquidity), block.timestamp == openedAt. Before any swap: hook.price() == 10_000_000_000, treasury.proposeCost() == 200_000e18. Same second: actor.swap(key, zeroForOne=true, amountSpecified=-0.5 ether, MIN_SQRT_PRICE+1). After: hook.price() == 17_666_840_277 (== observations(0).spot, the opening 1e10 is gone), treasury.proposeCost() == 113_206_434_690_177_620_379_241 (about 113,206 MEDICI). Expected under TWAP semantics: the cost of a proposal in that second is unchanged by the swap. Measured with a scratch test deriving from HookEdgeTest; test/Edges.t.sol test_openingSecondPriceIsTheSpotAndCanBeMovedWithinThatSecond documents the same behaviour.","severity":"info","snippet":"        if (end == openedAt) return observations[observations.length - 1].spot;","title":"In the opening second price() is the live spot, so a swap in the launch block moves proposeCost() and overwrites the opening observation"}],"hash":"273c998b066d3ca3426d2a79e7f1cbc701a4557672a6994d7381c11d9a9dbdf0","nodeId":"f2fd5194-ee24-4f4d-a010-f5e99e44b69d","outcome":"completed","summary":"The review is complete. The findings file is valid JSON with one info finding and all 36 entry points covered, and the scratch tests are removed so the tree is unchanged.\n\n**All five findings from the previous round are fixed.**\n\n- **Exact-in buy revert during the decay (medium):** afterSwap now accepts a fee up to two wei above the recomputed value. I re-derived the bound: the gap is in {0, 1, 2} for buy rates up to 5000 bps and at most 1 for exact-out sells. All four prior proofs, seven tests in total, pass on the revised code.\n- **Launch number collision across factories (medium):** recorded launches are keyed by factory and number, recordCommission probes both factories and reverts AmbiguousLaunch when both qualify, and recordCommissionAt names the factory. Both prior proofs pass.\n- **Bounty stuck in escrow (low):** a refused bounty is released into free ETH and paid once. Confirmed in code and in the author's test.\n- **Router harvest blocked by a rejecting TEAM (low):** the burn completes and the ETH waits in the router for payTeam. Confirmed.\n- **price() revert in the opening second (low):** it now returns the latest spot. Confirmed.\n\n**Specialist leads this round.** The harvest coupling lead is dropped as unreachable. The treasury has no code path that calls setRequester, and only the current requester may hand a position over, so a recorded position's requester can never stop being the treasury. The stray-ETH-in-hook lead is not a defect and was left out.\n\n**One new entry, info only.** In the launch block price() is the live spot, so a swap in that same second changes the proposal cost and overwrites the opening observation. I measured a 0.5 ETH buy moving the cost from 200,000 to about 113,206 MEDICI while paying 0.25 ETH in hook fees. It is one block wide and irrational to exploit, so it needs no change. A manipulation-free option is to return the opening spot captured at initialization.\n\nNothing blocks the launch from my side. The full project suite passes with 95 tests.","treeHash":null,"usage":{"cachedInputTokens":1195605,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":24141,"runtime":"claude","turns":21,"wallClockMs":763221}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa4b91aed45c94fa","findings":[],"hash":"3e5c67ae6d127b2528a446126237cc4306c0c1287b376b6f5e3ce4168ee26241","nodeId":"c9c2c07b-9a29-4b62-8fd5-b54f3966ae28","outcome":"failed","summary":"Source bundle is 15977174 bytes; the upload limit is 8388608 bytes (8 MiB). Remove unnecessary generated dependencies/caches or redundant package archives from your changes and rebuild the requested export. Preserve required source, runtime assets, declared deliverables and accepted/protected dependencies. Do not silently omit required work. Largest changed source files: [{\"path\":\"lib/v4-core/docs/security/audits/DRAFT_ABDK_audit_core.pdf\",\"bytes\":5110121},{\"path\":\"lib/v4-periphery/audits/DRAFT_ABDK_audit_periphery_universal_router.pdf\",\"bytes\":5018744},{\"path\":\"lib/v4-core/docs/security/audits/TrailOfBits_audit_core.pdf\",\"bytes\":2028568},{\"path\":\"lib/openzeppelin-contracts/audits/2018-10.pdf\",\"bytes\":1000527},{\"path\":\"lib/openzeppelin-contracts/audits/2023-10-v5.0.pdf\",\"bytes\":910284},{\"path\":\"lib/v4-core/docs/security/audits/DRAFT_Certora_audit_core.pdf\",\"bytes\":762350},{\"path\":\"lib/v4-periphery/audits/permissionedPools/Cantina_report_permissioned_pools.pdf\",\"bytes\":555764},{\"path\":\"lib/openzeppelin-contracts/audits/2023-05-v4.9.pdf\",\"bytes\":485395}].","treeHash":null,"usage":{"cachedInputTokens":5119125,"inputTokens":1026,"model":"claude-fable-5-1","outputTokens":132841,"runtime":"claude","turns":72,"wallClockMs":1665100}},{"artifacts":[],"attempt":1,"bundleHash":"daef7721267f3106572012bf0ba493737a779de039a53f380c83ea01c71e5a8e","device":"122a561f18b5bb1e","findings":[],"hash":"3efcdc6ad6155887218890653d7aa1d4f9e1023e929ce285030ed1922438aa85","nodeId":"3982909f-4c95-4827-97e6-4fa9ba985cb1","outcome":"completed","summary":"Created [launch.json](/home/imd/.identitymd-6/work/48844890-e197-4e13-b271-6b39a72a2b51/3982909f-4c95-4827-97e6-4fa9ba985cb1/launch.json) with matching constructor arguments, hook permissions, and required ETH pool settings. Notes are 3,210/4,000 characters.\n\nJSON structure and ABI checks passed. Offline `forge build` passed with existing warnings; all 47 tests passed using the required fuzz seed.\n\nOnly the manifest is included in the change.","treeHash":"098204fdf8c5a1ffa690df1cb616b443ece8af92","usage":{"cachedInputTokens":1541376,"inputTokens":73735,"model":"gpt-6-astra","outputTokens":9946,"runtime":"codex","turns":6,"wallClockMs":305300}},{"artifacts":[],"attempt":1,"bundleHash":"9dff3353e618ce6f90c1caa19d62a035618dfa9cd67a1c5bbd3f664c7473f39f","device":"85f29f4c09dba910","findings":[],"hash":"44e0a9b44f099096470d42d1e3b95a1e59147baeb81becf48ecd0f12482b9a3b","nodeId":"c9c2c07b-9a29-4b62-8fd5-b54f3966ae28","outcome":"completed","summary":"The responses file is excluded by the harness's own git exclude rule, which is expected: the harness reads it from the working tree directly. Nothing more to do.\n\n## Recap\n\nBoth reproducible findings reproduced exactly as reported and are fixed. The three advisory findings also reproduced and I judged all three real, so I fixed them too. The rejected attempt's size failure came from re-vendoring full libraries; the accepted tree already carries trimmed libs, so I left dependencies untouched and the tracked tree stays at 1.25 MB.\n\n**Fixes to the two blocking findings**\n\n- **Exact-in buy fee bound** in `src/MediciHook.sol`. The budgeted fee can exceed floor(base × rate / 10000) by two wei, not one, whenever the division remainder passes 10,000. That only happens at launch rates above 200 bps. The afterSwap check now accepts +2, with the derivation in a comment.\n- **Launch keying** in `src/MediciTreasury.sol`. Recorded launches are now keyed by (factory, number). recordCommission probes both factories for an eligible, unrecorded position, reverts AmbiguousLaunch if both qualify, and a new keeper-only recordCommissionAt names the factory for that case. harvest(n) handles every recorded launch with that number. The epoch records its factory, exposed by launchOf(e), and the Recorded and Harvested events carry it.\n\n**Advisory fixes**\n\n- A rejected bounty transfer is now kept by the treasury as free ETH, as the brief says. claimBounty is removed.\n- The fee router no longer reverts the MEDICI burn when TEAM rejects ETH. The ETH waits in the router and anyone can send it later with payTeam.\n- price() returns the current spot in the opening second instead of reverting.\n\n**Verification**\n\n- Both reviewer proofs pass on the result.\n- 56 tests pass with the verifier's fuzz seed, including six new tests covering the decay-window buys, the opening-second price, both collision scenarios, the kept bounty, reentrancy, and the deferred TEAM payment.\n- forge build, forge fmt --check on delivered files, and the bundle check all pass. The treasury runtime is 21,138 bytes, under the EIP-170 limit.\n\nREADME and docs/security-review.md describe the new keying, the two-wei bound, the kept bounty and the router deferral. All five findings are answered as fixed in .imd-responses.json.","treeHash":"08600cf6cadf5a9b39f5ad7b3ba455fd6429369b","usage":{"cachedInputTokens":2074834,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":35085,"runtime":"claude","turns":57,"wallClockMs":847449}},{"artifacts":[],"attempt":1,"bundleHash":"36bde2571ec96248426d287f2e79aec0f9c6bd458a7a787622ddb318f2a0e105","device":"2565f234b0a569e9","findings":[],"hash":"584e078657265ff13bab839be73376d5446c46f0226ce2c4a41974c4f1c229bc","nodeId":"3982909f-4c95-4827-97e6-4fa9ba985cb1","outcome":"completed","summary":"Done. The manifest is verified against the implementation and only launch.json changed.\n\n**What I checked**\n\n- Hook contract is MediciHook with constructor parameters pool manager then token, so constructorArgs are \"$poolManager\" and \"$token\" in declaration order.\n- getHookPermissions enables beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta and afterSwapReturnDelta, matching the manifest's list. The address bits 0x20cc stated in notes are correct for that set.\n- Token is MediciToken, named Medici with symbol MEDICI and 18 decimals, no constructor arguments.\n- Pool fields are the launch's own: native ETH pair, fee 12500, tickSpacing 60, the nominal initialPrice. beforeInitialize accepts exactly that pool.\n- Notes claims were cross-checked against the source: fee splits, decay window, claims fallback, buyback min-out at 95% of TWAP, fixed addresses in Protocol.sol all match.\n\n**What I changed**\n\nThe notes said a failed bounty transfer stays escrowed and is retryable through a claimBounty function. No such function exists. The treasury pays the bounty once at recordCommission and keeps the ETH as free treasury ETH if the proposer rejects it, which is what the brief requires. I replaced that sentence with an accurate one. Notes are now 3250 characters, under the 4000 cap, and every address in them is lowercase.\n\n**Verification**\n\n```\nforge build: ok\nforge test (FOUNDRY_FUZZ_SEED=0x9576d1f8...): 53 passed, 0 failed\ntools/check_bundle.py: imports resolve offline, bundle below 8 MiB\ngit status: only launch.json modified\n```","treeHash":"390bc4d35a8f52e0f945ed11e47be98de4889a7b","usage":{"cachedInputTokens":420933,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":4873,"runtime":"claude","turns":13,"wallClockMs":170903}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"027fa8eee8653e6f","findings":[],"hash":"650434e8526b67977462a3275de3a4fb165dbf4b0230667470f3935ff5431854","nodeId":"e36d3fa4-6e34-4547-b748-433126ee5436","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":6,"wallClockMs":895847}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7591760a616c6429","findings":[{"citation":"resolved","description":"recordCommission(e, n, jobId) refuses any n for which launchFactory[n] is already set, and _findLaunch(n) probes LaunchFactory before ProjectFactory. The two factories are separate contracts with separate positionOf(uint64) numbering (docs/integrations.md), so the same uint64 can name a hook launch on LaunchFactory and an unrelated project launch on ProjectFactory. Once a launch numbered n is recorded from one factory (src/MediciTreasury.sol:469-470 store launchFactory[n]/launchToken[n]), a later epoch whose winning brief produced launch n on the other factory can never be recorded even though the treasury is its requester and holds the 5% supply the check demands. Consequences for that epoch: the KEEPER's recordCommission reverts WrongState; the only exit is lapse(e) after 14 days, which drops the bounty back and promotes the runner-up or ends in status 3; the winner's voters never get a backerShare; the launch tokens already sitting in the treasury for that launch are permanently stuck (no reserve, no claim path, and harvest(n) at src/MediciTreasury.sol:623-626 resolves n to the first factory, so neither the surplus burn nor the fee claim ever reaches the second launch); and the LP fees the ProjectFactory owes the treasury for that launch can never be claimed by the treasury. The failure is unconditional on the contract side and is triggered by ordinary, legitimate use: it needs no attacker, only two commissioned launches that share a number across the two factories. Suggested minimal fix: key the recorded-launch bookkeeping by (factory, n), e.g. mapping(address => mapping(uint64 => address)) launchToken and a harvest(factory, n) or an index that stores both, and let recordCommission take or derive the factory so the probe order cannot shadow the other factory's position.","line":459,"path":"src/MediciTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciTreasury} from \"src/MediciTreasury.sol\";\nimport {Protocol, IIntake, IMediciHook} from \"src/Protocol.sol\";\n\ncontract CollisionMockToken is ERC20 {\n    constructor() ERC20(\"Launch asset\", \"ASSET\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Stands in for either factory. Each factory instance keeps its own numbering.\ncontract CollisionMockFactory {\n    struct Position {\n        PoolKey key;\n        int24 lower;\n        int24 upper;\n        address requester;\n    }\n    mapping(uint64 => Position) private positions;\n    mapping(uint64 => uint256) public pending1;\n\n    function register(uint64 n, PoolKey memory key, address requester) external {\n        positions[n] = Position(key, -60, 60, requester);\n    }\n\n    function positionOf(uint64 n) external view returns (PoolKey memory, int24, int24, address) {\n        Position storage p = positions[n];\n        return (p.key, p.lower, p.upper, p.requester);\n    }\n\n    function claimFees(uint64 n) external returns (uint256 a0, uint256 a1) {\n        Position storage p = positions[n];\n        require(p.requester != address(0));\n        a1 = pending1[n];\n        delete pending1[n];\n        if (a1 != 0) require(IERC20(Currency.unwrap(p.key.currency1)).transfer(p.requester, a1));\n    }\n\n    function withdraw(address, Currency) external {}\n    receive() external payable {}\n}\n\ncontract CollisionMockIntake is IIntake {\n    uint256 public nonce;\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(msg.sender, ++nonce));\n    }\n}\n\ncontract CollisionMockHook is IMediciHook {\n    uint64 public openedAt;\n    uint256 public price = 1e10;\n\n    constructor(uint64 at) {\n        openedAt = at;\n    }\n\n    function twap(uint64, uint64) external view returns (uint256) {\n        return price;\n    }\n\n    function poolKey() external pure returns (PoolKey memory) {\n        revert();\n    }\n}\n\n/// @notice Two different launches, one per factory, that happen to carry the same launch number.\n/// The treasury keys recorded launches by number alone, so the second one can never be recorded,\n/// its backers never get a share, its proposer never gets the bounty, and the launch tokens the\n/// treasury already holds for it can never be claimed, burned or harvested.\ncontract LaunchNumberCollisionTest is Test {\n    MediciToken token;\n    MediciTreasury treasury;\n    CollisionMockHook oracle;\n    CollisionMockFactory launchFactory;\n    CollisionMockFactory projectFactory;\n    CollisionMockToken imd;\n    address alice = address(0xA11CE);\n    uint64 constant OPEN = 1_800_000_000;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 100 ether);\n        token = new MediciToken();\n        oracle = new CollisionMockHook(OPEN);\n        treasury = new MediciTreasury(IPoolManager(address(this)), address(token), IMediciHook(address(oracle)));\n        token.transfer(alice, 300_000_000 ether);\n        vm.prank(alice);\n        token.approve(address(treasury), type(uint256).max);\n\n        CollisionMockFactory f = new CollisionMockFactory();\n        vm.etch(Protocol.LAUNCH_FACTORY, address(f).code);\n        vm.etch(Protocol.PROJECT_FACTORY, address(f).code);\n        launchFactory = CollisionMockFactory(payable(Protocol.LAUNCH_FACTORY));\n        projectFactory = CollisionMockFactory(payable(Protocol.PROJECT_FACTORY));\n        CollisionMockIntake i = new CollisionMockIntake();\n        vm.etch(Protocol.INTAKE, address(i).code);\n        CollisionMockToken t = new CollisionMockToken();\n        vm.etch(Protocol.IMD, address(t).code);\n        imd = CollisionMockToken(Protocol.IMD);\n    }\n\n    function _winEpoch(uint256 e, bytes memory brief) internal {\n        vm.prank(alice);\n        uint256 id = treasury.propose(\"A commissioned launch\", brief);\n        vm.prank(alice);\n        treasury.vote(e, id, 3_000_000 ether);\n        (bool ok,) = address(treasury).call{value: 1 ether}(\"\");\n        require(ok);\n        vm.warp(treasury.epochEnd(e));\n        treasury.settle(e);\n        imd.mint(address(treasury), 0.5 ether);\n        vm.prank(Protocol.KEEPER);\n        treasury.commission(e, brief);\n    }\n\n    function _position(CollisionMockFactory f, uint64 n, CollisionMockToken asset) internal {\n        PoolKey memory k =\n            PoolKey(Currency.wrap(address(0)), Currency.wrap(address(asset)), 12_500, 60, IHooks(address(0)));\n        f.register(n, k, address(treasury));\n        asset.mint(address(this), 950 ether);\n        asset.mint(address(treasury), 50 ether);\n    }\n\n    function test_sameLaunchNumberOnTheOtherFactoryCanStillBeRecorded() public {\n        // Epoch 1: a hook launch, number 77 on LaunchFactory, is recorded.\n        _winEpoch(1, bytes(\"{\\\"idea\\\":\\\"one\\\"}\"));\n        CollisionMockToken assetA = new CollisionMockToken();\n        _position(launchFactory, 77, assetA);\n        vm.prank(Protocol.KEEPER);\n        treasury.recordCommission(1, 77, \"job-1\");\n        (uint8 status,,,,,,) = treasury.epochResult(1);\n        assertEq(status, 2);\n\n        // Epoch 2: a project launch, number 77 on ProjectFactory. The treasury is its requester\n        // and holds 5% of its supply, exactly what recordCommission demands.\n        vm.warp(treasury.epochEnd(1) + 1);\n        _winEpoch(2, bytes(\"{\\\"idea\\\":\\\"two\\\"}\"));\n        CollisionMockToken assetB = new CollisionMockToken();\n        _position(projectFactory, 77, assetB);\n        vm.prank(Protocol.KEEPER);\n        treasury.recordCommission(2, 77, \"job-2\");\n\n        (status,,,,,,) = treasury.epochResult(2);\n        assertEq(status, 2, \"the second launch is recordable\");\n        assertEq(treasury.claimable(2, alice), 50 ether, \"its backers can claim their share\");\n    }\n}","reproduction":"State: treasury deployed, pool open. Epoch 1: alice proposes brief A and votes 3,000,000 MEDICI, 1 ETH donated, settle(1) picks her, KEEPER commission(1, A). Register launch 77 on LaunchFactory with key (ETH, assetA), requester = treasury; assetA supply 1000, treasury holds 50. KEEPER recordCommission(1, 77, \"job-1\") succeeds (status 2). Epoch 2: alice proposes brief B, votes, settle(2), commission(2, B). Register launch 77 on ProjectFactory with key (ETH, assetB), requester = treasury, treasury holds 50 of 1000 assetB. KEEPER calls recordCommission(2, 77, \"job-2\"). Expected: status of epoch 2 becomes 2, claimable(2, alice) == 50 assetB, harvest(77) later burns assetB surplus from the ProjectFactory. Actual: recordCommission reverts WrongState() because launchFactory[77] is already LaunchFactory; epoch 2 can only lapse, its 50 assetB stay in the treasury forever and ProjectFactory fees for 77 are never claimed. The attached test fails on the current code at that call.","severity":"medium","snippet":"        if (x.status != 1 || !x.commissioned || launchFactory[n] != address(0) || block.timestamp >= x.deadline) {","title":"Recorded launches are keyed by launch number alone, so a number collision across LaunchFactory and ProjectFactory makes the second launch unrecordable and strands its tokens and fees"}],"hash":"8640e38dbe3088015ee71b6dd539afd8ec64a178ff7fff6c4afabc8425711b3f","nodeId":"c9539261-813b-4b48-8160-3b61ec80af5a","outcome":"completed","summary":"The review is complete and the findings file is in place.\n\n**Outcome.** The economic design holds up well under the Economic Security, Invariant and Flow Gap guides. I found one substantiated defect, with a Foundry proof that fails on the current code and no other reproducible issues.\n\n**Finding 1 (medium), src/MediciTreasury.sol:459.** Recorded launches are keyed by launch number alone, and `_findLaunch` probes LaunchFactory before ProjectFactory. The two factories are separate contracts with separate numbering, so if an epoch's launch on ProjectFactory shares a number with an earlier recorded LaunchFactory launch, the keeper's `recordCommission` reverts forever. That epoch can only lapse, its backers get no share, its proposer loses the bounty, and the launch tokens the treasury already holds for it are stuck with no burn, claim or harvest path. The proof test in test/scratch fails with `WrongState` at the second record. The fix is to key the bookkeeping by factory and number.\n\n**What I checked and found sound.** Hook fee accounting in all four swap modes, including partial fills and the claims fallback, with the balance-plus-claims-equals-owed invariant. The self-quote cannot recurse because v4 skips callbacks when the hook is the swap caller. Treasury solvency: free ETH consumers exclude escrow and the 40% buyback reserve, and streamed amounts equal reserved amounts. Epoch, extension and settlement timing, quorum, bounty caps, promotion and lapse paths, and backer reserve accounting. The buyback sandwich is unprofitable because the attacker's round-trip hook and LP fees exceed the 5% min-out window.\n\n**Coverage.** All 35 entry points have rows. Two are marked as findings, `recordCommission` and `harvest`. The rest hold, plus four invariant rows.\n\n**Leads left unverified.** A possible one-wei `InvalidQuote` revert on exact-out sells partially filled at a price limit, which I derived arithmetically but could not engineer a concrete input for in the time available. It is not in the file.","treeHash":null,"usage":{"cachedInputTokens":2098971,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":49497,"runtime":"claude","turns":30,"wallClockMs":700965}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"918f8261a6fd589c","findings":[{"citation":"resolved","description":"beforeSwap derives the fee of a specified-side (exact-input) buy from the user's budget: target = specified*10000/(10000+rate) (rounded down) and fee = specified - target. afterSwap then recomputes exact = ethBase*rate/10000 with ethBase == target and requires fee <= exact + 1. The bound is wrong: fee < target*rate/10000 + 1 + rate/10000, so whenever the fractional part of target*rate/10000 exceeds 1 - rate/10000 the budget-derived fee is exact + 2 and afterSwap reverts with InvalidQuote. At rate == 200 (after 15 minutes) this never happens, but during the decay window (rate 5000 -> 200, integer bps) it happens for a large share of ordinary inputs: in a sampled run 6658 of 20000 consecutive wei amounts near 1 ETH fail one second after opening, 6322 at 60 s, 4776 at 300 s and 392 at 899 s. The whole swap reverts inside the hook callback, so every router that submits an exact ETH input during the launch window sees roughly one in three buys fail with no indication of why. The comment on line 191 states the assumption ('up by one wei') that the arithmetic violates. Fix: compute the fee in beforeSwap as target*rate/10000 (and reduce the budget by that) so both sides use the same formula, or widen the afterSwap tolerance to exact + 2 (the proven bound).","line":191,"path":"src/MediciHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciHook} from \"src/MediciHook.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\ncontract ProofActor {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey memory k, int24 lower, int24 upper, int256 liquidity) external {\n        manager.unlock(abi.encode(uint8(0), k, abi.encode(ModifyLiquidityParams(lower, upper, liquidity, 0))));\n    }\n\n    function swap(PoolKey memory k, bool zeroForOne, int256 amount, uint160 limit) external {\n        manager.unlock(abi.encode(uint8(1), k, abi.encode(SwapParams(zeroForOne, amount, limit))));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory k, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) (d,) = manager.modifyLiquidity(k, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        else d = manager.swap(k, abi.decode(args, (SwapParams)), \"\");\n        _settle(k.currency0, d.amount0());\n        _settle(k.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) internal {\n        if (d < 0) {\n            uint256 amount = uint256(-int256(d));\n            manager.sync(c);\n            if (c.isAddressZero()) {\n                manager.settle{value: amount}();\n            } else {\n                require(IERC20(Currency.unwrap(c)).transfer(address(manager), amount));\n                manager.settle();\n            }\n        } else if (d > 0) {\n            manager.take(c, address(this), uint128(d));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @notice During the 15-minute buy-fee decay, exact-input ETH buys of ordinary amounts revert in\n/// afterSwap with InvalidQuote because beforeSwap's budget-derived fee can exceed\n/// `ethBase * rate / 10_000` by two wei while afterSwap tolerates only one.\ncontract FeeRoundingProofTest is Test {\n    PoolManager manager;\n    MediciToken token;\n    MediciHook hook;\n    PoolKey key;\n    ProofActor actor;\n    uint64 constant OPEN = 1_800_000_000;\n    uint160 constant Q96 = 79228162514264337593543950336;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 1_000 ether);\n        manager = new PoolManager(address(this));\n        token = new MediciToken();\n        bytes memory code = abi.encodePacked(type(MediciHook).creationCode, abi.encode(manager, address(token)));\n        bytes32 hash = keccak256(code);\n        address at;\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (!HookFlags.matches(predicted, HookFlags.MEDICI)) continue;\n            assembly (\"memory-safe\") {\n                at := create2(0, add(code, 32), mload(code), salt)\n            }\n            break;\n        }\n        require(at != address(0), \"hook deployment failed\");\n        hook = MediciHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        manager.initialize(key, Q96 * 10_000);\n        actor = new ProofActor(manager);\n        vm.deal(address(actor), 100 ether);\n        token.transfer(address(actor), 600_000_000 ether);\n        actor.modify(key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e22);\n    }\n\n    /// 60 seconds after opening the buy rate is 4680 bps. For an exact input of\n    /// 1_000_000_000_000_000_001 wei: target = 681_198_910_081_743_869, fee = 318_801_089_918_256_132,\n    /// but afterSwap's exact = 318_801_089_918_256_130, so fee == exact + 2 and the swap reverts.\n    function test_exactInputBuyRevertsDuringDecay() public {\n        vm.warp(OPEN + 60);\n        assertEq(hook.buyFeeBps(), 4680);\n        int256 amount = -int256(1_000_000_000_000_000_001);\n        // Expected: the buy goes through (an exact-input buy is an ordinary trade).\n        actor.swap(key, true, amount, TickMath.MIN_SQRT_PRICE + 1);\n        assertGt(hook.treasuryOwed() + address(hook.treasury()).balance + hook.teamOwed(), 0);\n    }\n\n    /// Many consecutive amounts fail, not a single unlucky wei: out of 40 consecutive inputs around\n    /// 0.5 ETH at the same instant, at least 8 revert.\n    function test_manyAmountsRevertDuringDecay() public {\n        vm.warp(OPEN + 60);\n        uint256 reverted;\n        for (uint256 i; i < 40; ++i) {\n            uint256 snap = vm.snapshotState();\n            try actor.swap(key, true, -int256(0.5 ether + i), TickMath.MIN_SQRT_PRICE + 1) {}\n            catch {\n                ++reverted;\n            }\n            vm.revertToState(snap);\n        }\n        assertEq(reverted, 0, \"exact-input buys revert during the decay window\");\n    }\n}","reproduction":"Fresh pool, liquidity added, block.timestamp = openedAt + 60 (buyFeeBps() == 4680). Any router calls PoolManager.swap(key, {zeroForOne: true, amountSpecified: -1_000_000_000_000_000_001, sqrtPriceLimit: MIN_SQRT_PRICE+1}). beforeSwap: target = 681_198_910_081_743_869, fee = 318_801_089_918_256_132. afterSwap: ethBase = 681_198_910_081_743_869, exact = 318_801_089_918_256_130, fee == exact + 2 -> revert InvalidQuote() (0xf8618030) wrapped by the PoolManager as a failed afterSwap (0xb47b2fb1). Expected: the buy settles with a 46.8% fee. Actual: the swap reverts. The proof's second test shows 12 of 40 consecutive inputs around 0.5 ETH revert at the same instant; the test passes once the tolerance matches the formula.","severity":"medium","snippet":"            // Specified-side integer budgets may round the fee up by one wei.\n            uint256 exact = ethBase * rate / 10_000;\n            if (fee < exact || fee > exact + 1) revert InvalidQuote();","title":"Exact-input ETH buys revert in afterSwap for roughly a third of amounts during the 15-minute launch decay (fee tolerance off by one wei)"},{"citation":"resolved","description":"The two factories are separate contracts with separate positionOf storage, so a launch number identifies a launch only together with its factory. The treasury nevertheless stores launchFactory[n]/launchToken[n] keyed by n only and _findLaunch (lines 440-443) always probes LAUNCH_FACTORY first and returns it if its position n names the treasury as requester. Two consequences: (1) once launch n of one factory is recorded, launch n of the other factory is unrecordable for good (WrongState), so that epoch's winner lapses after 14 days although the brief did everything required: the treasury is the requester and holds >= 5% of the token; the proposer never receives the bounty, the voters never receive the backer share, and the 5% allocation of the launch token sits in the treasury with no function able to move or burn it; (2) if LaunchFactory n and ProjectFactory n both name the treasury as requester, the keeper cannot record the ProjectFactory one at all: _findLaunch returns the LaunchFactory position, and if that passes the 5% check the wrong token is reserved as the backer share. harvest(n) has the same single-namespace key. This is an asymmetry between the two factory branches of _findLaunch and a broken guarantee of the spec ('each launch once; positionOf(launchNumber).requester == treasury (either factory)'). Fix: key launchFactory/launchToken/harvest by (factory, n) (e.g. let recordCommission and harvest take the factory address, or map keccak256(factory, n)), and probe only the named factory.","line":459,"path":"src/MediciTreasury.sol","reproduction":"State: epoch 1 settled with winner W1, commissioned; LaunchFactory.positionOf(7) = (ETH/A pool, requester = treasury); treasury holds 5% of A. KEEPER calls recordCommission(1, 7, 'job-1') -> ok, launchFactory[7] = LAUNCH_FACTORY. Epoch 2 settled with winner W2, commissioned; its launch is ProjectFactory launch 7: ProjectFactory.positionOf(7) = (ETH/B pool, requester = treasury); treasury holds 5% of B. KEEPER calls recordCommission(2, 7, 'job-2'). Expected: epoch 2 status 2 with backer share B. Actual: revert WrongState() because launchFactory[7] != 0; after 14 days anyone calls lapse(2) and the runner-up is promoted or status becomes 3; voters of W2 never receive B, the proposer never receives the bounty. Variant: with launchFactory[7] still unset but both factories' position 7 naming the treasury, recordCommission(2, 7, ...) records LaunchFactory 7 / token A instead of ProjectFactory 7 / token B.","severity":"medium","snippet":"        if (x.status != 1 || !x.commissioned || launchFactory[n] != address(0) || block.timestamp >= x.deadline) {","title":"recordCommission keys launches by number alone, so a LaunchFactory launch and a ProjectFactory launch with the same number can never both be recorded (and the LaunchFactory one always shadows the othe"},{"citation":"resolved","description":"_payBounty keeps a failed bounty escrowed (as specified) and claimBounty(e) can retry it, but the recipient is fixed to the proposer address and there is no expiry. A proposer that is a contract without receive(), or that reverts deliberately, leaves up to 1 ETH per such epoch in escrowed forever. escrowed reduces freeETH(), which caps future bounties (10% of free ETH), the IMD purchase ceiling in commission() (InsufficientFreeETH when free ETH is 0) and keeperWithdraw. An attacker who wins an epoch with a reverting contract loses their burn but can trim the treasury's usable ETH; more likely it happens by accident with a multisig/contract proposer. Fix: let the proposer (or anyone) redirect the retry to a new address, or return the escrow to the free balance after the 90-day claim window.","line":488,"path":"src/MediciTreasury.sol","reproduction":"Proposer P is a contract whose receive() reverts. P proposes in epoch 1, wins with burns worth >= 0.02 ETH at the epoch TWAP, treasury holds 10 ETH free -> settle(1) escrows bounty 1 ETH. Commission and recordCommission(1, n, job) succeed; BountyPaid(1, P, 1e18, false) is emitted, bounty stays 1 ETH, escrowed stays 1 ETH. claimBounty(1) always emits success=false. freeETH() is permanently 1 ETH lower; with the treasury holding less than 0.5 IMD and no free ETH beyond that stuck escrow, commission() in a later epoch reverts InsufficientFreeETH because ceiling == 0.","severity":"low","snippet":"        (bool ok,) = proposer.call{value: amount}(\"\");\n        if (!ok) {\n            x.bounty = amount;\n            escrowed += amount;\n        }","title":"A winning proposer that cannot receive ETH keeps the bounty escrowed forever, permanently lowering freeETH for keeper, bounties and IMD purchases"},{"citation":"resolved","description":"harvest() claims both factory currencies, forwards MEDICI to the treasury and burns it, then sends the ETH to treasury.team() and reverts if that call fails. The hook's claimTeam() isolates the same failure (README: a TEAM that rejects ETH 'does not block trading'), but the router does not: with a TEAM contract that rejects ETH, every harvest() reverts once any ETH fee is owed, so the MEDICI side of the factory fees is never burned either and both currencies stay in the factory until TEAM hands itself over. TEAM is the only account that can fix it (setTeam). Fix: accrue ETH the team could not receive in the router (teamOwed-style) and let the burn proceed.","line":53,"path":"src/MediciFeeRouter.sol","reproduction":"TEAM calls treasury.setTeam(C) where C is a contract without receive()/fallback. The launch position has accrued 1 ETH and 1000 MEDICI of fees. Anyone calls router.harvest(): claimFees and both withdraws succeed, MEDICI is sent to the treasury and burned, then C.call{value: 1 ether} fails -> TransferFailed reverts the whole call including the burn. Expected per spec: MEDICI burned; actual: nothing happens until TEAM changes itself.","severity":"low","snippet":"            (bool ok,) = treasury.team().call{value: eth}(\"\");\n            if (!ok) revert TransferFailed();","title":"FeeRouter.harvest pays ETH to TEAM in the same call that burns MEDICI, so a TEAM that cannot receive ETH also blocks the MEDICI burn"},{"citation":"resolved","description":"The hook accepts ETH from anyone, but only teamOwed and treasuryOwed are ever paid out. ETH that arrives other than through take()/redeem (a direct transfer, a selfdestruct) raises address(this).balance, which _payTreasury only uses as a cap, so it is never distributed. Not a loss of protocol funds; recorded because the spec says the hook holds nothing of its own.","line":304,"path":"src/MediciHook.sol","reproduction":"Anyone sends 1 ETH to the hook address. teamOwed and treasuryOwed are unchanged; claimTeam() and redeemFees() pay only the owed amounts; the 1 ETH remains in the hook with no function able to move it.","severity":"info","snippet":"    receive() external payable {}","title":"ETH sent directly to the hook is stuck forever"}],"hash":"b6a8f4bfc5ffca8c259d0c0bc86bce158b6889867d011414e6704cd9fdf3cf64","nodeId":"963d0862-5762-4e41-872c-4561a6600c3e","outcome":"completed","summary":"Findings are written and verified against the tree. Final recap:\n\n**Deliverable**: `.imd-findings.json` holds 5 findings and 37 coverage rows (all 35 listed entry points plus two invariants). One proof test lives at `test/scratch/FeeRoundingProof.t.sol` and fails on the current code (2 of 2 tests fail, as intended).\n\n**Findings**\n\n1. **Medium, MediciHook.sol:191.** Exact-input ETH buys revert during the 15-minute fee decay. The budget-derived fee in `beforeSwap` can exceed `ethBase * rate / 10_000` by two wei, while `afterSwap` tolerates one. Confirmed with a Foundry test: at 60 seconds after open, 12 of 40 consecutive inputs near 0.5 ETH revert with InvalidQuote, and the sampled failure share near 1 ETH is about a third. Rate 200 never fails, so the defect is confined to the launch window.\n2. **Medium, MediciTreasury.sol:459.** Launches are keyed by number only across two separate factories, and `_findLaunch` always prefers LaunchFactory. A LaunchFactory launch and a ProjectFactory launch sharing a number can never both be recorded, so the second epoch's winner lapses, its proposer loses the bounty and its voters lose the backer share. I could not confirm the factories' numbering on chain, so the finding states that precondition explicitly.\n3. **Low, MediciTreasury.sol:488.** A proposer that cannot receive ETH keeps the bounty escrowed forever, lowering free ETH for bounties, keeper withdrawals and IMD purchases.\n4. **Low, MediciFeeRouter.sol:53.** A TEAM that rejects ETH makes the router's harvest revert, which also blocks the MEDICI burn, unlike the hook's isolated `claimTeam`.\n5. **Info, MediciHook.sol:304.** ETH sent straight to the hook is unrecoverable.\n\n**Coverage of my area**: the full permission map (manager-only callbacks, self-only quote, team and keeper self-handover, INTAKE-only callbacks, the once-only router binding) holds. The treasury-exempt swap path cannot be reached by outsiders, the hook's fee accounting invariant and the treasury's ETH liability invariant hold, and the keeper's veto power via double `skip` is recorded as a trust assumption rather than a defect. Finding 1 is outside my three guides but was severe enough to report with a proof.","treeHash":null,"usage":{"cachedInputTokens":1724036,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":48240,"runtime":"claude","turns":27,"wallClockMs":747135}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"faf271d05ee8ea0f","findings":[{"citation":"resolved","description":"For an exact-in buy (ETH specified) beforeSwap computes target = floor(S*10000/(10000+rate)) and fee = S - target, and afterSwap re-derives exact = floor(target*rate/10000) and requires fee <= exact + 1. The real bound is fee - target*rate/10000 = frac(S*10000/(10000+rate)) * (10000+rate)/10000, which is below 1.5 for rates up to 5000 bps, so fee can equal exact + 2 whenever that fractional part exceeds 10000/(10000+rate) and the fractional part of target*rate/10000 is also high. At the steady 200 bps rate the fractions are multiples of 1/51 and the case never occurs, which is why the existing fuzz (run after 1 day) passes. During the first 900 seconds the rate takes every integer value from 5000 down to 200; sampling random inputs between 1e15 and 1e19 wei gives revert rates of about 2% at 201-210 and 300 bps, 15% at 3000 bps, 14% at 4000 bps and 33% at 4999 bps. Every such swap reverts in afterSwap (wrapped as HookCallFailed), so a user whose exact-in buy would otherwise have been filled loses the gas and the trade. The amounts are ordinary: 0.01 ETH exactly fails 60 seconds after open (rate 4680). The rule-sensitive window is exactly the launch window where exact-in ETH buys are the dominant trade. Minimal fix: allow fee <= exact + 2 (the mathematically correct upper bound), or compute the fee in beforeSwap as target*rate/10000 rounded up and charge that, so the two sides agree.","line":193,"path":"src/MediciHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciHook} from \"src/MediciHook.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\ncontract Swapper {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey memory k, int24 lower, int24 upper, int256 liquidity) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(0), k, abi.encode(ModifyLiquidityParams(lower, upper, liquidity, 0)))),\n            (BalanceDelta)\n        );\n    }\n\n    function swap(PoolKey memory k, bool zeroForOne, int256 amount, uint160 limit) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(1), k, abi.encode(SwapParams(zeroForOne, amount, limit)))), (BalanceDelta)\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory k, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) (d,) = manager.modifyLiquidity(k, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        else d = manager.swap(k, abi.decode(args, (SwapParams)), \"\");\n        _settle(k.currency0, d.amount0());\n        _settle(k.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) internal {\n        if (d < 0) {\n            uint256 amount = uint256(-int256(d));\n            manager.sync(c);\n            if (c.isAddressZero()) {\n                manager.settle{value: amount}();\n            } else {\n                require(IERC20(Currency.unwrap(c)).transfer(address(manager), amount));\n                manager.settle();\n            }\n        } else if (d > 0) {\n            manager.take(c, address(this), uint128(d));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @notice Exact-in ETH buys during the launch decay revert in afterSwap because the\n/// specified-side fee computed in beforeSwap (specified - target) can exceed\n/// floor(target * rate / 10_000) by two wei, while afterSwap allows at most one.\ncontract FeeBoundTest is Test {\n    PoolManager internal manager;\n    MediciToken internal token;\n    MediciHook internal hook;\n    PoolKey internal key;\n    Swapper internal actor;\n    uint64 internal constant OPEN = 1_800_000_000;\n    uint160 internal constant Q96 = 79228162514264337593543950336;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 1_000 ether);\n        manager = new PoolManager(address(this));\n        token = new MediciToken();\n        bytes memory code = abi.encodePacked(type(MediciHook).creationCode, abi.encode(manager, address(token)));\n        bytes32 hash = keccak256(code);\n        address at;\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (!HookFlags.matches(predicted, HookFlags.MEDICI)) continue;\n            assembly (\"memory-safe\") {\n                at := create2(0, add(code, 32), mload(code), salt)\n            }\n            break;\n        }\n        require(at != address(0), \"CREATE2 mining/deployment failed\");\n        hook = MediciHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        manager.initialize(key, Q96 * 10_000);\n        actor = new Swapper(manager);\n        vm.deal(address(actor), 100 ether);\n        token.transfer(address(actor), 600_000_000 ether);\n        actor.modify(key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e22);\n    }\n\n    /// 60 seconds after open the buy rate is 4680 bps. For a 0.01 ETH exact-in buy:\n    /// target = 1e16 * 10000 / 14680 = 6811989100817438, fee = 3188010899182562,\n    /// but floor(target * 4680 / 10000) = 3188010899182560, so fee == exact + 2 and\n    /// afterSwap reverts with InvalidQuote. The trade itself is perfectly valid.\n    function test_exactInBuyOfRoundAmountDuringDecayMustNotRevert() public {\n        vm.warp(OPEN + 60);\n        assertEq(hook.buyFeeBps(), 4680);\n        uint256 amount = 0.01 ether;\n        uint256 target = amount * 10_000 / (10_000 + 4680);\n        assertEq(amount - target, target * 4680 / 10_000 + 2, \"fee is two wei above the rounded-down rate\");\n        BalanceDelta d = actor.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(amount)), \"the swapper pays exactly the specified ETH\");\n        assertGt(d.amount1(), 0, \"and receives MEDICI\");\n        assertEq(hook.teamOwed(), target * 50 / 10_000, \"team share accrued on the pool ETH delta\");\n    }\n\n    /// The same bound is reached at many other decay rates; one second after open (4994 bps)\n    /// with 0.01 ETH + 2 wei.\n    function test_exactInBuyOneSecondAfterOpen() public {\n        vm.warp(OPEN + 1);\n        assertEq(hook.buyFeeBps(), 4994);\n        uint256 amount = 10000000000000002;\n        BalanceDelta d = actor.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(amount)));\n        assertGt(d.amount1(), 0);\n    }\n}","reproduction":"State: pool opened at T0 with full-range liquidity. At T0 + 60 s (buyFeeBps() == 4680) a swapper calls PoolManager.swap(key, {zeroForOne: true, amountSpecified: -10_000_000_000_000_000, sqrtPriceLimitX96: MIN_SQRT_PRICE + 1}). beforeSwap: target = 1e16*10000/14680 = 6811989100817438, fee = 3188010899182562, full fill so quoted == target. afterSwap: ethBase = 6811989100817438, exact = 6811989100817438*4680/10000 = 3188010899182560; fee == exact + 2 > exact + 1 -> revert InvalidQuote() (selector 0xf8618030, surfaced by the manager as WrappedError(hook, afterSwap selector, InvalidQuote, HookCallFailed)). Expected: the swap settles, the swapper pays exactly 1e16 wei, 3188010899182562 wei of fee accrue (team 34059945504087). Also reproducible 1 second after open with 10000000000000002 wei (rate 4994). The proof test fails on this code and passes once the bound in afterSwap admits exact + 2 (or beforeSwap and afterSwap use the same rounding).","severity":"medium","snippet":"            if (fee < exact || fee > exact + 1) revert InvalidQuote();","title":"afterSwap fee bound is one wei too tight: exact-in ETH buys during the 15-minute launch decay revert with InvalidQuote"},{"citation":"resolved","description":"The two factories number their launches independently, but the treasury stores launchFactory[n] and launchToken[n] keyed only by n and _findLaunch probes LAUNCH_FACTORY first and stops at the first position whose requester is the treasury. If the treasury is the requester of LaunchFactory position n (recorded for an earlier epoch) and the winning brief of a later epoch produces ProjectFactory position n, recordCommission(e2, n, job) reverts WrongState forever because launchFactory[n] is already set; the epoch can only lapse, the bounty is never paid and the backers never receive a share even though the launch exists and the treasury holds 5% of its token. Conversely, if LaunchFactory position n belongs to the treasury but was never recorded, recording ProjectFactory n silently records the LaunchFactory position instead (wrong launchToken, wrong harvest target). The public interface fixes the signature as recordCommission(epoch, launchNumber, jobId), so a full fix needs a scope decision; within the interface, uniqueness could be keyed by (factory, n) and, when both factories hold a treasury-owned position n, the call should revert rather than pick one.","line":459,"path":"src/MediciTreasury.sol","reproduction":"State: epoch 1 status 2 with launchFactory[7] = LAUNCH_FACTORY (LaunchFactory.positionOf(7).requester == treasury). Epoch 2 settled, commissioned, and the commissioned project is ProjectFactory position 7 with requester == treasury and the treasury holding 5% of its token. Call recordCommission(2, 7, \"job-2\") as KEEPER. Actual: revert WrongState() from the launchFactory[n] != address(0) check; after 14 days lapse(2) promotes the runner-up or sets status 3 and the backers of the winning proposal get nothing. Expected: the ProjectFactory launch is recorded for epoch 2.","severity":"low","snippet":"        if (x.status != 1 || !x.commissioned || launchFactory[n] != address(0) || block.timestamp >= x.deadline) {","title":"recordCommission keys launch uniqueness and factory lookup by launch number alone, so a LaunchFactory position and a ProjectFactory position with the same number collide"}],"hash":"bb43b6e881caf27ac0d591f9be79ce9ad2d93f556e55603152993733bd1528f3","nodeId":"4d6579ee-b7e0-43bb-b2cd-6a146e6d2c02","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with 2 findings and a 37-row coverage record. No repository files were changed; the only new material is the proof under `test/scratch/`, which is not kept.\n\n**Findings**\n\n1. **Medium, `src/MediciHook.sol:193`.** The afterSwap fee bound is one wei too tight. For an exact-in ETH buy, beforeSwap charges `fee = S - floor(S*10000/(10000+rate))`, which can exceed `floor(target*rate/10000)` by two wei, but afterSwap only admits one and reverts with `InvalidQuote`. At the steady 200 bps rate this never happens, which is why the existing fuzz passes. During the 15-minute decay the rate takes every integer value and sampled failure rates run from about 2% to 33% of inputs, including a plain 0.01 ETH buy 60 seconds after open. The attached Foundry proof fails on the current code and passes when the bound admits `exact + 2`. I confirmed in a throwaway copy that this one-line fix also leaves the existing hook suite green.\n\n2. **Low, `src/MediciTreasury.sol:459`.** Launch uniqueness and the factory probe are keyed by launch number alone, although LaunchFactory and ProjectFactory number launches independently. A treasury-owned LaunchFactory position n blocks recording a ProjectFactory position n forever, and the epoch can only lapse. The public interface takes only a number, so a complete fix needs a scope decision.\n\n**Coverage.** Every listed entry point has a row. All hook, router, token and treasury entry points were traced from my area, with two invariants added. The nested self-quote path was traced through v4-core's `noSelfCall` dispatch and holds. Protocol.sol interfaces were compared against the recorded factory and Intake ABIs and match. Sell-side rounding was brute-forced and holds.\n\n**Not reached.** Live factory behaviour on `withdraw` of a zero balance and whether the real `claimFees` payout uses a gas-limited call could not be checked without the deployed source.\n\nBuild note for whoever runs the proof: this machine has a 1.5 GB memory cap that kills solc with `via_ir`, so I ran everything with `FOUNDRY_VIA_IR=false`.","treeHash":null,"usage":{"cachedInputTokens":2562209,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":58805,"runtime":"claude","turns":37,"wallClockMs":975413}},{"artifacts":[],"attempt":2,"bundleHash":"4ff144d229a8e47eaecbd01c6fafc9992feab05964e40a286162940db38b99d1","device":"fc87c05048aca47d","findings":[{"description":"beforeSwap reserves the specified-side buy fee as fee = specified - target with target = specified * 10000 / (10000 + rate). afterSwap then recomputes exact = ethBase * rate / 10000 and reverts with InvalidQuote unless exact <= fee <= exact + 1. That bound is wrong: fee = exact + (m + rho) / 10000 where m = (target*rate) mod 10000 and rho = (specified*10000) mod (10000+rate), so fee can be exact + 2 whenever rho > 10000, which happens for every decay rate whose (10000 + rate) does not divide 10000*specified neatly. At the steady 200 bps rate rho is at most 10000 and the swap is fine, but during the first 15 minutes the buy rate takes ~4800 distinct values and for most of them a fixed residue class of input amounts fails. Measured at 450 s after opening (rate 2600 bps): inputs congruent to 39 mod 63 always produce fee = exact + 2, and 12 of every 63 consecutive wei amounts revert, regardless of size (e.g. 1e15+4, 1e15+13, 1e15+18, ... and 39 + 63e14 wei). The pool fills the trade completely; only the hook's own consistency check rejects it. Impact: a deterministic denial of service on a large share of exact-input buys exactly in the launch window the decay is designed for, with wallets seeing an opaque HookCallFailed/InvalidQuote revert and no way to know which amounts are accepted. No funds are at risk. The README's claim that specified-side budgets round the fee up by at most one wei is incorrect. Fix options: widen the afterSwap tolerance to exact + 2, or compute the reserved fee in beforeSwap as target * rate / 10000 (+ at most 1) and reserve only that, returning the remainder of the specified input to the swapper through the delta.","line":193,"path":"src/MediciHook.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 {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, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {MediciToken} from \"src/MediciToken.sol\";\nimport {MediciHook} from \"src/MediciHook.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\n/// @dev Minimal locker: adds liquidity or swaps, settles every delta from its own balance.\ncontract Trader {\n    IPoolManager public immutable manager;\n\n    constructor(IPoolManager m) {\n        manager = m;\n    }\n\n    function modify(PoolKey memory k, int24 lower, int24 upper, int256 liquidity) external {\n        manager.unlock(abi.encode(uint8(0), k, abi.encode(ModifyLiquidityParams(lower, upper, liquidity, 0))));\n    }\n\n    function swap(PoolKey memory k, bool zeroForOne, int256 amount, uint160 limit) external returns (BalanceDelta) {\n        return abi.decode(\n            manager.unlock(abi.encode(uint8(1), k, abi.encode(SwapParams(zeroForOne, amount, limit)))), (BalanceDelta)\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory k, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) (d,) = manager.modifyLiquidity(k, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        else d = manager.swap(k, abi.decode(args, (SwapParams)), \"\");\n        _settle(k.currency0, d.amount0());\n        _settle(k.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) internal {\n        if (d < 0) {\n            uint256 amount = uint256(-int256(d));\n            manager.sync(c);\n            if (c.isAddressZero()) {\n                manager.settle{value: amount}();\n            } else {\n                require(IERC20(Currency.unwrap(c)).transfer(address(manager), amount));\n                manager.settle();\n            }\n        } else if (d > 0) {\n            manager.take(c, address(this), uint128(d));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @notice During the 15-minute buy-fee decay, the specified-side (exact-input buy) fee that\n/// `beforeSwap` reserves can be two wei above `ethBase * rate / 10_000`, and `afterSwap` only\n/// tolerates one, so the swap reverts with `InvalidQuote` for roughly a fifth of all input amounts.\ncontract HookRoundingProof is Test {\n    uint64 constant OPEN = 1_800_000_000;\n    uint160 constant Q96 = 79228162514264337593543950336;\n\n    PoolManager manager;\n    MediciToken token;\n    MediciHook hook;\n    Trader trader;\n    PoolKey key;\n\n    function setUp() public {\n        vm.warp(OPEN);\n        vm.deal(address(this), 1_000 ether);\n        manager = new PoolManager(address(this));\n        token = new MediciToken();\n        bytes memory code = abi.encodePacked(type(MediciHook).creationCode, abi.encode(manager, address(token)));\n        bytes32 hash = keccak256(code);\n        address at;\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (!HookFlags.matches(predicted, HookFlags.MEDICI)) continue;\n            assembly (\"memory-safe\") {\n                at := create2(0, add(code, 32), mload(code), salt)\n            }\n            break;\n        }\n        require(at != address(0), \"hook deployment failed\");\n        hook = MediciHook(payable(at));\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 12_500, 60, IHooks(at));\n        manager.initialize(key, Q96 * 10_000);\n        trader = new Trader(manager);\n        vm.deal(address(trader), 100 ether);\n        token.transfer(address(trader), 600_000_000 ether);\n        trader.modify(key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e22);\n    }\n\n    /// @dev rate = 2600 bps at 450 s. For input = 39 + 63k wei the reserved fee is exactly\n    /// (ethBase * 2600 / 10_000) + 2, so the real swap is refused although the fill is complete.\n    function test_exactInputBuyDuringDecayIsNotRefusedForRoundingAlone() public {\n        vm.warp(OPEN + 450);\n        assertEq(hook.buyFeeBps(), 2600);\n        uint256 amount = 39 + 63 * 1e14;\n        BalanceDelta d = trader.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1);\n        assertEq(d.amount0(), -int128(int256(amount)), \"the whole input must be spent\");\n        assertGt(d.amount1(), 0);\n    }\n\n    /// @dev Every input amount in a window of 63 consecutive wei is a legitimate buy; none should revert.\n    function test_everyConsecutiveInputAmountSwapsDuringDecay() public {\n        vm.warp(OPEN + 450);\n        for (uint256 i; i < 63; ++i) {\n            uint256 amount = 1e15 + i;\n            try trader.swap(key, true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1) {}\n            catch (bytes memory reason) {\n                emit log_named_uint(\"reverting exact-input buy amount\", amount);\n                emit log_named_bytes(\"reason\", reason);\n                fail();\n            }\n        }\n    }\n}","reproduction":"Deploy PoolManager, MediciToken and MediciHook (mined for flags 0x20cc), initialize ETH/MEDICI at sqrtPrice Q96*10000 with fee 12500 and spacing 60, add full-range liquidity 1e22. vm.warp(openedAt + 450) so hook.buyFeeBps() == 2600. Swap zeroForOne, amountSpecified = -(39 + 63e14), limit MIN_SQRT_PRICE+1. Expected: the swap succeeds, amount0 == -(39 + 63e14), fee == ethBase*2600/10000 (+1 at most). Actual: PoolManager reverts with WrappedError(hook, afterSwap selector, InvalidQuote() 0xf8618030, HookCallFailed). Looping amounts 1e15 .. 1e15+62 at the same timestamp, 12 of the 63 revert the same way. Run test/scratch/proof/HookRoundingProof.t.sol: both tests fail on the current code.","severity":"medium","title":"Exact-input buys revert with InvalidQuote for many amounts during the 15-minute fee decay (fee rounds 2 wei above the afterSwap tolerance)"}],"hash":"d8ba53585a663e569be9480708f2943a9ae6aa0fd1499f7788db701937f1f8b5","nodeId":"e36d3fa4-6e34-4547-b748-433126ee5436","outcome":"completed","summary":"Everything is in place. The findings file is intentionally excluded from git by the repository's own `.git/info/exclude`, which is how the harness expects to collect it.\n\n## Summary\n\n**Outcome:** `forge test` passes with 87 tests across 9 suites (up from 47), zero skipped, run with the verifier's fuzz seed. One defect in the hook could not be tested around and is reported in `.imd-findings.json` with a self-contained failing proof.\n\n**What I added** (all under `test/`):\n\n- `test/Edges.t.sol` — 39 failure-path tests. Treasury: zero-address constructors, epoch-zero reverts, income before pool open, empty and maximum title/brief, per-epoch cost ladder and clamps, votes on past/future epochs and unknown ids, over-balance votes, early/unknown settles, three-way ranking and a true three-way tie, the exact 0.01 ETH quorum boundary and one wei short, skip twice, commission state gates and the no-IMD/no-free-ETH path, callbacks from the wrong sender / unknown id / after recording, result not unlocking a second commission, every `recordCommission` gate (uncommissioned, wrong caller, empty job id, unknown launch, MEDICI or IMD as the \"launch token\", inverted range, 5% threshold rounding, after deadline, reuse across epochs), backer claim expiry and one-shot sweep, keeper withdraw zero/over-free/rejecting keeper, role handover edge cases, buyback with no budget, unlock callback authorization, harvest after the requester moved. Hook/router: constructor rejections, an unopened hook's reverts and refusal of wrong pools, TWAP window validation, the exact opening-second revert, other-pool callbacks, hook-data impersonation, exact-output buys across the whole decay window (fuzz), sells always 2%, router refusing a changed hook, router harvest blocked only by a rejecting TEAM.\n- `test/TreasuryInvariants.t.sol` — a 19-action handler over the real PoolManager, hook, treasury, mocked factories and intake (propose, vote, donate, warp, settle, commission with real IMD purchase, result/failure callbacks, lapse, skip, record, claim, sweep, harvest, keeper withdraw, trade, buyback, redeem). Nine invariants: solvency, escrow equals unpaid bounties, buyback reserve tracks 40% of income, burns are all at the dead address, final statuses never reopen, backer reserves fully backed, per-epoch caps, every ETH outflow accounted for, hook fee debt backed. 4,608 calls, zero reverts; a scripted probe confirmed the handler reaches commission, record, claim, buyback and sweep.\n- `test/helpers/Fixtures.sol` — small additions only: `swapWithData` on the pool actor and `setRange` on the mock factory.\n\n**Defect reported (medium), `src/MediciHook.sol:193`:** during the 15-minute buy-fee decay, the specified-side fee reserved in `beforeSwap` can be two wei above `ethBase * rate / 10_000`, but `afterSwap` only tolerates one, so the swap reverts with `InvalidQuote` although the pool filled it. At 450 s (2600 bps), inputs congruent to 39 mod 63 always fail: 12 of every 63 consecutive amounts revert, at any size. The steady 200 bps rate is unaffected. The proof in the findings file fails on the current code and passes when the tolerance is widened to two wei (I verified that by a temporary edit, then restored the source with git). The README's \"at most one wei\" claim is incorrect.\n\nNothing was committed, since no commit was requested, and no files outside `test/` and the findings file were changed.","treeHash":"ad7bf558ea433c8aa3a9396a9719ddbc000914e3","usage":{"cachedInputTokens":3079791,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":70450,"runtime":"claude","turns":50,"wallClockMs":1360733}},{"artifacts":[],"attempt":1,"bundleHash":"be833a6f7e670c27239b54db2b337faa4e8cee0ed023e5b41302e0568c6c6867","device":"ff35db055462d2ad","findings":[{"description":"The revision made price() return the latest observation's spot when block.timestamp == openedAt instead of reverting. _observe() overwrites that observation's spot on every swap in the same second, so during the launch second price() is whatever the last swap left, not an average. MediciTreasury.proposeCost() divides 0.002 ETH by that price and clamps to the 1,000 MEDICI floor, and buyback() derives its minimum output from it. A buyer in the launch block can therefore push the spot up and propose for the floor cost in the same block, and any proposal landing in that second pays a cost set by the previous swap rather than by a TWAP. From the next second on the 24h/since-open TWAP applies and the window closes, so the exposure is one block at launch.","line":307,"path":"src/MediciHook.sol","reproduction":"At block.timestamp == openedAt with the fixture pool at 1e10 wei/MEDICI: treasury.proposeCost() == 200_000 MEDICI. Swap 0.5 ETH exact-in for MEDICI in the same second. Expected (TWAP semantics): the cost of a proposal in that second is unchanged by the swap. Actual: hook.price() equals the post-swap spot and treasury.proposeCost() drops below 230_000 MEDICI for the next proposal. test/Edges.t.sol test_openingSecondPriceIsTheSpotAndCanBeMovedWithinThatSecond documents the current behaviour.","severity":"low","title":"Opening-second price() is the live spot, so proposeCost() and the buyback floor can be moved by a swap in the launch block"},{"description":"harvest(n) calls _harvest for LAUNCH_FACTORY then PROJECT_FACTORY and the whole call reverts WrongState if either recorded position no longer names the treasury as requester. Since the factories number positions independently and the revision keys recorded launches by (factory, number), two unrelated recorded launches can share a number. If the requester of one is ever changed (the factory's requester can hand it over), the other's fees can no longer be harvested through this contract either, because there is no per-factory harvest entry point. Fees are not lost, only stuck until the moved requester is restored.","line":655,"path":"src/MediciTreasury.sol","reproduction":"Record epoch 1 on LAUNCH_FACTORY position 77 and epoch 2 on PROJECT_FACTORY position 77 (test/Treasury.t.sol test_sameLaunchNumberOnEachFactoryIsRecordedAndHarvestedSeparately sets this up). Then, as the treasury, call LAUNCH_FACTORY.setRequester(77, other). Expected: harvest(77) still claims and withdraws the PROJECT_FACTORY launch. Actual: harvest(77) reverts WrongState from the LAUNCH_FACTORY leg before the PROJECT_FACTORY leg runs.","severity":"low","title":"harvest(n) couples the two factories' launches of the same number: one moved requester blocks both"}],"hash":"e044168e6bfb0001046654c9cf2c68551f2918ebd7980a2860a67dc277ebd602","nodeId":"e36d3fa4-6e34-4547-b748-433126ee5436","outcome":"completed","summary":"The revision is complete and verified. The full suite passes under the verifier's fuzz seed.\n\n```\nRan 9 test suites: 95 tests passed, 0 failed, 0 skipped\nTreasuryInvariantTest: runs 96, calls 4608, reverts 0\nFeeInvariantTest:      runs 128, calls 8192, reverts 0\n```\n\n**What was wrong.** The implementer's accepted revision removed `claimBounty`, re-keyed recorded launches by (factory, number), made the fee router defer ETH a rejecting TEAM refuses, and made `price()` return the spot in the opening second. My accepted test files still encoded the old behaviour, so the tree did not compile. Nothing from the rejected attempt survived, so I redid the revision against the current sources.\n\n**Changes in test/Edges.t.sol**\n\n- Re-recording a used (factory, number) now expects the ineligible-position refusal and checks `launchOf` and `launchToken` for both factories.\n- Replaced the removed bounty-retry test with one showing a proposer that rejects ETH loses the bounty for good, the escrow is released, and the winner can still claim its backer share.\n- Added gate coverage for `recordCommissionAt`: wrong caller, foreign and zero factory, epoch zero, unsettled epoch, empty job id, position on the other factory, wrong requester, past the deadline, and the same call twice after success.\n- Added a harvest test showing an unrecorded same-number position on the other factory is left alone.\n- Opening-second test now shows governance works from the first block and that a same-second swap moves `price()`.\n- Router test now covers harvest with nothing owed, a no-op `payTeam`, deferred payments accumulating across two harvests, the strict retry reverting, and the next harvest flushing everything after a TEAM handover.\n\n**Changes in test/TreasuryInvariants.t.sol**\n\n- Dropped the `claimBounty` handler and added a `payTeam` one.\n- The record handler now sometimes registers the number on both factories, asserts the bare call refuses as ambiguous, and records through the explicit-factory path. A scratch run confirmed both branches execute.\n- The harvest handler now seeds whichever factory holds the launch and asserts the treasury keeps exactly the reserve.\n- New invariant: every recorded launch maps back through `launchOf` and `launchToken` to its share token, and the mirror position is never recorded.\n\n**Findings reported** in `.imd-findings.json`, both low, no proof required:\n\n- In the opening second `price()` is the live spot, so a swap in the launch block changes `proposeCost()` and the buyback floor for that one second.\n- `harvest(n)` couples both factories' launches of the same number: if either recorded position's requester moves, neither can be harvested through the treasury.\n\nOne note for the verifier: a first version of my gate test failed because the via-IR optimizer caches `block.timestamp` across `vm.warp`. I switched the test to the cheatcode read, which is what the existing handlers already use.","treeHash":"3367d8e21b685d6012da1df26c8d36923d33c7a7","usage":{"cachedInputTokens":2567709,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":32459,"runtime":"claude","turns":48,"wallClockMs":570343}}],"verification":[{"checks":[{"durationMs":66017,"exitCode":0,"name":"build","output":"Compiling 89 files with Solc 0.8.26\nSolc 0.8.26 finished in 65.90s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:255:13:\n    |\n255 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:239:5:\n    |\n239 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:292:18:\n    |\n292 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":1346,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1520851)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365670)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1846969)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1521958)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1464811)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2679282)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 755391)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 757521)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 735193)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 43.79ms (6.60ms CPU time)\n\nRan 23 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2055062, ~: 2104551)\nLogs:\n  Bound result 556904650882627288625\n\n[PASS] test_belowQuorumNoWinner() (gas: 425599)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704532)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1486221)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229931)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14975691)\n[PASS] test_failedBountyRemainsEscrowedAndCanRetryNoReentry() (gas: 2492448)\n[PASS] test_failedRequestRollsBack() (gas: 1550573)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1779402)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368308)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581571)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2799165)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1492017)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456806)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4346351)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2562410)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2168314)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668434)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198057)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994897)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616446)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992397)\n[PASS] test_voteAndFlagValidation() (gas: 448786)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 51.66ms (79.18ms CPU time)\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 46032312)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 45885636)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 54.70ms (61.60ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 18095461)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 64.14ms (31.15ms CPU time)\n\nRan 11 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 372070, ~: 346737)\nLogs:\n  Bound result 99999000000000000018736\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 370188, ~: 352988)\nLogs:\n  Bound result 779\n\n[PASS] test_allFourSwapDirections() (gas: 1111364)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 463908)\n[PASS] test_feeDecay() (gas: 33699)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 441105)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 231573)\n[PASS] test_partialFillsUseActualETH() (gas: 1129347)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226212)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115755)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 501788)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 65.71ms (61.26ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000000000003193\n  Bound result 1692\n  Bound result 1765\n  Bound result 265\n  Bound result 340\n  Bound result 999990000008178\n  Bound result 999000000000000004456\n  Bound result 2159\n  Bound result 999000000000000017467\n  Bound result 1218\n  Bound result 120\n  Bound result 3600\n  Bound result 600\n  Bound result 210\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 1984\n  Bound result 999000000000000000019\n  Bound result 2950\n  Bound result 999000003465080169979\n  Bound result 10\n  Bound result 999990000000673\n  Bound result 299999999970\n  Bound result 999000657367825572330\n  Bound result 2607\n  Bound result 999000000058816767785\n  Bound result 1483\n  Bound result 999000000000000005615\n  Bound result 999000000000000012002\n  Bound result 999000000000000009668\n  Bound result 56\n  Bound result 999990000000162\n  Bound result 999000000000000001025\n  Bound result 999000000000000000080\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.22s (1.16s CPU time)\n\nRan 6 test suites in 1.23s (1.50s CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.claimBounty(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":42,\"docs/verification.md\":21,\"foundry.toml\":24,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":59,\"src/MediciHook.sol\":305,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":654,\"src/Protocol.sol\":53,\"test/Hook.t.sol\":204,\"test/Integration.t.sol\":186,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":536,\"test/helpers/Fixtures.sol\":316,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":8920,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/MediciHook.sol:247: MediciHook.claimTeam() (src/MediciHook.sol#247-256) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciFeeRouter.sol:38: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#38-57) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciTreasury.sol:481: MediciTreasury._payBounty(uint256) (src/MediciTreasury.sol#481-494) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciHook.sol:214: MediciHook._payTreasury() (src/MediciHook.sol#214-222) sends eth to arbitrary user\n[high/medium] reentrancy-eth at src/MediciTreasury.sol:481: Reentrancy in MediciTreasury._payBounty(uint256) (src/MediciTreasury.sol#481-494):\n[medium/high] incorrect-equality at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:612: MediciTreasury.keeperWithdraw(uint256) (src/MediciTreasury.sol#612-620) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:270: MediciHook._observe(uint192) (src/MediciHook.sol#270-279) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:565: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#565-586) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:565: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#565-586) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:508: MediciTreasury.claimBacker(uint256) (src/MediciTreasury.sol#508-517) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:542: MediciTreasury.buybackChunk() (src/MediciTreasury.sol#542-563) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:214: MediciHook._payTreasury() (src/MediciHook.sol#214-222) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:194: MediciTreasury.propose(string,bytes) (src/MediciTreasury.sol#194-209) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:372: MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#372-402) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:181: MediciTreasury.proposeCost() (src/MediciTreasury.sol#181-192) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:157: MediciTreasury.proposeDeadline(uint256) (src/MediciTreasury.sol#157-163) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:646: MediciTreasury.receive() (src/MediciTreasury.sol#646-653) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MediciTreasury.sol:565: Reentrancy in MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#565-586):\n[medium/medium] reentrancy-no-eth at src/MediciTreasury.sol:372: Reentrancy in MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#372-402):\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:307: MediciTreasury.settle(uint256).second (src/MediciTreasury.sol#307) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciHook.sol:283: MediciHook.cumulativeAt(uint64).lo (src/MediciHook.sol#283) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:552: MediciTreasury.buybackChunk().complete (src/MediciTreasury.sol#552) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:545: MediciTreasury.buybackChunk().lo (src/MediciTreasury.sol#545) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:306: MediciTreasury.settle(uint256).best (src/MediciTreasury.sol#306) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:553: MediciTreasury.buybackChunk().weightedComplete (src/MediciTreasury.sol#553) is a local variable never initialized\n[medium/medium] unused-return at src/MediciFeeRouter.sol:38: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#38-57) ignores return value by factory.claimFees(launchNumber) (src/MediciFeeRouter.sol#43)\n[medium/medium] unused-return at src/MediciTreasury.sol:597: MediciTreasury.unlockCallback(bytes) (src/MediciTreasury.sol#597-610) ignores return value by poolManager.settle{value: paid}() (src/MediciTreasury.sol#607)\n[medium/medium] unused-return at src/MediciTreasury.sol:622: MediciTreasury.harvest(uint64) (src/MediciTreasury.sol#622-635) ignores return value by (key,None,None,requester) = factory.positionOf(n) (src/MediciTreasury.sol#626)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:38: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#38-57) ignores return value by (key,None,None,requester) = factory.positionOf(launchNumber) (src/MediciFeeRouter.sol#41)\n[medium/medium] unused-return at src/MediciHook.sol:230: MediciHook._redeem() (src/MediciHook.sol#230-237) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MediciHook.sol#234)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:38: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#38-57) ignores return value by treasury.burnDeposited() (src/MediciFeeRouter.sol#49)\n[medium/medium] unused-return at src/MediciHook.sol:164: MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-212) ignores return value by (sqrtPriceX96,None,None,None) = poolManager.getSlot0(k.toId()) (src/MediciHook.sol#172)\n[medium/medium] unused-return at src/MediciTreasury.sol:622: MediciTreasury.harvest(uint64) (src/MediciTreasury.sol#622-635) ignores return value by factory.claimFees(n) (src/MediciTreasury.sol#628)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:29: MediciFeeRouter.setLaunchNumber(uint64) (src/MediciFeeRouter.sol#29-36) ignores return value by (key,None,None,None) = ILaunchFactory(Protocol.LAUNCH_FACTORY).positionOf(n) (src/MediciFeeRouter.sol#31)\n[low/high] shadowing-local at src/MediciTreasury.sol:239: MediciTreasury.proposalOf(uint256,uint256).flag (src/MediciTreasury.sol#255) shadows:\n[low/high] shadowing-local at src/MediciTreasury.sol:292: MediciTreasury.backerShare(uint256).token (src/MediciTreasury.sol#292) shadows:\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:372: Reentrancy in MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#372-402):\n[low/medium] reentrancy-benign at src/MediciHook.sol:230: Reentrancy in MediciHook._redeem() (src/MediciHook.sol#230-237):\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:588: Reentrancy in MediciTreasury._swap(PoolKey,int256,uint256,uint256) (src/MediciTreasury.sol#588-595):\n[low/medium] reentrancy-benign at src/MediciHook.sol:247: Reentrancy in MediciHook.claimTeam() (src/MediciHook.sol#247-256):\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:565: Reentrancy in MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#565-586):\n[low/medium] reentrancy-events at src/MediciHook.sol:214: Reentrancy in MediciHook._payTreasury() (src/MediciHook.sol#214-222):\n[low/medium] reentrancy-events at src/MediciHook.sol:164: Reentrancy in MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-212):\n[low/medium] reentrancy-events at src/MediciHook.sol:164: Reentrancy in MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-212):\n[low/medium] timestamp at src/MediciHook.sol:270: MediciHook._observe(uint192) (src/MediciHook.sol#270-279) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:75: MediciHook.poolKey() (src/MediciHook.sol#75-78) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:174: MediciTreasury.currentEpoch() (src/MediciTreasury.sol#174-179) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:211: MediciTreasury.vote(uint256,uint256,uint256) (src/MediciTreasury.sol#211-215) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:457: MediciTreasury.recordCommission(uint256,uint64,string) (src/MediciTreasury.sol#457-479) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:565: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#565-586) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:181: MediciTreasury.proposeCost() (src/MediciTreasury.sol#181-192) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:646: MediciTreasury.receive() (src/MediciTreasury.sol#646-653) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:426: MediciTreasury.lapse(uint256) (src/MediciTreasury.sol#426-431) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:157: MediciTreasury.proposeDeadline(uint256) (src/MediciTreasury.sol#157-163) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:542: MediciTreasury.buybackChunk() (src/MediciTreasury.sol#542-563) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:298: MediciTreasury.freeETH() (src/MediciTreasury.sol#298-301) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:294: MediciHook.twap(uint64,uint64) (src/MediciHook.sol#294-297) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:404: MediciTreasury._request(bytes32) (src/MediciTreasury.sol#404-408) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:194: MediciTreasury.propose(string,bytes) (src/MediciTreasury.sol#194-209) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:612: MediciTreasury.keeperWithdraw(uint256) (src/MediciTreasury.sol#612-620) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:519: MediciTreasury.burnUnclaimed(uint256) (src/MediciTreasury.sol#519-528) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:303: MediciTreasury.settle(uint256) (src/MediciTreasury.sol#303-321) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:217: MediciTreasury._vote(uint256,uint256,uint256) (src/MediciTreasury.sol#217-233) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:164: MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-212) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:98: MediciHook._checkPool(PoolKey) (src/MediciHook.sol#98-100) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:501: MediciTreasury.claimable(uint256,address) (src/MediciTreasury.sol#501-506) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:530: MediciTreasury._stream(uint256) (src/MediciTreasury.sol#530-540) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:80: MediciHook.beforeInitialize(address,PoolKey,uint160) (src/MediciHook.sol#80-90) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:323: MediciTreasury._select(uint256,uint256) (src/MediciTreasury.sol#323-341) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:372: MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#372-402) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:92: MediciHook.buyFeeBps() (src/MediciHook.sol#92-96) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:299: MediciHook.price() (src/MediciHook.sol#299-303) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:281: MediciHook.cumulativeAt(uint64) (src/MediciHook.sol#281-292) uses timestamp for comparisons","passed":true},{"durationMs":661,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/MediciHook.sol:225: ETH transferred without address checks (4 places)\n[high] reentrancy-state-change at src/MediciFeeRouter.sol:31: Reentrancy: State change after external call (7 places)\n[low] internal-function-used-once at src/HookFlags.sol:23: Internal Function Used Only Once\n[low] large-numeric-literal at src/MediciHook.sol:121: Large Numeric Literal (13 places)\n[low] literal-instead-of-constant at src/MediciHook.sol:95: Literal Instead of Constant (38 places)\n[low] local-variable-shadowing at src/MediciTreasury.sol:66: Local Variable Shadows State Variable (2 places)\n[low] non-reentrant-not-first at src/MediciTreasury.sol:433: `nonReentrant` is Not the First Modifier (3 places)\n[low] unchecked-return at src/MediciFeeRouter.sol:43: Unchecked Return (5 places)\n[low] uninitialized-local-variable at src/MediciTreasury.sol:186: Uninitialized Local Variable\n[low] unused-public-function at src/MediciHook.sol:225: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"024ce20ec6059c6ca9c0847cfa710452e0e59d57a003486187442ee2086989dd","verifiedTreeHash":"06c4f915f88aed3cc092a6dc36198e8a5d3b977c","verifierVersion":"0.1.0+aca6c688"},{"checks":[{"durationMs":56424,"exitCode":0,"name":"build","output":"Compiling 89 files with Solc 0.8.26\nSolc 0.8.26 finished in 56.31s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:255:13:\n    |\n255 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:239:5:\n    |\n239 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:292:18:\n    |\n292 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":1083,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1520851)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365670)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1846969)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1521958)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1464811)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2679282)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 755391)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 757521)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 735193)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 34.46ms (5.75ms CPU time)\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 46032312)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 45885636)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 39.73ms (67.58ms CPU time)\n\nRan 23 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2055062, ~: 2104551)\nLogs:\n  Bound result 328726\n\n[PASS] test_belowQuorumNoWinner() (gas: 425599)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704532)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1486221)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229931)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14975691)\n[PASS] test_failedBountyRemainsEscrowedAndCanRetryNoReentry() (gas: 2492448)\n[PASS] test_failedRequestRollsBack() (gas: 1550573)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1779402)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368308)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581571)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2799165)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1492017)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456806)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4346351)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2562410)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2168314)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668434)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198057)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994897)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616446)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992397)\n[PASS] test_voteAndFlagValidation() (gas: 448786)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 60.15ms (78.30ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 18095461)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 65.42ms (30.94ms CPU time)\n\nRan 11 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 372070, ~: 346737)\nLogs:\n  Bound result 99999000000000026563263\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 370188, ~: 352988)\nLogs:\n  Bound result 239\n\n[PASS] test_allFourSwapDirections() (gas: 1111364)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 463908)\n[PASS] test_feeDecay() (gas: 33699)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 441105)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 231573)\n[PASS] test_partialFillsUseActualETH() (gas: 1129347)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226212)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115755)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 501788)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 66.64ms (65.11ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000000000003193\n  Bound result 1692\n  Bound result 1765\n  Bound result 265\n  Bound result 340\n  Bound result 999990000008178\n  Bound result 999000000000000004456\n  Bound result 2159\n  Bound result 999000000000000017467\n  Bound result 1218\n  Bound result 120\n  Bound result 3600\n  Bound result 600\n  Bound result 210\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 1984\n  Bound result 999000000000000000019\n  Bound result 2950\n  Bound result 999000003465080169979\n  Bound result 10\n  Bound result 999990000000673\n  Bound result 299999999970\n  Bound result 999000657367825572330\n  Bound result 2607\n  Bound result 999000000058816767785\n  Bound result 1483\n  Bound result 999000000000000005615\n  Bound result 999000000000000012002\n  Bound result 999000000000000009668\n  Bound result 56\n  Bound result 999990000000162\n  Bound result 999000000000000001025\n  Bound result 999000000000000000080\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 960.90ms (910.88ms CPU time)\n\nRan 6 test suites in 962.54ms (1.23s CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.claimBounty(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":42,\"docs/verification.md\":21,\"foundry.toml\":24,\"launch.json\":30,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":59,\"src/MediciHook.sol\":305,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":654,\"src/Protocol.sol\":53,\"test/Hook.t.sol\":204,\"test/Integration.t.sol\":186,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":536,\"test/helpers/Fixtures.sol\":316,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"3efcdc6ad6155887218890653d7aa1d4f9e1023e929ce285030ed1922438aa85","verifiedTreeHash":"098204fdf8c5a1ffa690df1cb616b443ece8af92","verifierVersion":"0.1.0+aca6c688"},{"checks":[{"durationMs":155304,"exitCode":0,"name":"build","output":"Compiling 89 files with Solc 0.8.26\nSolc 0.8.26 finished in 155.07s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:258:13:\n    |\n258 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:242:5:\n    |\n242 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:301:18:\n    |\n301 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":2350,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 19691802)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 19545126)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 27.06ms (49.48ms CPU time)\n\nRan 10 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1520600)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365397)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1846649)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1521778)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1464472)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2682608)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 755391)\n[PASS] test_rejectingTeamDoesNotBlockRouterBurn() (gas: 793428)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 758617)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 736601)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 60.08ms (12.09ms CPU time)\n\nRan 13 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 372826, ~: 347495)\nLogs:\n  Bound result 99999000000000000003506\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 371448, ~: 353696)\nLogs:\n  Bound result 17\n\n[PASS] test_allFourSwapDirections() (gas: 1114144)\n[PASS] test_exactInBuysSettleAcrossTheDecayWindow() (gas: 12814617)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 463930)\n[PASS] test_feeDecay() (gas: 33743)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 441747)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 231595)\n[PASS] test_partialFillsUseActualETH() (gas: 1132171)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226295)\n[PASS] test_priceAtOpeningSecondIsTheOpeningSpot() (gas: 379028)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115777)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 501990)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 67.96ms (136.95ms CPU time)\n\nRan 26 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2069009, ~: 2120973)\nLogs:\n  Bound result 328726\n\n[PASS] test_ambiguousNumberNeedsExplicitFactory() (gas: 3874336)\n[PASS] test_belowQuorumNoWinner() (gas: 425504)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704371)\n[PASS] test_bountyPayoutCannotReenter() (gas: 2444960)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1485873)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229825)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14965923)\n[PASS] test_failedBountyIsKeptByTreasuryAsFreeETH() (gas: 2447270)\n[PASS] test_failedRequestRollsBack() (gas: 1550173)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1778942)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368088)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581165)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2802315)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1491648)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456720)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4345713)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2578783)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2188972)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668116)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198101)\n[PASS] test_sameLaunchNumberOnEachFactoryIsRecordedAndHarvestedSeparately() (gas: 5235272)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994784)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616138)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992170)\n[PASS] test_voteAndFlagValidation() (gas: 448568)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 75.40ms (166.37ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 19490988)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 90.91ms (71.48ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000001259352357\n  Bound result 1692\n  Bound result 153\n  Bound result 1215\n  Bound result 340\n  Bound result 999990000005624\n  Bound result 999000000000000005401\n  Bound result 2159\n  Bound result 999000000000000008306\n  Bound result 1237\n  Bound result 2800\n  Bound result 3600\n  Bound result 61\n  Bound result 2884\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 14\n  Bound result 999000000000000005001\n  Bound result 224\n  Bound result 999000003465080169979\n  Bound result 512\n  Bound result 999990000006509\n  Bound result 999990000000201\n  Bound result 999000657367825572330\n  Bound result 3304\n  Bound result 999000000058816767785\n  Bound result 2521\n  Bound result 999000000000000002324\n  Bound result 999000000000000012002\n  Bound result 999000000000000008692\n  Bound result 56\n  Bound result 999990000000163\n  Bound result 999000000002807011244\n  Bound result 3000000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.11s (2.08s CPU time)\n\nRan 6 test suites in 2.12s (2.44s CPU time): 53 tests passed, 0 failed, 0 skipped (53 total tests)\n","passed":true},{"durationMs":162,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.payTeam()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.recordCommissionAt(uint256,address,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":43,\"docs/verification.md\":21,\"foundry.toml\":24,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":77,\"src/MediciHook.sol\":312,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":693,\"src/Protocol.sol\":53,\"test/Hook.t.sol\":234,\"test/Integration.t.sol\":209,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":655,\"test/helpers/Fixtures.sol\":316,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":20397,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/MediciHook.sol:218: MediciHook._payTreasury() (src/MediciHook.sol#218-226) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciFeeRouter.sol:63: MediciFeeRouter._payTeam(bool) (src/MediciFeeRouter.sol#63-75) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciHook.sol:251: MediciHook.claimTeam() (src/MediciHook.sol#251-260) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/MediciTreasury.sol:522: MediciTreasury._payBounty(uint256) (src/MediciTreasury.sol#522-531) sends eth to arbitrary user\n[medium/high] incorrect-equality at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:274: MediciHook._observe(uint192) (src/MediciHook.sol#274-283) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:540: MediciTreasury.claimBacker(uint256) (src/MediciTreasury.sol#540-549) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:197: MediciTreasury.propose(string,bytes) (src/MediciTreasury.sol#197-212) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:184: MediciTreasury.proposeCost() (src/MediciTreasury.sol#184-195) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:685: MediciTreasury.receive() (src/MediciTreasury.sol#685-692) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:597: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#597-618) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:160: MediciTreasury.proposeDeadline(uint256) (src/MediciTreasury.sol#160-166) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:644: MediciTreasury.keeperWithdraw(uint256) (src/MediciTreasury.sol#644-652) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:381: MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#381-411) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:303: MediciHook.price() (src/MediciHook.sol#303-310) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:218: MediciHook._payTreasury() (src/MediciHook.sol#218-226) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:574: MediciTreasury.buybackChunk() (src/MediciTreasury.sol#574-595) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciFeeRouter.sol:63: MediciFeeRouter._payTeam(bool) (src/MediciFeeRouter.sol#63-75) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MediciTreasury.sol:597: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#597-618) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MediciTreasury.sol:381: Reentrancy in MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#381-411):\n[medium/medium] reentrancy-no-eth at src/MediciTreasury.sol:597: Reentrancy in MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#597-618):\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:316: MediciTreasury.settle(uint256).second (src/MediciTreasury.sol#316) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciHook.sol:287: MediciHook.cumulativeAt(uint64).lo (src/MediciHook.sol#287) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:584: MediciTreasury.buybackChunk().complete (src/MediciTreasury.sol#584) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:577: MediciTreasury.buybackChunk().lo (src/MediciTreasury.sol#577) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:315: MediciTreasury.settle(uint256).best (src/MediciTreasury.sol#315) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MediciTreasury.sol:585: MediciTreasury.buybackChunk().weightedComplete (src/MediciTreasury.sol#585) is a local variable never initialized\n[medium/medium] unused-return at src/MediciTreasury.sol:661: MediciTreasury._harvest(address,uint64) (src/MediciTreasury.sol#661-674) ignores return value by (key,None,None,requester) = factory.positionOf(n) (src/MediciTreasury.sol#665)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:40: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#40-56) ignores return value by factory.claimFees(launchNumber) (src/MediciFeeRouter.sol#45)\n[medium/medium] unused-return at src/MediciTreasury.sol:661: MediciTreasury._harvest(address,uint64) (src/MediciTreasury.sol#661-674) ignores return value by factory.claimFees(n) (src/MediciTreasury.sol#667)\n[medium/medium] unused-return at src/MediciTreasury.sol:629: MediciTreasury.unlockCallback(bytes) (src/MediciTreasury.sol#629-642) ignores return value by poolManager.settle{value: paid}() (src/MediciTreasury.sol#639)\n[medium/medium] unused-return at src/MediciHook.sol:164: MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-216) ignores return value by (sqrtPriceX96,None,None,None) = poolManager.getSlot0(k.toId()) (src/MediciHook.sol#172)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:40: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#40-56) ignores return value by treasury.burnDeposited() (src/MediciFeeRouter.sol#51)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:31: MediciFeeRouter.setLaunchNumber(uint64) (src/MediciFeeRouter.sol#31-38) ignores return value by (key,None,None,None) = ILaunchFactory(Protocol.LAUNCH_FACTORY).positionOf(n) (src/MediciFeeRouter.sol#33)\n[medium/medium] unused-return at src/MediciFeeRouter.sol:40: MediciFeeRouter.harvest() (src/MediciFeeRouter.sol#40-56) ignores return value by (key,None,None,requester) = factory.positionOf(launchNumber) (src/MediciFeeRouter.sol#43)\n[medium/medium] unused-return at src/MediciHook.sol:234: MediciHook._redeem() (src/MediciHook.sol#234-241) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MediciHook.sol#238)\n[low/high] shadowing-local at src/MediciTreasury.sol:242: MediciTreasury.proposalOf(uint256,uint256).flag (src/MediciTreasury.sol#258) shadows:\n[low/high] shadowing-local at src/MediciTreasury.sol:301: MediciTreasury.backerShare(uint256).token (src/MediciTreasury.sol#301) shadows:\n[low/medium] reentrancy-benign at src/MediciHook.sol:234: Reentrancy in MediciHook._redeem() (src/MediciHook.sol#234-241):\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:620: Reentrancy in MediciTreasury._swap(PoolKey,int256,uint256,uint256) (src/MediciTreasury.sol#620-627):\n[low/medium] reentrancy-benign at src/MediciHook.sol:251: Reentrancy in MediciHook.claimTeam() (src/MediciHook.sol#251-260):\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:381: Reentrancy in MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#381-411):\n[low/medium] reentrancy-benign at src/MediciTreasury.sol:597: Reentrancy in MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#597-618):\n[low/medium] reentrancy-events at src/MediciHook.sol:164: Reentrancy in MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-216):\n[low/medium] reentrancy-events at src/MediciHook.sol:218: Reentrancy in MediciHook._payTreasury() (src/MediciHook.sol#218-226):\n[low/medium] reentrancy-events at src/MediciHook.sol:164: Reentrancy in MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-216):\n[low/medium] timestamp at src/MediciHook.sol:75: MediciHook.poolKey() (src/MediciHook.sol#75-78) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:220: MediciTreasury._vote(uint256,uint256,uint256) (src/MediciTreasury.sol#220-236) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:177: MediciTreasury.currentEpoch() (src/MediciTreasury.sol#177-182) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:533: MediciTreasury.claimable(uint256,address) (src/MediciTreasury.sol#533-538) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:413: MediciTreasury._request(bytes32) (src/MediciTreasury.sol#413-417) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:274: MediciHook._observe(uint192) (src/MediciHook.sol#274-283) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:285: MediciHook.cumulativeAt(uint64) (src/MediciHook.sol#285-296) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:105: MediciHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MediciHook.sol#105-143) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:164: MediciHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MediciHook.sol#164-216) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:214: MediciTreasury.vote(uint256,uint256,uint256) (src/MediciTreasury.sol#214-218) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:597: MediciTreasury.buyback(uint256) (src/MediciTreasury.sol#597-618) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:184: MediciTreasury.proposeCost() (src/MediciTreasury.sol#184-195) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:303: MediciHook.price() (src/MediciHook.sol#303-310) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:435: MediciTreasury.lapse(uint256) (src/MediciTreasury.sol#435-440) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:685: MediciTreasury.receive() (src/MediciTreasury.sol#685-692) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:332: MediciTreasury._select(uint256,uint256) (src/MediciTreasury.sol#332-350) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:381: MediciTreasury.commission(uint256,bytes) (src/MediciTreasury.sol#381-411) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:574: MediciTreasury.buybackChunk() (src/MediciTreasury.sol#574-595) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:312: MediciTreasury.settle(uint256) (src/MediciTreasury.sol#312-330) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:307: MediciTreasury.freeETH() (src/MediciTreasury.sol#307-310) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:197: MediciTreasury.propose(string,bytes) (src/MediciTreasury.sol#197-212) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:644: MediciTreasury.keeperWithdraw(uint256) (src/MediciTreasury.sol#644-652) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:98: MediciHook._checkPool(PoolKey) (src/MediciHook.sol#98-100) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:298: MediciHook.twap(uint64,uint64) (src/MediciHook.sol#298-301) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:551: MediciTreasury.burnUnclaimed(uint256) (src/MediciTreasury.sol#551-560) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:80: MediciHook.beforeInitialize(address,PoolKey,uint160) (src/MediciHook.sol#80-90) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:160: MediciTreasury.proposeDeadline(uint256) (src/MediciTreasury.sol#160-166) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:562: MediciTreasury._stream(uint256) (src/MediciTreasury.sol#562-572) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciHook.sol:92: MediciHook.buyFeeBps() (src/MediciHook.sol#92-96) uses timestamp for comparisons\n[low/medium] timestamp at src/MediciTreasury.sol:501: MediciTreasury._recordable(uint256,string) (src/MediciTreasury.sol#501-505) uses timestamp for comparisons","passed":true},{"durationMs":1150,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/MediciFeeRouter.sol:59: ETH transferred without address checks (4 places)\n[high] reentrancy-state-change at src/MediciFeeRouter.sol:33: Reentrancy: State change after external call (5 places)\n[low] internal-function-used-once at src/HookFlags.sol:23: Internal Function Used Only Once\n[low] large-numeric-literal at src/MediciHook.sol:121: Large Numeric Literal (13 places)\n[low] literal-instead-of-constant at src/MediciHook.sol:95: Literal Instead of Constant (38 places)\n[low] local-variable-shadowing at src/MediciTreasury.sol:67: Local Variable Shadows State Variable (2 places)\n[low] non-reentrant-not-first at src/MediciTreasury.sol:442: `nonReentrant` is Not the First Modifier (4 places)\n[low] unchecked-return at src/MediciFeeRouter.sol:45: Unchecked Return (6 places)\n[low] uninitialized-local-variable at src/MediciTreasury.sol:189: Uninitialized Local Variable\n[low] unused-public-function at src/MediciHook.sol:229: Public Function Not Used Internally","passed":true},{"durationMs":35299,"exitCode":0,"name":"proof f53d6cf6394a","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 33.97s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:258:13:\n    |\n258 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:242:5:\n    |\n242 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:301:18:\n    |\n301 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\nRan 2 tests for test/imd-proof-14f41abe/Proof_f53d6cf6394a.t.sol:FeeBoundTest\n[PASS] test_exactInBuyOfRoundAmountDuringDecayMustNotRevert() (gas: 366748)\n[PASS] test_exactInBuyOneSecondAfterOpen() (gas: 352534)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.08ms (1.47ms CPU time)\n\nRan 1 test suite in 15.95ms (15.08ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true},{"durationMs":16633,"exitCode":0,"name":"proof e66cb1903a03","output":"2026-10-14T23:07:46.477602Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-k6BHpy/repo/test/imd-proof-14f41abe/Proof_f53d6cf6394a.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 15.55s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:258:13:\n    |\n258 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:242:5:\n    |\n242 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:301:18:\n    |\n301 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning.\n  --> test/imd-proof-f83e50ba/Proof_e66cb1903a03.t.sol:43:52:\n   |\n43 |     function claimFees(uint64 n) external returns (uint256 a0, uint256 a1) {\n   |                                                    ^^^^^^^^^^\n\n\nRan 1 test for test/imd-proof-f83e50ba/Proof_e66cb1903a03.t.sol:LaunchNumberCollisionTest\n[PASS] test_sameLaunchNumberOnTheOtherFactoryCanStillBeRecorded() (gas: 3631637)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.51ms (1.70ms CPU time)\n\nRan 1 test suite in 6.37ms (3.51ms 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":"44e0a9b44f099096470d42d1e3b95a1e59147baeb81becf48ecd0f12482b9a3b","verifiedTreeHash":"08600cf6cadf5a9b39f5ad7b3ba455fd6429369b","verifierVersion":"0.1.0+aca6c688"},{"checks":[{"durationMs":66923,"exitCode":0,"name":"build","output":"Compiling 89 files with Solc 0.8.26\nSolc 0.8.26 finished in 66.83s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:258:13:\n    |\n258 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:242:5:\n    |\n242 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:301:18:\n    |\n301 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":1096,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1520600)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365397)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1846649)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1521778)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1464472)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2682608)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 755391)\n[PASS] test_rejectingTeamDoesNotBlockRouterBurn() (gas: 793428)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 758617)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 736601)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 9.83ms (6.53ms CPU time)\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 19691802)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 19545126)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 17.39ms (29.87ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 19490988)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 35.16ms (27.80ms CPU time)\n\nRan 26 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2069009, ~: 2120973)\nLogs:\n  Bound result 556904650882627288625\n\n[PASS] test_ambiguousNumberNeedsExplicitFactory() (gas: 3874336)\n[PASS] test_belowQuorumNoWinner() (gas: 425504)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704371)\n[PASS] test_bountyPayoutCannotReenter() (gas: 2444960)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1485873)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229825)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14965923)\n[PASS] test_failedBountyIsKeptByTreasuryAsFreeETH() (gas: 2447270)\n[PASS] test_failedRequestRollsBack() (gas: 1550173)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1778942)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368088)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581165)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2802315)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1491648)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456720)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4345713)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2578783)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2188972)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668116)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198101)\n[PASS] test_sameLaunchNumberOnEachFactoryIsRecordedAndHarvestedSeparately() (gas: 5235272)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994784)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616138)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992170)\n[PASS] test_voteAndFlagValidation() (gas: 448568)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 37.08ms (67.87ms CPU time)\n\nRan 13 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 372826, ~: 347495)\nLogs:\n  Bound result 99999000000000026563263\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 371448, ~: 353696)\nLogs:\n  Bound result 28\n\n[PASS] test_allFourSwapDirections() (gas: 1114144)\n[PASS] test_exactInBuysSettleAcrossTheDecayWindow() (gas: 12814617)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 463930)\n[PASS] test_feeDecay() (gas: 33743)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 441747)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 231595)\n[PASS] test_partialFillsUseActualETH() (gas: 1132171)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226295)\n[PASS] test_priceAtOpeningSecondIsTheOpeningSpot() (gas: 379028)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115777)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 501990)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 37.77ms (63.91ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000001259352357\n  Bound result 1692\n  Bound result 153\n  Bound result 1215\n  Bound result 340\n  Bound result 999990000005624\n  Bound result 999000000000000005401\n  Bound result 2159\n  Bound result 999000000000000008306\n  Bound result 1237\n  Bound result 2800\n  Bound result 3600\n  Bound result 61\n  Bound result 2884\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 14\n  Bound result 999000000000000005001\n  Bound result 224\n  Bound result 999000003465080169979\n  Bound result 512\n  Bound result 999990000006509\n  Bound result 999990000000201\n  Bound result 999000657367825572330\n  Bound result 3304\n  Bound result 999000000058816767785\n  Bound result 2521\n  Bound result 999000000000000002324\n  Bound result 999000000000000012002\n  Bound result 999000000000000008692\n  Bound result 56\n  Bound result 999990000000163\n  Bound result 999000000002807011244\n  Bound result 3000000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 992.90ms (982.71ms CPU time)\n\nRan 6 test suites in 995.53ms (1.13s CPU time): 53 tests passed, 0 failed, 0 skipped (53 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.payTeam()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.recordCommissionAt(uint256,address,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":43,\"docs/verification.md\":21,\"foundry.toml\":24,\"launch.json\":30,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":77,\"src/MediciHook.sol\":312,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":693,\"src/Protocol.sol\":53,\"test/Hook.t.sol\":234,\"test/Integration.t.sol\":209,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":655,\"test/helpers/Fixtures.sol\":316,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"584e078657265ff13bab839be73376d5446c46f0226ce2c4a41974c4f1c229bc","verifiedTreeHash":"390bc4d35a8f52e0f945ed11e47be98de4889a7b","verifierVersion":"0.1.0+aca6c688"},{"checks":[{"durationMs":83043,"exitCode":0,"name":"build","output":"Compiling 91 files with Solc 0.8.26\nSolc 0.8.26 finished in 82.94s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:255:13:\n    |\n255 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:239:5:\n    |\n239 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:292:18:\n    |\n292 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":1245,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1522857)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365670)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1847453)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1522442)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1465295)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2679870)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 759403)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 758212)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 737706)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 29.19ms (7.76ms CPU time)\n\nRan 28 tests for test/Edges.t.sol:TreasuryEdgeTest\n[PASS] test_backerClaimsExpireAndUnclaimedBurnIsOneShot() (gas: 2479117)\n[PASS] test_beforePoolOpenNothingGovernsButIncomeLandsInEpochOne() (gas: 408604)\n[PASS] test_burnDepositedCountsDonatedMedici() (gas: 193332)\n[PASS] test_buybackWithoutBudgetReverts() (gas: 391725)\n[PASS] test_callbacksRefuseWrongSenderUnknownIdAndWrongStatus() (gas: 2370813)\n[PASS] test_claimBountyRequiresRecordedLaunch() (gas: 1018419)\n[PASS] test_commissionStateGates() (gas: 1095991)\n[PASS] test_commissionWithoutIMDAndWithoutFreeETHReverts() (gas: 535196)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8910)\n[PASS] test_epochZeroViewsAndWritesRevert() (gas: 94868)\n[PASS] test_harvestRequiresRecordedLaunchWithTreasuryRequester() (gas: 2314811)\n[PASS] test_keeperWithdrawZeroAboveFreeAndRejectingKeeper() (gas: 424230)\n[PASS] test_launchNumberRecordedOnceAcrossEpochs() (gas: 2991683)\n[PASS] test_positionWithInvertedRangeIsNotALaunch() (gas: 2119513)\n[PASS] test_proposalCostEscalatesAndClampsPerEpoch() (gas: 4370004)\n[PASS] test_proposeRejectsEmptyTitleAndEmptyBriefAndAcceptsTheMaxima() (gas: 519050)\n[PASS] test_quorumBoundaryIsInclusiveAtOneHundredthEth() (gas: 487359)\n[PASS] test_quorumOneWeiShortAtTheEpochTwapHasNoWinner() (gas: 483198)\n[PASS] test_recordCommissionAfterDeadlineRevertsAndLapseStillWorks() (gas: 2166658)\n[PASS] test_recordCommissionGates() (gas: 2882366)\n[PASS] test_resultDoesNotUnlockASecondCommissionOrSkip() (gas: 1533339)\n[PASS] test_roleHandoverRejectsZeroAndOldRoleLosesPower() (gas: 157312)\n[PASS] test_secondSkipClosesTheEpochAndReleasesEscrow() (gas: 1455883)\n[PASS] test_settleRejectsEarlyAndUnknownEpochsAndRanksThree() (gas: 1161748)\n[PASS] test_settleThreeWayTieKeepsEarliestOrder() (gas: 1081992)\n[PASS] test_unlockCallbackOnlyDuringOwnSwap() (gas: 66197)\n[PASS] test_voteBurnsExactlyAndFailsWithoutBalance() (gas: 486497)\n[PASS] test_voteRejectsPastAndFutureEpochsAndUnknownProposal() (gas: 418560)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 32.04ms (22.06ms CPU time)\n\nRan 23 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2054598, ~: 2104919)\nLogs:\n  Bound result 11220\n\n[PASS] test_belowQuorumNoWinner() (gas: 425599)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704532)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1486221)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229931)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14975691)\n[PASS] test_failedBountyRemainsEscrowedAndCanRetryNoReentry() (gas: 2492816)\n[PASS] test_failedRequestRollsBack() (gas: 1550573)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1779402)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368308)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581571)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2799772)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1492017)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456806)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4346351)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2562778)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2169111)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668434)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198057)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994897)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616446)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992397)\n[PASS] test_voteAndFlagValidation() (gas: 448786)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 32.02ms (57.33ms CPU time)\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 46204835)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 46058159)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 42.24ms (64.92ms CPU time)\n\nRan 11 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 374052, ~: 348743)\nLogs:\n  Bound result 99999000000000000018389\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 373103, ~: 354994)\nLogs:\n  Bound result 780\n\n[PASS] test_allFourSwapDirections() (gas: 1119388)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 466398)\n[PASS] test_feeDecay() (gas: 33699)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 443111)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 233579)\n[PASS] test_partialFillsUseActualETH() (gas: 1137371)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226212)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115755)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 503794)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 51.36ms (61.91ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 18095461)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 54.19ms (30.66ms CPU time)\n\nRan 11 tests for test/Edges.t.sol:HookEdgeTest\n[PASS] test_constructorRejectsZeroArgumentsAndWrongAddressFlags() (gas: 6526)\n[PASS] test_exactOutputBuysAcrossTheDecayWindowPayTheDecayedRate(uint16,uint64) (runs: 256, μ: 345608, ~: 349773)\nLogs:\n  Bound result 19\n  Bound result 999999137955376868049064\n\n[PASS] test_hookDataCannotImpersonateTheTreasury() (gas: 351277)\n[PASS] test_priceRevertsAtTheExactOpeningSecondOnly() (gas: 80554)\n[PASS] test_redeemWithoutClaimsIsANoOpAndClaimTeamWithNothingOwedPaysZero() (gas: 96937)\n[PASS] test_routerHarvestWithNothingOwedEmitsZeroAndRejectingTeamBlocksOnlyETH() (gas: 727690)\n[PASS] test_routerRejectsHarvestWhenPositionHookChanged() (gas: 246262)\n[PASS] test_sellFeeIsAlwaysTwoPercentEvenDuringLaunch() (gas: 339376)\n[PASS] test_swapOnAnotherPoolWithThisHookIsRefused() (gas: 88344)\n[PASS] test_twapWindowValidation() (gas: 74100)\n[PASS] test_unopenedHookRefusesPriceAndPoolReads() (gas: 298490253)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 108.50ms (101.03ms CPU time)\n\nRan 1 test for test/TreasuryInvariants.t.sol:TreasuryInvariantTest\n[PASS]\nTreasuryInvariantTest invariants:\n[PASS] invariant_backerReservesAreBacked\n[PASS] invariant_buybackReserveTracksIncome\n[PASS] invariant_escrowMatchesUnpaidBounties\n[PASS] invariant_ethOutflowsAreAccountedFor\n[PASS] invariant_finalStatesAreFinal\n[PASS] invariant_hookFeeDebtBacked\n[PASS] invariant_perEpochCaps\n[PASS] invariant_totalBurnedIsAtDead\n[PASS] invariant_treasuryIsSolvent\n TreasuryInvariantTest invariants (runs: 96, calls: 4608, reverts: 0)\n\n╭-----------------+----------------+-------+---------+----------╮\n| Contract        | Selector       | Calls | Reverts | Discards |\n+===============================================================+\n| TreasuryHandler | burnUnclaimed  | 243   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | buyback        | 250   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | claimBacker    | 265   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | claimBounty    | 210   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | commission     | 239   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | deliverFailure | 266   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | deliverResult  | 251   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | donate         | 240   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | harvest        | 254   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | keeperWithdraw | 252   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | lapse          | 232   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | propose        | 214   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | record         | 223   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | redeemHookFees | 241   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | settle         | 240   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | skip           | 260   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | trade          | 237   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | vote           | 248   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | warp           | 243   | 0       | 0        |\n╰-----------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1\n  Bound result 11774\n  Bound result 8183\n  Bound result 1\n  Bound result 4527\n  Bound result 163200\n  Bound result 4999999000000000000005780\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1158\n  Bound result 121\n  Bound result 4999999000007033219754697\n  Bound result 4571\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 92249521\n  Bound result 1\n  Bound result 1\n  Bound result 49999990076915874\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.08s (1.05s CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000000000000333\n  Bound result 1692\n  Bound result 1802\n  Bound result 265\n  Bound result 340\n  Bound result 999990000008038\n  Bound result 999000000000000005082\n  Bound result 2159\n  Bound result 999000000000000017466\n  Bound result 1830\n  Bound result 3600\n  Bound result 3600\n  Bound result 51\n  Bound result 511\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 3600\n  Bound result 999000000000000000076\n  Bound result 3427\n  Bound result 999000003465080169979\n  Bound result 9\n  Bound result 999990303504771\n  Bound result 999990000000901\n  Bound result 999000657367825572330\n  Bound result 1867\n  Bound result 999000000058816767785\n  Bound result 2188\n  Bound result 999000000000000887273\n  Bound result 999000000000000000128\n  Bound result 999000000000000009461\n  Bound result 56\n  Bound result 999990000000445\n  Bound result 999000000000000009733\n  Bound result 999000000000001296001\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.13s (1.08s CPU time)\n\nRan 9 test suites in 1.13s (2.56s CPU time): 87 tests passed, 0 failed, 0 skipped (87 total tests)\n","passed":true},{"durationMs":69,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.claimBounty(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":42,\"docs/verification.md\":21,\"foundry.toml\":24,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":59,\"src/MediciHook.sol\":305,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":654,\"src/Protocol.sol\":53,\"test/Edges.t.sol\":712,\"test/Hook.t.sol\":204,\"test/Integration.t.sol\":186,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":536,\"test/TreasuryInvariants.t.sol\":452,\"test/helpers/Fixtures.sol\":338,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d8ba53585a663e569be9480708f2943a9ae6aa0fd1499f7788db701937f1f8b5","verifiedTreeHash":"ad7bf558ea433c8aa3a9396a9719ddbc000914e3","verifierVersion":"0.1.0+aca6c688"},{"checks":[{"durationMs":233049,"exitCode":0,"name":"build","output":"Compiling 91 files with Solc 0.8.26\nSolc 0.8.26 finished in 232.79s\nCompiler run successful with warnings:\nWarning (8760): This declaration has the same name as another declaration.\n   --> src/MediciTreasury.sol:258:13:\n    |\n258 |             string memory flag\n    |             ^^^^^^^^^^^^^^^^^^\nNote: The other declaration is here:\n   --> src/MediciTreasury.sol:242:5:\n    |\n242 |     function flag(uint256 e, uint256 id, string calldata note) external onlyKeeper {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/MediciTreasury.sol:301:18:\n    |\n301 |         returns (address token, uint256 amount, uint256 claimed, uint64 claimUntil)\n    |                  ^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> src/MediciTreasury.sol:27:5:\n   |\n27 |     address public immutable token;\n   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":2798,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/Hook.t.sol:FreshManagerTest\n[PASS] test_buyWithNoETHInManagerMintsAndRedeemsClaims() (gas: 19862001)\n[PASS] test_exactOutputBuyWithNoInitialNativeBalance() (gas: 19715325)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 25.40ms (47.28ms CPU time)\n\nRan 10 tests for test/Integration.t.sol:IntegrationTest\n[PASS] test_buybackBeforeUnlockAndManipulatedSpotRefuse() (gas: 1522606)\n[PASS] test_buybackUsesOnlyUnlockedBudgetBurnsAndExemptsHookFee() (gas: 1365397)\n[PASS] test_commissionBuysExactlyHalfIMDThenPaysIntake() (gas: 1847133)\n[PASS] test_commissionIMDPriceCeilingRevertsAtomically() (gas: 1522262)\n[PASS] test_commissionUsesSmallFreeBalanceWithoutTouchingEscrow() (gas: 1464956)\n[PASS] test_factoryHarvestKeepsIMDAndOnlyBurnsSurplusLaunchTokens() (gas: 2683196)\n[PASS] test_rejectingTeamCannotBlockTradingAndClaimRemainsOwed() (gas: 759403)\n[PASS] test_rejectingTeamDoesNotBlockRouterBurn() (gas: 793945)\n[PASS] test_routerBindsOnceOnlyMatchingHookAndRequiresRequester() (gas: 759308)\n[PASS] test_teamRotationAffectsHookAndRouter() (gas: 739114)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 29.83ms (12.85ms CPU time)\n\nRan 11 tests for test/Edges.t.sol:HookEdgeTest\n[PASS] test_constructorRejectsZeroArgumentsAndWrongAddressFlags() (gas: 6570)\n[PASS] test_exactOutputBuysAcrossTheDecayWindowPayTheDecayedRate(uint16,uint64) (runs: 256, μ: 345061, ~: 349795)\nLogs:\n  Bound result 726\n  Bound result 999999000000000000170350\n\n[PASS] test_hookDataCannotImpersonateTheTreasury() (gas: 351321)\n[PASS] test_openingSecondPriceIsTheSpotAndCanBeMovedWithinThatSecond() (gas: 684995)\n[PASS] test_redeemWithoutClaimsIsANoOpAndClaimTeamWithNothingOwedPaysZero() (gas: 96915)\n[PASS] test_routerHarvestWithNothingOwedAndDeferredTeamPaymentsAccumulate() (gas: 929211)\n[PASS] test_routerRejectsHarvestWhenPositionHookChanged() (gas: 246319)\n[PASS] test_sellFeeIsAlwaysTwoPercentEvenDuringLaunch() (gas: 339398)\n[PASS] test_swapOnAnotherPoolWithThisHookIsRefused() (gas: 88388)\n[PASS] test_twapWindowValidation() (gas: 74160)\n[PASS] test_unopenedHookRefusesPriceAndPoolReads() (gas: 9239357)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 61.63ms (51.46ms CPU time)\n\nRan 13 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeAccounting(bool,bool,uint80) (runs: 256, μ: 374838, ~: 349501)\nLogs:\n  Bound result 99999000000000026563263\n\n[PASS] testFuzz_launchFeeUsesSettledPartialFill(uint16,bool) (runs: 256, μ: 373626, ~: 355702)\nLogs:\n  Bound result 510\n\n[PASS] test_allFourSwapDirections() (gas: 1122168)\n[PASS] test_exactInBuysSettleAcrossTheDecayWindow() (gas: 12896863)\n[PASS] test_factoryZeroLiquidityCollectAlwaysWorks() (gas: 466420)\n[PASS] test_feeDecay() (gas: 33743)\n[PASS] test_launchFeeSurplusAndTeamClaim() (gas: 443753)\n[PASS] test_oneWeiBuyDoesNotChargeARoundedUpWholeInput() (gas: 233601)\n[PASS] test_partialFillsUseActualETH() (gas: 1140195)\n[PASS] test_permissionsInitializationAndCallbackAuthentication() (gas: 226295)\n[PASS] test_priceAtOpeningSecondIsTheOpeningSpot() (gas: 381034)\n[PASS] test_tokenSupplyTransferAndNoMint() (gas: 115777)\n[PASS] test_twapHistoryExcludesSameBlockManipulation() (gas: 503996)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 62.62ms (117.09ms CPU time)\n\nRan 30 tests for test/Edges.t.sol:TreasuryEdgeTest\n[PASS] test_backerClaimsExpireAndUnclaimedBurnIsOneShot() (gas: 2495304)\n[PASS] test_beforePoolOpenNothingGovernsButIncomeLandsInEpochOne() (gas: 408494)\n[PASS] test_burnDepositedCountsDonatedMedici() (gas: 193288)\n[PASS] test_buybackWithoutBudgetReverts() (gas: 391615)\n[PASS] test_callbacksRefuseWrongSenderUnknownIdAndWrongStatus() (gas: 2387226)\n[PASS] test_commissionStateGates() (gas: 1095821)\n[PASS] test_commissionWithoutIMDAndWithoutFreeETHReverts() (gas: 535054)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8954)\n[PASS] test_epochZeroViewsAndWritesRevert() (gas: 94780)\n[PASS] test_harvestOfANumberTouchesOnlyRecordedPositions() (gas: 3241954)\n[PASS] test_harvestRequiresRecordedLaunchWithTreasuryRequester() (gas: 2331325)\n[PASS] test_keeperWithdrawZeroAboveFreeAndRejectingKeeper() (gas: 424032)\n[PASS] test_launchNumberRecordedOnceAcrossEpochs() (gas: 3100955)\n[PASS] test_positionWithInvertedRangeIsNotALaunch() (gas: 2121849)\n[PASS] test_proposalCostEscalatesAndClampsPerEpoch() (gas: 4369058)\n[PASS] test_proposeRejectsEmptyTitleAndEmptyBriefAndAcceptsTheMaxima() (gas: 519033)\n[PASS] test_quorumBoundaryIsInclusiveAtOneHundredthEth() (gas: 487242)\n[PASS] test_quorumOneWeiShortAtTheEpochTwapHasNoWinner() (gas: 483125)\n[PASS] test_recordCommissionAfterDeadlineRevertsAndLapseStillWorks() (gas: 2164220)\n[PASS] test_recordCommissionAtGates() (gas: 2824395)\n[PASS] test_recordCommissionGates() (gas: 2934155)\n[PASS] test_rejectedBountyIsNotRetriedAndTheWinnerStillBacks() (gas: 2410658)\n[PASS] test_resultDoesNotUnlockASecondCommissionOrSkip() (gas: 1532963)\n[PASS] test_roleHandoverRejectsZeroAndOldRoleLosesPower() (gas: 157223)\n[PASS] test_secondSkipClosesTheEpochAndReleasesEscrow() (gas: 1455659)\n[PASS] test_settleRejectsEarlyAndUnknownEpochsAndRanksThree() (gas: 1161345)\n[PASS] test_settleThreeWayTieKeepsEarliestOrder() (gas: 1081859)\n[PASS] test_unlockCallbackOnlyDuringOwnSwap() (gas: 66219)\n[PASS] test_voteBurnsExactlyAndFailsWithoutBalance() (gas: 486389)\n[PASS] test_voteRejectsPastAndFutureEpochsAndUnknownProposal() (gas: 418384)\nSuite result: ok. 30 passed; 0 failed; 0 skipped; finished in 69.28ms (45.47ms CPU time)\n\nRan 26 tests for test/Treasury.t.sol:TreasuryTest\n[PASS] testFuzz_backerProRata(uint96) (runs: 256, μ: 2069855, ~: 2121402)\nLogs:\n  Bound result 328726\n\n[PASS] test_ambiguousNumberNeedsExplicitFactory() (gas: 3875194)\n[PASS] test_belowQuorumNoWinner() (gas: 425504)\n[PASS] test_bountyAbsoluteAndFreeBalanceCaps() (gas: 704371)\n[PASS] test_bountyPayoutCannotReenter() (gas: 2445389)\n[PASS] test_commissionHashTimingExactApprovalAndDuplicateProtection() (gas: 1485873)\n[PASS] test_epochsAdvanceWithoutSettlementAndKeepStatusZero() (gas: 229825)\n[PASS] test_extensionsOnlyIncreaseEndAndCapAt24Hours() (gas: 14965923)\n[PASS] test_failedBountyIsKeptByTreasuryAsFreeETH() (gas: 2447699)\n[PASS] test_failedRequestRollsBack() (gas: 1550173)\n[PASS] test_failurePromotesRunnerWithOwnBountyAndUnder200k() (gas: 1778942)\n[PASS] test_keeperBudgetFreeETHAndEpochReset() (gas: 368088)\n[PASS] test_lapseCancelsRequestAndSecondLapseReturnsEscrowWithoutIncome() (gas: 1581165)\n[PASS] test_projectFactoryAndReserveSurvivesHarvest() (gas: 2802921)\n[PASS] test_promotionClearsPreviousResultMetadata() (gas: 1491648)\n[PASS] test_proposalCostBurnAndMetadata() (gas: 456720)\n[PASS] test_proposeBoundsCapAndAllowanceFailure() (gas: 4345713)\n[PASS] test_recordLaunchAndBackerDistribution() (gas: 2579212)\n[PASS] test_recordRejectsWrongRequesterMissingShareAndUnknownHarvest() (gas: 2189830)\n[PASS] test_resultAuthenticatedOneShotTruncatedAndUnder200k() (gas: 1668116)\n[PASS] test_roleHandoverOnlyByThatRole() (gas: 198101)\n[PASS] test_sameLaunchNumberOnEachFactoryIsRecordedAndHarvestedSeparately() (gas: 5236367)\n[PASS] test_skipBeforeCommissionAndRunnerQuorum() (gas: 994784)\n[PASS] test_streamsUnlockIndependentlyOutOfOrderAndCarry() (gas: 616138)\n[PASS] test_tieEarlierWinsAndBountyEscrow() (gas: 992170)\n[PASS] test_voteAndFlagValidation() (gas: 448568)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 76.15ms (132.63ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BytecodeTest\n[PASS] test_allProductionContractsAreImmutableAndFitDeploymentLimits() (gas: 19490988)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 82.23ms (66.68ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FeeInvariantTest\n[PASS]\nFeeInvariantTest invariants:\n[PASS] invariant_ETHIsConservedAndFeeClaimsFullyBacked\n[PASS] invariant_TokenSupplyAndBalancesAreConserved\n FeeInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------+-----------+-------+---------+----------╮\n| Contract   | Selector  | Calls | Reverts | Discards |\n+=====================================================+\n| FeeHandler | advance   | 2111  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | claimTeam | 2030  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | redeem    | 2054  | 0       | 0        |\n|------------+-----------+-------+---------+----------|\n| FeeHandler | trade     | 1997  | 0       | 0        |\n╰------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 999000000000000016247\n  Bound result 1692\n  Bound result 486\n  Bound result 161\n  Bound result 340\n  Bound result 999990000008091\n  Bound result 999000000000000005368\n  Bound result 2159\n  Bound result 999000000000000002447\n  Bound result 292\n  Bound result 900\n  Bound result 610\n  Bound result 51\n  Bound result 232\n  Bound result 21\n  Bound result 999990000017487\n  Bound result 115\n  Bound result 999000000000000000077\n  Bound result 474\n  Bound result 999000003465080169979\n  Bound result 9\n  Bound result 999990000006685\n  Bound result 999990000000193\n  Bound result 999000657367825572330\n  Bound result 213\n  Bound result 999000000058816767785\n  Bound result 1460\n  Bound result 999000000000000002435\n  Bound result 999000000000000000047\n  Bound result 999000000000000009328\n  Bound result 56\n  Bound result 999994214001188\n  Bound result 999000000000000007472\n  Bound result 3000000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.26s (2.24s CPU time)\n\nRan 1 test for test/TreasuryInvariants.t.sol:TreasuryInvariantTest\n[PASS]\nTreasuryInvariantTest invariants:\n[PASS] invariant_backerReservesAreBacked\n[PASS] invariant_buybackReserveTracksIncome\n[PASS] invariant_escrowMatchesUnpaidBounties\n[PASS] invariant_ethOutflowsAreAccountedFor\n[PASS] invariant_finalStatesAreFinal\n[PASS] invariant_hookFeeDebtBacked\n[PASS] invariant_perEpochCaps\n[PASS] invariant_recordedLaunchesAreKeyedByFactory\n[PASS] invariant_totalBurnedIsAtDead\n[PASS] invariant_treasuryIsSolvent\n TreasuryInvariantTest invariants (runs: 96, calls: 4608, reverts: 0)\n\n╭-----------------+----------------+-------+---------+----------╮\n| Contract        | Selector       | Calls | Reverts | Discards |\n+===============================================================+\n| TreasuryHandler | burnUnclaimed  | 221   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | buyback        | 248   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | claimBacker    | 267   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | commission     | 249   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | deliverFailure | 272   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | deliverResult  | 250   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | donate         | 215   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | harvest        | 227   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | keeperWithdraw | 233   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | lapse          | 249   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | payRouterTeam  | 220   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | propose        | 222   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | record         | 235   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | redeemHookFees | 262   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | settle         | 210   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | skip           | 265   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | trade          | 248   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | vote           | 263   | 0       | 0        |\n|-----------------+----------------+-------+---------+----------|\n| TreasuryHandler | warp           | 252   | 0       | 0        |\n╰-----------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 900\n  Bound result 1334355015634531378\n  Bound result 44123\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 749\n  Bound result 1\n  Bound result 12001\n  Bound result 1\n  Bound result 9599\n  Bound result 1\n  Bound result 1109\n  Bound result 4457\n  Bound result 11616\n  Bound result 2\n  Bound result 1\n  Bound result 7070\n  Bound result 1\n  Bound result 7697\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 1\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.57s (2.54s CPU time)\n\nRan 9 test suites in 2.58s (5.23s CPU time): 95 tests passed, 0 failed, 0 skipped (95 total tests)\n","passed":true},{"durationMs":199,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MediciFeeRouter.harvest()\",\"MediciFeeRouter.payTeam()\",\"MediciFeeRouter.receive()\",\"MediciFeeRouter.setLaunchNumber(uint64)\",\"MediciHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MediciHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MediciHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MediciHook.claimTeam()\",\"MediciHook.quoteSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"MediciHook.receive()\",\"MediciHook.redeemFees()\",\"MediciHook.unlockCallback(bytes)\",\"MediciToken.approve(address,uint256)\",\"MediciToken.transfer(address,uint256)\",\"MediciToken.transferFrom(address,address,uint256)\",\"MediciTreasury.burnDeposited()\",\"MediciTreasury.burnUnclaimed(uint256)\",\"MediciTreasury.buyback(uint256)\",\"MediciTreasury.claimBacker(uint256)\",\"MediciTreasury.commission(uint256,bytes)\",\"MediciTreasury.flag(uint256,uint256,string)\",\"MediciTreasury.harvest(uint64)\",\"MediciTreasury.keeperWithdraw(uint256)\",\"MediciTreasury.lapse(uint256)\",\"MediciTreasury.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MediciTreasury.onResult(bytes32,bytes32,string)\",\"MediciTreasury.propose(string,bytes)\",\"MediciTreasury.receive()\",\"MediciTreasury.recordCommission(uint256,uint64,string)\",\"MediciTreasury.recordCommissionAt(uint256,address,uint64,string)\",\"MediciTreasury.setKeeper(address)\",\"MediciTreasury.setTeam(address)\",\"MediciTreasury.settle(uint256)\",\"MediciTreasury.skip(uint256,string)\",\"MediciTreasury.unlockCallback(bytes)\",\"MediciTreasury.vote(uint256,uint256,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":111,\"dependencies.lock.json\":22,\"docs/integrations.md\":25,\"docs/protocol-abi.json\":358,\"docs/security-review.md\":43,\"docs/verification.md\":21,\"foundry.toml\":24,\"src/HookFlags.sol\":30,\"src/MediciFeeRouter.sol\":77,\"src/MediciHook.sol\":312,\"src/MediciToken.sol\":11,\"src/MediciTreasury.sol\":693,\"src/Protocol.sol\":53,\"test/Edges.t.sol\":881,\"test/Hook.t.sol\":234,\"test/Integration.t.sol\":209,\"test/Invariants.t.sol\":108,\"test/Treasury.t.sol\":655,\"test/TreasuryInvariants.t.sol\":506,\"test/helpers/Fixtures.sol\":338,\"tools/check_bundle.py\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e044168e6bfb0001046654c9cf2c68551f2918ebd7980a2860a67dc277ebd602","verifiedTreeHash":"3367d8e21b685d6012da1df26c8d36923d33c7a7","verifierVersion":"0.1.0+aca6c688"}]}