{"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":"720ae500-7c66-4a8f-ae51-39e0b77689a5","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"dea22ac78decad2ee717319c9e02406188115a9f098ac4dc20d23c461505abab","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":"0b08d64201e119eb14f5e729671839e1c9703b2f0a707a30a7be2d37bbef0572","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":"c6b8809e3be841b56487d58e429d762ea386d20156b2252a2ff09be8c8bed841","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":"6d3c60abca01e66878add1d23ec5fd085f4b466263724f1bc425cf67ada325a9","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":"7cda57afe6b60489b8e9d0acf252b53c1fb54a1ea47c6a90f0d47d95c2558837","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":"d1ea9488370349eac3d3547bb0039253a2be6b07bc075f0422f47f9dddc825e7","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6e3e328aa196f6965a2821b098b101e9ff09eef55cdaafaec1d6ea18007b96f6","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6e3e328aa196f6965a2821b098b101e9ff09eef55cdaafaec1d6ea18007b96f6","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"96decdc2859ab0b4a3783f96bcb2da5d43ce76ef17709412f6d322d683cc7c16","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"ad13ee08da4fb503c46bb2e988107803af8e16fd15ecf47975477c74f1ca76c3","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Kiln: a Uniswap v4 hook for a new ETH/ZTO pool that charges a lower fee to wallets holding Pepeolithic NFTs, keeps the fee everyone else pays as a ZTO reserve, and uses that reserve to buy Pepeolithic pieces from anyone and sell them back. Two contracts, Launcher and Kiln. Deploy on Ethereum mainnet (chain id 1). Nothing is upgradeable, pausable or ownable; no admin exists anywhere; the reserve can never be withdrawn, only paid out for pieces.\n\nADDRESSES (constants). ZTO 0xd782bdea4ef02a0bd391eb9089470c8080f0a68e (plain ERC-20, 18 decimals, write 18 as a constant). Pepeolithic (PEPEO, ERC-721, 737 ids) 0x765956a7307222346b08fff681820a5d77e92028. Uniswap v4 PoolManager 0x000000000004444c5dc75cB358380D2e3dE08A90. Native ETH is currency0 (address 0), ZTO currency1.\n\nCONSTRUCTORS take static words only (address, uint, bool, bytes32: the deployment manifest supports nothing else, no arrays, no int24), make no external calls and read nothing on-chain (the verifier deploys in an empty EVM). Launcher constructor args: zto, pepeo, poolManager (three addresses, nothing else). EVERY OTHER NUMBER IS A CODE CONSTANT: tickSpacing 60 (int24 constant), lpFee 2000 (0.20%, static pool fee), the pass tiers minPepes 0 / 1 / 3 / 7 / 12 / 21 with kilnCut 13000 / 10000 / 7500 / 5000 / 2500 / 0 in hundredths of a bip (1.30%, 1.00%, 0.75%, 0.50%, 0.25%, 0%), spreadBps 1500 (15%), depth 50. The Kiln is created by the Launcher with CREATE2 and takes (zto, pepeo, poolManager) too; it must NOT validate its own address bits in its constructor; the Launcher checks them after CREATE2. The Launcher exposes initCodeHash() (view) so the salt can be mined off-chain.\n\nLAUNCHER. One permissionless function open(bytes32 salt, uint160 sqrtPriceX96) that succeeds once: (1) deploys the Kiln with CREATE2 and reverts unless its address carries exactly the permission bits for beforeSwap, afterSwap, beforeSwapReturnDelta and afterSwapReturnDelta and no others; (2) initializes the ETH/ZTO pool on the PoolManager with lpFee, tickSpacing and the Kiln as hook at sqrtPriceX96; emits Opened(kiln, poolId). No liquidity is added by the Launcher: the deployer adds a ZTO-only range position later through the normal PositionManager, so the Kiln must not restrict liquidity in any way (no liquidity callbacks).\n\nKILN, FEE PASS. On every swap in its pool the Kiln reads pepes = PEPEO.balanceOf(tx.origin) (routers are msg.sender; tx.origin is the trader) and picks the highest tier whose minPepes <= pepes. The pool's static lpFee goes to liquidity as usual; on top, the Kiln takes kilnCut of the swap as its cut, ALWAYS IN ZTO: when ZTO is the input, from the input (beforeSwap return delta on the specified currency for exact-input, afterSwap on the unspecified for exact-output); when ETH is the input, from the ZTO output (afterSwap return delta for exact-input, beforeSwap for exact-output). Work out each of the four cases so the trader is charged kilnCut of the ZTO side and the pool's accounting settles. The cut is NOT taken as real tokens inside the swap (the trader's input is settled by the router only after the swap, so poolManager.take would revert on an empty manager): the hook settles its return delta by MINTING ERC-6909 claim tokens for ZTO to itself (poolManager.mint(address(this), zto, amount)) and adds the amount to `claims`. collect(): permissionless, burns the Kiln's whole ZTO claim balance and takes real ZTO out of the PoolManager (poolManager.unlock -> burn + take, or the equivalent), moving the amount from `claims` into `reserve`; sell() and buy() call collect() first. Tier 21 pays no cut at all. Emit Passed(trader, pepes, kilnCut, ztoTaken) per swap and Collected(amount) per collect(). No block-held guard; README states that a pass only needs to be in the wallet during the swap.\n\nKILN, PIECES. State: claims (ZTO cut still held as ERC-6909 claims), reserve (real ZTO held for pieces; grows by collect(), seeds and sales of pieces; shrinks only by buying pieces; reserve <= ZTO.balanceOf(Kiln) always), inventory (ids held). Views: claims(), bid() = reserve / depth (real ZTO only, so it is always payable); ask() = bid() * (10000 + spreadBps) / 10000; inventory(), reserve(), tierOf(address), poolKey(). sell(uint256 id): the caller's PEPEO piece is pulled with transferFrom (caller approves first), price = bid() before the transfer, reserve -= price, ZTO.transfer(caller, price) requiring the bool, emits Sold(id, seller, price); reverts if bid() is 0. buy(uint256 id): id must be in inventory; price = ask(); ZTO.transferFrom(caller, kiln, price) requiring the bool, reserve += price, piece sent to caller with transferFrom (never safeTransferFrom, no receiver callbacks), emits Bought(id, buyer, price). seed(uint256 amount): anyone adds ZTO to reserve by transferFrom, emits Seeded(from, amount). No other way moves ZTO or pieces. Pieces arriving by plain transfer without sell() are not inventory and are stuck; README says so. Because bid is reserve/depth it is always payable, falls geometrically as pieces come in and rises with every cut, seed and sale.\n\nTESTS against the real v4 PoolManager (vendor v4-core and v4-periphery test routers) with a mock ZTO and a mock ERC-721: open() once and only at an address with the right bits; a ZTO-only range position above the opening price added through the test liquidity router; swaps in all four cases (ETH in / ZTO in, exact in / exact out) for wallets holding 0, 1, 3, 7, 12 and 21 pieces, checking the ZTO cut equals kilnCut of the ZTO side within rounding, that tier 21 pays nothing, that the cut shows in claims() and after collect() in reserve() and the Kiln's real ZTO balance, that collect() with nothing to collect is a harmless no-op, and that the trader also paid lpFee; sell() pays bid and bid falls afterwards; buy() charges ask and the piece leaves inventory; buy of an id not held reverts; sell at zero reserve reverts; seed() grows bid; reserve never exceeds the Kiln's ZTO balance; nobody can withdraw. README with the rules, the tier table and the two caveats (tx.origin, stuck transfers). BUILD: solidity 0.8.26, optimizer + via-IR (via_ir = true, optimizer_runs = 1), custom errors only, no ReentrancyGuard (external token calls last), Kiln deployed code under 12,000 bytes. Slither: multiply before dividing; string.concat not encodePacked.","parentJobId":null,"planHash":"d98a1e0a44bbacbadf211622e7ed9b5bfc3a7c4a1daa6e81a2ca70ca5f57c66d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"720ae500-7c66-4a8f-ae51-39e0b77689a5","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1138-kiln-uniswap-v4-hook"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52137","feedbackHash":"258d7a4993cf5ce97731a80837800971990acdf52fab0aa746c9e2cd1369ed31","nodeKey":"audit_economics","submissionHash":"dea22ac78decad2ee717319c9e02406188115a9f098ac4dc20d23c461505abab","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51473","feedbackHash":"11d38c796c6117c148f3accaba0c245cdf9f3601674c93e68822b93f068ac7bd","nodeKey":"audit_flow","submissionHash":"0b08d64201e119eb14f5e729671839e1c9703b2f0a707a30a7be2d37bbef0572","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52166","feedbackHash":"179a63d1347a13dbf78792c2c75bf34616d6cd66e4a036deb68f2f3c78083774","nodeKey":"audit_judge","submissionHash":"55e64b20f1a65270eba5acc5a47dda49e70f3c416f09ba2b8890ba7d61213164","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52162","feedbackHash":"1d52af0ef7013c30ac1b098e9a0dcd2ab0f13b50b7fb5ac72bef8af156c74fa0","nodeKey":"audit_judge","submissionHash":"c6b8809e3be841b56487d58e429d762ea386d20156b2252a2ff09be8c8bed841","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51487","feedbackHash":"9a6caa2e6eb9ec5d705ac3c8dd26d9f35f79e33fc72f4fb39c7c8dbac2f59b66","nodeKey":"audit_math","submissionHash":"6d3c60abca01e66878add1d23ec5fd085f4b466263724f1bc425cf67ada325a9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50995","feedbackHash":"ba105503eaaaf9dc6365d8c9f2bb410aae074d39011b52e8cc644305aad518c9","nodeKey":"audit_permissions","submissionHash":"7cda57afe6b60489b8e9d0acf252b53c1fb54a1ea47c6a90f0d47d95c2558837","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52164","feedbackHash":"50512f0b616a66caf2e2ff7a4f0ecc5faf83c773834f17843eef17745f022d72","nodeKey":"build_contract_project","submissionHash":"53eb7f2a76e5a86bce8d0acdbfbbf3a9c08451dd91aa6c6069c73520467f8506","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51509","feedbackHash":"5fe1e54f5c388579851e08ac4354301ac5bf1f6121f1963d3eba6148eea49e97","nodeKey":"build_contract_project","submissionHash":"d1ea9488370349eac3d3547bb0039253a2be6b07bc075f0422f47f9dddc825e7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52159","feedbackHash":"aa17dd3fdd2cd9836d6fcd2e2fe326a952a9988f5d846ebfb89db8da929ad5ed","nodeKey":"manifest","submissionHash":"a68a9d68644783171f0616205b6f7b07cc22899cf1728bbe05ba31db74d8c476","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51162","feedbackHash":"468e08926774e9e945e3d5bc8d9286dd1243a13167b96eb50c7b9c58a99b82d2","nodeKey":"manifest","submissionHash":"96decdc2859ab0b4a3783f96bcb2da5d43ce76ef17709412f6d322d683cc7c16","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52263","feedbackHash":"5c63995a3f3d0400c92a24a5da0a901badc7551652250ef323e912887de61598","nodeKey":"write_foundry_tests","submissionHash":"2c280a0fcadcd7e9908ab2cc8178175b155f06fc21497aaddb4d051b0faafb70","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52253","feedbackHash":"6af5204c9fbfe38113901c7c1755f71ba3898b165e31b5dfe9c5f77fc0adf561","nodeKey":"write_foundry_tests","submissionHash":"ad13ee08da4fb503c46bb2e988107803af8e16fd15ecf47975477c74f1ca76c3","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"94c0b72a0c4503064279c47bc840120c5d62bb55202280b5dc02115902e4c747","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dc34db8e17664ebd","findings":[{"citation":"resolved","description":"In the two cases where ZTO is the specified currency (ZTO-in exact-input, ETH-in exact-output) the cut is computed in beforeSwap from params.amountSpecified and returned as a specified-currency BeforeSwapDelta. PoolManager.swap then runs the pool for amountSpecified +/- cut, and Hooks.afterSwap charges the full hook delta to the swapper regardless of how much of the swap the pool actually filled (swapDelta = swapDelta - hookDelta). Whenever the swap stops early (liquidity exhausted or sqrtPriceLimitX96 reached) the trader pays kilnCut of the requested amount, not of the ZTO side that moved, which contradicts the rule 'the trader is charged kilnCut of the ZTO side'. In this pool partial fills are the normal case, not an edge: liquidity is a single ZTO-only range, so the ETH available to ZTO-in swaps is only what earlier ETH-in traders deposited, and any ZTO-in exact-input order larger than that fills partially. Case 1 (ZTO in, exact in): the trader pays consumed + 1.30% of the requested input, so the effective cut is unbounded relative to what was traded. Case 4 (ETH in, exact out): swapDelta.amount1 = delivered - cut; when delivered < cut the trader's ZTO delta turns negative, so an 'exact output ZTO' swap makes the trader pay ETH and ZTO and receive nothing. Cases 2 and 3 are unaffected because afterSwap measures the pool's realised delta. The README caveat only says that an exact-output ETH-in swap 'may receive less than the specified output'; it does not mention case 1 at all, nor that the trader can become a net ZTO payer. Mitigation: routers with a correct amountOutMinimum / TAKE_ALL check revert, but the hook itself provides no protection and the test router in this repo, custom integrations, or a loose minimum pay. Fix that keeps the brief's beforeSwap design: in afterSwap, when _ztoIsSpecified(params), compare |delta.amount1()| with the amount the pool was asked to swap (amount - cut for exact-input, amount + cut for exact-output) and revert with a custom error (e.g. PartialFill) when they differ, so a ZTO-specified swap either fills completely at the advertised cut or does not execute. Document the behaviour for both cases in the README.","line":179,"path":"src/Kiln.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 {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {BalanceDelta, BalanceDeltaLibrary} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {HookMiner} from \"v4-periphery/test/shared/HookMiner.sol\";\nimport {Launcher} from \"src/Launcher.sol\";\nimport {Kiln} from \"src/Kiln.sol\";\n\ncontract ProofZTO {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract ProofPepeo {\n    mapping(uint256 => address) public ownerOf;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 id) external {\n        ownerOf[id] = to;\n        balanceOf[to] += 1;\n    }\n\n    function transferFrom(address f, address t, uint256 id) external {\n        require(ownerOf[id] == f && msg.sender == f);\n        ownerOf[id] = t;\n        balanceOf[f] -= 1;\n        balanceOf[t] += 1;\n    }\n}\n\n/// @notice The Kiln cut must be kilnCut (1.30% for a wallet with no pieces) of the ZTO the pool actually moved.\n///         When a ZTO-specified swap only partially fills, the current code charges 1.30% of the *specified*\n///         amount instead, which can be a multiple of the ZTO actually swapped. A fix may either measure the cut\n///         on the realised amount or reject partial fills; both make these tests pass.\ncontract KilnPartialFillProof is Test {\n    using BalanceDeltaLibrary for BalanceDelta;\n\n    uint160 internal constant FLAGS = Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n    int24 internal constant OPEN_TICK = 138_180;\n    uint256 internal constant CUT_0 = 13_000;\n    uint256 internal constant PIPS = 1_000_000;\n\n    IPoolManager internal manager;\n    ProofZTO internal zto;\n    ProofPepeo internal pepeo;\n    Launcher internal launcher;\n    Kiln internal kiln;\n    PoolKey internal key;\n    PoolSwapTest internal swapRouter;\n    PoolModifyLiquidityTest internal lpRouter;\n    address internal lp = makeAddr(\"lp\");\n    address internal trader = makeAddr(\"trader\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        zto = new ProofZTO();\n        pepeo = new ProofPepeo();\n        launcher = new Launcher(address(zto), address(pepeo), address(manager));\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        uint160 openPrice = TickMath.getSqrtPriceAtTick(OPEN_TICK);\n        (, bytes32 salt) = HookMiner.find(\n            address(launcher), FLAGS, type(Kiln).creationCode, abi.encode(address(zto), address(pepeo), address(manager))\n        );\n        launcher.open(salt, openPrice);\n        kiln = launcher.kiln();\n        key = kiln.poolKey();\n\n        // ZTO-only range below the opening tick, modest size.\n        zto.mint(lp, 1e27);\n        vm.startPrank(lp);\n        zto.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity(\n            key,\n            IPoolManager.ModifyLiquidityParams({\n                tickLower: OPEN_TICK - 6000, tickUpper: OPEN_TICK, liquidityDelta: 1e22, salt: bytes32(0)\n            }),\n            \"\"\n        );\n        vm.stopPrank();\n\n        // Trader with no pieces (tier 0, 1.30%).\n        vm.deal(trader, 10_000 ether);\n        zto.mint(trader, 1e30);\n        vm.prank(trader);\n        zto.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function _swap(bool zeroForOne, int256 amountSpecified, uint256 value)\n        internal\n        returns (bool ok, BalanceDelta delta)\n    {\n        vm.prank(trader, trader);\n        try swapRouter.swap{value: value}(\n            key,\n            IPoolManager.SwapParams({\n                zeroForOne: zeroForOne,\n                amountSpecified: amountSpecified,\n                sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1\n            }),\n            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),\n            \"\"\n        ) returns (BalanceDelta d) {\n            return (true, d);\n        } catch {\n            return (false, BalanceDelta.wrap(0));\n        }\n    }\n\n    function _abs(int128 x) internal pure returns (uint256) {\n        return x < 0 ? uint256(uint128(-x)) : uint256(uint128(x));\n    }\n\n    /// Case 1: ZTO in, exact input, larger than the ETH the range holds. The pool consumes only part of the\n    /// input; the cut must be at most 1.30% of the ZTO the pool consumed (plus 1 wei rounding).\n    function test_case1_ztoInExactIn_partialFill_cutBoundedByRealisedInput() public {\n        // Put 1 ETH into the range with a no-cut wallet.\n        address whale = makeAddr(\"whale\");\n        for (uint256 i = 1; i <= 21; ++i) pepeo.mint(whale, i);\n        vm.deal(whale, 10 ether);\n        vm.prank(whale, whale);\n        swapRouter.swap{value: 1 ether}(\n            key,\n            IPoolManager.SwapParams({\n                zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n            }),\n            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),\n            \"\"\n        );\n        assertEq(kiln.claims(), 0);\n\n        uint256 claimsBefore = kiln.claims();\n        (bool ok, BalanceDelta delta) = _swap(false, -int256(100_000_000e18), 0);\n        if (!ok) return; // a fix that rejects partial fills is acceptable\n        uint256 cut = kiln.claims() - claimsBefore;\n        uint256 poolZtoIn = _abs(delta.amount1()) - cut; // ZTO the pool actually consumed\n        assertLe(cut, poolZtoIn * CUT_0 / PIPS + 1, \"cut exceeds 1.30% of the ZTO the pool consumed\");\n    }\n\n    /// Case 4: ETH in, exact ZTO output, far larger than the ZTO the range holds. The pool delivers only part of\n    /// the output; the cut must be at most 1.30% of the ZTO the pool delivered, and the trader must not end up\n    /// paying ZTO on a swap that was supposed to deliver ZTO.\n    function test_case4_ethInExactOut_partialFill_cutBoundedByRealisedOutput() public {\n        uint256 poolZto = zto.balanceOf(address(manager));\n        uint256 claimsBefore = kiln.claims();\n        (bool ok, BalanceDelta delta) = _swap(true, int256(poolZto * 200), 5000 ether);\n        if (!ok) return; // a fix that rejects partial fills is acceptable\n        uint256 cut = kiln.claims() - claimsBefore;\n        assertGe(delta.amount1(), 0, \"trader paid ZTO on an exact-output ZTO swap\");\n        uint256 poolZtoOut = uint256(uint128(delta.amount1())) + cut; // ZTO the pool actually delivered\n        assertLe(cut, poolZtoOut * CUT_0 / PIPS + 1, \"cut exceeds 1.30% of the ZTO the pool delivered\");\n    }\n}","reproduction":"Fixture: real PoolManager, Launcher.open at tick 138180, ZTO-only range [132180,138180] with liquidity 1e22 (about 2.594e24 wei ZTO), tier-0 trader (0 pieces), PoolSwapTest router with msg.sender = tx.origin = trader. Case 1: a 21-piece wallet first swaps 1 ETH in (range now holds 1 ETH). Trader swaps zeroForOne=false, amountSpecified=-100_000_000e18 (100M ZTO exact in), limit MAX_SQRT_PRICE-1. Actual: delta0 = +0.998 ETH, delta1 = -2_210_818.7e18 ZTO, claims = 1_300_000e18. The pool consumed only 910_818.7e18 ZTO; the Kiln took 1_300_000e18, i.e. 143% of the ZTO actually swapped. Expected: cut <= 1.30% of 910_818.7e18 = 11_840.6e18 (or a revert). Trader loses ~1_288_000e18 ZTO (about 1.29 ETH at the opening price) to the reserve. Case 4: fresh fixture, trader swaps zeroForOne=true, amountSpecified=+518_807_686e18 (200x the pool's ZTO), msg.value 5000 ether. Actual: delta0 = -3.502 ETH, delta1 = -4_150_461e18 ZTO, claims = 6_744_499e18. The pool delivered 2_594_038e18 ZTO, the hook took 6_744_499e18, so the trader paid 3.5 ETH plus 4_150_461e18 ZTO and received nothing. Expected: delta1 >= 0 and cut <= 1.30% of 2_594_038e18 = 33_722e18 (or a revert). Proof file test/scratch/KilnPartialFillProof.t.sol fails on the current code with 'cut exceeds 1.30% of the ZTO the pool consumed: 1300000000000000000000000 > 11840643311917444107791' and 'trader paid ZTO on an exact-output ZTO swap: -4150461490747872250558682 < 0'. It passes if the cut is bounded by the realised amount or if partial fills of ZTO-specified swaps revert.","severity":"medium","snippet":"        uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n        uint256 cut = _takeCut(amount);\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(cut.toInt128(), 0), 0);","title":"beforeSwap charges kilnCut on the specified amount, so a partially filled ZTO-specified swap is overcharged; an exact-output ETH-in swap can leave the trader paying ZTO instead of receiving it"},{"citation":"resolved","description":"open(salt, sqrtPriceX96) is permissionless and succeeds exactly once, as the brief requires, but the two inputs are not bound to the deployer in any way. (a) The salt is public the moment the deployer's transaction enters the mempool (and any valid salt can be mined independently), so a third party can call open(salt, anyPrice) first. The Kiln is deployed at the intended address and the pool is initialized at the attacker's price; the deployer's own call then reverts AlreadyOpened. (b) PoolManager.initialize does not require the hook address to have code (the Kiln has no beforeInitialize flag), so anyone who can compute launcher.kilnAddress(salt) can initialize the exact key (ETH, ZTO, 2000, 60, predictedKiln) before open() runs; open(salt, ...) then reverts PoolAlreadyInitialized inside initialize and the whole call (including the CREATE2) is rolled back. Neither breaks funds: for (a) the price can be moved for free by a swap through the empty pool (zero liquidity, price jumps to sqrtPriceLimitX96, the only cost is the cut on a dust input: 13 wei for a 1000-wei exact-input), and for (b) the deployer mines a new salt and submits through a private relay. This is reported as a trust assumption of the permissionless design rather than a bug: the deployment runbook should say that open() must be sent privately and that the price in slot0 must be verified before the ZTO-only position is added, because a position added while the price sits below the intended range would require ETH instead of ZTO. Optional hardening that keeps open() permissionless: have open() revert unless the pool is uninitialized at the predicted key (it already does, by failing), and in the README instruct the deployer to verify getSlot0 after open().","line":56,"path":"src/Launcher.sol","reproduction":"(a) Fixture: Launcher(zto, pepeo, manager), salt mined for FLAGS. griefer calls launcher.open(salt, TickMath.MIN_SQRT_PRICE + 1): succeeds, Opened emitted, slot0.sqrtPriceX96 == MIN_SQRT_PRICE+1. Deployer calls launcher.open(salt, getSqrtPriceAtTick(138180)): reverts AlreadyOpened. Recovery shown in test/scratch/Probe.t.sol::test_frontRunOpenWithBadPrice: a oneForZero exact-input swap of 1000 wei with sqrtPriceLimitX96 = intended price moves slot0 to the intended price and the Kiln records 13 wei of claims. (b) griefer calls manager.initialize(PoolKey(ETH, zto, 2000, 60, launcher.kilnAddress(salt)), MIN_SQRT_PRICE+1) before open(); then launcher.open(salt, openPrice) reverts with Pool.PoolAlreadyInitialized and launcher.kiln() stays address(0). Shown in test/scratch/Probe.t.sol::test_preInitializeBlocksOpen.","severity":"low","snippet":"    function open(bytes32 salt, uint160 sqrtPriceX96) external returns (address kilnAddr, PoolId poolId) {\n        if (address(kiln) != address(0)) revert AlreadyOpened();\n        Kiln deployed = new Kiln{salt: salt}(ZTO, PEPEO, POOL_MANAGER);","title":"open() is first-come: a front-runner picks the opening price for the one-shot Launcher, and anyone can pre-initialize the predicted pool key so open(salt) reverts for that salt"},{"citation":"resolved","description":"collect() settles claims by calling POOL_MANAGER.unlock, which reverts with AlreadyUnlocked when the manager is already unlocked. sell() and buy() call collect() unconditionally first, so any contract that tries to sell or buy a piece from inside its own unlockCallback (a router action that swaps and then sells a piece in one unlock, or a flash-accounting integration) reverts whenever the Kiln's ERC-6909 ZTO balance is non-zero, and succeeds only when it happens to be zero. Not a loss and not contrary to the brief, which does not promise composability inside an unlock, but the behaviour is state-dependent and undocumented; an integrator cannot rely on it either way. Worth a README line, or a variant of sell()/buy() that skips collect() when Lock.isUnlocked() is true (the claims would then be collected by the next external call).","line":221,"path":"src/Kiln.sol","reproduction":"Fixture as in finding 1. A tier-0 trader swaps 1 ETH in, so kiln.claims() > 0. Contract L implements unlockCallback() { kiln.collect(); } and calls manager.unlock(''). Actual: the call reverts (AlreadyUnlocked from PoolManager.unlock inside collect). Same for kiln.sell(id) / kiln.buy(id) from inside the callback. With claims == 0 the same call sequence succeeds. Shown in test/scratch/Probe.t.sol::test_collectInsideUnlockReverts.","severity":"info","snippet":"        POOL_MANAGER.unlock(abi.encode(amount));","title":"collect(), and therefore sell() and buy(), cannot run inside a PoolManager unlock while claims exist: unlock() reverts AlreadyUnlocked"}],"hash":"0b08d64201e119eb14f5e729671839e1c9703b2f0a707a30a7be2d37bbef0572","nodeId":"2b625c21-484a-4cc5-9b8a-9751473c7fdb","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds three findings plus a 12-row coverage record. No source or test file in the tree was changed; the two scratch tests live in the ignored `test/scratch/` directory.\n\n**Findings**\n\n- **Medium, `src/Kiln.sol:179`.** The two beforeSwap cases take kilnCut of the requested amount, not of the ZTO the pool actually moved. On a partial fill the trader is overcharged without bound. Concrete numbers from a real PoolManager fixture: a ZTO-in exact-input order of 100M ZTO against a range holding 1 ETH consumed 910,818 ZTO but was charged 1,300,000 ZTO, so 143% of the traded amount went to the reserve. An ETH-in exact-output request for 200x the pool's ZTO left the trader paying 3.5 ETH and 4,150,461 ZTO and receiving nothing, on a swap meant to deliver ZTO. Partial fills are the normal case here because the only liquidity is a ZTO-only range. This finding carries the proof file, which fails on the current code and passes if the cut is bounded by the realised amount or if partial fills of ZTO-specified swaps revert.\n- **Low, `src/Launcher.sol:56`.** The one-shot open is first-come. A front-runner can set the opening price with the deployer's salt, and anyone can pre-initialize the predicted pool key so open reverts for that salt. Both are recoverable, a zero-liquidity swap for the price and a new salt for the key, so this is recorded as a trust assumption of the permissionless design with a runbook recommendation.\n- **Info, `src/Kiln.sol:221`.** collect, sell and buy revert with AlreadyUnlocked when called inside a PoolManager unlock while claims exist, and succeed when claims happen to be zero. Not a loss and not promised by the brief, but undocumented and state-dependent.\n\n**Coverage**\n\nAll eight listed entry points have rows. Three carry findings and five hold after tracing their guards, state changes and failure paths. The Slither and Aderyn reentrancy leads on sell, buy and collect were traced and not reproducible given the plain ERC-20 and ERC-721 the brief fixes. The partial-fill behaviour in the swap cases that measure on realised deltas, the hook delta netting to zero, reserve never exceeding the Kiln's balance, and the inventory bookkeeping all hold. Not reached: live mainnet behaviour of the real Pepeolithic and ZTO contracts, which this environment cannot query.","treeHash":null,"usage":{"cachedInputTokens":1293228,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":51645,"runtime":"claude","turns":25,"wallClockMs":816869}},{"artifacts":[],"attempt":1,"bundleHash":"8627bbaad49f123364a1c05a525a7a31ffbbe323c9b6cf7a516c4f6f29029a07","device":"6b16b4979d227241","findings":[{"description":"In the two cases the Kiln charges from beforeSwap (ZTO-in exact-input and ETH-in exact-output) the cut is kilnCut of params.amountSpecified, computed before the pool runs. v4 does not revert on a partial fill: when the request exceeds the liquidity in range (or the trader's sqrtPriceLimitX96 stops the swap early) the pool fills part of it, but the hook has already minted kilnCut of the full request to itself. The trader is then charged far more than kilnCut of the ZTO that actually traded, which is what the brief and README promise. For ETH-in exact-output the cut can exceed the whole ZTO output, so the trader's currency1 delta goes negative: they pay ETH for the partial output and pay ZTO on top of it (the router settles the negative delta from their ZTO allowance). Exact-output requests larger than the available liquidity are an ordinary user mistake and neither the v4 test router nor the periphery V4Router checks the delivered output. The afterSwap cases are unaffected because they read the realised delta. Suggested fix within the brief's structure: in afterSwap, compare the realised ZTO delta plus the beforeSwap cut with the request and revert (custom error) when the swap did not fill completely, or refund the excess through the unspecified side; the proof passes with either a refusal or a correctly sized cut.","line":179,"path":"src/Kiln.sol","reproduction":"Fresh pool at tick 138180 (~1,000,000 ZTO/ETH) with the deployer's ZTO-only range [138180-6000, 138180], liquidity 1e24 (~2.6e26 ZTO in range). Tier-0 wallet (no pieces) swaps ETH in, exact output = 200x the ZTO in range, limit MIN_SQRT_PRICE+1 through PoolSwapTest. Expected: cut <= 1.30% of the ~2.6e26 ZTO delivered (~3.37e24) and the trader's ZTO balance does not decrease. Actual: cut = 6.74e26 (1.30% of the request), trader's ZTO balance falls by ~4.15e26 on top of the ETH they paid. Second shape: after a 21-piece wallet puts 5 ETH into the range, a tier-0 wallet swaps ZTO in, exact input 1e27 (1e9 ZTO). The pool consumes ~5e24 ZTO for the ~5 ETH available; expected cut ~6.5e22, actual cut 1.3e25, two hundred times the filled side.","severity":"medium","title":"beforeSwap cut is charged on the requested amount, so a partial fill overcharges and can make an ETH seller pay ZTO"},{"description":"open(salt, sqrtPriceX96) is callable by anyone and the mined salt is visible in the mempool. A searcher can replay it with a different sqrtPriceX96 ahead of the deployer, so the pool initialises at an arbitrary price and the deployer's planned ZTO-only range may land on the wrong side of the tick. This is the design the brief asks for and no funds are at risk: with zero liquidity any swap of dust against a price limit moves the price for free, so the deployer can correct it before adding liquidity. Reported so the deployment runbook checks slot0 after open() rather than assuming the price it submitted.","line":56,"path":"src/Launcher.sol","reproduction":"Launcher deployed; deployer broadcasts open(salt, P). Attacker sends open(salt, 1<<96) with higher gas. Expected by the deployer: pool at P. Actual: pool at price 1 (tick 0); the deployer's open() reverts AlreadyOpened and a ZTO-only range placed below tick 138180 would sit above the live tick and be ETH-only.","severity":"low","title":"open() is permissionless and the salt is public, so the opening price can be front-run"}],"hash":"2c280a0fcadcd7e9908ab2cc8178175b155f06fc21497aaddb4d051b0faafb70","nodeId":"c8a17269-661e-4170-ac57-08301539767b","outcome":"completed","summary":"Everything is in place and verified. Final recap:\n\n**Delivered** two new suites under `test/`, built on the existing harness without rewriting it. The full delivered suite passes: 56 tests across 5 suites, with `forge build` clean.\n\n- `test/KilnInvariant.t.sol` adds a handler-driven invariant suite over random call sequences against the real v4 PoolManager. The handler drives all four swap shapes by six tier wallets, plus seed, sell, buy, collect, stray piece transfers and withdrawal attempts, with ghost totals. Invariants: reserve is backed exactly by real ZTO, `claims()` mirrors the ERC-6909 balance, ZTO conservation from ghosts, inventory consistency, bid/ask follow the reserve, cut rounding within one wei, and the pool and Launcher stay fixed. It runs with fail-on-revert and passed a scratch run at depth 300 with 19,200 calls and zero reverts.\n- `test/KilnProperties.t.sol` adds fuzz and edge tests: cut equals floor of kilnCut times the ZTO side for fuzzed sizes in every tier and shape, one-wei swaps in each shape, pass follows `tx.origin` not `msg.sender`, borrowed-pass caveat, claim-settled swaps, collect idempotence and that it cannot nest inside an unlock, bid/ask arithmetic, the sell-then-buy round trip never drains the reserve, geometric repeated sells, tier monotonicity, and buy reverting on dust reserve.\n\n**One defect reported rather than tested around**, in `.imd-findings.json` with a self-contained failing proof. In the two cases charged from `beforeSwap`, the Kiln computes the cut on the requested amount before the pool runs. On a partial fill, which v4 allows when the request exceeds in-range liquidity or a price limit stops early, the trader is charged far more than kilnCut of the ZTO that actually traded. For ETH-in exact-output the cut can exceed the whole output, so the trader pays ETH and ZTO. The proof under `test/scratch/PartialFillProof.t.sol` fails on the current code with both shapes and passes if the hook either refuses partial fills or charges on the filled amount. Rated medium. A second low finding notes that permissionless `open()` is front-runnable on price, mitigated since the price can be moved for free before liquidity exists.\n\n**Not covered:** fork runs against the live mainnet PoolManager, ZTO and Pepeolithic contracts are still owed, since this environment and the verifier have no network. The mocks reproduce plain ERC-20 and ERC-721 `transferFrom` behaviour only.","treeHash":"2a4e55da85b2b33bdca1a52e4c034dace3dc20a8","usage":{"cachedInputTokens":4292250,"inputTokens":1282,"model":"claude-fable-5-1","outputTokens":56766,"runtime":"claude","turns":41,"wallClockMs":870519}},{"artifacts":[],"attempt":1,"bundleHash":"91e7446ae95f1827370d4bd8adab3554ee8d04ac0096e7a041bb50fab91f2473","device":"63458a1bd2b2d376","findings":[],"hash":"53eb7f2a76e5a86bce8d0acdbfbbf3a9c08451dd91aa6c6069c73520467f8506","nodeId":"0a94f389-d5bf-4289-bd16-70006fb6a31f","outcome":"completed","summary":"The deliverable is complete and committed on `main` as a86237f. Clean `forge build`, `forge test` (43 passed, 0 failed) and `forge fmt --check` all pass with solc 0.8.26, via-IR, optimizer_runs 1.\n\n**What was built**\n\n- `src/Launcher.sol`: takes `(zto, pepeo, poolManager)`, exposes `initCodeHash()` and `kilnAddress(salt)`, and `open(salt, sqrtPriceX96)` deploys the Kiln with CREATE2, reverts unless the address's low 14 bits are exactly `0xCC` (beforeSwap, afterSwap, both swap return deltas), initializes the ETH/ZTO pool with lpFee 2000 and tickSpacing 60, emits `Opened`, and succeeds once. Adds no liquidity.\n- `src/Kiln.sol`: the hook. Reads `PEPEO.balanceOf(tx.origin)`, applies the 0/1/3/7/12/21 tier table, and takes the cut always in ZTO across the four swap shapes (beforeSwap return delta when ZTO is the specified currency, afterSwap when unspecified), settling by minting ERC-6909 ZTO claims to itself. `collect()` burns and takes into `reserve`. `sell`/`buy`/`seed` with `bid = reserve/50`, `ask = bid × 1.15`, `transferFrom` only, bool checks, custom errors, no owner, no withdrawal path, no receive. Rejects callbacks from other pools that name it as hook. Runtime is 5,797 bytes.\n- Tests run against the real v4 `PoolManager` with the v4-core test routers, a ZTO-only range below the opening tick, and a priming swap by a 21-piece wallet so the ZTO-in cases have ETH to buy. They cover every tier in all four cases (cut equals kilnCut of the ZTO side, tier 21 pays nothing, claims then reserve then real balance, LP fee paid from fee growth), collect no-op, sell/buy/seed success and failure paths, stuck transfers, a fuzzed action sequence for `reserve <= balance`, no withdrawal, and the deployment probe's opcode scan.\n- `README.md` documents the rules, tier table, the four cases, deployment steps with salt mining and the sqrtPrice formula, the ZTO-only range meaning in Uniswap tick terms, operational responsibilities and the caveats (tx.origin, stuck transfers, plus dust and claim sweeping).\n\n**Things to know**\n\n- Vendored as plain files under `lib/` (no submodules): v4-core v4.0.0 `src/` plus `CurrencySettler`, v4-periphery's `HookMiner` test helper, forge-std 1.17.0 and solmate's `Owned.sol`. Commits are listed in `lib/VENDORED.md`. v4-periphery's `BaseHook` was deliberately not used because it validates the hook address in its constructor.\n- Slither was not run. There is no pip or slither in this environment. The two named rules (multiply before divide, no dynamic `encodePacked`) hold by construction, and this is stated in the README.\n- The commit used your email as author since the workspace had no git identity configured.\n- Only the Launcher needs to be in the deployment manifest. The Kiln is created by `open()`, and the manifest step is not written here.","treeHash":"53b5e0c6a4e1ad76862908eba5fdbac85ba4b746","usage":{"cachedInputTokens":4062363,"inputTokens":1026,"model":"claude-fable-5-1","outputTokens":89681,"runtime":"claude","turns":51,"wallClockMs":1251995}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7f1dec5ffcbde1d8","findings":[{"citation":"resolved","description":"Merged from write_foundry_tests, audit_math, audit_permissions and audit_flow/audit_economics (same root cause). In the two cases where ZTO is the specified currency (case 1: ZTO in exact-input, case 4: ETH in exact-output) the cut is computed in beforeSwap from params.amountSpecified and minted as ERC-6909 claims before the pool runs. Uniswap v4 does not revert on a partial fill: Pool.swap stops when sqrtPriceLimitX96 is reached or liquidity runs out, and Hooks.afterSwap then subtracts the full hook delta from the swapper's delta (`swapDelta = swapDelta - hookDelta`) regardless of how much actually traded. afterSwap returns 0 for these cases (line 194) and never reconciles the cut with the realised delta.amount1(). Consequences, all reproduced against the real PoolManager: (a) case 1: the trader pays consumed + 1.30% of the *requested* input; with the pool's single ZTO-only range the ETH available to ZTO sellers is only what earlier ETH buyers deposited, so partial fills are the normal case, not an edge; (b) case 4: swapDelta.amount1 = delivered - cut goes negative when the pool delivers less than the cut, so an ETH->ZTO buyer pays ETH *and* ZTO and receives no ZTO; (c) on an empty pool a ZTO exact-input swap pays 1.3% of the request for nothing. The brief's rule 'the trader is charged kilnCut of the ZTO side' and the Passed.ztoTaken event are both wrong on partial fills. README line 103 documents only case 4 and only as 'may receive less than the specified output'. Routers with correct minimum-output / TAKE_ALL checks revert the worst shapes, which bounds the practical loss to kilnCut of the unfilled fraction inside the slippage tolerance; integrators settling deltas directly (as the v4-core test router does) pay in full. Cases 2 and 3 (afterSwap) are correct because they measure the realised delta. Fix within the brief's structure: in afterSwap, when _ztoIsSpecified(params), compare |delta.amount1()| with the amount the pool was asked to move (|amountSpecified| - cut for exact-input, amountSpecified + cut for exact-output) and revert with a custom error on a mismatch; or recompute the cut from the realised amount and refund the excess (burn claims, credit the trader on the unspecified side). The attached proof accepts either.","line":179,"path":"src/Kiln.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 {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {BalanceDelta, BalanceDeltaLibrary} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {HookMiner} from \"v4-periphery/test/shared/HookMiner.sol\";\nimport {Launcher} from \"src/Launcher.sol\";\nimport {Kiln} from \"src/Kiln.sol\";\n\ncontract ProofZTO {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract ProofPepeo {\n    mapping(uint256 => address) public ownerOf;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 id) external {\n        ownerOf[id] = to;\n        balanceOf[to] += 1;\n    }\n\n    function transferFrom(address f, address t, uint256 id) external {\n        require(ownerOf[id] == f && msg.sender == f);\n        ownerOf[id] = t;\n        balanceOf[f] -= 1;\n        balanceOf[t] += 1;\n    }\n}\n\n/// @notice The Kiln cut must be kilnCut (1.30% for a wallet with no pieces) of the ZTO the pool actually moved.\n///         When a ZTO-specified swap only partially fills, the current code charges 1.30% of the *specified*\n///         amount instead, which can be a multiple of the ZTO actually swapped. A fix may either measure the cut\n///         on the realised amount or reject partial fills; both make these tests pass.\ncontract KilnPartialFillProof is Test {\n    using BalanceDeltaLibrary for BalanceDelta;\n\n    uint160 internal constant FLAGS = Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n    int24 internal constant OPEN_TICK = 138_180;\n    uint256 internal constant CUT_0 = 13_000;\n    uint256 internal constant PIPS = 1_000_000;\n\n    IPoolManager internal manager;\n    ProofZTO internal zto;\n    ProofPepeo internal pepeo;\n    Launcher internal launcher;\n    Kiln internal kiln;\n    PoolKey internal key;\n    PoolSwapTest internal swapRouter;\n    PoolModifyLiquidityTest internal lpRouter;\n    address internal lp = makeAddr(\"lp\");\n    address internal trader = makeAddr(\"trader\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        zto = new ProofZTO();\n        pepeo = new ProofPepeo();\n        launcher = new Launcher(address(zto), address(pepeo), address(manager));\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        uint160 openPrice = TickMath.getSqrtPriceAtTick(OPEN_TICK);\n        (, bytes32 salt) = HookMiner.find(\n            address(launcher), FLAGS, type(Kiln).creationCode, abi.encode(address(zto), address(pepeo), address(manager))\n        );\n        launcher.open(salt, openPrice);\n        kiln = launcher.kiln();\n        key = kiln.poolKey();\n\n        // ZTO-only range below the opening tick, modest size.\n        zto.mint(lp, 1e27);\n        vm.startPrank(lp);\n        zto.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity(\n            key,\n            IPoolManager.ModifyLiquidityParams({\n                tickLower: OPEN_TICK - 6000, tickUpper: OPEN_TICK, liquidityDelta: 1e22, salt: bytes32(0)\n            }),\n            \"\"\n        );\n        vm.stopPrank();\n\n        // Trader with no pieces (tier 0, 1.30%).\n        vm.deal(trader, 10_000 ether);\n        zto.mint(trader, 1e30);\n        vm.prank(trader);\n        zto.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function _swap(bool zeroForOne, int256 amountSpecified, uint256 value)\n        internal\n        returns (bool ok, BalanceDelta delta)\n    {\n        vm.prank(trader, trader);\n        try swapRouter.swap{value: value}(\n            key,\n            IPoolManager.SwapParams({\n                zeroForOne: zeroForOne,\n                amountSpecified: amountSpecified,\n                sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1\n            }),\n            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),\n            \"\"\n        ) returns (BalanceDelta d) {\n            return (true, d);\n        } catch {\n            return (false, BalanceDelta.wrap(0));\n        }\n    }\n\n    function _abs(int128 x) internal pure returns (uint256) {\n        return x < 0 ? uint256(uint128(-x)) : uint256(uint128(x));\n    }\n\n    /// Case 1: ZTO in, exact input, larger than the ETH the range holds. The pool consumes only part of the\n    /// input; the cut must be at most 1.30% of the ZTO the pool consumed (plus 1 wei rounding).\n    function test_case1_ztoInExactIn_partialFill_cutBoundedByRealisedInput() public {\n        // Put 1 ETH into the range with a no-cut wallet.\n        address whale = makeAddr(\"whale\");\n        for (uint256 i = 1; i <= 21; ++i) pepeo.mint(whale, i);\n        vm.deal(whale, 10 ether);\n        vm.prank(whale, whale);\n        swapRouter.swap{value: 1 ether}(\n            key,\n            IPoolManager.SwapParams({\n                zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n            }),\n            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),\n            \"\"\n        );\n        assertEq(kiln.claims(), 0);\n\n        uint256 claimsBefore = kiln.claims();\n        (bool ok, BalanceDelta delta) = _swap(false, -int256(100_000_000e18), 0);\n        if (!ok) return; // a fix that rejects partial fills is acceptable\n        uint256 cut = kiln.claims() - claimsBefore;\n        uint256 poolZtoIn = _abs(delta.amount1()) - cut; // ZTO the pool actually consumed\n        assertLe(cut, poolZtoIn * CUT_0 / PIPS + 1, \"cut exceeds 1.30% of the ZTO the pool consumed\");\n    }\n\n    /// Case 4: ETH in, exact ZTO output, far larger than the ZTO the range holds. The pool delivers only part of\n    /// the output; the cut must be at most 1.30% of the ZTO the pool delivered, and the trader must not end up\n    /// paying ZTO on a swap that was supposed to deliver ZTO.\n    function test_case4_ethInExactOut_partialFill_cutBoundedByRealisedOutput() public {\n        uint256 poolZto = zto.balanceOf(address(manager));\n        uint256 claimsBefore = kiln.claims();\n        (bool ok, BalanceDelta delta) = _swap(true, int256(poolZto * 200), 5000 ether);\n        if (!ok) return; // a fix that rejects partial fills is acceptable\n        uint256 cut = kiln.claims() - claimsBefore;\n        assertGe(delta.amount1(), 0, \"trader paid ZTO on an exact-output ZTO swap\");\n        uint256 poolZtoOut = uint256(uint128(delta.amount1())) + cut; // ZTO the pool actually delivered\n        assertLe(cut, poolZtoOut * CUT_0 / PIPS + 1, \"cut exceeds 1.30% of the ZTO the pool delivered\");\n    }\n}","reproduction":"Real PoolManager, Launcher.open at tick 138180, ZTO-only range [132180,138180] liquidity 1e22 (~2.594e24 wei ZTO), tier-0 trader (0 pieces), PoolSwapTest with msg.sender = tx.origin = trader. Case 1: a 21-piece wallet swaps 1 ETH in (no cut) so the range holds 1 ETH; trader swaps zeroForOne=false, amountSpecified=-100_000_000e18, limit MAX_SQRT_PRICE-1. Expected: cut <= 1.30% of the ZTO the pool consumed (910_818.7e18 -> 11_840.6e18), or a revert. Actual: claims() = 1_300_000e18 (143% of the ZTO swapped); trader's ZTO delta -2_210_818.7e18 for +0.998 ETH. Case 4: fresh fixture, trader swaps zeroForOne=true, amountSpecified=+(200 x pool ZTO) = 518_807_686e18 with 5000 ETH value. Expected: delta.amount1() >= 0 and cut <= 1.30% of the 2_594_038e18 delivered. Actual: delta = (-3.502 ETH, -4_150_461e18 ZTO), claims() = 6_744_499e18: the ETH-in trader paid ZTO and received none. Case (c) from audit_economics also reproduced by trace: empty pool, ZTO exact-in 1_000_000e18 -> delta (0, -13_000e18). The attached proof fails on this tree: 'cut exceeds 1.30% of the ZTO the pool consumed: 1300000000000000000000000 > 11840643311917444107791' and 'trader paid ZTO on an exact-output ZTO swap: -4150461490747872250558682 < 0'.","severity":"medium","snippet":"        uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n        uint256 cut = _takeCut(amount);\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(cut.toInt128(), 0), 0);","title":"beforeSwap charges kilnCut on amountSpecified, so a partially filled ZTO-specified swap is overcharged and an ETH-in exact-output trader can end up paying ZTO"},{"citation":"resolved","description":"Merged from audit_permissions, audit_economics, audit_flow and write_foundry_tests (same root cause: open()'s inputs are public and unauthenticated, and the pool key is predictable before the Kiln exists). The Kiln address is a pure function of (launcher, salt, initCodeHash()); both initCodeHash() and kilnAddress(salt) are public views and the salt appears in the mempool with the deployer's open() transaction. PoolManager.initialize (lib/v4-core/src/PoolManager.sol:116-140) validates only the hook address bits and calls the hook only when it carries BEFORE_INITIALIZE/AFTER_INITIALIZE bits (Hooks.beforeInitialize: `if (self.hasPermission(BEFORE_INITIALIZE_FLAG))`); the Kiln carries only 0xCC, so initialize succeeds for a hook address with no code. Primary variant: a griefer calls PoolManager.initialize({ETH, ZTO, 2000, 60, kilnAddress(salt)}, anyPrice) first; the deployer's open() deploys the Kiln, passes the bit check, then reverts PoolAlreadyInitialized at this line, the CREATE2 is rolled back and launcher.kiln() stays zero. open(salt, *) can never succeed for that salt; the griefer pays ~60k gas per attempt while each failed open() costs the deployer a full Kiln deployment, and every new mined salt can be griefed the same way as long as open() is sent through the public mempool. Secondary variant (lower impact): anyone can call open(salt, otherPrice) first, which succeeds at the attacker's price and the deployer's call reverts AlreadyOpened; this is recoverable because an empty pool's price can be moved to the intended value with a dust swap (reproduced: a 1000-wei oneForZero exact-input swap with sqrtPriceLimitX96 = intended price moved slot0 from MIN_SQRT_PRICE+1 to tick 138180 with delta (0, -13)). Minimal fix that keeps open() permissionless: read slot0 of the pool id via StateLibrary.getSlot0 (extsload) and call initialize only when sqrtPriceX96 == 0, emitting Opened either way; the key's fee, tickSpacing and hook are fixed so only the price can differ, and the README should instruct the deployer to verify slot0 before adding the ZTO-only range (a range placed on the wrong side of the live tick would demand ETH instead of ZTO). Operationally, until fixed, send open() through a private relay.","line":64,"path":"src/Launcher.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\n\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 {PoolId, PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {HookMiner} from \"v4-periphery/test/shared/HookMiner.sol\";\n\nimport {Launcher} from \"src/Launcher.sol\";\nimport {Kiln} from \"src/Kiln.sol\";\n\ncontract ProofZTO {\n    string public constant name = \"ZTO\";\n    string public constant symbol = \"ZTO\";\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract ProofPepeo {\n    mapping(uint256 => address) public ownerOf;\n    mapping(address => uint256) public balanceOf;\n}\n\n/// @notice Launcher.open() is permissionless and its salt is public once the open() transaction is in the mempool.\n///         PoolManager.initialize() makes no call to a hook whose address lacks the initialize bits, so anyone can\n///         initialize the ETH/ZTO pool key for the predicted Kiln address BEFORE the Kiln exists. From then on\n///         open(salt, ...) reverts with PoolAlreadyInitialized for that salt, forever, and the griefer can repeat\n///         this for every new salt the deployer mines. Fails now; passes once open() tolerates a pre-initialized\n///         pool (e.g. read slot0 and skip initialize when sqrtPriceX96 != 0) or otherwise cannot be blocked.\ncontract Proof1_OpenFrontRunTest is Test {\n    using PoolIdLibrary for PoolKey;\n    using StateLibrary for IPoolManager;\n\n    uint160 internal constant FLAGS = Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    function test_openCannotBeBlockedByPreInitializingThePoolKey() public {\n        IPoolManager manager = new PoolManager(address(this));\n        ProofZTO zto = new ProofZTO();\n        ProofPepeo pepeo = new ProofPepeo();\n        Launcher launcher = new Launcher(address(zto), address(pepeo), address(manager));\n        uint160 openPrice = TickMath.getSqrtPriceAtTick(138_180);\n\n        // The deployer mines a salt and broadcasts open(salt, openPrice).\n        (address predicted, bytes32 salt) = HookMiner.find(\n            address(launcher), FLAGS, type(Kiln).creationCode, abi.encode(address(zto), address(pepeo), address(manager))\n        );\n        assertEq(predicted.code.length, 0, \"Kiln does not exist yet\");\n\n        // A griefer sees it in the mempool and initializes the exact pool key first. No hook call happens because\n        // the predicted address has no initialize permission bits.\n        PoolKey memory key = PoolKey({\n            currency0: Currency.wrap(address(0)),\n            currency1: Currency.wrap(address(zto)),\n            fee: 2000,\n            tickSpacing: 60,\n            hooks: IHooks(predicted)\n        });\n        vm.prank(makeAddr(\"griefer\"));\n        manager.initialize(key, TickMath.getSqrtPriceAtTick(0));\n\n        // The deployer's open() must still succeed: deploy the Kiln at the mined address and report the pool.\n        (address kilnAddr, PoolId poolId) = launcher.open(salt, openPrice);\n        assertEq(kilnAddr, predicted, \"Kiln deployed at the mined address\");\n        assertEq(address(launcher.kiln()), predicted, \"launcher records the Kiln\");\n        assertEq(PoolId.unwrap(poolId), PoolId.unwrap(key.toId()), \"pool id\");\n        (uint160 sqrtPriceX96,,, uint24 lpFee) = manager.getSlot0(poolId);\n        assertGt(sqrtPriceX96, 0, \"pool initialized\");\n        assertEq(lpFee, 2000);\n    }\n}","reproduction":"State: Launcher(zto, pepeo, poolManager) deployed, no Kiln yet, real PoolManager. 1) Deployer mines salt S with kilnAddress(S) & 0x3FFF == 0xCC and broadcasts open(S, 79228162514264337593543950336000). 2) Griefer (any EOA) sends PoolManager.initialize(PoolKey(address(0), ZTO, 2000, 60, launcher.kilnAddress(S)), TickMath.getSqrtPriceAtTick(0)) first: succeeds, no hook call, predicted address has no code. 3) Deployer's open(S, price): CREATE2 succeeds, bit check passes, initialize reverts PoolAlreadyInitialized(); whole call reverts; launcher.kiln() == address(0). 4) Any later open(S, *) reverts identically. Expected: open() succeeds once and emits Opened. Actual: open() can never succeed for S. The attached proof (and the audit_permissions proof, run as well) fails on this tree with 'PoolAlreadyInitialized()'.","severity":"medium","snippet":"        IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);","title":"Launcher.open() can be blocked by anyone who pre-initializes the pool key for the predicted Kiln address; the salt and opening price are also not bound to the deployer"},{"citation":"resolved","description":"Merged from audit_permissions and audit_economics. ask() and bid() read `reserve` only, as the brief requires, but buy() (line 241) and sell() (line 226) call collect() first, which folds the Kiln's whole pending ERC-6909 ZTO claim balance into reserve before the price is computed. Whenever claims() > 0, which after launch is almost always (every cut-paying swap adds to it), the price buy() charges is strictly above the ask() the contract quoted a moment earlier, and the divergence is unbounded relative to the quote (any third party can push claims up with a swap, or reserve up with seed(), between quote and execution). A buyer who approves exactly ask() has buy() revert on allowance; a buyer with an open allowance (Permit2/max pattern) pays the higher amount with no maxPrice parameter to cap it. For sell() the divergence favours the seller, but sellers also have no minPrice, so a competing sell() landing first lowers the next bid by 2% with no protection. The deviation always favours the reserve, so no funds are extracted from the Kiln; the defect is that the only price oracle the contract exposes does not describe what the write functions do, contrary to 'buy() charges ask()' / 'sell() pays bid()'. Fix keeping the brief's ABI: add quote views that include POOL_MANAGER.balanceOf(this, ztoId) (e.g. quoteBid()/quoteAsk()), and/or overloads buy(id, maxPrice) / sell(id, minPrice); at minimum document that the executed price includes pending claims.","line":244,"path":"src/Kiln.sol","reproduction":"test/scratch/Review.t.sol::test_quoteVsExecution on this tree (real PoolManager, ZTO-only range 1e24 liquidity, OZ-style PEPEO mock): seller calls seed(5000e18) then sell(1) -> reserve 4900e18, kiln.ask() == 112.7e18. A 0-piece trader swaps 1 ETH in -> claims() == 12_984.39e18, kiln.ask() still returns 112.7e18. Buyer b2 approves exactly 112.7e18 and calls buy(1): reverts. Buyer with max allowance calls buy(1): pays 411.34e18 ZTO (= (4900e18 + 12_984.39e18) / 50 * 11500 / 10000), 3.65x the quote. Expected: the executed price equals the quoted ask or the buyer can bound it.","severity":"low","snippet":"        uint256 price = ask();","title":"buy()/sell() execute at a post-collect() price different from the ask()/bid() they quote, and neither takes a caller-side price bound"},{"citation":"resolved","description":"From audit_permissions; verified against the live contract. The Pepeolithic source verified on Sourcify (chain 1, 0x765956a7307222346b08fff681820a5d77e92028) inherits OpenZeppelin v5 ERC721, whose balanceOf does `if (owner == address(0)) revert ERC721InvalidOwner(address(0))`. _takeCut reads PEPEO.balanceOf(tx.origin) on every cut-bearing swap with no guard. In a mined transaction tx.origin is never zero, but eth_call / debug simulation without a `from` field runs with tx.origin == 0 (the default for many quoters, aggregators and indexers calling V4Quoter or simulating the Universal Router), so every swap simulation through this pool reverts inside the hook. The project's MockPepeo returns 0 for address(0), which is why the suite does not see it. No funds at risk; impact is lost volume / broken quoting integrations. Fix: treat tx.origin == address(0) as zero pieces, or wrap the balance read in try/catch defaulting to 0.","line":303,"path":"src/Kiln.sol","reproduction":"test/scratch/Review.t.sol::test_zeroOriginReverts on this tree, with a PEPEO stand-in that has OZ v5's exact zero-address revert: swapRouter.swap(zeroForOne=true, amountSpecified=-1 ether) with vm.prank(trader, trader) succeeds; the identical call with vm.prank(trader, address(0)) (tx.origin = 0, as in a from-less eth_call) reverts, bubbled from Kiln._takeCut -> PEPEO.balanceOf(0). Expected: the simulation returns the swap delta; actual: revert.","severity":"low","snippet":"        uint256 pepes = PEPEO.balanceOf(tx.origin);","title":"Every swap reverts when tx.origin is address(0): from-less eth_call simulations against the live OpenZeppelin ERC-721 fail"},{"citation":"resolved","description":"Merged from audit_flow and audit_economics. collect() settles claims through POOL_MANAGER.unlock, which reverts AlreadyUnlocked when the manager is already unlocked (PoolManager.sol:104). sell() and buy() call collect() unconditionally first, so a contract that tries to buy or sell a piece from inside its own unlockCallback (swap and trade a piece atomically) reverts whenever at least one cut-paying swap happened since the last collect(), and succeeds otherwise. Not a loss and not contrary to the brief, but the behaviour is state-dependent and undocumented. Worth a README line, or skipping collect() in sell()/buy() when the manager is already unlocked (the next external call collects).","line":221,"path":"src/Kiln.sol","reproduction":"test/scratch/Review.t.sol::test_collectInsideUnlock on this tree: contract C whose unlockCallback calls kiln.collect(); C calls manager.unlock(''). With claims() == 0 the call succeeds. After a 0-piece wallet swaps 1 ETH in (claims() > 0) the same call reverts (AlreadyUnlocked from the nested unlock). Same for kiln.sell(id)/kiln.buy(id) inside the callback.","severity":"info","snippet":"        POOL_MANAGER.unlock(abi.encode(amount));","title":"collect(), and therefore sell() and buy(), revert inside another PoolManager unlock whenever claims() > 0"},{"citation":"resolved","description":"From audit_permissions; verified against the Sourcify-verified Pepeolithic source: `sweep(uint256 c, uint256 max)` mints every unsold sale piece of a closed cave to `admin` at no cost (`_mintPiece(admin, id)`), and the contract has `admin` and `adam` addresses. The Kiln buys from anyone at reserve/50 with no per-seller limit, so an actor who obtains pieces at zero cost converts them into the traders' accumulated ZTO cuts: k pieces drain 1 - 0.98^k of the reserve (50 pieces: 63.6%, 100 pieces: 86.7%). This is exactly the brief's 'buys pieces from anyone' design and the admin is not a Kiln role, so it is recorded as a trust assumption for the README rather than a code defect.","line":225,"path":"src/Kiln.sol","reproduction":"After a cave closes on mainnet: admin calls Pepeolithic.sweep(c, max), receiving unsold ids at no cost, calls setApprovalForAll(kiln, true) and Kiln.sell(id) repeatedly. Each call pays reserve/50 and lowers the next bid by 2%; with reserve R and 100 swept pieces the admin receives 0.867 R ZTO funded by swap cuts. No guard in sell() distinguishes this seller, as designed.","severity":"info","snippet":"    function sell(uint256 id) external {","title":"Trust assumption to document: Pepeolithic's admin receives unsold pieces for free via sweep() and can sell them into the reserve at bid()"}],"hash":"55e64b20f1a65270eba5acc5a47dda49e70f3c416f09ba2b8890ba7d61213164","nodeId":"03d3ea92-8b69-42ee-8392-bde504239bf9","outcome":"completed","summary":"Review complete. The findings file at `.imd-findings.json` holds 6 findings and an 11-row coverage record, and I changed nothing outside `test/scratch/`.\n\n**Kept findings** (all reproduced against the real v4 PoolManager on this tree):\n\n1. **Medium, Kiln.sol:179.** In the two beforeSwap cases the cut is kilnCut of `amountSpecified`, never reconciled with the realised fill. A partially filled ZTO exact-input swap paid 143% of the ZTO actually traded, and an ETH-in exact-output swap left the trader paying ETH and ZTO and receiving nothing. Merged from five specialist reports. Proof attached: fails here, passes in a scratch copy patched to revert on partial fills.\n2. **Medium, Launcher.sol:64.** Anyone can call PoolManager.initialize on the predicted Kiln's pool key before open() runs, since the hook has no initialize bits and needs no code. open() then reverts PoolAlreadyInitialized forever for that salt. The price front-run variant is folded in as recoverable with a dust swap. Merged from four reports. Proof attached: fails here, passes once open() skips initialize when slot0 is set.\n3. **Low, Kiln.sol:244.** buy() and sell() price after collect(), so the executed price diverges from ask()/bid(). A buyer approving the quoted 112.7 ZTO reverted while one with an open allowance paid 411.3 ZTO. No price bounds exist.\n4. **Low, Kiln.sol:303.** The live Pepeolithic is OpenZeppelin v5 and reverts balanceOf(0), so every from-less eth_call simulation of a swap reverts. I confirmed this against the Sourcify-verified source.\n5. **Info.** collect(), and so sell() and buy(), revert inside another unlock whenever claims exist.\n6. **Info.** Pepeolithic's admin receives unsold pieces free via sweep() and can sell them into the reserve. Verified in the live source, recorded as a trust assumption.\n\n**Dropped:** the rate-base asymmetry between shapes and the rounding note, both consequences of the brief's own mechanism and already documented in the README table. Slither and Aderyn reentrancy, unchecked-return and initializer leads did not reproduce as defects: state is written before external calls, token addresses are fixed, and the flagged function is a view.\n\n**Coverage:** all 8 entry points answered, plus the reserve-balance invariant, the no-withdrawal invariant and the size/opcode checks, all holding. The project's own 56 tests pass on this tree.","treeHash":null,"usage":{"cachedInputTokens":981693,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":26711,"runtime":"claude","turns":25,"wallClockMs":501053}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e51ef2afd7c2af8","findings":[{"citation":"resolved","description":"In the two shapes where ZTO is the specified currency (ZTO-in exact-input, ETH-in exact-output) the cut is computed in beforeSwap from params.amountSpecified, i.e. the amount the trader asked for, and minted as claims before the pool runs. Uniswap v4 stops a swap when sqrtPriceLimitX96 is reached or liquidity runs out and returns a partial fill (Pool.swap loop: `while (!(amountSpecifiedRemaining == 0 || result.sqrtPriceX96 == params.sqrtPriceLimitX96))`). The cut is never reconciled against the amount actually swapped, so (a) ZTO-in exact-input: the trader pays actualPoolInput + cut where cut = 1.3% of the full request, which can be a multiple of the actual trade; (b) ETH-in exact-output: swapDelta.amount1 = poolOutput - cut, which goes negative when the pool delivers less than the cut, so a trader who sent ETH to buy ZTO also sends ZTO and receives nothing. The afterSwap shapes are correct because they read the realised delta. The invariant the brief states (\"the trader is charged kilnCut of the ZTO side\") and the Passed.ztoTaken event are both wrong on partial fills. README line 103 documents only the exact-output half and says the trader \"may receive less\"; it does not cover the exact-input ZTO shape nor the negative-output case. Seam: boundary (partial fill) x invariant (cut == kilnCut of ZTO side). Fix: in afterSwap, when _ztoIsSpecified(params), compare |delta.amount1()| with the expected full fill (|amountSpecified| - cut for exact-input, amountSpecified + cut for exact-output) and revert on a mismatch, or recompute the cut from the realised delta and refund the difference (burn claims and transfer the excess as ERC-6909 to the trader). Reverting is the smaller change and is what the attached proof accepts.","line":179,"path":"src/Kiln.sol","reproduction":"Fixture as in test/KilnBase.t.sol (ZTO-only range below tick 138180, primed with 20 ETH). Trader with 0 pieces (tier 0, 1.30%).\nCase A: swap zeroForOne=false, amountSpecified=-1_000_000e18, sqrtPriceLimitX96 = current sqrtPrice * 100001/100000 (a fraction of a tick). Expected: cut <= 1.3% of the ZTO the trader actually paid. Actual (proof run): trader paid 22,832 ZTO of which the cut was 13,000 ZTO (1.3% of the 1,000,000 requested; 57% of the actual trade). Assertion: 13000e18 > 296.8e18.\nCase B: swap zeroForOne=true, amountSpecified=+1_000_000e18, sqrtPriceLimitX96 = current sqrtPrice * 99999/100000, 200 ETH sent. Expected: trader receives >= 0 ZTO. Actual: pool delivered ~9,813 ZTO, cut 13,000 ZTO, trader's ZTO balance fell by 3,187 ZTO (delta.amount1 = -3187e18) while also paying ETH. Case C (no custom limit): same fixture, zeroForOne=true, amountSpecified=+300_000_000e18 (more ZTO than the range holds), sqrtPriceLimitX96 = MIN_SQRT_PRICE+1. Verified: the swap succeeds, the trader receives 235,899,564e18 ZTO for 330.22 ETH, the cut taken is 3,900,000e18 (1.3% of the request) where 1.3% of the ZTO received is 3,066,694e18: an overcharge of 833,306e18 ZTO with default router limits. The attached proof was also run against a copy of Kiln patched to revert in afterSwap when |delta.amount1()| differs from the expected full fill: both tests pass there, and fail on the current code.","severity":"medium","snippet":"        uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n        uint256 cut = _takeCut(amount);\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(cut.toInt128(), 0), 0);","title":"beforeSwap charges kilnCut of the requested amount, so a partially filled swap is overcharged and an exact-output ETH-in trader can end up paying ZTO"},{"citation":"resolved","description":"Shapes 1 and 4 (beforeSwap) compute the cut on what the trader pays/receives gross of the cut: ZTO-in exact-input charges 1.3% of X where the pool only receives X - cut; ETH-in exact-output charges 1.3% of Y where the pool outputs Y + cut. Shapes 2 and 3 (afterSwap, snippet) compute it on the pool's realised ZTO delta, which is net of the cut: ZTO-in exact-output charges 1.3% of the pool input W and the trader pays W + cut; ETH-in exact-input charges 1.3% of the pool output Z and the trader receives Z - cut. So the same economic trade is charged rate/(1) of gross in one shape and rate/(1+rate) or rate/(1-rate) of gross in the paired shape. Two formulas that should agree (invariant) use different bases (precision/scale). Not exploitable for profit, but the brief's single rule (\"kilnCut of the ZTO side\") is realised as two different rates, and the existing tests encode the discrepancy per case rather than detect it. Fix: pick one base (recommend gross, i.e. what the trader pays or receives before the cut) and in the afterSwap shapes solve for it: cut = amount * rate / (PIPS - rate) for ZTO-in exact-output and cut = amount * rate / PIPS for ETH-in exact-input is already gross-of-output; document the chosen base in the README.","line":196,"path":"src/Kiln.sol","reproduction":"Two tier-0 traders each sell ZTO for ETH with the pool at the same state. Trader A: ZTO-in exact-input X = 1,000,000e18 -> cut = 13,000e18, pool receives 987,000e18, cut/gross = 1.300%. Trader B: ZTO-in exact-output sized so the pool input is W = 987,000e18 -> cut = 987,000e18 * 13000 / 1e6 = 12,831e18, trader pays 999,831e18, cut/gross = 1.283%. Same pool input, same ETH out, but A pays 169e18 ZTO more cut than B. Mirror on the ETH side: ETH-in exact-input with pool output Z = 1,000,000e18 charges 13,000e18 (1.317% of the 987,000e18 the trader keeps); ETH-in exact-output for Y = 987,000e18 charges 12,831e18.","severity":"low","snippet":"        int128 amount1 = delta.amount1();\n        uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n        uint256 cut = _takeCut(amount);","title":"The cut is measured on the trader-facing amount in the beforeSwap shapes but on the pool-facing amount in the afterSwap shapes, so the effective rate differs by shape"},{"citation":"resolved","description":"cut = ztoAmount * kilnCut / PIPS truncates, so each swap under-collects by up to 1 wei of ZTO and any ZTO side below PIPS/kilnCut (77 wei at tier 0, 400 wei at tier 4) pays no cut at all. ask() = bid() * 11500 / 10000 likewise truncates by up to 1 wei. Multiplication precedes division in both, there is no overflow for any amount v4 accepts (int128), and the loss is bounded by 1 wei per call and cannot accumulate into anything material with 18 decimals. Recorded for completeness of the precision pass; no action needed beyond optionally rounding the fee up.","line":305,"path":"src/Kiln.sol","reproduction":"ZTO-in exact-input with amountSpecified = -76 (wei) by a tier-0 wallet: cut = 76 * 13000 / 1e6 = 0, Passed(…, 13000, 0), no claims minted. reserve = 149 wei: bid = 2, ask = 2 * 11500 / 10000 = 2 (exact would be 2.3).","severity":"info","snippet":"        cut = ztoAmount * kilnCut / PIPS;","title":"Cut and ask round down (fee rounds in the trader's favour); cut is zero below 77 wei of ZTO"}],"hash":"6d3c60abca01e66878add1d23ec5fd085f4b466263724f1bc425cf67ada325a9","nodeId":"37d85b55-0ad7-4c69-bd11-267e276719fc","outcome":"completed","summary":"The review is complete. The findings file is at the repository root with three findings and a coverage row for every listed entry point, and no tracked file was changed.\n\n**Main finding (medium, with proof).** In the two swap shapes where ZTO is the specified currency, `beforeSwap` computes the Kiln cut from the requested amount and never reconciles it with what the pool actually filled. Uniswap v4 returns partial fills when the price limit is hit or liquidity runs out, so the cut becomes a multiple of the real trade. The proof test at `test/scratch/KilnPartialFill.t.sol` fails on the current code in both shapes and passes on a copy patched to revert on partial fills.\n\n| Case | Request | Actual ZTO moved | Cut charged | Correct cut |\n|---|---|---|---|---|\n| ZTO in, exact in, tight limit | 1,000,000 | 22,832 paid | 13,000 | 297 |\n| ETH in, exact out, tight limit | 1,000,000 | trader net pays 3,187 | 13,000 | 0 |\n| ETH in, exact out, default limit, range exhausted | 300,000,000 | 235,899,564 received | 3,900,000 | 3,066,694 |\n\nIn the second row the trader sent ETH to buy ZTO and ended the swap having also sent ZTO. The README caveat covers only the exact-output shape and says the trader \"may receive less\". The `afterSwap` shapes are correct because they read the realised delta.\n\n**Other findings.**\n- **Low.** The cut base differs by shape. The `beforeSwap` shapes charge the rate on the trader-facing gross amount, the `afterSwap` shapes on the pool-facing net amount, so the same trade pays 1.300% or 1.283% of gross depending on exact-in versus exact-out.\n- **Info.** The cut and the ask both round down by at most 1 wei, and any ZTO side under 77 wei pays no cut. Immaterial at 18 decimals.\n\n**What held.** Bid and ask arithmetic multiplies before dividing and cannot overflow or underflow. The reserve-never-exceeds-balance and claims-match-ERC-6909-balance invariants hold through every path. Inventory swap-and-pop handles the last element correctly. The Launcher's mask check is exact because `&` binds tighter than `!=` in Solidity. The Kiln runtime is 5,797 bytes. The Slither and Aderyn reentrancy leads on `sell`, `buy` and `collect` did not reproduce: state is written before the external calls and the ERC-721 path uses plain `transferFrom`.\n\n**Not reached.** I did not model behaviour of the live Pepeolithic or ZTO contracts beyond the brief's \"plain\" assumption, and I did not assess the permissionless `open()` price choice, which is outside the math area.","treeHash":null,"usage":{"cachedInputTokens":1190890,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":39390,"runtime":"claude","turns":26,"wallClockMs":628769}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b5e3297a04468fd3","findings":[{"citation":"resolved","description":"open() must succeed once, but its second step is a plain PoolManager.initialize for the key {ETH, ZTO, 2000, 60, hooks = kiln}. The Kiln address is a pure function of (launcher, salt, initCodeHash()) and both initCodeHash() and kilnAddress(salt) are public views. The Kiln carries no beforeInitialize/afterInitialize flag, so PoolManager.initialize never calls the hook and does not require code at the hook address (Hooks.beforeInitialize only calls when the flag bit is set). An unprivileged actor who sees open(salt, price) in the mempool can therefore call PoolManager.initialize(sameKey, anyPrice) first; it succeeds, and the deployer's open() then reverts with Pool.PoolAlreadyInitialized at this line, after the CREATE2 is rolled back. The Launcher has no way to adopt an already-initialized pool, so that salt is burned and every public retry can be griefed the same way; the guarantee 'open() succeeds once' can be denied for as long as the attacker keeps paying initialize gas. The pre-initialized pool is otherwise harmless (swaps on it revert with InvalidHookResponse because the hook address has no code). Mitigations: submit open() through a private relay, or make open() tolerant of an existing pool (read slot0 via extsload/StateLibrary and skip initialize when sqrtPriceX96 != 0; the pool's fee/tickSpacing/hook are fixed by the key so only the price can differ, and with zero liquidity the price can be moved to the intended value by a 1 wei swap, verified in test/scratch/EmptyPoolPriceMove.t.sol). Related and lower impact: because open() itself is permissionless, anyone can also call open(ownSalt, ownPrice) before the deployer; that succeeds at an attacker-chosen price and Kiln address but is recoverable the same way (zero-liquidity swap) and is within the brief's 'permissionless' wording, so it is noted here rather than reported separately.","line":64,"path":"src/Launcher.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 {PoolId, PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {Launcher} from \"src/Launcher.sol\";\nimport {Kiln} from \"src/Kiln.sol\";\n\ncontract DummyERC20 {\n    function balanceOf(address) external pure returns (uint256) {\n        return 0;\n    }\n}\n\ncontract DummyERC721 {\n    function balanceOf(address) external pure returns (uint256) {\n        return 0;\n    }\n}\n\n/// @notice Launcher.open() can be blocked by anyone who initializes the ETH/ZTO pool with the predicted Kiln\n///         address as hook before open() runs. The Kiln has no initialize flags, so the PoolManager never calls\n///         the (not yet existing) hook and the pre-initialization succeeds; open() then reverts with\n///         PoolAlreadyInitialized for that salt. Fails on the current code; passes once open() tolerates an\n///         already-initialized pool (or otherwise cannot be blocked by a front-runner).\ncontract OpenFrontRunTest is Test {\n    using PoolIdLibrary for PoolKey;\n    using StateLibrary for IPoolManager;\n\n    uint160 internal constant FLAGS = Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    IPoolManager internal manager;\n    address internal zto;\n    address internal pepeo;\n    Launcher internal launcher;\n    address internal attacker = makeAddr(\"attacker\");\n    address internal deployer = makeAddr(\"deployer\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        zto = address(new DummyERC20());\n        pepeo = address(new DummyERC721());\n        launcher = new Launcher(zto, pepeo, address(manager));\n    }\n\n    function _mineSalt() internal view returns (bytes32 salt, address predicted) {\n        for (uint256 s;; ++s) {\n            predicted = launcher.kilnAddress(bytes32(s));\n            if (uint160(predicted) & Hooks.ALL_HOOK_MASK == FLAGS) return (bytes32(s), predicted);\n        }\n    }\n\n    function test_openIsBlockedByPreInitializedPool() public {\n        // The deployer mines a salt off-chain and broadcasts open(salt, price). The attacker sees the salt in the\n        // mempool, derives the Kiln address (initCodeHash() and kilnAddress() are public) and initializes the pool\n        // first, at a price of their choosing.\n        (bytes32 salt, address predicted) = _mineSalt();\n        PoolKey memory key = PoolKey({\n            currency0: Currency.wrap(address(0)),\n            currency1: Currency.wrap(zto),\n            fee: 2000,\n            tickSpacing: 60,\n            hooks: IHooks(predicted)\n        });\n        uint160 attackerPrice = TickMath.getSqrtPriceAtTick(-200_000);\n        vm.prank(attacker);\n        manager.initialize(key, attackerPrice); // succeeds: no code at `predicted`, no initialize hook flags\n        assertEq(predicted.code.length, 0, \"hook does not exist yet\");\n\n        // The deployer's open() must still succeed: the Kiln is deployed at the mined address, the pool exists\n        // with the Kiln as its hook, and the Launcher records it. On the current code this reverts with\n        // Pool.PoolAlreadyInitialized, so the deployer's salt is burned and every retry can be griefed the same way.\n        uint160 openPrice = TickMath.getSqrtPriceAtTick(138_180);\n        vm.prank(deployer);\n        (address kilnAddr,) = launcher.open(salt, openPrice);\n\n        assertEq(kilnAddr, predicted, \"kiln at the mined address\");\n        assertEq(address(launcher.kiln()), predicted, \"launcher records the kiln\");\n        (uint160 sqrtPriceX96,,, uint24 lpFee) = manager.getSlot0(key.toId());\n        assertGt(sqrtPriceX96, 0, \"pool exists\");\n        assertEq(lpFee, 2000);\n    }\n}","reproduction":"State: Launcher deployed, open() not yet called, real PoolManager. 1) Deployer mines salt S with kilnAddress(S) & 0x3FFF == 0xCC and broadcasts open(S, 79228162514264337593543950336000). 2) Attacker (any EOA) front-runs with PoolManager.initialize(PoolKey{currency0: 0x0, currency1: ZTO, fee: 2000, tickSpacing: 60, hooks: kilnAddress(S)}, any valid price, e.g. TickMath.getSqrtPriceAtTick(-200000)). Expected: either the attacker's call fails or the deployer's open() still completes. Actual: the attacker's initialize succeeds (no hook code required, no initialize flags), and open(S, ...) reverts with PoolAlreadyInitialized(); launcher.kiln() stays zero. The proof test reproduces exactly this and asserts open() succeeds afterwards; it fails on the current code with PoolAlreadyInitialized().","severity":"medium","snippet":"        IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);","title":"Launcher.open() can be blocked by anyone who pre-initializes the pool at the predicted Kiln address"},{"citation":"resolved","description":"The two branches that take the cut are not economically symmetric. afterSwap (ETH-in exact-input, ZTO-in exact-output) measures kilnCut on delta.amount1(), the ZTO the pool actually moved. beforeSwap (ZTO-in exact-input, ETH-in exact-output) measures it on params.amountSpecified, which in Uniswap v4 is only an upper bound: a swap stops early when it reaches sqrtPriceLimitX96 (or runs out of liquidity) and the pool then consumes/delivers less than specified. The hook's specified-currency delta is credited in full regardless of how much the pool executed, so a partially filled ZTO exact-input swap pays 1.3% of what was offered, not of what was swapped; in the reproduction that is 26% of the ZTO actually traded. The mirror case (ETH-in exact-output hitting the limit) is mentioned in the README; this ZTO-in case is not, and in both the charge can exceed the trade itself (for ETH-in exact-output with a limit that stops the pool at zero output, swapDelta.amount1 becomes negative: the trader pays ZTO on a swap that was supposed to buy ZTO). The loss is self-inflicted through the trader's own price limit and bounded by kilnCut of amountSpecified, hence low; routers that implement slippage via sqrtPriceLimitX96 rather than minimum-output checks would hit it routinely on a thin pool. Fix: in beforeSwap take the cut in afterSwap too for exact-input ZTO (measure on the actual specified-side delta), or cap the beforeSwap cut by the executed amount, and document the residual case.","line":179,"path":"src/Kiln.sol","reproduction":"Pool with 1e24 two-sided liquidity at tick 138180 (test/scratch/PartialFillCut.t.sol). Trader with 0 PEPEO calls swap(zeroForOne=false, amountSpecified=-1_000_000e18, sqrtPriceLimitX96 = sqrtPrice(currentTick+1)). Expected: cut = 1.3% of the ZTO the pool consumed. Actual: pool consumes 50,144.02 ZTO, trader receives 0.04995 ETH, claims() = 13,000 ZTO (1.3% of the 1,000,000 specified) and the trader's ZTO balance falls by 63,144 ZTO. The same shape with ETH in (zeroForOne=true, amountSpecified=-1000 ether, limit one tick below) charges 650.5 ZTO on 49,390.7 ZTO received, exactly 1.3% of the actual output.","severity":"low","snippet":"        uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);","title":"beforeSwap charges the cut on amountSpecified, afterSwap on the actual delta: partially filled swaps are overcharged"},{"citation":"resolved","description":"ask() and bid() are computed from `reserve` only, but buy() and sell() call collect() first, which folds the Kiln's pending ERC-6909 claims (and any claims third parties transferred to it) into reserve before pricing. Whenever claims() > 0 the price buy() charges is strictly above the ask() the same contract quoted a moment earlier, and neither buy(id) nor sell(id) accepts a maximum/minimum price. A buyer who approves exactly ask() has the transaction revert (allowance); a buyer with an open allowance pays the higher amount, which anyone can push up between quote and execution (a swap by a 0-piece wallet adds 1.3% of its ZTO side to claims; seed() adds directly). The deviation always favours the reserve, so no funds are extracted from the Kiln, but the brief's 'buy() charges ask()' is not what an integrator observes, and the reads are the only price oracle the contract exposes. Fix: have ask()/bid() include the pending claim balance (POOL_MANAGER.balanceOf(this, ztoId)) so the views match execution, and/or add maxPrice/minPrice parameters.","line":244,"path":"src/Kiln.sol","reproduction":"test/scratch/AskQuote.t.sol: reserve seeded with 1,000,000 ZTO, piece 7 sold into the Kiln (reserve 980,000 ZTO), then a 0-piece wallet swaps 1 ETH in, leaving 12,986.8 ZTO in claims. kiln.ask() returns 22,540 ZTO. Buyer approves exactly 22,540 and calls buy(7): reverts. Buyer approves max and calls buy(7): pays 22,838.64 ZTO (= (980,000 + 12,986.8)/50 * 1.15). Expected: pays the quoted 22,540 or can bound the price; actual: pays 298.6 ZTO (1.3%) more with no way to cap it.","severity":"low","snippet":"        uint256 price = ask();","title":"buy()/sell() execute at a price different from the ask()/bid() they quote, with no caller-side bound"},{"citation":"resolved","description":"The live Pepeolithic (0x765956a7307222346b08fff681820a5d77e92028, source verified on Sourcify) inherits OpenZeppelin v5 ERC721, whose balanceOf reverts with ERC721InvalidOwner(0) for the zero address. The hook reads balanceOf(tx.origin) on every swap without guarding address(0). In a mined transaction tx.origin is never zero, but eth_call / simulation without a `from` field (the default for many quoters, aggregators and indexers calling V4Quoter or simulating the Universal Router) runs with tx.origin == 0, so every swap simulation through this pool reverts inside the hook and the pool cannot be quoted by such tooling. The project's MockPepeo returns 0 for address(0), which is why the suite does not see it. No funds are at risk; impact is lost volume/integration. Fix: treat tx.origin == address(0) as zero pieces (or wrap the balance read in a try/catch defaulting to 0).","line":303,"path":"src/Kiln.sol","reproduction":"test/scratch/ZeroOrigin.t.sol uses a PEPEO stand-in with OpenZeppelin's exact `if (owner == address(0)) revert ERC721InvalidOwner(address(0))`. swapRouter.swap(zeroForOne=true, amountSpecified=-1 ether) with vm.prank(quoter, quoter) succeeds; the identical call with vm.prank(quoter, address(0)) (tx.origin = 0, as in a from-less eth_call) reverts. Expected: the simulation returns the swap delta; actual: revert bubbled from the hook.","severity":"low","snippet":"        uint256 pepes = PEPEO.balanceOf(tx.origin);","title":"Every swap reverts when tx.origin is address(0): eth_call quotes against the live OpenZeppelin ERC-721 fail"},{"citation":"resolved","description":"Not a Kiln code defect but a cross-contract access x economics seam the author should state. The live Pepeolithic contract (verified source) mints every unsold sale piece of a closed cave to its `admin` through sweep(), and `admin`/`adam` already hold ids 736 and 0 (total supply today is 2). The Kiln buys from anyone at reserve/50 with no per-seller limit, so an actor who obtains pieces at zero cost can convert them into the traders' accumulated ZTO cuts: selling k pieces drains 1 - 0.98^k of the reserve (k = 50 pieces: 63.6%; k = 100: 86.7%). This is exactly the brief's 'buy from anyone' design and the admin is not a Kiln role, so it is recorded as an assumption for the README's trust section rather than a finding to fix.","line":225,"path":"src/Kiln.sol","reproduction":"On mainnet after a cave closes: admin calls Pepeolithic.sweep(c, max) receiving the unsold ids at no cost, approves the Kiln and calls Kiln.sell(id) repeatedly; each call pays reserve/50 and lowers the next bid by 2%. With reserve R and 100 sweeps the admin receives 0.867 R ZTO funded by swap cuts. No guard in sell() distinguishes this seller from any other, as designed.","severity":"info","snippet":"    function sell(uint256 id) external {","title":"Trust assumption: Pepeolithic's admin receives unsold pieces for free via sweep() and can sell them into the reserve at bid()"}],"hash":"7cda57afe6b60489b8e9d0acf252b53c1fb54a1ea47c6a90f0d47d95c2558837","nodeId":"d9f6da8e-349e-4966-bba8-9c2b0421a324","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with five entries and coverage rows for all eight entry points plus four invariants. The project's own 43 tests still pass; the only failing test is my scratch proof, which is expected and lives in the gitignored `test/scratch/` directory. No tracked file was changed.\n\n**What I found**\n\n- **Medium: `open()` can be blocked by a front-runner.** The Kiln address is derivable from the public `initCodeHash()` and the salt visible in the mempool, and the Kiln has no initialize hook flags, so the PoolManager accepts `initialize` for that key with no code at the hook address. The deployer's `open()` then reverts with `PoolAlreadyInitialized` and the salt is burned. A proof test at `test/scratch/OpenFrontRun.t.sol` fails on the current code and is embedded in the findings file. A fix is to skip `initialize` when the pool already exists, since with zero liquidity a dust swap moves the price for free, which I verified separately.\n- **Low: partial-fill asymmetry.** The `beforeSwap` branches measure the cut on `amountSpecified` while `afterSwap` measures it on the executed delta. A 1,000,000 ZTO exact-input swap stopped by a price limit after 50,144 ZTO still pays a 13,000 ZTO cut.\n- **Low: `buy()` charges above the quoted `ask()`.** The views ignore pending claims that `collect()` folds in first, and neither `buy` nor `sell` takes a price bound. Exact-allowance buyers revert; open-allowance buyers overpay.\n- **Low: zero `tx.origin` reverts every swap.** The live Pepeolithic is OpenZeppelin ERC-721, whose `balanceOf(address(0))` reverts, so from-less `eth_call` quotes fail. The mock hides this.\n- **Info: trust assumption.** Pepeolithic's admin receives unsold pieces for free through `sweep()` and can sell them into the reserve like anyone else.\n\n**What held in my area.** Hook callbacks are restricted to the PoolManager and the one pool id. The ERC-6909 claim settlement balances in all four swap cases against the vendored v4 `Hooks` library. `unlockCallback` is reachable only through the Kiln's own `collect()`. The Kiln grants no approvals or operators, so no withdrawal path exists. The sell and buy pricing cannot be arbitraged in round trips. Live ZTO is a minimal ERC-20 returning bools, and live Pepeolithic's `transferFrom` makes no receiver callback. All static-analysis leads traced to benign patterns.\n\n**Not reached.** I did not fuzz the hook accounting beyond the project's existing swap tests, and I did not model protocol-fee activation on the pool.","treeHash":null,"usage":{"cachedInputTokens":1526388,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":61437,"runtime":"claude","turns":39,"wallClockMs":856723}},{"artifacts":[],"attempt":1,"bundleHash":"d829a8230e59159f1dc643d9764161630aa49488c91df5053b1a5f65515aff45","device":"771f83f312eace21","findings":[],"hash":"96decdc2859ab0b4a3783f96bcb2da5d43ce76ef17709412f6d322d683cc7c16","nodeId":"633e53be-c8e8-457b-a637-1fca7f2444f9","outcome":"completed","summary":"The manifest is done and verified. Only launch.json changed.\n\n**What launch.json describes**\n\n- One contract, Launcher, with constructor arguments in declaration order: ZTO, Pepeolithic, PoolManager as the static mainnet addresses from the brief.\n- Kiln is deliberately not listed. The Launcher creates it with CREATE2 inside the permissionless `open()`, and a factory-deployed Kiln would sit at an address without the hook permission bits.\n- No `$owner` is used because neither contract has an owner or admin.\n- The notes explain the post-deployment flow (read `initCodeHash()`, mine a salt, call `open()`), the code constants, and the two README caveats.\n\n**What changed this round**\n\nThe revised Launcher now adopts a pool that someone pre-initialized at the predicted key instead of reverting, and emits a Preinitialized event. I added one sentence to the notes describing that behavior and the advice to check slot0 before adding liquidity. Everything else in the manifest was already correct against the revised source.\n\n**Checks run**\n\n- The build succeeds. Kiln runtime is 6,366 bytes, under the 12,000-byte limit. Launcher runtime is 8,745 bytes, under EIP-170.\n- All 59 tests pass.\n- I rehearsed the protected deployment test in test/scratch with the manifest's three constructor words and a mock factory. It passes, so the constructor deploys on an empty chain and the runtime has no forbidden opcodes.\n- The manifest validates: exactly kind, contracts, notes. Notes are 2,518 characters. Every constructor argument is a string.","treeHash":"eeca85c7b4e176d945673bde3452ca2cfa2ba862","usage":{"cachedInputTokens":296862,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":4965,"runtime":"claude","turns":9,"wallClockMs":124491}},{"artifacts":[],"attempt":1,"bundleHash":"d3f2216945fea728f8444ec15cb3b1593b3616e12fa81b6981915483a2794064","device":"6b37e4ab65246705","findings":[],"hash":"a68a9d68644783171f0616205b6f7b07cc22899cf1728bbe05ba31db74d8c476","nodeId":"633e53be-c8e8-457b-a637-1fca7f2444f9","outcome":"completed","summary":"The manifest is written and verified. Build and all 43 tests pass, and `launch.json` is the only changed file.\n\n**What launch.json contains**\n\n- One contract, `Launcher`, with the three mainnet addresses from the brief as its constructor arguments in declaration order: ZTO, Pepeolithic, PoolManager. These match the accepted constructor at `src/Launcher.sol:33`.\n- No `$owner`. Neither contract has an owner or admin, so there is no owner argument to fill.\n- Notes explain the deployment flow: deploy the Launcher, mine a salt off-chain against `initCodeHash()`, then call `open(salt, sqrtPriceX96)` once.\n\n**Why the Kiln is not listed**\n\nThe Launcher creates the Kiln itself with CREATE2 at a mined salt and reverts unless the address carries exactly the four swap hook bits. A Kiln deployed directly by the factory would land at an address without those bits and could never serve as the pool's hook. The notes say this explicitly so a reviewer does not read the single entry as an omission.\n\n**Checks run**\n\n- Manifest keys, name length, string-only arguments and notes length validated by script.\n- `forge build` succeeded, with only pre-existing lint warnings from the accepted source.\n- `forge test` passed all 43 tests across 3 suites.\n- `git status` shows only the new `launch.json`.","treeHash":"e4e641a2c5cdfcca664217dde68f57bed7d23d87","usage":{"cachedInputTokens":197588,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":3594,"runtime":"claude","turns":9,"wallClockMs":96870}},{"artifacts":[],"attempt":1,"bundleHash":"e91b90c21d3ee470bb4cb2929c10cdd5eb4beffaabad3a13565839ea29234ae3","device":"7591760a616c6429","findings":[{"description":"Deployment step 4 tells the deployer to move a pre-initialized (or front-run) empty pool to the intended tick with 'a dust swap whose sqrtPriceLimitX96 is the intended price (ZTO in, zeroForOne = false, raises the price; ETH in lowers it)' and to 'use a tier-5 wallet or accept the dust cut'. A dust amount of ZTO in is an exact-input swap with ZTO as the specified currency. Since the PartialFill guard was added, Kiln.afterSwap reverts every ZTO-specified swap by a wallet that pays a cut unless the pool moved exactly |amountSpecified| - cut. On an empty pool the pool moves nothing, so the prescribed swap reverts with PartialFill(dust - cut, 0) and the price does not move. The same applies to the ETH-in direction if the deployer specifies the ZTO output. 'Accept the dust cut' is therefore not an option: the only shapes a paying wallet can use are the afterSwap ones, i.e. ZTO in with a tiny exact ETH OUTPUT (amountSpecified > 0) to raise the price, and ETH in with a tiny exact ETH INPUT (amountSpecified < 0) to lower it. Both move an empty pool to the limit for zero cost and take no cut. The code behaves as its own partial-fill section documents; the defect is that the operational instruction contradicts it, and a deployer following it on mainnet gets a reverting transaction at the moment they are trying to recover from a griefed launch. Suggested fix: in step 4, state the shapes explicitly ('ZTO in, exact output of 1 wei ETH, zeroForOne = false' to raise; 'ETH in, exact input of 1 wei, zeroForOne = true' to lower), and drop 'accept the dust cut', or say that a tier-5 wallet may use any shape.","line":166,"path":"README.md","reproduction":"Real v4 PoolManager, mock ZTO and PEPEO, Launcher(zto, pepeo, manager), salt mined for the Kiln bits. (1) Griefer: manager.initialize(key, getSqrtPriceAtTick(0)). (2) launcher.open(salt, getSqrtPriceAtTick(138180)) -> pool adopted at tick 0, Preinitialized emitted. (3) Deployer with 0 PEPEO follows the README: PoolSwapTest.swap(key, {zeroForOne: false, amountSpecified: -1e18, sqrtPriceLimitX96: getSqrtPriceAtTick(138180)}). Expected per README: price moves to tick 138180 at the cost of the dust and its 1.30% cut. Actual: revert WrappedError(kiln, afterSwap.selector, PartialFill(987000000000000000, 0), HookCallFailed()); slot0 tick stays 0. (4) Same deployer, amountSpecified: +1 (exact output of 1 wei ETH), same limit: succeeds, tick becomes 138180, delta.amount1() == 0 (no ZTO paid, no cut). The delivered unit test test/KilnSwap.t.sol test_partialFill_emptyPool and the fuzz test test/KilnProperties.t.sol testFuzz_partialFill_ztoSpecified_revertsForPayingTiers show the same revert for every paying tier.","severity":"low","title":"README recovery step for a pre-initialized pool prescribes a swap shape that always reverts PartialFill for a paying wallet"}],"hash":"ad13ee08da4fb503c46bb2e988107803af8e16fd15ecf47975477c74f1ca76c3","nodeId":"c8a17269-661e-4170-ac57-08301539767b","outcome":"completed","summary":"The revision is complete. `forge build` and `forge test` pass with all 77 tests green, and only my two files changed.\n\n**What this round did.** The implementation had been revised by another contributor since my earlier round, adding `quoteBid`/`quoteAsk`, bounded `sell`/`buy` overloads, a `PartialFill` guard in `afterSwap`, a tier-0 fallback when the PEPEO read fails, and pre-initialized-pool adoption in the Launcher. My invariant and property files still passed against it, so I extended them to those surfaces rather than rewriting anything.\n\n- **Handler, new actions.** Bounded sell and buy with a random price bound, which must either revert with `PriceBelowMin`/`PriceAboveMax` naming the live execution price and move nothing, or behave exactly like the plain calls. A partial-fill action that drives any of the four swap shapes to a one-bip price limit inside the range: paying tiers on ZTO-specified shapes must revert and the handler decodes the wrapped `PartialFill(asked, realised)` and checks both values; tier 21 and the afterSwap shapes fill partially and are charged on the realised ZTO. An ERC-6909 claim donation action, so `collect()` sweeping more than `claims` runs inside random sequences.\n- **Invariants.** New `invariant_quotesIncludePending` ties the quote views to reserve plus the live ERC-6909 balance. The 6909 mirror and ZTO conservation invariants now account for pending and swept donations. A deterministic pass over every handler action guards against a branch silently never firing.\n- **Properties.** Fuzzed the `PartialFill` guard over all paying tiers and price limits with a probe-under-snapshot oracle for the realised amount, fuzzed the afterSwap shapes charging exactly floor of the realised ZTO, fuzzed the bounded overloads against an independent price computation, and fuzzed the tier-0 fallback.\n\n**One finding reported** in `.imd-findings.json`, low severity, documentation. README step 4 tells a deployer to recover a pre-initialized pool with a ZTO-in dust swap to the intended price and to \"accept the dust cut\". For any wallet that pays a cut that exact-input swap is a partial fill by construction, so it reverts with `PartialFill(dust − cut, 0)` and the price does not move. I reproduced it in scratch against the real PoolManager. The shape that works is ZTO in with a tiny exact ETH output, which moved the empty pool to the intended tick for zero ZTO. The code matches its own partial-fill section; the operational instruction contradicts it.\n\n**Still owed**, as before: no fork run against mainnet state was possible here, so the live ZTO and Pepeolithic contracts remain covered only by the mocks.","treeHash":"d61664d9bbceed02a06c6c3c76c0a68555a70eb6","usage":{"cachedInputTokens":1781513,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":56963,"runtime":"claude","turns":43,"wallClockMs":947641}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f9cb4fd544aa3c68","findings":[{"citation":"resolved","description":"Confirmed from the write_foundry_tests lead. The PartialFill guard added this round (src/Kiln.sol _requireFullFill, called from afterSwap for every ZTO-specified swap) reverts any ZTO-in exact-input swap by a wallet whose cut is non-zero unless the pool consumed exactly |amountSpecified| - cut. On an empty pool the pool consumes nothing, so the 'dust swap, ZTO in, zeroForOne = false' the README tells the deployer to send when recovering from a Preinitialized or front-run open() reverts with PartialFill(dust - cut, 0) for every tier 0-4 wallet, and the price does not move. The README's own 'Partial fills' section (line 75) states this, and test/KilnSwap.t.sol test_partialFill_emptyPool asserts it, so 'or accept the dust cut' contradicts the code. The only test of the recovery (test/Launcher.t.sol test_open_adoptsPreinitializedPool) mints 21 pieces to the fixer, so the paying-wallet path is untested. A deployer following step 4 from an ordinary wallet while recovering a griefed launch gets a reverting transaction. Shapes that do work for any wallet, because the cut is measured in afterSwap on a realised amount of zero: ZTO in with an exact ETH output (zeroForOne = false, amountSpecified = +1) to raise the price, and ETH in with an exact ETH input (zeroForOne = true, amountSpecified = -1) to lower it; both move an empty pool to the limit at zero cost and take no cut. Fix: state these shapes explicitly in step 4 and delete 'or accept the dust cut' (a tier-5 wallet may keep using any shape). No code change needed.","line":166,"path":"README.md","reproduction":"test/scratch/ReadmeRecovery.t.sol on this tree (real PoolManager, mocks, salt mined for 0xCC). setUp: griefer calls manager.initialize(key{ETH, ZTO, 2000, 60, kilnAddress(salt)}, getSqrtPriceAtTick(0)); launcher.open(salt, getSqrtPriceAtTick(138180)) adopts the pool at tick 0. Deployer with 0 PEPEO, 1e18 ZTO approved to PoolSwapTest, msg.sender = tx.origin = deployer, follows step 4: swap({zeroForOne: false, amountSpecified: -1e18, sqrtPriceLimitX96: getSqrtPriceAtTick(138180)}). Expected per README: price moves to tick 138180 at the cost of the dust plus its 1.30% cut. Actual: revert WrappedError(kiln, afterSwap.selector, PartialFill(987000000000000000, 0), HookCallFailed()); slot0 tick stays 0 (test_readmeShape_payingWallet_reverts passes with that expectRevert). Same deployer, swap({zeroForOne: false, amountSpecified: 1, sqrtPriceLimitX96: same}): succeeds, slot0 == intended price, tick 138180, kiln.claims() == 0 (test_alternativeShape_exactOutputEth_works).","severity":"low","snippet":"   the empty pool to the intended tick with a dust swap whose `sqrtPriceLimitX96` is the intended price (ZTO in,\n   `zeroForOne = false`, raises the price; ETH in lowers it); with no liquidity the swap moves the price and costs\n   only dust. Use a tier-5 wallet or accept the dust cut. A range placed on the wrong side of the live tick would","title":"README recovery step 4 prescribes a dust-swap shape that reverts PartialFill for every cut-paying wallet; 'accept the dust cut' is not possible"}],"hash":"c6b8809e3be841b56487d58e429d762ea386d20156b2252a2ff09be8c8bed841","nodeId":"03d3ea92-8b69-42ee-8392-bde504239bf9","outcome":"completed","summary":"The review is complete and `.imd-findings.json` is written. Nothing in the source tree was changed; the scratch tests were removed.\n\n**Prior findings, all settled**\n\n- **Partial-fill overcharge (medium):** fixed. All three attached proofs pass on the revised tree. I traced the new check in afterSwap: the amount it expects equals exactly what the PoolManager hands the pool after the beforeSwap delta, and the pool's specified-side delta is exact on a full fill, so the guard only fires on a price-limit or liquidity stop. Cut-free wallets skip it.\n- **Pre-initialized pool blocks open() (medium):** fixed. Both proof variants pass. The Launcher now reads slot0 and adopts an already-initialized pool, emitting Preinitialized then Opened.\n- **Quote versus execution (low):** fixed with quoteBid/quoteAsk and the bounded sell/buy overloads.\n- **Zero tx.origin reverts (low):** fixed with the zero-address short circuit and try/catch; the mock now mirrors OpenZeppelin's revert.\n- **collect() inside unlock, and Pepeolithic admin sweep (info):** addressed by README caveats, as agreed.\n\nThe full project suite passes (77 tests). Kiln runtime is 6,366 bytes, well under the 12,000 limit.\n\n**One new finding, low**\n\nThe README's deployment step 4 tells a deployer recovering from a griefed or front-run open() to send a dust ZTO-in swap and \"accept the dust cut\". That swap is a ZTO-specified exact-input, so for any wallet with fewer than 21 pieces the new partial-fill guard reverts it on an empty pool and the price never moves. I reproduced it: the prescribed swap reverts with PartialFill and the tick stays at 0, while a swap specifying 1 wei of ETH output moves the price to the intended tick at zero cost. The project's own recovery test only uses a 21-piece wallet, which is why it did not surface. The fix is documentation only: name the working swap shapes in step 4 and drop the \"accept the dust cut\" wording.\n\nThe specialist's other leads (rate base differing between before- and after-swap cases, 1-wei rounding) are design consequences of the brief's prescribed mechanics, not defects, and were not kept. The static-analysis lines are state-before-call patterns and ignored empty return values, none reproducible as a defect.","treeHash":null,"usage":{"cachedInputTokens":810666,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":17861,"runtime":"claude","turns":17,"wallClockMs":391694}},{"artifacts":[],"attempt":1,"bundleHash":"6bf8a2db93d11903296fd9ecc4ac9975a6d046849faeb37b918283cc321db655","device":"74a99f640688d37b","findings":[],"hash":"d1ea9488370349eac3d3547bb0039253a2be6b07bc075f0422f47f9dddc825e7","nodeId":"0a94f389-d5bf-4289-bd16-70006fb6a31f","outcome":"completed","summary":"Both reopened findings reproduced on the starting tree and are fixed; the four advisories are addressed too. The full suite passes with the reviewer's proofs included.\n\n**What changed**\n\n- **Partial fills (medium, proof-bound).** In the two cases where ZTO is the specified currency, `afterSwap` now compares the ZTO the pool moved with what it was asked to move and reverts with a custom `PartialFill(asked, realised)` error on any mismatch. A tier-5 wallet pays no cut and may still fill partially. Cases 2 and 3 were already measured on the realised delta and are untouched. Both proof tests now pass.\n- **Pool pre-initialization griefing (medium, proof-bound).** `open()` reads slot0 through `StateLibrary.getSlot0` and only initializes when the pool is untouched. Otherwise it adopts the live price and emits a new `Preinitialized` event before `Opened`. The proof passes and `open()` stays permissionless and one-shot.\n- **Quote vs execution (low).** Added `quoteBid()` and `quoteAsk()` views that include pending claims, plus `sell(id, minPrice)` and `buy(id, maxPrice)` overloads. The brief's `bid()`, `ask()`, `sell(id)` and `buy(id)` are unchanged.\n- **Zero `tx.origin` (low).** The pass read now treats a zero origin, or any reverting `balanceOf`, as zero pieces. The mock was changed to revert like OpenZeppelin v5 so the suite exercises it.\n- **Collect inside unlock and Pepeolithic admin sweep (info).** Documented in the README caveats. No code change, since skipping `collect()` conditionally would deviate from the brief.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| `forge build` | clean |\n| `forge test` | 62 passed, 0 failed (includes both proofs under test/scratch) |\n| `forge fmt --check` | clean |\n| Kiln runtime | 6,366 bytes (limit 12,000) |\n\nThe README deployment steps now tell the deployer to send `open()` through a private relay, verify slot0 before adding the ZTO-only range, and how to move a mis-set price with a dust swap. All six findings are answered in `.imd-responses.json` at the repository root, which the harness excludes from git on purpose.","treeHash":"019ec815af327e105db80c951e68670c56d4244f","usage":{"cachedInputTokens":3486189,"inputTokens":866,"model":"claude-fable-5-1","outputTokens":66652,"runtime":"claude","turns":59,"wallClockMs":1208000}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c4617da5f1fd5ce8","findings":[{"citation":"resolved","description":"open() is permissionless and its salt is visible in the public mempool. The Kiln's address is a pure function of the Launcher, the salt and initCodeHash() (kilnAddress(salt) even computes it on chain). PoolManager.initialize(key, price) only validates the hook address bits and calls the hook only if it carries the beforeInitialize/afterInitialize bits (Hooks.beforeInitialize: `if (self.hasPermission(BEFORE_INITIALIZE_FLAG))`). The Kiln address carries only the four swap bits (0xCC), so the PoolManager initializes a pool whose hook address has no code yet without calling it. A griefer who sees open(salt, p) pending calls PoolManager.initialize({ETH, ZTO, 2000, 60, predictedKiln}, anyPrice) first. The deployer's open() deploys the Kiln, passes the bit check, then reverts in initialize() with PoolAlreadyInitialized(); the CREATE2 is rolled back and `kiln` stays zero. Every later open() with that salt reverts the same way, so the deployer must mine a new salt, which the griefer can front-run again at the cost of one initialize (~60k gas) per attempt while each failed open() costs the deployer a full Kiln deployment. Economic Security guide: cheapest griefing vector that blocks the launch; Flow Gap seam: execution (open succeeds once) x periphery (initialize is permissionless on any key and skips a hook with no init bits) x first principles (the launch must be openable). The same salt exposure also lets anyone call open(salt, otherPrice) first; that variant is harmless because an empty pool's price can be moved to any target for free with a 1-wei swap (verified: a oneForZero exact-in swap of 1 wei with sqrtPriceLimitX96 = openPrice moved the tick from 0 to 138180 with zero deltas), but the pre-initialize variant has no on-chain recovery. Minimal fix that keeps the design: in open(), read slot0 of the pool id through StateLibrary.getSlot0 (extsload) and call initialize() only when sqrtPriceX96 == 0; emit Opened either way. Operationally, until fixed, submit open() through a private relay. The proof test fails now (PoolAlreadyInitialized) and passes with that change (verified; the existing 43 tests still pass).","line":64,"path":"src/Launcher.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\n\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 {PoolId, PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {HookMiner} from \"v4-periphery/test/shared/HookMiner.sol\";\n\nimport {Launcher} from \"src/Launcher.sol\";\nimport {Kiln} from \"src/Kiln.sol\";\n\ncontract ProofZTO {\n    string public constant name = \"ZTO\";\n    string public constant symbol = \"ZTO\";\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract ProofPepeo {\n    mapping(uint256 => address) public ownerOf;\n    mapping(address => uint256) public balanceOf;\n}\n\n/// @notice Launcher.open() is permissionless and its salt is public once the open() transaction is in the mempool.\n///         PoolManager.initialize() makes no call to a hook whose address lacks the initialize bits, so anyone can\n///         initialize the ETH/ZTO pool key for the predicted Kiln address BEFORE the Kiln exists. From then on\n///         open(salt, ...) reverts with PoolAlreadyInitialized for that salt, forever, and the griefer can repeat\n///         this for every new salt the deployer mines. Fails now; passes once open() tolerates a pre-initialized\n///         pool (e.g. read slot0 and skip initialize when sqrtPriceX96 != 0) or otherwise cannot be blocked.\ncontract Proof1_OpenFrontRunTest is Test {\n    using PoolIdLibrary for PoolKey;\n    using StateLibrary for IPoolManager;\n\n    uint160 internal constant FLAGS = Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    function test_openCannotBeBlockedByPreInitializingThePoolKey() public {\n        IPoolManager manager = new PoolManager(address(this));\n        ProofZTO zto = new ProofZTO();\n        ProofPepeo pepeo = new ProofPepeo();\n        Launcher launcher = new Launcher(address(zto), address(pepeo), address(manager));\n        uint160 openPrice = TickMath.getSqrtPriceAtTick(138_180);\n\n        // The deployer mines a salt and broadcasts open(salt, openPrice).\n        (address predicted, bytes32 salt) = HookMiner.find(\n            address(launcher), FLAGS, type(Kiln).creationCode, abi.encode(address(zto), address(pepeo), address(manager))\n        );\n        assertEq(predicted.code.length, 0, \"Kiln does not exist yet\");\n\n        // A griefer sees it in the mempool and initializes the exact pool key first. No hook call happens because\n        // the predicted address has no initialize permission bits.\n        PoolKey memory key = PoolKey({\n            currency0: Currency.wrap(address(0)),\n            currency1: Currency.wrap(address(zto)),\n            fee: 2000,\n            tickSpacing: 60,\n            hooks: IHooks(predicted)\n        });\n        vm.prank(makeAddr(\"griefer\"));\n        manager.initialize(key, TickMath.getSqrtPriceAtTick(0));\n\n        // The deployer's open() must still succeed: deploy the Kiln at the mined address and report the pool.\n        (address kilnAddr, PoolId poolId) = launcher.open(salt, openPrice);\n        assertEq(kilnAddr, predicted, \"Kiln deployed at the mined address\");\n        assertEq(address(launcher.kiln()), predicted, \"launcher records the Kiln\");\n        assertEq(PoolId.unwrap(poolId), PoolId.unwrap(key.toId()), \"pool id\");\n        (uint160 sqrtPriceX96,,, uint24 lpFee) = manager.getSlot0(poolId);\n        assertGt(sqrtPriceX96, 0, \"pool initialized\");\n        assertEq(lpFee, 2000);\n    }\n}","reproduction":"State: Launcher deployed with (zto, pepeo, poolManager); no Kiln yet. 1) Deployer mines salt S so that kilnAddress(S) has low bits 0xCC and broadcasts open(S, 79228162514264337593543950336000). 2) Griefer computes K = launcher.kilnAddress(S) and sends PoolManager.initialize(PoolKey(address(0), ZTO, 2000, 60, K), 79228162514264337593543950336) with higher gas, mined first (succeeds: no hook call, K has no code). 3) Deployer's open(S, price) executes: CREATE2 succeeds, bit check passes, PoolManager.initialize reverts PoolAlreadyInitialized(); whole call reverts, launcher.kiln() == address(0). 4) Any further open(S, *) reverts identically. Expected: open() succeeds once and emits Opened. Actual: open() can never succeed for S and the griefer can repeat for every new salt.","severity":"medium","snippet":"        IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);","title":"Launcher.open() can be blocked indefinitely: anyone can pre-initialize the ETH/ZTO pool key for the predicted Kiln address, after which open(salt, ...) reverts forever for that salt"},{"citation":"resolved","description":"For ZTO-in exact-input (case 1) and ETH-in exact-output (case 4) the cut is computed in beforeSwap from the specified amount and settled as a positive specified-currency hook delta before the pool runs. The pool may then fill only part of the order: v4 swaps stop at sqrtPriceLimitX96 or when liquidity runs out without reverting. The hook delta is nevertheless the full `specified * kilnCut / 1e6`, so the trader is charged kilnCut of an amount that was not swapped. Three concrete outcomes against the real PoolManager (tier 0, 1.30%): (a) empty pool, ZTO-in exact-in 1,000,000 ZTO: swapDelta = (0 ETH, -13,000 ZTO); the trader pays 13,000 ZTO and receives nothing, claims() = 13,000e18. (b) range of 1e24 liquidity primed with 1 ETH, ZTO-in exact-in 100,000,000 ZTO: the pool swaps 1,000,800 ZTO for 0.998 ETH but the cut is 1,300,000 ZTO, i.e. 129.9% of the ZTO actually swapped instead of 1.30% (13,010 ZTO); the trader pays 2,300,800 ZTO. (c) range holding ~259,404 ZTO, ETH-in exact-out of 1e27 ZTO: the pool delivers 259,404 ZTO for 0.35 ETH, the cut is 13,000,000 ZTO, the trader's ZTO delta is -12,740,596 ZTO: an ETH->ZTO buyer pays ZTO on top of ETH and receives no ZTO. afterSwap returns 0 in these cases and does not compare the pool's actual delta with the amount the cut was computed on. The README documents only case 4 and only as 'may receive less than the specified output'; case 1 and the net-negative outcome are undocumented. The brief's guarantee is 'the trader is charged kilnCut of the ZTO side'. Who loses: the trader (and any integrator settling deltas without a min-output check); who gains: the reserve. Routers that enforce TAKE_ALL/min-output revert the worst cases, which bounds the practical loss to kilnCut of the unfilled fraction allowed by the slippage tolerance, hence medium. Minimal fix that keeps the brief's beforeSwap mechanism: in afterSwap, when ZTO is specified, recompute the expected pool ZTO delta (specified - cut for exact-in, specified + cut for exact-out) and revert (e.g. PartialFill()) when |delta.amount1()| differs; alternatively take the cut on the pool's actual ZTO delta. The proof test fails now in all three cases and passes with the revert-on-partial-fill fix (verified; the existing 43 tests still pass).","line":179,"path":"src/Kiln.sol","reproduction":"Pool opened at tick 138180 (about 1,000,000 ZTO per ETH), trader holds 0 PEPEO. Case (a): no liquidity; trader calls PoolSwapTest.swap(key, {zeroForOne:false, amountSpecified:-1_000_000e18, sqrtPriceLimitX96: MAX_SQRT_PRICE-1}). Expected: cut <= 1.30% of ZTO swapped (0) i.e. nothing charged, or revert. Actual: delta = (0, -13_000e18); trader's ZTO balance falls by 13,000 ZTO; kiln.claims() == 13_000e18. Case (b): add ZTO-only range [132180, 138180] liquidity 1e24, 21-piece wallet swaps 1 ETH in; trader swaps amountSpecified=-100_000_000e18 oneForZero. Expected cut about 13,010e18 (1.30% of the 1,000,800e18 swapped). Actual: claims() = 1_300_000e18, trader paid 2_300_800e18 ZTO for 0.998 ETH. Case (c): range [132180,138180] liquidity 1e21 (about 259,404 ZTO); trader swaps zeroForOne exact-out amountSpecified=+1e27 with 500 ETH value. Expected: delta.amount1() >= 0 (an ETH-in trader never pays ZTO). Actual: delta = (-0.35 ETH, -12_740_596e18 ZTO), claims() = 13_000_000e18.","severity":"medium","snippet":"        uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n        uint256 cut = _takeCut(amount);\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(cut.toInt128(), 0), 0);","title":"Kiln cut is charged on params.amountSpecified in the two beforeSwap cases, so a partial fill (price limit, exhausted or empty liquidity) charges kilnCut of ZTO that was never swapped; an ETH-in exact-"},{"citation":"resolved","description":"ask() and bid() read `reserve` only, as the brief requires, but buy() and sell() call collect() before pricing, which folds the whole pending ERC-6909 claim balance into reserve. Whenever claims > 0 (after any cut-paying swap) the view and the executed price diverge: `queryX` returns one value and `doX` charges another (Invariant guide: diverge view from write). The divergence is not small at the pool's scale: in the repro a single 1 ETH tier-0 swap leaves 12,481 ZTO of claims against a 4,900 ZTO reserve, so the quoted ask of 112.7 ZTO becomes a charged ask of 399.76 ZTO (3.5x). A buyer who approves exactly ask() has buy() revert on allowance; a buyer with an open allowance (the common Permit2/max pattern) pays 399.76 ZTO with no maxPrice parameter to bound it. The same divergence works in the seller's favour for sell(), and sellers have no minPrice either, so two sellers racing each lower the other's bid by 2% per piece with no protection. No attacker profit was found (a sandwich loses the 15% spread), so this is a quoting/UX defect rather than an exploit. Fixes that keep the brief's ABI: add views that include pending claims (e.g. quoteBid() = (reserve + POOL_MANAGER.balanceOf(this, ztoId)) / DEPTH and quoteAsk()), and/or an overload buy(id, maxPrice) / sell(id, minPrice); at minimum document that the executed price includes claims().","line":241,"path":"src/Kiln.sol","reproduction":"seed(5000e18); seller sells piece #1 (reserve 4900e18); ask() == 112.7e18. A 0-piece trader swaps 1 ETH in exact-in (claims() == 12_480_969_632_112_903_541_382). ask() still returns 112.7e18. Buyer approves kiln for exactly 112.7e18 and calls buy(1): reverts (allowance). Buyer approves max and calls buy(1): pays 399_762_301_538_596_781_451 wei ZTO == (4900e18 + claims) / 50 * 11500 / 10000, 3.55x the quote. Expected: the executed price equals the quoted price, or the buyer can cap it.","severity":"low","snippet":"        collect();\n        uint256 index = _slot[id];\n        if (index == 0) revert NotInInventory(id);\n        uint256 price = ask();","title":"bid()/ask() quote the pre-collect price while sell()/buy() collect() first and pay/charge the post-collect price, and buy() has no price cap: a buyer who approves the quoted ask reverts, one with an o"},{"citation":"resolved","description":"collect() calls POOL_MANAGER.unlock(), which reverts AlreadyUnlocked() if the PoolManager is already unlocked. sell() and buy() call collect() first (as the brief requires), so any contract that tries to sell a piece or buy one inside its own unlock callback (for example 'swap ETH->ZTO and buy a piece atomically', or 'sell a piece and sell the ZTO atomically') reverts whenever at least one cut-paying swap has happened since the last collect(), which after launch is nearly always. The caller can work around it by calling collect() before unlocking, or by doing the piece trade outside the unlock. Design consequence of the brief rather than a defect; recorded so integrators are warned (the README does not mention it).","line":221,"path":"src/Kiln.sol","reproduction":"Pool with liquidity; a 0-piece wallet swaps 1 ETH in (claims() > 0). A contract C holding piece #500 with setApprovalForAll(kiln) calls POOL_MANAGER.unlock(data) and in unlockCallback calls kiln.sell(500). Actual: reverts AlreadyUnlocked() from collect()'s unlock. After kiln.collect() the same call succeeds.","severity":"info","snippet":"        POOL_MANAGER.unlock(abi.encode(amount));","title":"sell() and buy() cannot be composed inside another PoolManager unlock while claims() > 0, because collect() must itself call unlock()"}],"hash":"dea22ac78decad2ee717319c9e02406188115a9f098ac4dc20d23c461505abab","nodeId":"bb782ee8-24e0-4d01-bff9-b284cab7ec7d","outcome":"completed","summary":"Review complete. The findings file is written at the repository root and the tracked tree is unchanged (`git diff` is empty; only ignored scratch files and the findings file were created).\n\n**What I found (area: Economic Security, Invariants, Flow Gaps)**\n\n1. **Medium: `open()` can be griefed indefinitely** (`src/Launcher.sol:64`). The salt is public once the open transaction is pending, and the PoolManager initializes a pool for a hook address that has no code and no initialize bits without calling it. A griefer who pre-initializes the predicted key makes every later `open()` with that salt revert with `PoolAlreadyInitialized`, and can repeat for each new salt. Proof test fails now and passes once `open()` skips initialization when slot0 is already set. The \"front-run `open()` with another price\" variant was checked and is harmless, since an empty pool's price moves for free.\n\n2. **Medium: the cut is charged on the specified amount on partial fills** (`src/Kiln.sol:179`). In the two beforeSwap cases the hook charges kilnCut of `amountSpecified` before the pool runs. Verified against the real PoolManager: an empty-pool ZTO-in swap pays 13,000 ZTO for nothing; a 100M ZTO sell into a pool holding 1 ETH pays a cut equal to 130% of what was swapped; an ETH-in exact-out buyer ends up paying 12.74M ZTO instead of receiving ZTO. The README documents only the exact-out case and not the net-negative outcome. Proof test fails now and passes with a revert-on-partial-fill check in `afterSwap`. Router min-output checks bound the practical loss, hence medium.\n\n3. **Low: quoted `ask()` differs from the charged ask, with no cap** (`src/Kiln.sol:241`). `buy()` collects first, so pending claims change the price. In the repro a 112.7 ZTO quote became a 399.8 ZTO charge after one 1 ETH swap. Exact-allowance buyers revert; open-allowance buyers overpay. No profitable attack found.\n\n4. **Info: `sell()`/`buy()` revert inside any other PoolManager unlock** while claims are pending, because `collect()` must unlock itself.\n\n**What held**: hook flash accounting in all four swap shapes, `collect()`, `unlockCallback()`, `seed()`, the reserve-never-exceeds-balance invariant, claims bookkeeping, no withdrawal path, and the real mainnet tokens. I verified on a fork that ZTO is a plain ERC-20 and read Pepeolithic's verified source (OpenZeppelin v5, `transferFrom` has no receiver callback, so no reentrancy). The slither reentrancy leads did not substantiate. Coverage rows answer all eight entry points plus five invariant rows.","treeHash":null,"usage":{"cachedInputTokens":1949382,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":71997,"runtime":"claude","turns":44,"wallClockMs":1186567}}],"verification":[{"checks":[{"durationMs":39100,"exitCode":0,"name":"build","output":"Compiling 80 files with Solc 0.8.26\nSolc 0.8.26 finished in 38.96s\nCompiler run successful!\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:65:9\n   │\n65 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:64:9\n   │\n64 │         IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:55\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:90\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:40\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:48\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:69\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:77\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                             ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:233:9\n    │\n233 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:252:9\n    │\n252 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":1325,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 22 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 7968149, ~: 7989666)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 497838)\n[PASS] test_buy_collectsClaimsFirst() (gas: 861350)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239474)\n[PASS] test_buy_revertsSecondTime() (gas: 460667)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440092)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453319)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189064)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1031376)\n[PASS] test_nobodyCanWithdraw() (gas: 980286)\n[PASS] test_plainTransferIsNotInventory() (gas: 293663)\n[PASS] test_seed_growsBid() (gas: 185731)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110906)\n[PASS] test_seed_zeroReverts() (gas: 32984)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2353968)\n[PASS] test_sell_collectsClaimsFirst() (gas: 731659)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398258)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246218)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 288809)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367635)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 372783)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287452)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 504.10ms (348.91ms CPU time)\n\nRan 8 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32468)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 29791141)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40151)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 37519100)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 30952731)\n[PASS] test_open_onlyOnce() (gas: 30950241)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 32149047)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 32161980)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 878.62ms (1.17s CPU time)\n\nRan 13 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 728083)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 55852)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 596056)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 91986)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337942)\n[PASS] test_liquidity_isUnrestricted() (gas: 415621)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32726)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5346151)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5380397)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5358698)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5361494)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 586893)\n[PASS] test_tierOf_boundaries() (gas: 10072615)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 878.70ms (26.17ms CPU time)\n\nRan 12 tests for test/KilnProperties.t.sol:KilnPropertiesTest\n[PASS] testFuzz_bidAskArithmetic(uint256) (runs: 1000, μ: 225929, ~: 226092)\n[PASS] testFuzz_cutIsKilnCutOfZtoSide(uint256,uint8,uint8) (runs: 400, μ: 735323, ~: 545499)\n[PASS] testFuzz_repeatedSells_geometricAndPayable(uint256,uint8) (runs: 200, μ: 3135151, ~: 2280912)\n[PASS] testFuzz_sellThenBuy_neverDrainsReserve(uint256) (runs: 1000, μ: 735502, ~: 735520)\n[PASS] testFuzz_tierMonotone(uint16,uint16) (runs: 500, μ: 23159054, ~: 22041989)\n[PASS] test_buy_revertsWhenAskIsZero() (gas: 751619)\n[PASS] test_collect_insideUnlockReverts() (gas: 799183)\n[PASS] test_collect_isIdempotent() (gas: 549246)\n[PASS] test_oneWeiSwaps_cutFloors() (gas: 1224317)\n[PASS] test_passFollowsTxOrigin_notMsgSender() (gas: 1890128)\n[PASS] test_passOnlyNeedsToBeHeldDuringTheSwap() (gas: 4672537)\n[PASS] test_swapSettledWithClaims_paysSameCut() (gas: 747484)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 905.37ms (2.25s CPU time)\n\nRan 1 test for test/KilnInvariant.t.sol:KilnInvariantTest\n[PASS]\nKilnInvariantTest invariants:\n[PASS] invariant_bidAskFromReserve\n[PASS] invariant_claimsMirrorErc6909\n[PASS] invariant_cutRounding\n[PASS] invariant_inventoryConsistent\n[PASS] invariant_poolAndLauncherFixed\n[PASS] invariant_reserveBackedByRealZto\n[PASS] invariant_ztoConservation\n KilnInvariantTest invariants (runs: 96, calls: 4608, reverts: 0)\n\n╭-------------+-----------------+-------+---------+----------╮\n| Contract    | Selector        | Calls | Reverts | Discards |\n+============================================================+\n| KilnHandler | attemptWithdraw | 664   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | buy             | 682   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | collect         | 637   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | seed            | 660   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | sell            | 639   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | strayTransfer   | 706   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | swap            | 620   | 0       | 0        |\n╰-------------+-----------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.23s (1.07s CPU time)\n\nRan 5 test suites in 1.23s (4.40s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":183,\"foundry.toml\":18,\"remappings.txt\":5,\"src/Kiln.sol\":312,\"src/Launcher.sol\":67,\"test/KilnBase.t.sol\":177,\"test/KilnInvariant.t.sol\":446,\"test/KilnPieces.t.sol\":390,\"test/KilnProperties.t.sol\":402,\"test/KilnSwap.t.sol\":293,\"test/Launcher.t.sol\":193,\"test/mocks/MockPepeo.sol\":56,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2c280a0fcadcd7e9908ab2cc8178175b155f06fc21497aaddb4d051b0faafb70","verifiedTreeHash":"2a4e55da85b2b33bdca1a52e4c034dace3dc20a8","verifierVersion":"0.1.0+14166f2e"},{"checks":[{"durationMs":21060,"exitCode":0,"name":"build","output":"Compiling 78 files with Solc 0.8.26\nSolc 0.8.26 finished in 20.94s\nCompiler run successful!\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:65:9\n   │\n65 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:64:9\n   │\n64 │         IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:55\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:90\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:40\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:48\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:69\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:77\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                             ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:233:9\n    │\n233 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:252:9\n    │\n252 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":492,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 13 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 728083)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 55852)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 596056)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 91986)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337942)\n[PASS] test_liquidity_isUnrestricted() (gas: 415621)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32726)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5346151)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5380397)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5358698)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5361494)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 586893)\n[PASS] test_tierOf_boundaries() (gas: 10072615)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 157.56ms (18.41ms CPU time)\n\nRan 8 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32468)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 29791141)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40151)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 37519100)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 30952731)\n[PASS] test_open_onlyOnce() (gas: 30950241)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 32149047)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 32161980)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 404.26ms (911.38ms CPU time)\n\nRan 22 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 7919900, ~: 7946780)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 497838)\n[PASS] test_buy_collectsClaimsFirst() (gas: 861350)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239474)\n[PASS] test_buy_revertsSecondTime() (gas: 460667)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440092)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453319)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189064)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1031376)\n[PASS] test_nobodyCanWithdraw() (gas: 980286)\n[PASS] test_plainTransferIsNotInventory() (gas: 293663)\n[PASS] test_seed_growsBid() (gas: 185731)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110906)\n[PASS] test_seed_zeroReverts() (gas: 32984)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2353968)\n[PASS] test_sell_collectsClaimsFirst() (gas: 731659)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398258)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246218)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 288809)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367635)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 372783)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287452)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 404.35ms (259.81ms CPU time)\n\nRan 3 test suites in 405.20ms (966.18ms CPU time): 43 tests passed, 0 failed, 0 skipped (43 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":183,\"foundry.toml\":18,\"remappings.txt\":5,\"src/Kiln.sol\":312,\"src/Launcher.sol\":67,\"test/KilnBase.t.sol\":177,\"test/KilnPieces.t.sol\":390,\"test/KilnSwap.t.sol\":293,\"test/Launcher.t.sol\":193,\"test/mocks/MockPepeo.sol\":56,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1516,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/Kiln.sol:225: Reentrancy in Kiln.sell(uint256) (src/Kiln.sol#225-237):\n[medium/medium] reentrancy-no-eth at src/Kiln.sol:240: Reentrancy in Kiln.buy(uint256) (src/Kiln.sol#240-255):\n[medium/medium] unused-return at src/Kiln.sol:215: Kiln.collect() (src/Kiln.sol#215-222) ignores return value by POOL_MANAGER.unlock(abi.encode(amount)) (src/Kiln.sol#221)\n[medium/medium] unused-return at src/Launcher.sol:56: Launcher.open(bytes32,uint160) (src/Launcher.sol#56-66) ignores return value by IPoolManager(POOL_MANAGER).initialize(key,sqrtPriceX96) (src/Launcher.sol#64)\n[low/medium] reentrancy-benign at src/Kiln.sol:225: Reentrancy in Kiln.sell(uint256) (src/Kiln.sol#225-237):\n[low/medium] reentrancy-benign at src/Kiln.sol:240: Reentrancy in Kiln.buy(uint256) (src/Kiln.sol#240-255):\n[low/medium] reentrancy-events at src/Kiln.sol:225: Reentrancy in Kiln.sell(uint256) (src/Kiln.sol#225-237):\n[low/medium] reentrancy-events at src/Kiln.sol:240: Reentrancy in Kiln.buy(uint256) (src/Kiln.sol#240-255):\n[low/medium] reentrancy-events at src/Launcher.sol:56: Reentrancy in Launcher.open(bytes32,uint160) (src/Launcher.sol#56-66):","passed":true},{"durationMs":295,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/Kiln.sol:216: Reentrancy: State change after external call (2 places)\n[high] unprotected-initializer at src/Launcher.sol:43: Unprotected initializer\n[low] large-numeric-literal at src/Kiln.sol:53: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/Kiln.sol:156: Literal Instead of Constant (6 places)\n[low] unchecked-return at src/Kiln.sol:221: Unchecked Return (2 places)\n[low] unsafe-erc20-operation at src/Kiln.sol:236: Unsafe ERC20 Operation (3 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"53eb7f2a76e5a86bce8d0acdbfbbf3a9c08451dd91aa6c6069c73520467f8506","verifiedTreeHash":"53b5e0c6a4e1ad76862908eba5fdbac85ba4b746","verifierVersion":"0.1.0+14166f2e"},{"checks":[{"durationMs":24838,"exitCode":0,"name":"build","output":"Compiling 78 files with Solc 0.8.26\nSolc 0.8.26 finished in 24.69s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/KilnSwap.t.sol:229:5:\n    |\n229 |     function _assertPassed(address trader, uint256 pepes, uint24 cut) internal returns (uint256 taken) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:77:13\n   │\n77 │             emit Preinitialized(poolId, livePrice, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:366:13\n    │\n366 │             claims += cut;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:79:9\n   │\n79 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:75:13\n   │\n75 │             IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:236:9\n    │\n236 │         claims = 0;\n    │         ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:237:9\n    │\n237 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:282:9\n    │\n282 │         _slot[id] = _inventory.length;\n    │         ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_inventory` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:298:9\n    │\n298 │         _inventory[index - 1] = last;\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:299:9\n    │\n299 │         _slot[last] = index;\n    │         ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:267:9\n    │\n267 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:283:9\n    │\n283 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:302:9\n    │\n302 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:24\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                        ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:38\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                                      ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":353,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32480)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 1198271)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40282)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 6955324)\n[PASS] test_open_adoptsPreinitializedPool() (gas: 5172947)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 2591446)\n[PASS] test_open_doesNotEmitPreinitializedNormally() (gas: 2550335)\n[PASS] test_open_onlyOnce() (gas: 2595381)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 3907730)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 3922161)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 27.53ms (83.41ms CPU time)\n\nRan 23 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 735002)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 56067)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 598895)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 92012)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337954)\n[PASS] test_liquidity_isUnrestricted() (gas: 415512)\n[PASS] test_partialFill_emptyPool() (gas: 1285599)\n[PASS] test_partialFill_ethIn_exactIn_chargesRealisedAmount() (gas: 433393)\n[PASS] test_partialFill_ethIn_exactOut_reverts() (gas: 1319097)\n[PASS] test_partialFill_priceLimit_revertsForEveryPayingTier() (gas: 3123337)\n[PASS] test_partialFill_tier21_isAllowed() (gas: 669914)\n[PASS] test_partialFill_ztoIn_exactIn_reverts() (gas: 1229660)\n[PASS] test_partialFill_ztoIn_exactOut_chargesRealisedAmount() (gas: 750996)\n[PASS] test_pepeoReadFailure_fallsBackToTierZero() (gas: 439655)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32900)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5361163)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5406972)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5385580)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5377909)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 601790)\n[PASS] test_tierOf_boundaries() (gas: 10103369)\n[PASS] test_zeroTxOrigin_isTierZero() (gas: 17776)\n[PASS] test_zeroTxOrigin_swapSimulationSucceeds() (gas: 412804)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 264.32ms (37.13ms CPU time)\n\nRan 26 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 8040732, ~: 8052495)\n[PASS] test_boundedOverloads_sameChecksAsPlainOnes() (gas: 240371)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 498395)\n[PASS] test_buy_collectsClaimsFirst() (gas: 864187)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239709)\n[PASS] test_buy_revertsSecondTime() (gas: 461082)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440407)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453756)\n[PASS] test_buy_withMaxPrice() (gas: 995306)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189255)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1034693)\n[PASS] test_nobodyCanWithdraw() (gas: 985673)\n[PASS] test_plainTransferIsNotInventory() (gas: 293985)\n[PASS] test_quote_includesPendingClaims() (gas: 968999)\n[PASS] test_seed_growsBid() (gas: 185890)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110974)\n[PASS] test_seed_zeroReverts() (gas: 33052)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2356396)\n[PASS] test_sell_collectsClaimsFirst() (gas: 734503)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398664)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246465)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 289071)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367985)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 373033)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287724)\n[PASS] test_sell_withMinPrice() (gas: 636032)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 264.38ms (264.35ms CPU time)\n\nRan 3 test suites in 266.49ms (556.23ms CPU time): 59 tests passed, 0 failed, 0 skipped (59 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.buy(uint256,uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.sell(uint256,uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":234,\"foundry.toml\":18,\"launch.json\":14,\"remappings.txt\":5,\"src/Kiln.sol\":388,\"src/Launcher.sol\":81,\"test/KilnBase.t.sol\":177,\"test/KilnPieces.t.sol\":483,\"test/KilnSwap.t.sol\":511,\"test/Launcher.t.sol\":263,\"test/mocks/MockPepeo.sol\":71,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"96decdc2859ab0b4a3783f96bcb2da5d43ce76ef17709412f6d322d683cc7c16","verifiedTreeHash":"eeca85c7b4e176d945673bde3452ca2cfa2ba862","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":21763,"exitCode":0,"name":"build","output":"Compiling 78 files with Solc 0.8.26\nSolc 0.8.26 finished in 21.62s\nCompiler run successful!\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:65:9\n   │\n65 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:64:9\n   │\n64 │         IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:55\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:179:90\n    │\n179 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:40\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:48\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:69\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:197:77\n    │\n197 │         uint256 amount = amount1 < 0 ? uint256(uint128(-amount1)) : uint256(uint128(amount1));\n    │                                                                             ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:221:9\n    │\n221 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:233:9\n    │\n233 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:252:9\n    │\n252 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":514,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32468)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 29791141)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40151)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 37519100)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 30952731)\n[PASS] test_open_onlyOnce() (gas: 30950241)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 32149047)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 32161980)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 176.30ms (978.94ms CPU time)\n\nRan 13 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 728083)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 55852)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 596056)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 91986)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337942)\n[PASS] test_liquidity_isUnrestricted() (gas: 415621)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32726)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5346151)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5380397)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5358698)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5361494)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 586893)\n[PASS] test_tierOf_boundaries() (gas: 10072615)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 400.79ms (22.51ms CPU time)\n\nRan 22 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 7970243, ~: 7962540)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 497838)\n[PASS] test_buy_collectsClaimsFirst() (gas: 861350)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239474)\n[PASS] test_buy_revertsSecondTime() (gas: 460667)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440092)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453319)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189064)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1031376)\n[PASS] test_nobodyCanWithdraw() (gas: 980286)\n[PASS] test_plainTransferIsNotInventory() (gas: 293663)\n[PASS] test_seed_growsBid() (gas: 185731)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110906)\n[PASS] test_seed_zeroReverts() (gas: 32984)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2353968)\n[PASS] test_sell_collectsClaimsFirst() (gas: 731659)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398258)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246218)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 288809)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367635)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 372783)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287452)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 418.38ms (267.05ms CPU time)\n\nRan 3 test suites in 419.07ms (995.47ms CPU time): 43 tests passed, 0 failed, 0 skipped (43 total tests)\n","passed":true},{"durationMs":38,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":183,\"foundry.toml\":18,\"launch.json\":14,\"remappings.txt\":5,\"src/Kiln.sol\":312,\"src/Launcher.sol\":67,\"test/KilnBase.t.sol\":177,\"test/KilnPieces.t.sol\":390,\"test/KilnSwap.t.sol\":293,\"test/Launcher.t.sol\":193,\"test/mocks/MockPepeo.sol\":56,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a68a9d68644783171f0616205b6f7b07cc22899cf1728bbe05ba31db74d8c476","verifiedTreeHash":"e4e641a2c5cdfcca664217dde68f57bed7d23d87","verifierVersion":"0.1.0+14166f2e"},{"checks":[{"durationMs":47976,"exitCode":0,"name":"build","output":"Compiling 80 files with Solc 0.8.26\nSolc 0.8.26 finished in 47.81s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/KilnSwap.t.sol:229:5:\n    |\n229 |     function _assertPassed(address trader, uint256 pepes, uint24 cut) internal returns (uint256 taken) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:77:13\n   │\n77 │             emit Preinitialized(poolId, livePrice, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:366:13\n    │\n366 │             claims += cut;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:79:9\n   │\n79 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:75:13\n   │\n75 │             IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:236:9\n    │\n236 │         claims = 0;\n    │         ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:237:9\n    │\n237 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:282:9\n    │\n282 │         _slot[id] = _inventory.length;\n    │         ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_inventory` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:298:9\n    │\n298 │         _inventory[index - 1] = last;\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:299:9\n    │\n299 │         _slot[last] = index;\n    │         ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:267:9\n    │\n267 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:283:9\n    │\n283 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:302:9\n    │\n302 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:24\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                        ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:38\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                                      ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":1806,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 23 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 735002)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 56067)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 598895)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 92012)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337954)\n[PASS] test_liquidity_isUnrestricted() (gas: 415512)\n[PASS] test_partialFill_emptyPool() (gas: 1285599)\n[PASS] test_partialFill_ethIn_exactIn_chargesRealisedAmount() (gas: 433393)\n[PASS] test_partialFill_ethIn_exactOut_reverts() (gas: 1319097)\n[PASS] test_partialFill_priceLimit_revertsForEveryPayingTier() (gas: 3123337)\n[PASS] test_partialFill_tier21_isAllowed() (gas: 669914)\n[PASS] test_partialFill_ztoIn_exactIn_reverts() (gas: 1229660)\n[PASS] test_partialFill_ztoIn_exactOut_chargesRealisedAmount() (gas: 750996)\n[PASS] test_pepeoReadFailure_fallsBackToTierZero() (gas: 439655)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32900)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5361163)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5406972)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5385580)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5377909)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 601790)\n[PASS] test_tierOf_boundaries() (gas: 10103369)\n[PASS] test_zeroTxOrigin_isTierZero() (gas: 17776)\n[PASS] test_zeroTxOrigin_swapSimulationSucceeds() (gas: 412804)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 87.80ms (57.58ms CPU time)\n\nRan 10 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32480)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 1198271)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40282)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 6955324)\n[PASS] test_open_adoptsPreinitializedPool() (gas: 5172947)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 2591446)\n[PASS] test_open_doesNotEmitPreinitializedNormally() (gas: 2550335)\n[PASS] test_open_onlyOnce() (gas: 2595381)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 3907730)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 3922161)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 773.93ms (80.03ms CPU time)\n\nRan 16 tests for test/KilnProperties.t.sol:KilnPropertiesTest\n[PASS] testFuzz_bidAskArithmetic(uint256) (runs: 1000, μ: 225901, ~: 226185)\n[PASS] testFuzz_boundedOverloads(uint256,uint256,uint256) (runs: 300, μ: 1056797, ~: 1047620)\n[PASS] testFuzz_cutIsKilnCutOfZtoSide(uint256,uint8,uint8) (runs: 400, μ: 735461, ~: 636023)\n[PASS] testFuzz_partialFill_afterSwapShapes_chargeRealised(uint8,uint16,bool) (runs: 200, μ: 891782, ~: 849141)\n[PASS] testFuzz_partialFill_ztoSpecified_revertsForPayingTiers(uint8,uint16,bool) (runs: 200, μ: 1123462, ~: 1255263)\n[PASS] testFuzz_pepeoReadFailure_everyTierPaysFull(uint8) (runs: 64, μ: 868315, ~: 718019)\n[PASS] testFuzz_repeatedSells_geometricAndPayable(uint256,uint8) (runs: 200, μ: 3121173, ~: 2395243)\n[PASS] testFuzz_sellThenBuy_neverDrainsReserve(uint256) (runs: 1000, μ: 736036, ~: 736069)\n[PASS] testFuzz_tierMonotone(uint16,uint16) (runs: 500, μ: 23686014, ~: 24676205)\n[PASS] test_buy_revertsWhenAskIsZero() (gas: 752223)\n[PASS] test_collect_insideUnlockReverts() (gas: 801955)\n[PASS] test_collect_isIdempotent() (gas: 551940)\n[PASS] test_oneWeiSwaps_cutFloors() (gas: 1246155)\n[PASS] test_passFollowsTxOrigin_notMsgSender() (gas: 1895710)\n[PASS] test_passOnlyNeedsToBeHeldDuringTheSwap() (gas: 4682835)\n[PASS] test_swapSettledWithClaims_paysSameCut() (gas: 754177)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 773.92ms (1.77s CPU time)\n\nRan 26 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 7978709, ~: 7977620)\n[PASS] test_boundedOverloads_sameChecksAsPlainOnes() (gas: 240371)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 498395)\n[PASS] test_buy_collectsClaimsFirst() (gas: 864187)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239709)\n[PASS] test_buy_revertsSecondTime() (gas: 461082)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440407)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453756)\n[PASS] test_buy_withMaxPrice() (gas: 995306)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189255)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1034693)\n[PASS] test_nobodyCanWithdraw() (gas: 985673)\n[PASS] test_plainTransferIsNotInventory() (gas: 293985)\n[PASS] test_quote_includesPendingClaims() (gas: 968999)\n[PASS] test_seed_growsBid() (gas: 185890)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110974)\n[PASS] test_seed_zeroReverts() (gas: 33052)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2356396)\n[PASS] test_sell_collectsClaimsFirst() (gas: 734503)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398664)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246465)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 289071)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367985)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 373033)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287724)\n[PASS] test_sell_withMinPrice() (gas: 636032)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 773.92ms (383.89ms CPU time)\n\nRan 2 tests for test/KilnInvariant.t.sol:KilnInvariantTest\n[PASS]\nKilnInvariantTest invariants:\n[PASS] invariant_bidAskFromReserve\n[PASS] invariant_claimsMirrorErc6909\n[PASS] invariant_cutRounding\n[PASS] invariant_inventoryConsistent\n[PASS] invariant_poolAndLauncherFixed\n[PASS] invariant_quotesIncludePending\n[PASS] invariant_reserveBackedByRealZto\n[PASS] invariant_ztoConservation\n KilnInvariantTest invariants (runs: 96, calls: 4608, reverts: 0)\n\n╭-------------+-----------------+-------+---------+----------╮\n| Contract    | Selector        | Calls | Reverts | Discards |\n+============================================================+\n| KilnHandler | attemptWithdraw | 446   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | buy             | 408   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | buyBounded      | 390   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | collect         | 438   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | donateClaims    | 392   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | partialFill     | 437   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | seed            | 429   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | sell            | 381   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | sellBounded     | 423   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | strayTransfer   | 455   | 0       | 0        |\n|-------------+-----------------+-------+---------+----------|\n| KilnHandler | swap            | 409   | 0       | 0        |\n╰-------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handler_everyActionFires() (gas: 4783978)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.71s (1.70s CPU time)\n\nRan 5 test suites in 1.71s (4.12s CPU time): 77 tests passed, 0 failed, 0 skipped (77 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.buy(uint256,uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.sell(uint256,uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":234,\"foundry.toml\":18,\"remappings.txt\":5,\"src/Kiln.sol\":388,\"src/Launcher.sol\":81,\"test/KilnBase.t.sol\":177,\"test/KilnInvariant.t.sol\":809,\"test/KilnPieces.t.sol\":483,\"test/KilnProperties.t.sol\":624,\"test/KilnSwap.t.sol\":511,\"test/Launcher.t.sol\":263,\"test/mocks/MockPepeo.sol\":71,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"ad13ee08da4fb503c46bb2e988107803af8e16fd15ecf47975477c74f1ca76c3","verifiedTreeHash":"d61664d9bbceed02a06c6c3c76c0a68555a70eb6","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":25144,"exitCode":0,"name":"build","output":"Compiling 78 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.01s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/KilnSwap.t.sol:229:5:\n    |\n229 |     function _assertPassed(address trader, uint256 pepes, uint24 cut) internal returns (uint256 taken) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:77:13\n   │\n77 │             emit Preinitialized(poolId, livePrice, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/Launcher.sol:79:9\n   │\n79 │         emit Opened(kilnAddr, poolId);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n   ╭▸ src/Launcher.sol:75:13\n   │\n75 │             IPoolManager(POOL_MANAGER).initialize(key, sqrtPriceX96);\n   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:366:13\n    │\n366 │             claims += cut;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `claims` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:236:9\n    │\n236 │         claims = 0;\n    │         ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:237:9\n    │\n237 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:282:9\n    │\n282 │         _slot[id] = _inventory.length;\n    │         ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_inventory` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:298:9\n    │\n298 │         _inventory[index - 1] = last;\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `_slot` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:299:9\n    │\n299 │         _slot[last] = index;\n    │         ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-events-access-control]: `reserve` is changed without an event but is used for access control\n    ╭▸ src/Kiln.sol:267:9\n    │\n267 │         reserve += amount;\n    │         ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `reserve` is updated\n    ╭▸ src/Kiln.sol:239:9\n    │\n239 │         POOL_MANAGER.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:283:9\n    │\n283 │         emit Sold(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/Kiln.sol:302:9\n    │\n302 │         emit Bought(id, msg.sender, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:24\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                        ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Kiln.sol:386:38\n    │\n386 │         return x < 0 ? uint256(-x) : uint256(x);\n    │                                      ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":354,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/Launcher.t.sol:LauncherTest\n[PASS] test_constructor_storesAddressesAndRejectsZero() (gas: 32480)\n[PASS] test_initCodeHash_matchesCreationCodeWithArgs() (gas: 1198271)\n[PASS] test_kilnConstructor_doesNotValidateItsAddress() (gas: 40282)\n[PASS] test_kiln_runtimeIsSmallAndHasNoEscapeOpcodes() (gas: 6955324)\n[PASS] test_open_adoptsPreinitializedPool() (gas: 5172947)\n[PASS] test_open_deploysKilnAtMinedAddressAndInitializesPool() (gas: 2591446)\n[PASS] test_open_doesNotEmitPreinitializedNormally() (gas: 2550335)\n[PASS] test_open_onlyOnce() (gas: 2595381)\n[PASS] test_open_revertsWhenAddressBitsAreWrong_thenSucceedsWithGoodSalt() (gas: 3907730)\n[PASS] test_open_revertsWhenPoolInitFails_thenSucceeds() (gas: 3922161)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 249.66ms (77.07ms CPU time)\n\nRan 23 tests for test/KilnSwap.t.sol:KilnSwapTest\n[PASS] test_collect_movesClaimsIntoReserveAndRealZto() (gas: 735002)\n[PASS] test_collect_nothingToCollect_isNoop() (gas: 56067)\n[PASS] test_collect_sweepsZtoClaimsSentByOthers() (gas: 598895)\n[PASS] test_hookCallbacks_rejectNonPoolManager() (gas: 92012)\n[PASS] test_hook_rejectsAnotherPoolUsingIt() (gas: 337954)\n[PASS] test_liquidity_isUnrestricted() (gas: 415512)\n[PASS] test_partialFill_emptyPool() (gas: 1285599)\n[PASS] test_partialFill_ethIn_exactIn_chargesRealisedAmount() (gas: 433393)\n[PASS] test_partialFill_ethIn_exactOut_reverts() (gas: 1319097)\n[PASS] test_partialFill_priceLimit_revertsForEveryPayingTier() (gas: 3123337)\n[PASS] test_partialFill_tier21_isAllowed() (gas: 669914)\n[PASS] test_partialFill_ztoIn_exactIn_reverts() (gas: 1229660)\n[PASS] test_partialFill_ztoIn_exactOut_chargesRealisedAmount() (gas: 750996)\n[PASS] test_pepeoReadFailure_fallsBackToTierZero() (gas: 439655)\n[PASS] test_setup_poolOpenedWithKilnHook() (gas: 32900)\n[PASS] test_swap_ethIn_exactIn_allTiers() (gas: 5361163)\n[PASS] test_swap_ethIn_exactOut_allTiers() (gas: 5406972)\n[PASS] test_swap_ztoIn_exactIn_allTiers() (gas: 5385580)\n[PASS] test_swap_ztoIn_exactOut_allTiers() (gas: 5377909)\n[PASS] test_tier21_paysNothing_everyCase() (gas: 601790)\n[PASS] test_tierOf_boundaries() (gas: 10103369)\n[PASS] test_zeroTxOrigin_isTierZero() (gas: 17776)\n[PASS] test_zeroTxOrigin_swapSimulationSucceeds() (gas: 412804)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 263.01ms (35.08ms CPU time)\n\nRan 26 tests for test/KilnPieces.t.sol:KilnPiecesTest\n[PASS] testFuzz_reserveNeverExceedsBalance(uint256) (runs: 256, μ: 8046976, ~: 8073043)\n[PASS] test_boundedOverloads_sameChecksAsPlainOnes() (gas: 240371)\n[PASS] test_buy_chargesAskAndPieceLeavesInventory() (gas: 498395)\n[PASS] test_buy_collectsClaimsFirst() (gas: 864187)\n[PASS] test_buy_revertsForIdNotHeld() (gas: 239709)\n[PASS] test_buy_revertsSecondTime() (gas: 461082)\n[PASS] test_buy_revertsWhenZtoTransferReturnsFalse() (gas: 440407)\n[PASS] test_buy_revertsWithoutZtoAllowance() (gas: 453756)\n[PASS] test_buy_withMaxPrice() (gas: 995306)\n[PASS] test_directZtoDonationIsNotReserve() (gas: 189255)\n[PASS] test_inventory_removalKeepsOtherPieces() (gas: 1034693)\n[PASS] test_nobodyCanWithdraw() (gas: 985673)\n[PASS] test_plainTransferIsNotInventory() (gas: 293985)\n[PASS] test_quote_includesPendingClaims() (gas: 968999)\n[PASS] test_seed_growsBid() (gas: 185890)\n[PASS] test_seed_revertsWhenTransferReturnsFalse() (gas: 110974)\n[PASS] test_seed_zeroReverts() (gas: 33052)\n[PASS] test_sell_bidFallsGeometrically() (gas: 2356396)\n[PASS] test_sell_collectsClaimsFirst() (gas: 734503)\n[PASS] test_sell_paysBidAndBidFalls() (gas: 398664)\n[PASS] test_sell_revertsAtZeroReserve() (gas: 246465)\n[PASS] test_sell_revertsIfCallerDoesNotOwnPiece() (gas: 289071)\n[PASS] test_sell_revertsIfKilnAlreadyHoldsPiece() (gas: 367985)\n[PASS] test_sell_revertsWhenZtoTransferReturnsFalse() (gas: 373033)\n[PASS] test_sell_revertsWithoutApproval() (gas: 287724)\n[PASS] test_sell_withMinPrice() (gas: 636032)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 263.48ms (257.81ms CPU time)\n\nRan 3 test suites in 264.36ms (776.14ms CPU time): 59 tests passed, 0 failed, 0 skipped (59 total tests)\n","passed":true},{"durationMs":46,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Kiln.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"Kiln.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"Kiln.buy(uint256)\",\"Kiln.buy(uint256,uint256)\",\"Kiln.collect()\",\"Kiln.seed(uint256)\",\"Kiln.sell(uint256)\",\"Kiln.sell(uint256,uint256)\",\"Kiln.unlockCallback(bytes)\",\"Launcher.open(bytes32,uint160)\"],\"files\":{\".gitignore\":6,\"README.md\":234,\"foundry.toml\":18,\"remappings.txt\":5,\"src/Kiln.sol\":388,\"src/Launcher.sol\":81,\"test/KilnBase.t.sol\":177,\"test/KilnPieces.t.sol\":483,\"test/KilnSwap.t.sol\":511,\"test/Launcher.t.sol\":263,\"test/mocks/MockPepeo.sol\":71,\"test/mocks/MockZTO.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1997,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/Kiln.sol:274: Reentrancy in Kiln._sell(uint256,uint256) (src/Kiln.sol#274-287):\n[medium/medium] reentrancy-no-eth at src/Kiln.sol:289: Reentrancy in Kiln._buy(uint256,uint256) (src/Kiln.sol#289-305):\n[medium/medium] unused-return at src/Kiln.sol:233: Kiln.collect() (src/Kiln.sol#233-240) ignores return value by POOL_MANAGER.unlock(abi.encode(amount)) (src/Kiln.sol#239)\n[medium/medium] unused-return at src/Launcher.sol:65: Launcher.open(bytes32,uint160) (src/Launcher.sol#65-80) ignores return value by (livePrice,None,None,None) = IPoolManager(POOL_MANAGER).getSlot0(poolId) (src/Launcher.sol#73)\n[medium/medium] unused-return at src/Launcher.sol:65: Launcher.open(bytes32,uint160) (src/Launcher.sol#65-80) ignores return value by IPoolManager(POOL_MANAGER).initialize(key,sqrtPriceX96) (src/Launcher.sol#75)\n[low/medium] reentrancy-benign at src/Kiln.sol:274: Reentrancy in Kiln._sell(uint256,uint256) (src/Kiln.sol#274-287):\n[low/medium] reentrancy-benign at src/Kiln.sol:289: Reentrancy in Kiln._buy(uint256,uint256) (src/Kiln.sol#289-305):\n[low/medium] reentrancy-events at src/Kiln.sol:289: Reentrancy in Kiln._buy(uint256,uint256) (src/Kiln.sol#289-305):\n[low/medium] reentrancy-events at src/Kiln.sol:274: Reentrancy in Kiln._sell(uint256,uint256) (src/Kiln.sol#274-287):\n[low/medium] reentrancy-events at src/Launcher.sol:65: Reentrancy in Launcher.open(bytes32,uint160) (src/Launcher.sol#65-80):","passed":true},{"durationMs":338,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/Kiln.sol:234: Reentrancy: State change after external call (2 places)\n[high] unprotected-initializer at src/Launcher.sol:48: Unprotected initializer\n[low] large-numeric-literal at src/Kiln.sol:53: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/Kiln.sol:173: Literal Instead of Constant (6 places)\n[low] unchecked-return at src/Kiln.sol:239: Unchecked Return (2 places)\n[low] unsafe-erc20-operation at src/Kiln.sol:269: Unsafe ERC20 Operation (3 places)\n[low] unused-public-function at src/Kiln.sol:154: Public Function Not Used Internally","passed":true},{"durationMs":8283,"exitCode":0,"name":"proof dbf7438dc381","output":"Compiling 75 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.71s\nCompiler run successful!\n\nRan 2 tests for test/imd-proof-d313c427/Proof_dbf7438dc381.t.sol:KilnPartialFillProof\n[PASS] test_case1_ztoInExactIn_partialFill_cutBoundedByRealisedInput() (gas: 1796385)\n[PASS] test_case4_ethInExactOut_partialFill_cutBoundedByRealisedOutput() (gas: 592006)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 9.61ms (1.65ms CPU time)\n\nRan 1 test suite in 10.36ms (9.61ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true},{"durationMs":7211,"exitCode":0,"name":"proof 8765607df8e1","output":"2026-10-09T05:44:44.578995Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-nYMDlt/repo/test/imd-proof-d313c427/Proof_dbf7438dc381.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.68s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-9f870b8c/Proof_8765607df8e1.t.sol:Proof1_OpenFrontRunTest\n[PASS] test_openCannotBeBlockedByPreInitializingThePoolKey() (gas: 6745938)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.99ms (7.79ms CPU time)\n\nRan 1 test suite in 8.55ms (7.99ms 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":"d1ea9488370349eac3d3547bb0039253a2be6b07bc075f0422f47f9dddc825e7","verifiedTreeHash":"019ec815af327e105db80c951e68670c56d4244f","verifierVersion":"0.1.0+c4d32abc"}]}