{"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":"21fcac6f-76a4-4f72-9e35-519f27dd2784","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"1a9a4916bffb7304fed6be9446a1a842c6fab776700c0f99c0808489b328f23b","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":"5051eed0f5cdebc992412cd5acb1f4b83d146bf6f32f18cb80bc1bd5442f411f","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":"57937dbacf17e2c31dd355a8443afd39109ffe058492ceec13848637aa3b5d44","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":"5fced49f988441c64b64f809f60ae641799468ec352e867374bb5316643d4959","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":"578cc99f7b9ea23347eaef1b60b3ad2bc49cce7124f0a1982b4e406e4ee163c2","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":"d7cae8101078af6609b85815bccbd8decd4183bb4af2c736c21b87d79d6096b9","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"cb85b1500f8fef679b7a1908074524f5b2e469b1d3db1657952263923cae22b0","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"f3cfe08be463d78c725d1aa029943eae24db6804ce3a4c093bc7feaa5e3568ec","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":"Launch the BMC Illuminati Community token on Ethereum, paired with ETH on Uniswap v4.\n\nToken\nName: BMC Illuminati\nSymbol: BMC\nSupply: 1,000,000,000 with 18 decimals, minted once.\nTransfers: plain only — no fees, limits, pausing, or minting after launch. No owner powers.\n\nPool\nPaired with ETH, opening at a 10 ETH market cap.\nStandard trading fee: 1.25% of every trade — 1% claimable by the launcher, 0.25% to IMD.\n\nHook (attached to the pool this launch opens)\n\nWhen the hook acts: on every swap.\nFee rule:\nOn buys: takes an additional 1% of the ETH side and sends it as ETH to 0x83027AD9d0D5374335086c539dADD1db983441B0.\nOn sells: burns 1% of the BMC tokens being sold (permanently reduces supply).\nWhere fees go: ETH fees go to the fixed community wallet above; sell burns go to the zero address.\nWho can change it: nobody. All percentages and the recipient are immutable constructor arguments.\nNo owner, no pause, no other fees, does not block or alter swaps otherwise.\nUtility note\nSelling permanently removes tokens from supply. Over time this reduces circulating supply, which can support price if demand stays constant or grows. This is not a guaranteed price increase.\n\nSplit\n10% to the swarm. The other 90% is the launcher’s: 88% into the pool, the rest to the launcher wallet.\n\nWebsite\nBuild and publish a usable public site at bmcilluminati.sites.imd.fun.\n\nThe site shows:\n\nToken name, symbol, contract, total supply + circulating supply (after burns)\nLive Uniswap pool link\nClear explanation of the 1% sell burn and 1% buy fee to the community wallet\nBuy/sell buttons and community links\nConnected wallet can do\n\nConnect and show BMC + ETH balances\nApprove (exact amount) then buy/sell against the ETH pool\nClaim the launcher’s 1% fee share if the connected wallet is the recipient\nNo admin actions\nInclude Foundry tests verifying the sell burn and the buy fee transfer.","parentJobId":null,"planHash":"06b22f3300806952f95fa0065827541b803fce64df27d6cf70a08b6a359be3c7","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"21fcac6f-76a4-4f72-9e35-519f27dd2784","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1341-launch-bmc-illuminati-community"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51487","feedbackHash":"e9f61e4cef6c70d022822ecf8f31c27eec7b757567f513b8314a30e53db7d138","nodeKey":"audit_economics","submissionHash":"1a9a4916bffb7304fed6be9446a1a842c6fab776700c0f99c0808489b328f23b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51232","feedbackHash":"9fd883b49166423b9a560486ff7acf089aaea5441e622feb3b52b9039af7d9ed","nodeKey":"audit_flow","submissionHash":"5051eed0f5cdebc992412cd5acb1f4b83d146bf6f32f18cb80bc1bd5442f411f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52481","feedbackHash":"fce08848397841a7efe8847332c3ce668d84ec7e6a37595af9bf29c3dbe867de","nodeKey":"audit_judge","submissionHash":"16986a820ca1605970ebd09c4c239d2d9265f60f779463f1419a89c208d17498","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52480","feedbackHash":"e12b25222c4bae4867fbbce4cf2c29674193e5d7e78afe2bcbcbe41203e2e76c","nodeKey":"audit_judge","submissionHash":"57937dbacf17e2c31dd355a8443afd39109ffe058492ceec13848637aa3b5d44","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52252","feedbackHash":"2726682439e61d8bcc134b994c5f3b9d1e439a1bf4f8f2af3a32b5df9555fb47","nodeKey":"audit_math","submissionHash":"5fced49f988441c64b64f809f60ae641799468ec352e867374bb5316643d4959","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51039","feedbackHash":"b87634be7acf5241b4472d1080c9320732688d2e996737cbe6798a76e7ac6ae8","nodeKey":"audit_permissions","submissionHash":"578cc99f7b9ea23347eaef1b60b3ad2bc49cce7124f0a1982b4e406e4ee163c2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52639","feedbackHash":"598923a97e73215bc465f61a7d5dc5a32374eebb066f782cbe23fe413de52da2","nodeKey":"build_contract_project","submissionHash":"d7cae8101078af6609b85815bccbd8decd4183bb4af2c736c21b87d79d6096b9","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51514","feedbackHash":"b60abb2c2dd8fb3157cf32963592bae3082aa5fa80070cd45ab7d334d12fdea0","nodeKey":"build_contract_project","submissionHash":"1132d833b0f014522c7cce6fdb37780f08f7374ac8c745ea72fcce1e993e0dfb","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51423","feedbackHash":"a6974d5363e32191318687bfa3408d61f20b647f559b12ae718d6d582c7cd5c0","nodeKey":"manifest","submissionHash":"01c449fa88c92d49d0413a00d2329b3ecd37588b300cede455073699b1df26db","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52193","feedbackHash":"f03ce03fcdfe924a9c6c798781a44fd7dd9d508154946775eddbc13fdf9550ef","nodeKey":"manifest","submissionHash":"cb85b1500f8fef679b7a1908074524f5b2e469b1d3db1657952263923cae22b0","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52216","feedbackHash":"55cc586234fa9c57d81024b836ec50dbe4acbfd13cbd434a5faff8fdcb7e68fd","nodeKey":"write_foundry_tests","submissionHash":"2b2d5cb4583f4c9cb9aff03731cdcaa3b9a0d1fb97709d2b85918c4fabda68c2","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52254","feedbackHash":"eaca10deb4f517305938f36e93e69690c35c4770f8b133568ae7e20fd2678ff7","nodeKey":"write_foundry_tests","submissionHash":"f3cfe08be463d78c725d1aa029943eae24db6804ce3a4c093bc7feaa5e3568ec","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"127bcbed8439b8295549857298137270485004d2059786723db2efbbabdf6f42","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"1792378c71fabfd7dff9b66db5b10b48d27378c4e169abc00fce770d06261450","device":"56e50117311155be","findings":[],"hash":"01c449fa88c92d49d0413a00d2329b3ecd37588b300cede455073699b1df26db","nodeId":"3de2415d-8de5-4ee7-beb3-5f34a629a4a1","outcome":"completed","summary":"Done. `launch.json` is written at the repository root and is the only changed file.\n\n**What it describes**\n\n- **Hook**: `BMCHook`, constructor arguments in declaration order: `$poolManager`, `$token`, the community wallet the brief names in lowercase, then `\"100\"` and `\"100\"` for the 1% buy fee and 1% sell burn in basis points.\n- **Permissions**: beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta. These are exactly what `getHookPermissions` returns and what the contract's `HOOK_FLAGS` constant encodes.\n- **Token**: `BMCToken`, \"BMC Illuminati\" / \"BMC\", 18 decimals.\n- **Pool**: native ETH pair at the zero address, fee 12500, tick spacing 60, initial price as the reference gives it.\n- **Notes**: explain the fee paths, the claim fallback when the PoolManager cannot pay yet, the one-pool beforeInitialize rule, the ownerless design, and the EOA expectation for the recipient.\n\n**Checks run**\n\n- `forge build` and `forge test` pass with all 48 existing tests green.\n- The manifest parses as JSON, its first key is the `univ4_hook` discriminator, and notes are well under the cap.\n\n| Check | Result |\n| --- | --- |\n| Notes length | 2799 of 4000 |\n| Scratch test deploying the hook at a flagged address and comparing permissions and immutables to the manifest | pass |\n\n**One judgment call**: the third constructor argument is the fee recipient, not an owner, so I wrote the brief's literal address in lowercase rather than `$owner`. The notes say it is the same address as the launch owner.","treeHash":"6aab93cb18fc0cb397e548d7dfc9ac522020099c","usage":{"cachedInputTokens":376079,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":6330,"runtime":"claude","turns":13,"wallClockMs":129354}},{"artifacts":[],"attempt":1,"bundleHash":"483f57ca11823d934592bf31d7baf43fe7a638f7008a1d819f42a5b0d6c084ac","device":"4ebee02fc506f27c","findings":[],"hash":"1132d833b0f014522c7cce6fdb37780f08f7374ac8c745ea72fcce1e993e0dfb","nodeId":"a6ad88fa-89b8-46bf-9198-fa2d39c73654","outcome":"completed","summary":"Committed. Everything is in place and verified.\n\n**Outcome:** `forge build`, `forge test` (48 passed, 0 failed) and `forge fmt --check` all pass with `solc = \"0.8.26\"`, `bytecode_hash = \"none\"`, no ffi or fs permissions. The protected floor tests from `.imd/reads` were also run in scratch with a launch-like environment (hook creation code against a token probe, flags 8396, fee 12500, spacing 60, native pair) and passed 11/11.\n\n**What was built**\n\n- `src/BMCToken.sol`: \"BMC Illuminati\" / \"BMC\", 1,000,000,000 × 10^18 minted once to the deployer. No owner, mint, pause, fee or limits. Only `burn`/`burnFrom` change supply, which the hook uses so `totalSupply()` is the post-burn circulating supply.\n- `src/BMCHook.sol`: constructor `(poolManager, token, feeRecipient, buyFeeBps, sellBurnBps)` written as `$poolManager`, `$token`, the brief's community wallet, 100, 100. Buys pay 1% of the ETH side to the wallet inside the swap; sells burn 1% of the BMC sold. Exact-input via `beforeSwapReturnDelta`, exact-output via `afterSwapReturnDelta`. When the PoolManager cannot cover a fee yet, the hook mints itself an ERC-6909 claim and anyone can call `collect()`. `beforeInitialize` accepts exactly one ETH/BMC pool from any initializer. No owner, no setters, unused callbacks revert, no DELEGATECALL/SELFDESTRUCT.\n- `src/BMCSwapRouter.sol`: ownerless router (approve exact amount, then buy/sell) that the site uses. Optional to launch.\n- `src/HookFlags.sol` and `test/mocks/MockERC20.sol`: the files the protected tests import.\n- `test/`: token, hook (funded pool, token-only fresh manager, ETH-only position), router and deploy-script tests, including fuzzed fee maths and failure cases.\n- `site/`: static page with stats, pool link, fee explanation, wallet connect, balances, quote/buy/sell, and recipient-only collect/claim buttons. `site/config.json` is filled after launch.\n- `README.md`: launch parameters, hook configuration, assumptions, operational responsibilities and after-launch site settings.\n\n**Things to know**\n\n- GitHub was blocked by the proxy, so dependencies came from the npm registry (v4-core 1.0.2 bundles forge-std, solmate and OpenZeppelin). One local patch restores `fail(string)` in the vendored forge-std because the floor tests call it; it is commented and documented.\n- The launcher's 1% LP-fee claim goes through IMD's launch fee contract, whose address and ABI I do not have. The site's claim button reads them from `config.json` and stays disabled until the publisher fills them in after launch.\n- The community wallet should be an externally owned account: a contract that rejects ETH would make direct-pay buys revert. This is documented in the README.\n- Slither and Mythril did not run (unavailable offline). An independent adversarial review is still needed before release.","treeHash":"4cb15601c32930bb0f4abfc705f6ef8ab0f90920","usage":{"cachedInputTokens":4543657,"inputTokens":1154,"model":"claude-fable-5-1","outputTokens":89937,"runtime":"claude","turns":59,"wallClockMs":1467530}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a4e285247c6f8aed","findings":[{"citation":"resolved","description":"beforeSwap (src/BMCHook.sol:172-186) computes the fee from params.amountSpecified and takes it at once (_takeEthFee / _takeAndBurn), then returns it as a positive specified BeforeSwapDelta, before Pool.swap runs. Uniswap v4 fills an exact-input order only up to sqrtPriceLimitX96 or the end of liquidity and charges the swapper only the consumed part; afterSwap returns 0 for exact input (line 198-200) and never re-reads the fill. So the swapper's net cost is consumed + 1% of requested, and the fee can be any multiple of what was traded. Exact-output swaps are charged in afterSwap from the pool's actual delta and are not affected. This is reachable by any unprivileged trader: (a) through any router exposing sqrtPriceLimitX96 (Universal Router, V4Router, PoolSwapTest) on any pool state; (b) through the project's own BMCSwapRouter (limits pinned to MIN/MAX) whenever liquidity on the output side runs out, which on the launch pool is the default state: the factory seeds BMC only, below the opening price, so the pool holds no ETH until buys have happened and every sale larger than the ETH buyers put in fills partially; a sale before the first buy fills nothing, returns 0 ETH and still burns 1% of the seller's tokens. The site's sell flow quotes with minEthOut 0 (site/app.js:163) and submits with the quoted minimum, so a user who sees a tiny quote and confirms loses the full 1%. The brief promises 'burns 1% of the BMC tokens being sold' and 'takes an additional 1% of the ETH side'; the README's note that this 'does not arise' with deep liquidity does not hold for the launch's own one-sided seeding. Merged from audit_permissions, audit_math, write_foundry_tests, audit_economics and audit_flow (same root cause). Who loses: the swapper; who gains: the community wallet on buys, nobody on sells. Fix (author's choice, keeps the rule): in afterSwap for exact-input swaps compare the pool's consumed specified amount (-delta.amount0() on buys, -delta.amount1() on sells) with amountIn - fee and revert on a shortfall, so a trade the pool cannot fill is refused instead of overcharged; or defer the charge to afterSwap, charge only on the consumed amount and return the surplus to the swapper. Also remove the README/launch.json claim that the fee is '1% of the ETH side' for exact input if the first option is chosen.","line":175,"path":"src/BMCHook.sol","reproduction":"State A (launch shape): real PoolManager; BMCHook(manager, BMC, wallet, 100, 100) at a mined address; pool ETH/BMC fee 12500 spacing 60 at sqrtPrice 10000<<96; one position of liquidity 1e22 on [-887220, 184200] (BMC only, no ETH); no buy yet. Call: seller (holding BMC, router approved) -> BMCSwapRouter.sellExactIn(key, 1_000_000e18, 0). Expected: 0 BMC burned and the seller keeps all BMC (or a revert). Actual: returns ethOut = 0, totalSupply drops by 10_000e18, seller's balance drops by 10_000e18, pool tick left at 887271. State B: same pool after buyer -> buyExactIn{value: 0.1 ether}(key, 0) (pool holds 0.099 ETH). Call: seller -> sellExactIn(key, 100_000_000e18, 0). Expected: burn <= 1% of the BMC that left the seller. Actual: ethOut = 97_762_499_999_999_999 wei, the pool bought 9_004_553e18 BMC, 1_000_000e18 BMC burned (1% of requested), 10_004_553e18 left the seller: the burn is ~10% of the sale. State C (funded pool, full-range liquidity 1e22 with 1.1 ETH): trader swaps via PoolSwapTest zeroForOne, amountSpecified -1e18, sqrtPriceLimitX96 = 0.999 * current. Expected: fee ~1% of the ETH traded. Actual: pool consumed 1_013_671_899_747_851 wei, wallet received 10_000_000_000_000_000 wei (987% of the ETH traded), trader paid 11_013_671_899_747_851 wei. Run: forge test --match-path test/scratch/PartialFillFee.t.sol (all three tests fail on the current code).","severity":"high","snippet":"        uint256 amountIn = uint256(-params.amountSpecified);","title":"Exact-input swaps are charged 1% of the amount requested, not of the amount the pool filled: a partial or zero fill still burns/pays 1% of the whole order"},{"citation":"resolved","description":"_takeEthFee (src/BMCHook.sol:224-233) calls poolManager.take(ETH, feeRecipient, fee) whenever the manager's ETH balance covers the fee, which on the shared mainnet PoolManager is always. CurrencyLibrary.transfer does call(gas(), to, amount) and reverts the whole swap with NativeTransferFailed if the recipient refuses; the claim path (mint) is reached only when the balance is short, never when the transfer fails, and unlockCallback (line 270) uses the same take, so collect() cannot redeem claims either. The recipient is immutable. The README assumes 'The community wallet is an externally owned account'. It is not a plain EOA: on Ethereum mainnet 0x83027AD9d0D5374335086c539dADD1db983441B0 carries EIP-7702 delegation code 0xef0100000000005c84f8fd50b21cac312528a64437030e (eth_getCode via ethereum-rpc.publicnode.com, 2026-10-11), so whatever code it is delegated to runs on every fee push, and its holder can re-delegate at any time to code whose receive reverts or burns the forwarded gas. From that moment every buy on the launch pool reverts (sells still work, so holders can only exit) and nobody can repoint the hook. The launch reference for this kind of hook names exactly this: 'a direct transfer to a contract that rejects ETH halts every fee-bearing swap, and a claim does not'. The trigger sits with the recipient (or its wallet provider), not an outside attacker, so this is a broken guarantee ('does not block or alter swaps otherwise') under a reachable state rather than theft. Secondary: the push runs the recipient's code while the PoolManager is unlocked, before Pool.swap on exact-input buys, so a delegate could re-enter and trade ahead of the buyer inside the buyer's transaction; moot with a benign delegate. Merged from write_foundry_tests, audit_economics and audit_flow. Fix that keeps the design: wrap the direct payment in try/catch and fall back to the claim (try poolManager.take(ADDRESS_ZERO, feeRecipient, fee) {...} catch { poolManager.mint(address(this), ADDRESS_ZERO.toId(), fee); }); do the same in unlockCallback so collect keeps the claim instead of reverting; correct the README and launch.json EOA assumption.","line":226,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @dev Stands in for the community wallet once its EIP-7702 delegate rejects plain ETH.\ncontract RejectsEth {\n    receive() external payable {\n        revert(\"no ETH\");\n    }\n}\n\n/// @notice BMCHook._takeEthFee pushes the buy fee to the immutable recipient with poolManager.take inside\n/// the swap whenever the manager's ETH balance covers it, and only mints a claim when the balance is short.\n/// The brief's recipient 0x8302...41B0 carries EIP-7702 delegation code on mainnet, so its holder can at any\n/// time point it at code that reverts on receiving ETH. From then on every buy on the pool reverts, and\n/// collect() reverts too. Expected: the buy completes and the fee is kept as an ERC-6909 claim.\ncontract RecipientRejectsEthTest is Test {\n    address constant COMMUNITY_WALLET = 0x83027AD9d0D5374335086c539dADD1db983441B0;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolKey key;\n    address buyer = makeAddr(\"buyer\");\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory code = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), COMMUNITY_WALLET, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(code, BMC_HOOK_FLAGS));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        token.approve(address(lp), type(uint256).max);\n        vm.deal(address(this), 2 ether);\n        // Full-range liquidity: the manager holds ~1.1 ETH, so the hook takes the direct-pay branch.\n        lp.modifyLiquidity{value: 1.1 ether}(key, ModifyLiquidityParams(-887_220, 887_220, 1e22, 0), \"\");\n        // The wallet's delegate now rejects ETH.\n        vm.etch(COMMUNITY_WALLET, address(new RejectsEth()).code);\n        vm.deal(buyer, 1 ether);\n    }\n\n    function test_buyStillCompletesWhenTheWalletRejectsEth() public {\n        vm.prank(buyer);\n        uint256 out = router.buyExactIn{value: 0.1 ether}(key, 0);\n        assertGt(out, 0, \"the buy should go through\");\n        assertEq(hook.pendingEthFees(), 0.001 ether, \"the fee should be kept as a claim for later redemption\");\n    }\n\n    function test_exactOutputBuyStillCompletesWhenTheWalletRejectsEth() public {\n        vm.prank(buyer);\n        uint256 ethIn = router.buyExactOut{value: 1 ether}(key, 1_000_000 ether);\n        assertGt(ethIn, 0, \"the buy should go through\");\n        assertGt(hook.pendingEthFees(), 0, \"the fee should be kept as a claim for later redemption\");\n    }\n}","reproduction":"State: real PoolManager with full-range liquidity 1e22 and 1.1 ETH (manager holds ~1.1 ETH so the direct-pay branch is taken); BMCHook deployed with feeRecipient = 0x83027AD9d0D5374335086c539dADD1db983441B0; vm.etch that address with a contract whose receive() reverts (stands in for the delegated wallet after a re-delegation). Call 1: buyer -> BMCSwapRouter.buyExactIn{value: 0.1 ether}(key, 0). Expected: trade completes, buyer receives BMC, hook.pendingEthFees() == 1e15 kept as a claim. Actual: reverts with WrappedError(hook, beforeSwap selector, NativeTransferFailed('no ETH'), HookCallFailed). Call 2: buyer -> buyExactOut{value: 1 ether}(key, 1_000_000e18): same revert from afterSwap. Call 3: with pendingEthFees() > 0, hook.collect() reverts the same way. Mainnet evidence: eth_getCode(0x83027AD9d0D5374335086c539dADD1db983441B0) = 0xef0100000000005c84f8fd50b21cac312528a64437030e. Run: forge test --match-path test/scratch/RecipientRejectsEth.t.sol (both tests fail on the current code).","severity":"medium","snippet":"            poolManager.take(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, fee);","title":"Buy fee is pushed to the immutable recipient inside the swap with no fallback; the brief's wallet is an EIP-7702 delegated account, so if it ever rejects ETH every buy and collect() reverts permanentl"},{"citation":"resolved","description":"The brief requires the connected wallet to 'Approve (exact amount) then buy/sell'. The sell submit handler (site/app.js:201-223) first calls quoteSell, which does routerRead.sellExactIn.staticCall(key, tokensIn, 0, {from: account}) (line 163). BMCSwapRouter.sellExactIn begins with IERC20Minimal(token).transferFrom(msg.sender, address(this), tokensIn) (src/BMCSwapRouter.sol:70) and BMCToken is OpenZeppelin ERC20 5.6.1, whose _spendAllowance reverts with ERC20InsufficientAllowance(router, 0, tokensIn) when the allowance is below tokensIn. The eth_call therefore reverts for every wallet that has not already approved at least tokensIn, the exception is caught by the handler's catch and 'Sell failed: ...' is shown; lines 209-214 (the allowance check and the approve transaction) are never reached. The live quote while typing (line 179) fails the same way, showing 'Could not quote: ...'. The approve-then-sell flow only works for a wallet that approved the router by other means, which defeats the exact-amount approval the brief and the button label ('Approve exact amount, then sell') promise. Fix: check the allowance and approve first, then quote; or quote through a path that does not pull tokens (a view quoter, or eth_call with a state override on the allowance slot).","line":206,"path":"site/app.js","reproduction":"Launched config (token/hook/router set in site/config.json). Connect a wallet holding 1,000 BMC with allowance(wallet, router) == 0. Type 1000 in the sell box: the quote line shows 'Could not quote: ... ERC20InsufficientAllowance'. Click 'Approve exact amount, then sell'. Expected: 'Step 1/2: approve exactly the amount you are selling' followed by an approve transaction for 1000e18, then the sale. Actual: quoteSell at line 206 throws before line 209 runs, 'Sell failed: ...' is shown, no approval is requested and no sale is possible from the site. Contract side reproduced in test/scratch/Repro.t.sol test_sellQuoteWithoutAllowanceReverts: a staticcall from a holder with no allowance to router.sellExactIn(key, 1000e18, 0) fails with revert data 0xfb8f41b2 (ERC20InsufficientAllowance) ... allowance 0, needed 1000e18.","severity":"medium","snippet":"      const expected = await quoteSell(tokensIn);","title":"Site sell flow quotes with a staticcall to sellExactIn before approving, so a wallet without a prior allowance can never sell from the site"},{"citation":"resolved","description":"sellExactOut (src/BMCSwapRouter.sol:79-91) checks only tokensIn <= maxTokensIn and buyExactOut (57-64) checks nothing about the delivered amount, while the exact-input functions enforce a minimum output (lines 51, 73). The router pins sqrtPriceLimitX96 to MIN+1 / MAX-1 (line 124), so when liquidity on the output side runs out the v4 swap stops with amountSpecifiedRemaining > 0 and the router returns as if filled: the caller receives less than ethOut / tokensOut (down to nothing), is charged for what was delivered plus the hook fee, and Swapped is emitted with the requested amount. The NatSpec promises 'Receive exactly ethOut ETH' / 'Buy exactly tokensOut tokens'. On the launch pool this is reachable whenever a sale exceeds the ETH buyers have put in; before any buy a sellExactOut delivers 0 and walks the pool's recorded tick from 184216 to 887271 at no cost to the caller, which any price-reading integration then sees. No funds are lost beyond the trade being smaller than asked (the input bound still holds); the website only uses the exact-input entry points. Merged from audit_permissions, audit_math, write_foundry_tests, audit_economics and audit_flow. Fix: in sellExactOut require uint256(uint128(delta.amount0())) == ethOut and in buyExactOut uint256(uint128(delta.amount1())) == tokensOut (revert with a PartialFill error), and emit the delivered amounts.","line":87,"path":"src/BMCSwapRouter.sol","reproduction":"State: launch-shaped pool (BMC-only liquidity 1e22 on [-887220, 184200], no ETH yet), seller holds BMC and approved the router. Call 1: seller -> sellExactOut(key, 0.01 ether, 10_000_000e18). Expected: revert (nothing can be delivered) or 0.01 ETH delivered. Actual: returns tokensIn = 0, seller's ETH balance unchanged, Swapped(seller, false, 1e16, 0) emitted, slot0 tick moves 184216 -> 887271. Call 2: after buyer -> buyExactIn{value: 0.1 ether}(key, 0), seller -> sellExactOut(key, 1 ether, 100_000_000e18). Expected: revert or exactly 1 ETH. Actual: returns tokensIn = 9_094_598_646_275_693_992_300_158, seller receives 97_762_499_999_999_999 wei, no revert. Call 3: a single 60-tick BMC-only position just below the opening tick; buyer -> buyExactOut{value: 1 ether}(key, 1_000_000e18). Expected: exactly 1_000_000e18 BMC or revert. Actual: returns ethIn = 3_075_269_516_491_940 wei and delivers 299_295_711_229_260_130_699_406 BMC. Run: forge test --match-path test/scratch/Repro.t.sol --match-test 'ExactOut' -vv.","severity":"low","snippet":"        tokensIn = uint256(uint128(-delta.amount1()));","title":"BMCSwapRouter exact-output functions return normally when the pool delivers less than the exact amount requested, and the Swapped event reports the requested amount"},{"citation":"resolved","description":"For exact-input swaps beforeSwap takes 1% of the total input the swapper sends and the pool receives the other 99%. For exact-output swaps afterSwap (src/BMCHook.sol:201-215) computes the fee on ethIn / tokenIn, the pool's own charge, and adds it on top via afterSwapReturnDelta, so the swapper's total input is poolIn + poolIn/100 and the fee is poolIn/(1.01*poolIn) = 0.9901% of the input side. The same holds for the sell burn on exact-output sells. Two routes to the same economic trade therefore pay different fee totals (about 0.01% of volume apart, not compounding). The author's own test test_buyExactOutput_feeIsOnePercentOfTheEthThePoolCharged asserts fee == poolEth / 100, documenting the 1/101 choice. Merged from audit_math. Fix: compute fee = poolIn * buyFeeBps / (BPS - buyFeeBps) (and the same for tokenIn) in afterSwap so both shapes pay 1% of the total, or document that exact-output trades pay 1/101.","line":206,"path":"src/BMCHook.sol","reproduction":"State: funded pool (full-range liquidity 1e22, 1.1 ETH). Call: buyer -> BMCSwapRouter.buyExactOut{value: 1 ether}(key, 1_000_000e18). Actual (test/scratch/Repro.t.sol test_feeBaseDiffersByShape): total ETH paid 10_331_159_698_248_307 wei, pool received 10_228_870_988_364_661 wei, wallet received 102_288_709_883_646 wei = 99 bps of the total. Call: buyer -> buyExactIn{value: 10_331_159_698_248_307}(key, 0): wallet receives 103_311_596_982_483 wei = 100 bps of the total. Expected: equal rates for the same ETH side; actual: 0.990% vs 1.000%.","severity":"low","snippet":"            fee = (ethIn * buyFeeBps) / BPS;","title":"Fee base differs by swap shape: exact-output trades pay 1/101 (0.990%) of the input side while exact-input trades pay 1/100"},{"citation":"resolved","description":"The one-time attachment (src/BMCHook.sol:145-157) is first-come-first-served: the sender argument is unnamed and unchecked, and the only checks are poolInitialized, currency0 == native ETH and currency1 == token. The launch factory deploys the hook and calls PoolManager.initialize in the same transaction, so there is no window in the documented flow and this is a hardening note, not an exploit of the launch as described. If the two steps are ever separated (a rehearsal with script/Deploy.s.sol followed by a later initialize, a redeploy, or a factory change), anyone who sees the hook land can call PoolManager.initialize with the same currencies but their own fee (e.g. 3000 or the dynamic-fee flag), tick spacing and opening price; poolInitialized becomes true, the factory's own initialize at fee 12500 / spacing 60 reverts with AlreadyInitialized, and the only pool carrying the launch hook is the stranger's. The launch reference asks a hook that checks its initializer to take the factory as a constructor argument written '$factory'. Merged from audit_permissions. Fix (no behaviour change for the factory): add an immutable factory argument ('$factory' in launch.json) and require sender == factory in beforeInitialize, and/or pin the manifest's fee and tick spacing.","line":145,"path":"src/BMCHook.sol","reproduction":"State: BMCHook deployed at a flagged address (HookDeployer.deploy), pool not initialized. Call 1: from any EOA, manager.initialize({currency0: 0x0, currency1: token, fee: 3000, tickSpacing: 1, hooks: hook}, 1<<96). Expected: refused. Actual: succeeds and hook.poolInitialized() == true. Call 2: from the launch factory, manager.initialize({currency0: 0x0, currency1: token, fee: 12500, tickSpacing: 60, hooks: hook}, 10000<<96). Actual: reverts (AlreadyInitialized). Run: forge test --match-path test/scratch/Repro.t.sol --match-test test_openInitializer. Precondition: deployment and initialization in separate transactions.","severity":"low","snippet":"    function beforeInitialize(address, PoolKey calldata key, uint160)","title":"beforeInitialize ignores the initializer and accepts any fee, tick spacing and price, so the hook attaches to the first pool it sees if deployment and initialization are ever separate transactions"},{"citation":"resolved","description":"deploy() mines the salt with HookDeployer.mine(address(this), ...) and deploys with new BMCHook{salt: salt}, expecting the script contract to be the CREATE2 deployer. Under vm.startBroadcast forge rejects any use of address(this) in a script contract, so run() reverts right after deploying the token. Even without that guard, a broadcast CREATE2 goes through the default CREATE2 deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C), so the predicted address and its flag bits would not match and the hook constructor's validateHookPermissions would revert. The README calls the script a fork/local rehearsal and test/Deploy.t.sol calls deploy() directly, which works, but run() as written can never rehearse against a live or local node. The launch itself is performed by the IMD factory from launch.json, so this does not block deployment. Merged from audit_flow. Fix: mine the salt against the CREATE2 deployer the broadcast uses (or deploy through a small on-chain factory contract whose address is known) and avoid address(this) in the broadcast path.","line":43,"path":"script/Deploy.s.sol","reproduction":"Command on a local anvil (port 8599, default funded key 0): POOL_MANAGER=0x000000000004444c5dc75cB358380D2e3dE08A90 FEE_RECIPIENT=0x83027AD9d0D5374335086c539dADD1db983441B0 forge script script/Deploy.s.sol:DeployBMC --rpc-url http://127.0.0.1:8599 --broadcast --private-key <anvil key 0>. Expected: token, hook and router deployed and logged. Actual (reproduced here with forge 1.8.3): DeployBMC::run() traces new BMCToken, then 'Error: script failed: Usage of `address(this)` detected in script contract. Script contracts are ephemeral and their addresses should not be relied upon.'","severity":"low","snippet":"        (bytes32 salt, address predicted) = HookDeployer.mine(address(this), creationCode, flags, 500_000);","title":"DeployBMC.run() cannot broadcast: the hook salt is mined against address(this) of the script contract, which forge refuses under broadcast and which is not the CREATE2 deployer anyway"},{"citation":"resolved","description":"launch.json writes pool.initialPrice = 79228162514264337593543950336 = 2^96, which as sqrtPriceX96 is a price of 1 token1 per token0, i.e. 1 BMC = 1 ETH and a market cap of 1e9 ETH for the 1e9 supply. The brief asks for a pool 'opening at a 10 ETH market cap', which for 1e9 BMC against ETH is 1e8 BMC per ETH, sqrtPriceX96 = 10000<<96 = 792281625142643375935439503360000 (the value every test in test/ and the fixture use as OPENING_SQRT_PRICE). The README says 'the deployer derives it from a 10 ETH market cap for the whole supply', and the launch reference's own example manifest for this launch's pool carries the same 2^96 value, so this is most likely a placeholder the deployer overrides and not a defect in the code; it is recorded so the author can confirm that reading. If the deployer used the manifest value literally, the pool would open 1e8 times above the intended price and, with BMC-only liquidity seeded below it, the first buyer would pay 1 ETH per BMC. Not blocking on its own; no code change implied.","line":30,"path":"launch.json","reproduction":"Computation: node -e \"const q=2n**96n; const p=79228162514264337593543950336n; console.log(p===q, (p*p)/(q*q))\" prints 'true 1' (price 1 BMC per ETH). The brief's 10 ETH cap gives price = 1e9 BMC / 10 ETH = 1e8 BMC per ETH, sqrt = 1e4, sqrtPriceX96 = 10000n*2n**96n = 792281625142643375935439503360000n, which is the OPENING_SQRT_PRICE in test/utils/BMCFixture.sol:27. Expected: manifest and tests agree, or the deployer documents that it computes the price; actual: they differ by a factor of 1e4 in sqrt price.","severity":"info","snippet":"    \"initialPrice\": \"79228162514264337593543950336\"","title":"launch.json pool.initialPrice is 2^96 (1 BMC = 1 ETH), while the brief and README describe a 10 ETH opening market cap (sqrtPrice 10000<<96); confirm the deployer derives the price"}],"hash":"16986a820ca1605970ebd09c4c239d2d9265f60f779463f1419a89c208d17498","nodeId":"e0e12860-ed75-450d-a3f2-7033a47cd818","outcome":"completed","summary":"The review is complete. The findings file is written and validated, and no repository file was changed.\n\n**Outcome.** Eight findings survive reproduction, merged from the sixteen specialist reports. The two most severe carry Foundry proofs under test/scratch that fail on the current code.\n\n1. **High, exact-input fee charged on the requested amount.** The hook takes 1% of the typed amount in beforeSwap before the pool fills. On the launch's BMC-only pool, a sale before the first buy returns 0 ETH and still burns 1% of the order, an oversized sale burns about 10% of what sold, and a price-limited buy pays the wallet about 987% of the ETH traded. Five specialists reported this at different severities. I rate it high because any unprivileged trader loses funds and the launch's own seeding makes it the default early state.\n2. **Medium, fee push with no fallback.** The community wallet carries EIP-7702 delegation code on mainnet today, which I confirmed by RPC. If its delegate ever rejects ETH, every buy and every collect reverts permanently. Three specialists reported this. Proof attached.\n3. **Medium, site sell flow.** The quote simulates sellExactIn before approving, so any wallet without a prior allowance gets \"Sell failed\" and never reaches the approve step.\n4. **Low, router exact-output under-delivery.** Both exact-output functions return normally on a short fill and emit the requested amount. Five reports merged.\n5. **Low, exact-output fee base is 1/101.** Confirmed numerically at 99 bps of the total.\n6. **Low, open initializer.** Only matters if deploy and initialize are ever split.\n7. **Low, deploy script cannot broadcast.** Reproduced against a local anvil with the exact forge error.\n8. **Info, manifest opening price.** launch.json opens at 1 BMC per ETH while the brief, README and every test use the 10 ETH market cap price. Flagged for confirmation only, since the reference's own example carries the same value.\n\n**Coverage.** All 16 entry points are answered, plus three invariant rows. Nine hold, seven point at findings, none are unreached. The existing 72-test suite passes as a baseline.\n\n**Dropped.** Nothing from the specialists was dropped outright. Slither's arbitrary-send and unchecked-transfer leads do not reproduce as defects, since the router holds nothing between transactions and the token reverts on failed transfers.","treeHash":null,"usage":{"cachedInputTokens":1366552,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":37831,"runtime":"claude","turns":33,"wallClockMs":680377}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e51ef2afd7c2af8","findings":[{"citation":"resolved","description":"Invariant / Flow-gap (execution x first principles). The brief's rule is 1% of the ETH side of a buy and 1% of the BMC being sold. For exact-input swaps `beforeSwap` computes the fee from `params.amountSpecified` and takes it (or burns it) immediately, before `Pool.swap` runs. Uniswap v4 fills exact-input swaps only up to `sqrtPriceLimitX96` or until liquidity runs out, and then charges the swapper only the consumed part (`swapDelta` uses `amountSpecified - amountSpecifiedRemaining`). The hook never re-reads the consumed amount in `afterSwap` (it returns 0 there for exact input), so the full fee on the typed amount stays. The swapper's net delta becomes consumed + fee, and the fee can be any multiple of the ETH or BMC that actually changed hands. Who loses: the swapper. Who gains: the community wallet (buys) or nobody (sells, tokens are burned). This is reachable by any unprivileged trader in normal use: (a) any router that exposes a price limit (Universal Router, V4Router, PoolSwapTest) lets a trader bound slippage with `sqrtPriceLimitX96`; (b) through the project's own BMCSwapRouter (limits pinned to MIN/MAX) whenever the pool's liquidity range is exhausted, which on the launch pool is the state at launch: the factory seeds BMC only, below the opening price, so there is no ETH-side liquidity above it and every exact-input sell before the first buy fills zero, pays 0 ETH and still burns 1% of the seller's input (verified: `sellExactIn(key, 1_000_000e18, 0)` on a token-only pool returns 0 ETH and burns 10,000 BMC; the swarm and launcher wallets hold 12% of supply at that moment). The README acknowledges the behaviour for exact input but states it does not arise with deep liquidity; the launch state and price-limited routers show it does. Fix (author's choice, each keeps the fee rule): in `afterSwap` for exact-input swaps compare the pool's consumed specified amount (`-delta.amount0()` on buys, `-delta.amount1()` on sells) with `amountIn - fee` and revert (e.g. `PartialFillNotSupported`) when the fill was short so the trader is never over-charged; or restructure so the fee is computed from the actual fill (for buys: take the fee in `afterSwap` from the consumed ETH is not possible on the specified side in v4, so the revert, or a documented requirement that exact-input swaps use a MIN/MAX price limit on a pool with unbounded-range liquidity, is the practical option). Whatever is chosen, the test below passes when the fee is <= ~1% of the ETH/BMC that really went to the pool or when the partial fill is refused.","line":175,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @notice Exact-input swaps are charged 1% of the amount the swapper *specified* in `beforeSwap`, before the\n/// pool fills. When the fill stops early (price limit hit, liquidity exhausted) the pool consumes only part of\n/// the input, but the whole fee stays: the community wallet receives far more than 1% of the ETH side, and the\n/// sell burn destroys far more than 1% of the BMC actually sold.\ncontract PartialFillFeeTest is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n    int24 constant MIN_USABLE_TICK = -887_220;\n    int24 constant MAX_USABLE_TICK = 887_220;\n\n    address feeRecipient = makeAddr(\"communityWallet\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    PoolModifyLiquidityTest lpRouter;\n    PoolSwapTest swapRouter;\n    PoolKey key;\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode, abi.encode(address(manager), address(token), feeRecipient, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(creationCode, BMC_HOOK_FLAGS));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        // Full-range, two-sided liquidity: about 1 ETH and 100,000,000 BMC at the opening price.\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(address(this), 10 ether);\n        lpRouter.modifyLiquidity{value: 1.1 ether}(\n            key, ModifyLiquidityParams(MIN_USABLE_TICK, MAX_USABLE_TICK, int256(1e22), 0), \"\"\n        );\n        vm.deal(trader, 10 ether);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n    }\n\n    /// Buy 1 ETH exact-input with a price limit 0.1% below the current price (any router that exposes\n    /// `sqrtPriceLimitX96`, e.g. the Universal Router, lets a trader set this). The pool consumes about\n    /// 0.001 ETH; the trader is charged a 0.01 ETH fee: 1% of what was *typed*, roughly 1000% of what was traded.\n    function test_exactInputBuy_partialFill_feeIsOnePercentOfTheEthActuallyTraded() public {\n        uint256 managerEthBefore = address(manager).balance;\n        uint160 limit = uint160((uint256(OPENING_SQRT_PRICE) * 999) / 1000);\n        SwapParams memory params = SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: limit});\n\n        vm.prank(trader);\n        try swapRouter.swap{value: 1 ether}(key, params, PoolSwapTest.TestSettings(false, false), \"\") returns (\n            BalanceDelta delta\n        ) {\n            uint256 paidByTrader = uint256(uint128(-delta.amount0()));\n            uint256 feePaid = feeRecipient.balance + hook.pendingEthFees();\n            uint256 ethToPool = address(manager).balance - managerEthBefore - hook.pendingEthFees();\n            assertEq(paidByTrader, ethToPool + feePaid, \"trader paid the pool's ETH plus the fee\");\n            assertLt(ethToPool, 0.01 ether, \"the fill really was partial (far less than the 1 ETH typed)\");\n            // The rule is 1% of the ETH side of the trade. Allow 2% of the ETH that actually went to the pool.\n            assertLe(feePaid, (ethToPool * 2) / 100, \"fee must be ~1% of the ETH actually traded, not of the input typed\");\n        } catch {\n            // A hook that refuses partial exact-input fills instead of over-charging also satisfies the rule.\n            assertEq(feeRecipient.balance + hook.pendingEthFees(), 0, \"a refused trade pays no fee\");\n        }\n    }\n\n    /// Sell 1,000,000 BMC exact-input with a price limit 0.1% above the current price. The pool takes about\n    /// 100,000 BMC; 10,000 BMC are burned: 1% of what was typed, about 10% of what was actually sold.\n    function test_exactInputSell_partialFill_burnIsOnePercentOfTheTokensActuallySold() public {\n        uint256 supplyBefore = token.totalSupply();\n        uint256 managerTokensBefore = token.balanceOf(address(manager));\n        uint160 limit = uint160((uint256(OPENING_SQRT_PRICE) * 1001) / 1000);\n        SwapParams memory params =\n            SwapParams({zeroForOne: false, amountSpecified: -int256(1_000_000 ether), sqrtPriceLimitX96: limit});\n\n        vm.prank(trader);\n        try swapRouter.swap(key, params, PoolSwapTest.TestSettings(false, false), \"\") returns (BalanceDelta delta) {\n            uint256 paidByTrader = uint256(uint128(-delta.amount1()));\n            uint256 burned = (supplyBefore - token.totalSupply()) + hook.pendingBurns();\n            uint256 tokensToPool = token.balanceOf(address(manager)) - managerTokensBefore - hook.pendingBurns();\n            assertEq(paidByTrader, tokensToPool + burned, \"trader paid the pool's BMC plus the burn\");\n            assertLt(tokensToPool, 500_000 ether, \"the fill really was partial\");\n            assertLe(burned, (tokensToPool * 2) / 100, \"burn must be ~1% of the BMC actually sold, not of the input typed\");\n        } catch {\n            assertEq(token.totalSupply() + hook.pendingBurns(), supplyBefore, \"a refused trade burns nothing\");\n        }\n    }\n}","reproduction":"State: real PoolManager, BMCToken, BMCHook(buy 100 bps, sell 100 bps) at a mined address, pool ETH/BMC fee 12500 spacing 60 opened at sqrtPrice 10_000<<96, full-range liquidity 1e22 (about 1 ETH + 1e8 BMC). Call 1 (buy): trader swaps zeroForOne, amountSpecified = -1e18, sqrtPriceLimitX96 = 0.999 * current price, via v4-core PoolSwapTest. Actual: pool consumes 1_013_671_899_747_850 wei (~0.00101 ETH); community wallet receives 10_000_000_000_000_000 wei (0.01 ETH); trader pays 0.01101 ETH; fee is ~987% of the ETH traded. Expected: fee ~1% of 0.00101 ETH = ~0.0000101 ETH, or the swap refused. Call 2 (sell): trader swaps oneForZero, amountSpecified = -1_000_000e18, sqrtPriceLimitX96 = 1.001 * current price. Actual: pool takes 101_265_822_784_810_126_582_278 (~101,266 BMC), 10_000e18 BMC burned (1% of the typed amount, ~9.9% of the BMC sold). Expected: ~1,013 BMC burned. Call 3 (launch state): token-only liquidity below the opening tick, no ETH in the pool; seller calls BMCSwapRouter.sellExactIn(key, 1_000_000e18, 0). Actual: returns 0 ETH, totalSupply falls by 10_000e18, seller keeps 990_000e18. Expected: nothing burned when nothing was sold. Run: forge test --match-path test/scratch/PartialFillFee.t.sol (both tests fail on the current code).","severity":"medium","snippet":"        uint256 amountIn = uint256(-params.amountSpecified);\n        uint256 fee;\n        if (params.zeroForOne) {\n            // Buy: ETH is the specified input.\n            fee = (amountIn * buyFeeBps) / BPS;\n            if (fee > 0) _takeEthFee(fee);\n        } else {\n            // Sell: BMC is the specified input.\n            fee = (amountIn * sellBurnBps) / BPS;\n            if (fee > 0) _takeAndBurn(fee);\n        }\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(fee.toInt128(), 0), 0);","title":"Exact-input fee and burn are 1% of the amount typed, not of the amount traded: partial and zero fills over-charge the swapper"},{"citation":"resolved","description":"Economic security / Flow-gap (periphery x first principles). `_takeEthFee` calls `poolManager.take(ETH, feeRecipient, fee)` whenever the manager's ETH balance covers the fee, which on the shared mainnet PoolManager is always. `take` does `call(gas(), to, amount)` and reverts the whole swap with `NativeTransferFailed` if the recipient refuses. The claim path (`mint`) is reached only when the manager is short of ETH, never when the transfer fails, and `collect()`/`unlockCallback` use the same `take`, so nothing can redeem claims either. The recipient is immutable, so the condition is permanent for as long as the recipient's code rejects ETH; the brief's guarantee that the hook 'does not block or alter swaps otherwise' and the launch's sell-only utility (sells still work) are broken. The README assumes 'the community wallet is an externally owned account'. It is not a plain EOA: on Ethereum mainnet today 0x83027AD9d0D5374335086c539dADD1db983441B0 carries EIP-7702 delegation code 0xef0100000000005c84f8fd50b21cac312528a64437030e (eth_getCode, 2026-10-11, publicnode), i.e. a smart-account implementation at 0x000000005c84f8fd50b21cac312528a64437030e runs whenever it is called. A bare 1 wei eth_call to it succeeds right now (gas ~21k), so launch-day buys would work, but the account owner can re-delegate at any time (any wallet upgrade, a different account provider, or a delegate whose receive is restricted) and the hook has no way to cope. The reference for this launch kind names exactly this failure: 'a direct transfer to a contract that rejects ETH halts every fee-bearing swap, and a claim does not'. Trust note: the trigger sits with the recipient (or its wallet provider), so this is reported as a broken guarantee under a reachable state rather than theft; it cannot be exploited for profit by an outsider, but it bricks the buy side for every trader and is unfixable after deployment. A secondary consequence of the same seam: the push runs the recipient's code while the PoolManager is unlocked and before `Pool.swap` on exact-input buys, so a delegate could re-enter and trade ahead of the buyer inside the buyer's own transaction; with a benign delegate this is moot. Fix that preserves the design (ETH still sent during the swap when possible): wrap the direct payment in try/catch and fall back to the claim: `try poolManager.take(ADDRESS_ZERO, feeRecipient, fee) { totalEthFeesPaid += fee; } catch { poolManager.mint(address(this), ADDRESS_ZERO.toId(), fee); }` (a reverted `take` leaves no delta behind because the manager's transient accounting reverts with the call); do the same in `unlockCallback` so `collect` keeps the claim instead of reverting; and correct the README's EOA assumption.","line":225,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @dev Stands in for the community wallet once its code no longer accepts plain ETH. The launch's recipient\n/// 0x83027AD9d0D5374335086c539dADD1db983441B0 is an EIP-7702 delegated account on mainnet (code 0xef0100…),\n/// so what runs when the hook pushes ETH to it is whatever contract it is delegated to at that moment.\ncontract RejectingWallet {\n    fallback() external payable {\n        revert(\"no plain ETH\");\n    }\n}\n\n/// @notice When the PoolManager holds ETH (always, on mainnet) the hook pushes the buy fee with `take`, which\n/// reverts the whole swap if the recipient refuses ETH. There is no fallback to the claim path, so every buy\n/// on the pool reverts for as long as the recipient's code rejects ETH, and `collect()` reverts too.\ncontract RecipientRejectsEthTest is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n    int24 constant MIN_USABLE_TICK = -887_220;\n    int24 constant MAX_USABLE_TICK = 887_220;\n\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolModifyLiquidityTest lpRouter;\n    RejectingWallet recipient;\n    PoolKey key;\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        recipient = new RejectingWallet();\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), address(recipient), uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(creationCode, BMC_HOOK_FLAGS));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(address(this), 10 ether);\n        lpRouter.modifyLiquidity{value: 1.1 ether}(\n            key, ModifyLiquidityParams(MIN_USABLE_TICK, MAX_USABLE_TICK, int256(1e22), 0), \"\"\n        );\n        vm.deal(trader, 10 ether);\n    }\n\n    /// The manager holds about 1 ETH, so the hook takes the fee directly; the recipient reverts; the buy reverts.\n    /// Expected: the trade goes through and the fee is parked as an ERC-6909 claim (`pendingEthFees`).\n    function test_buyStillSucceedsWhenTheRecipientRejectsEth() public {\n        assertGt(address(manager).balance, 0.001 ether, \"the manager can cover the fee, so the direct path is taken\");\n        uint256 supplyBefore = token.totalSupply();\n\n        vm.prank(trader);\n        (bool ok, bytes memory ret) =\n            address(router).call{value: 0.1 ether}(abi.encodeCall(BMCSwapRouter.buyExactIn, (key, 0)));\n        assertTrue(ok, string(abi.encodePacked(\"the buy reverted because the fee recipient refused ETH: \", ret)));\n\n        assertGt(token.balanceOf(trader), 0, \"the trader received BMC\");\n        assertEq(hook.pendingEthFees(), 0.001 ether, \"the 1% fee is held as a claim instead of blocking the swap\");\n        assertEq(token.totalSupply(), supplyBefore, \"buys burn nothing\");\n    }\n\n    /// Exact-output buys take the same `take` path in `afterSwap`.\n    function test_exactOutputBuyStillSucceedsWhenTheRecipientRejectsEth() public {\n        vm.prank(trader);\n        (bool ok,) =\n            address(router).call{value: 1 ether}(abi.encodeCall(BMCSwapRouter.buyExactOut, (key, 1_000_000 ether)));\n        assertTrue(ok, \"the exact-output buy reverted because the fee recipient refused ETH\");\n        assertGt(hook.pendingEthFees(), 0, \"the fee is held as a claim\");\n    }\n}","reproduction":"State: real PoolManager with full-range liquidity (manager holds ~1.1 ETH), BMCHook deployed with feeRecipient = a contract whose fallback reverts (stands in for the community wallet once delegated to code that rejects plain ETH). Call: trader -> BMCSwapRouter.buyExactIn{value: 0.1 ether}(key, 0). Actual: reverts (bubbled NativeTransferFailed wrapping 'no plain ETH'); the trader cannot buy at all. Same for buyExactOut{value: 1 ether}(key, 1_000_000e18), and hook.collect() reverts whenever pendingEthFees() > 0. Expected: the trade completes, trader receives BMC, and the 0.001 ETH fee is held as an ERC-6909 claim (pendingEthFees() == 1e15) for later redemption. Run: forge test --match-path test/scratch/RecipientRejectsEth.t.sol (both tests fail on the current code). Mainnet evidence: eth_getCode(0x83027AD9d0D5374335086c539dADD1db983441B0) = 0xef0100000000005c84f8fd50b21cac312528a64437030e.","severity":"medium","snippet":"        if (address(poolManager).balance >= fee) {\n            poolManager.take(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, fee);\n            totalEthFeesPaid += fee;\n            emit BuyFeeTaken(fee, true);","title":"Buy fee is pushed to the recipient with no fallback; the community wallet is an EIP-7702 delegated account, so if its code ever rejects ETH every buy on the pool reverts and claims cannot be collected"},{"citation":"resolved","description":"Invariant (interface guarantee vs execution). `buyExactOut(key, tokensOut)` and `sellExactOut(key, ethOut, maxTokensIn)` promise 'buy exactly tokensOut' / 'receive exactly ethOut'. The router pins `sqrtPriceLimitX96` to MIN+1/MAX-1 and never compares `delta.amount1()` (resp. `delta.amount0()`) with the requested amount. When the pool's liquidity range is exhausted the swap stops short, the trader receives less, no revert happens, and `Swapped` is emitted with the requested `tokensOut`/`ethOut` rather than the delivered amount. The only guard that still applies is the input bound (msg.value / maxTokensIn), so the trader is not over-charged, but the exact-output contract and the site's 'Bought N' message are wrong, and a reader of the event or of the return value cannot tell. Fix: after the swap, `if (uint256(uint128(delta.amount1())) != tokensOut) revert PartialFill(received, tokensOut);` in buyExactOut, and the mirror check on `delta.amount0()` in sellExactOut; emit the delivered amounts.","line":60,"path":"src/BMCSwapRouter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @notice `buyExactOut` / `sellExactOut` never check that the pool delivered the requested output. With a\n/// bounded liquidity range the swap stops when it is exhausted, returns less, and the router reports success.\ncontract RouterExactOutputTest is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n\n    address feeRecipient = makeAddr(\"communityWallet\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode, abi.encode(address(manager), address(token), feeRecipient, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(creationCode, BMC_HOOK_FLAGS));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        // Tokens only, in a single 60-tick range just below the opening price.\n        int24 tick = TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE);\n        int24 upper = (tick / TICK_SPACING) * TICK_SPACING;\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(upper - TICK_SPACING, upper, int256(1e22), 0), \"\");\n        vm.deal(trader, 10 ether);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(router), type(uint256).max);\n    }\n\n    function test_buyExactOutDeliversLessThanRequestedWithoutReverting() public {\n        uint256 before = token.balanceOf(trader);\n        vm.prank(trader);\n        uint256 ethIn = router.buyExactOut{value: 1 ether}(key, 1_000_000 ether);\n        uint256 got = token.balanceOf(trader) - before;\n        emit log_named_uint(\"requested BMC\", 1_000_000 ether);\n        emit log_named_uint(\"received  BMC\", got);\n        emit log_named_uint(\"ETH paid\", ethIn);\n        assertEq(got, 1_000_000 ether, \"exact-output buy must deliver exactly the requested amount or revert\");\n    }\n}","reproduction":"State: pool opened at 10_000<<96 with a single 60-tick token-only position (1e22 liquidity) just below the opening tick; trader has 10 ETH. Call: trader -> BMCSwapRouter.buyExactOut{value: 1 ether}(key, 1_000_000e18). Actual: returns ethIn = 3_075_269_516_491_940 wei, the trader receives 299_295_711_229_260_130_699_406 BMC (about 299,296 instead of 1,000,000), no revert, Swapped(trader, true, ethIn, 1_000_000e18). Expected: either exactly 1_000_000e18 BMC delivered or a revert. Run: forge test --match-path test/scratch/RouterExactOutput.t.sol.","severity":"low","snippet":"        BalanceDelta delta = _swap(key, true, int256(tokensOut));\n        ethIn = uint256(uint128(-delta.amount0()));","title":"Router exact-output swaps do not enforce the requested output: on a partial fill the trader silently receives less and the event reports the requested amount"}],"hash":"1a9a4916bffb7304fed6be9446a1a842c6fab776700c0f99c0808489b328f23b","nodeId":"51399287-ea11-46a7-bca5-7bd63274c11e","outcome":"completed","summary":"The findings file is written and validated. Final recap:\n\n**Scope covered.** I applied the Economic Security, Invariant and Flow Gap guides to `BMCHook`, `BMCSwapRouter` and `BMCToken`, traced every listed entry point, and ran the repo's 48 tests (all pass). Coverage has 20 rows: all 16 entry points plus four invariants I checked. Nothing was left `unreached`.\n\n**Findings, written to `.imd-findings.json` with proofs under `test/scratch/`:**\n\n1. **Medium, hook `beforeSwap`.** Exact-input fees are computed from the amount the trader typed and taken before the pool fills. On a partial fill the trader still pays the whole fee. With a 0.1% price limit, a 1 ETH buy traded about 0.001 ETH and paid 0.01 ETH to the community wallet. On the launch pool, which is seeded with BMC only, any sell before the first buy fills nothing and still burns 1% of the seller's tokens. Proof test fails on current code and accepts either a proportional fee or a refused partial fill.\n\n2. **Medium, hook `_takeEthFee` / `collect`.** The fee is pushed with `take`, which reverts the whole buy if the recipient refuses ETH, and there is no fallback to the claim path. I checked mainnet: the community wallet is an EIP-7702 delegated account, not the plain EOA the README assumes. The push works today, but the account owner can re-delegate at any time, and the recipient is immutable. Fix is a try/catch fallback to minting the claim, in both the swap path and `collect`.\n\n3. **Low, router `buyExactOut` / `sellExactOut`.** The requested output is never compared with what the pool delivered. A 1,000,000 BMC request against a narrow range returned about 299,296 BMC with no revert, and the event reported the requested amount.\n\n**Not reported.** Slither's arbitrary-send and unchecked-transfer leads are safe here: refunds go to `msg.sender`, and BMC is OpenZeppelin ERC20 which reverts on failure. The exact-in versus exact-out fee base differs by about 1% of the fee, which is dust. Hook delta accounting nets to zero on every path, and all ERC-6909 claims are backed by the swapper's own settlement.","treeHash":null,"usage":{"cachedInputTokens":1406226,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":55403,"runtime":"claude","turns":38,"wallClockMs":869341}},{"artifacts":[],"attempt":1,"bundleHash":"deb4a5badfccbf2327e865fa40352816af5c9c531490d97bf88d1bbd01f85672","device":"47f3603854a893a3","findings":[{"description":"_takeEthFee sends the buy fee with poolManager.take(ETH, feeRecipient, fee) inside the swap whenever the manager's balance covers it. The brief's community wallet 0x83027AD9d0D5374335086c539dADD1db983441B0 is not a plain EOA on Ethereum mainnet: eth_getCode returns 0xef0100000000005c84f8fd50b21cac312528a64437030e, an EIP-7702 delegation to 0x000000005c84f8fd50b21cac312528a64437030e. It accepts ETH today (an eth_call sending 1 wei with empty calldata succeeds), but its holder can re-delegate at any time to code that reverts on receiving ETH or uses up the forwarded gas. The NativeTransferFailed revert then propagates out of beforeSwap/afterSwap and every buy on the launch pool reverts. Sells still work, so holders can only exit and nobody can enter. The hook is immutable and cannot be repointed. The README says the wallet is an externally owned account, which no longer holds. collect() also pushes ETH to the same address, so the claim path offers no fallback either.","line":226,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\ncontract RejectsEth {\n    receive() external payable {\n        revert(\"no ETH\");\n    }\n}\n\n/// The brief's community wallet 0x83027AD9d0D5374335086c539dADD1db983441B0 is, on mainnet today, an\n/// EIP-7702 delegated account (code 0xef0100 || 0x000000005c84f8fd50b21cac312528a64437030e). Its\n/// holder can re-delegate it to code that reverts on receiving ETH (or burns the call's gas). The hook\n/// pushes the buy fee to it inside the swap, so from then on every buy on the pool reverts and the\n/// immutable hook cannot be repointed. Expected: the buy succeeds and the fee is kept as a claim.\ncontract RejectingRecipientProof is Test {\n    address constant COMMUNITY_WALLET = 0x83027AD9d0D5374335086c539dADD1db983441B0;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n    PoolManager manager;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        BMCToken token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory code = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), COMMUNITY_WALLET, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(code, BMC_HOOK_FLAGS));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        token.approve(address(lp), type(uint256).max);\n        vm.deal(address(this), 2 ether);\n        lp.modifyLiquidity{value: 1.1 ether}(key, ModifyLiquidityParams(-887_220, 887_220, 1e22, 0), \"\");\n        // The wallet's delegate now rejects ETH.\n        vm.etch(COMMUNITY_WALLET, address(new RejectsEth()).code);\n    }\n\n    function test_buyStillWorksWhenTheWalletRejectsEth() public {\n        address buyer = makeAddr(\"buyer\");\n        vm.deal(buyer, 1 ether);\n        vm.prank(buyer);\n        uint256 out = router.buyExactIn{value: 0.1 ether}(key, 0);\n        assertGt(out, 0, \"the buy should go through\");\n    }\n\n    receive() external payable {}\n}","reproduction":"Deploy the hook with feeRecipient = 0x8302...41B0, then seed the pool with full-range liquidity. vm.etch a contract whose receive() reverts at the wallet address, then call router.buyExactIn{value: 0.1 ether}(key, 0). Expected: the buy succeeds and the 0.001 ETH fee is kept as an ERC-6909 claim, for example by minting a claim whenever the transfer fails, or by always minting and letting the recipient pull it. Actual: the transaction reverts with WrappedError(... NativeTransferFailed / 'no ETH').","severity":"medium","title":"Buy fee is pushed to a wallet that is an EIP-7702 delegated account; if it ever rejects ETH, every buy on the pool reverts, permanently"},{"description":"beforeSwap computes the fee from params.amountSpecified before the swap runs, then takes (burns or pays out) that whole amount. The pool may fill less than the specified amount: liquidity on that side can run out, or the price limit can be hit. The swapper still pays the full fee, and only on the portion that actually filled is the fee 1%. On the launch pool this is reachable on day one. The pool is seeded with BMC only, so it holds no ETH until the first buy. Any sale sent before that first buy fills nothing, yet burns 1% of the tokens offered and returns 0 ETH. The same happens to any later sale or buy larger than the liquidity on that side. A minOut > 0 on the router stops it; the site's own slippage setting has to be relied on, and a third-party router sending amountOutMinimum = 0 has no such protection. The brief says the burn is 1% of the tokens being sold and the fee is 1% of the ETH side of the trade, not 1% of the amount requested.","line":183,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// Launch-shaped pool (fresh manager, BMC-only liquidity at the 10 ETH opening price). A sale sent\n/// before the first buy fills nothing (the pool holds no ETH), yet the hook burns 1% of the amount\n/// specified: the seller loses 1% of the tokens and receives 0 ETH. The burn should be 1% of the\n/// BMC actually sold (zero here), not of the amount requested.\ncontract PartialFillFeeProof is Test {\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolKey key;\n    address seller = makeAddr(\"seller\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory code = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), makeAddr(\"communityWallet\"), uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(code, BMC_HOOK_FLAGS));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        token.approve(address(lp), type(uint256).max);\n        int24 upper = (TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE) / 60) * 60;\n        lp.modifyLiquidity(key, ModifyLiquidityParams(-887_220, upper, 1e22, 0), \"\");\n        token.transfer(seller, 1_000_000 ether);\n    }\n\n    function test_saleThatFillsNothingBurnsNothing() public {\n        uint256 supplyBefore = token.totalSupply();\n        vm.startPrank(seller);\n        token.approve(address(router), 1_000_000 ether);\n        uint256 ethOut = router.sellExactIn(key, 1_000_000 ether, 0);\n        vm.stopPrank();\n        assertEq(ethOut, 0, \"precondition: the pool had no ETH to pay\");\n        // Actual: 10,000 BMC burned from the seller for nothing.\n        assertEq(supplyBefore - token.totalSupply(), 0, \"BMC burned on a sale that sold nothing\");\n        assertEq(token.balanceOf(seller), 1_000_000 ether, \"seller lost tokens for 0 ETH\");\n    }\n}","reproduction":"Fresh PoolManager. The pool is initialized at sqrtPriceX96 = 10000<<96 with BMC-only liquidity (1e22, ticks [-887220, opening tick floored to spacing 60]). The seller calls router.sellExactIn(key, 1_000_000e18, 0). Expected: 0 BMC burned, because nothing was sold, and the seller keeps all 1,000,000 BMC. Actual: ethOut = 0, 10,000 BMC is burned from the seller's tokens, and the seller ends with 990,000 BMC.","severity":"low","title":"Exact-input swaps are charged on the full amount specified, even when the pool fills only part of it or nothing at all"},{"description":"sellExactOut(key, ethOut, maxTokensIn) and buyExactOut(key, tokensOut) pass a fixed extreme price limit and never check that the pool's delta matches the requested exact output. When the pool cannot fill the request, the swap stops at the price limit and the router settles whatever happened. The caller receives less than the requested amount, possibly 0, and the call does not revert, although the function names and NatSpec promise an exact output (sellExactOut: 'Receive exactly ethOut ETH'; buyExactOut: 'Buy exactly tokensOut tokens'). The site's buy/sell buttons go through this router. Fix: revert unless uint128(delta.amount0()) == ethOut (or uint128(delta.amount1()) == tokensOut).","line":79,"path":"src/BMCSwapRouter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// BMCSwapRouter.sellExactOut / buyExactOut promise an exact output but never check that the pool\n/// delivered it. On the launch-shaped pool (BMC only, no ETH yet) sellExactOut(0.5 ETH) succeeds and\n/// pays 0 ETH,.\ncontract ExactOutShortfallProof is Test {\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96;\n    PoolManager manager;\n    BMCToken token;\n    BMCSwapRouter router;\n    PoolKey key;\n    address seller = makeAddr(\"seller\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory code = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), makeAddr(\"communityWallet\"), uint256(100), uint256(100))\n        );\n        address hook = HookDeployer.deploy(code, BMC_HOOK_FLAGS);\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(hook));\n        manager.initialize(key, OPENING_SQRT_PRICE);\n        token.approve(address(lp), type(uint256).max);\n        int24 upper = (TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE) / 60) * 60;\n        lp.modifyLiquidity(key, ModifyLiquidityParams(-887_220, upper, 1e22, 0), \"\");\n        token.transfer(seller, 1_000_000 ether);\n    }\n\n    function test_sellExactOutMustDeliverTheExactEthOrRevert() public {\n        vm.startPrank(seller);\n        token.approve(address(router), 1_000_000 ether);\n        uint256 ethBefore = seller.balance;\n        try router.sellExactOut(key, 0.5 ether, 1_000_000 ether) {\n            assertEq(seller.balance - ethBefore, 0.5 ether, \"sellExactOut returned without the exact ETH\");\n        } catch {}\n        vm.stopPrank();\n    }\n}","reproduction":"On the launch-shaped pool (BMC-only liquidity, no ETH yet), the seller calls router.sellExactOut(key, 0.5 ether, 1_000_000e18). Expected: it reverts, or the seller receives exactly 0.5 ETH. Actual: it succeeds and the seller receives 0 ETH. The test/BMCHookInvariant.t.sol handler counts these cases in ghostExactOutShortfalls and does not assert on them.","severity":"low","title":"BMCSwapRouter exact-output functions return successfully without delivering the requested output"}],"hash":"2b2d5cb4583f4c9cb9aff03731cdcaa3b9a0d1fb97709d2b85918c4fabda68c2","nodeId":"d0b011ab-49e1-4d59-922d-ebc7805f8c58","outcome":"completed","summary":"I added 24 tests in two new files and reported three defects in `.imd-findings.json`. `forge build` and `forge test` pass: 72 tests, 0 failed, 0 skipped. Nothing was committed.\n\n**`test/BMCHookEdge.t.sol`** (21 tests) covers the edge and failure cases the existing suite missed:\n- **Rate limits:** a 50% rate is accepted, anything above it is refused (fuzzed), and rates of 0 charge nothing and leave swaps unchanged.\n- **Tiny trades:** under 100 wei pays no fee and doesn't revert; exactly 100 wei pays 1 wei.\n- **Exact-output fees:** fuzzed for buys and sells; the fee is 1% of what the pool charged.\n- **The brief's wallet:** the buy fee reaches `0x83027AD9…41B0`, and also reaches a contract wallet that accepts ETH.\n- **Other pools:** a BMC pool without the hook is unaffected; the hook can't be attached to a second pool, or to a pool on a different PoolManager.\n- **Other routes:** passing extra data to the hook can't switch the fee off; the fee applies through third-party routers and when the swapper takes output as claims.\n- **`collect()`:** it can't be called mid-swap, and calling it twice doesn't pay twice. The hook refuses plain ETH.\n\n**`test/BMCHookInvariant.t.sol`** runs random sequences of buys, sells (both exact-in and exact-out), `collect`, transfers and time jumps from three traders. It runs against three pool setups: liquidity on both sides, BMC-only (how the launch seeds it), and ETH-only. Run counts are set inline (128 runs, depth 60). These hold after every sequence:\n- Each swap charges what the 1% rule says.\n- The wallet's balance equals what the hook reports paying, and paid plus pending equals what the rule charged.\n- Supply only falls, and only by the amount burned.\n- The hook and router keep no ETH or BMC.\n- Pending claims are backed by what the PoolManager holds.\n- All balances add up to the total supply, and the hook's settings never change.\n\n**Findings** (each has a self-contained test that fails on the current code):\n1. **Medium – one wallet change could stop all buys** (`src/BMCHook.sol:226`). On mainnet the community wallet isn't a plain wallet: it's an EIP-7702 account that runs delegated code. It accepts ETH today, but its owner can switch it to code that rejects ETH. Because the hook sends the fee during the swap, every buy would then revert for good; sells would still work, and the hook can't be changed. The README's assumption that the wallet is a plain EOA no longer holds.\n2. **Low – fee charged on the unfilled part of a trade** (`src/BMCHook.sol:183`). For exact-input swaps the hook charges 1% of the full requested amount, even if the pool fills only part of it. On the launch pool, a sell sent before the first buy with no minimum-output limit burns 1% of the seller's tokens and pays 0 ETH.\n3. **Low – \"exact output\" doesn't guarantee the output** (`src/BMCSwapRouter.sol:79`, and `buyExactOut` likewise). If the pool can't fill the request, these functions succeed and deliver less than asked, possibly zero. The invariant run found this; the site's buy/sell buttons use this router.\n\nFor findings 2 and 3, the invariant handler counts the affected trades separately and doesn't check them against the 1% rule. So the suite neither treats this behaviour as correct nor lets it fail the run.","treeHash":"d8068fb33ff12226e0344062848c15c91c84fb2a","usage":{"cachedInputTokens":2228520,"inputTokens":46,"model":"claude-opus-5-5","outputTokens":36221,"runtime":"claude","turns":25,"wallClockMs":537180}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a18a0c6087e1362f","findings":[{"citation":"resolved","description":"For exact-input swaps beforeSwap computes the fee from params.amountSpecified and takes it (burn or ETH) immediately, then returns it as a positive specified delta. The pool fills afterwards, and Pool.swap stops at the price limit when liquidity runs out, so the specified side can be consumed only in part or not at all; the hook never looks at the actual fill. The brief says the hook 'burns 1% of the BMC tokens being sold' and 'takes 1% of the ETH side'. On the launch pool this is not a corner case: the factory seeds BMC only, below the opening price, so the pool holds no ETH until buys have happened, and the ETH available to sellers is only what earlier buys left. A holder (for example one of the 10% swarm airdrop) who sells more than that gets a partial fill, and a holder who sells before any buy gets nothing at all, yet in both cases 1% of the full request is burned and the seller's balance drops by that amount. The website's sell form reaches this path: quoteSell is a staticCall with minEthOut 0, so an empty ETH side quotes 'about 0 ETH' and the sale is submitted with minOut 0. The same mechanism overcharges exact-input buys that stop at a caller-set sqrtPriceLimitX96 (a Universal Router swap with a price limit): 1% of the ETH specified goes to the community wallet even though only part of it was swapped. Exact-output swaps are not affected because afterSwap uses the actual delta. Proposed fix (author's choice): for exact-input swaps, in afterSwap compare the filled specified amount (|delta.amount0()| on buys, |delta.amount1()| on sells; the delta passed to afterSwap is the pool's delta before hook adjustment) with amountIn minus the fee taken; if the fill is short, revert (a swap the pool cannot fill in full is refused rather than overcharged), or re-price the fee against the filled amount and return the surplus to the swapper. The README's 'known behaviour' note is not enough when the launch pool's seeding makes this state the default for every early seller.","line":175,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @notice The launch pool is seeded with BMC only, below the opening price. Until buys put ETH into\n/// it, a sale can only be filled by whatever ETH earlier buys left. BMCHook charges the sell burn in\n/// `beforeSwap` as 1% of the amount *requested*, before the pool fills, so a sale the pool can only\n/// partly (or not at all) absorb still burns 1% of the full request. The brief says the hook \"burns 1%\n/// of the BMC tokens being sold\". The tests below fail on the current code and pass once the burn is\n/// 1% of what the pool actually took, or once a swap that cannot be filled in full is refused.\ncontract PartialFillOverchargeTest is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96; // 1e8 BMC per ETH: 1e9 BMC = 10 ETH\n    int24 constant MIN_USABLE_TICK = -887_220;\n\n    address feeRecipient = makeAddr(\"communityWallet\");\n    address holder = makeAddr(\"airdropHolder\");\n    address buyer = makeAddr(\"buyer\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode, abi.encode(address(manager), address(token), feeRecipient, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(creationCode, BMC_HOOK_FLAGS));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n\n        // The launch seeds BMC only, from the floor up to the opening tick: no ETH in the pool yet.\n        token.approve(address(lpRouter), type(uint256).max);\n        int24 upper = (TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE) / TICK_SPACING) * TICK_SPACING;\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(MIN_USABLE_TICK, upper, int256(1e22), 0), \"\");\n\n        // An airdrop recipient holding BMC, and a buyer with ETH.\n        token.transfer(holder, 50_000_000 ether);\n        vm.deal(buyer, 10 ether);\n        vm.prank(holder);\n        token.approve(address(router), type(uint256).max);\n    }\n\n    /// @dev Burn must be 1% of what the pool actually took from the seller, or the sale must be refused.\n    function _sellAndCheck(uint256 tokensIn) internal {\n        uint256 supplyBefore = token.totalSupply();\n        uint256 holderBefore = token.balanceOf(holder);\n        uint256 poolBefore = token.balanceOf(address(manager));\n\n        vm.prank(holder);\n        try router.sellExactIn(key, tokensIn, 0) returns (uint256 ethOut) {\n            uint256 burned = supplyBefore - token.totalSupply();\n            uint256 soldToPool = token.balanceOf(address(manager)) - poolBefore;\n            emit log_named_uint(\"requested\", tokensIn);\n            emit log_named_uint(\"taken by the pool\", soldToPool);\n            emit log_named_uint(\"eth received\", ethOut);\n            emit log_named_uint(\"burned\", burned);\n            assertEq(holderBefore - token.balanceOf(holder), soldToPool + burned, \"seller paid pool + burn\");\n            assertEq(burned, soldToPool / 100, \"the burn is 1% of the BMC actually sold\");\n        } catch {\n            assertEq(token.totalSupply(), supplyBefore, \"a refused sale burns nothing\");\n            assertEq(token.balanceOf(holder), holderBefore, \"a refused sale takes nothing\");\n        }\n    }\n\n    /// @notice No buy has happened: the pool holds no ETH. The seller receives 0 ETH, sells nothing,\n    /// and still loses 1% of the requested amount to the burn.\n    function test_saleIntoAPoolWithNoEthStillBurnsOnePercent() public {\n        assertEq(address(manager).balance, 0, \"no ETH in the pool before any buy\");\n        _sellAndCheck(1_000_000 ether);\n    }\n\n    /// @notice One 0.1 ETH buy happened. A sale larger than the pool's ETH can absorb is filled only in\n    /// part, but the burn is 1% of the whole request.\n    function test_saleLargerThanThePoolsEthBurnsOnePercentOfTheRequest() public {\n        vm.prank(buyer);\n        uint256 bought = router.buyExactIn{value: 0.1 ether}(key, 0);\n        assertGt(bought, 0);\n        assertLe(bought * 4, token.balanceOf(holder), \"the holder can cover the request\");\n        _sellAndCheck(bought * 4);\n    }\n}","reproduction":"State: real PoolManager; BMCHook(manager, token, wallet, 100, 100); pool ETH/BMC fee 12500 spacing 60 at sqrtPrice 1e4<<96; liquidity 1e22 in [-887220, 184200] (BMC only, as the launch seeds it); no buy yet. Call: holder approves the router and calls BMCSwapRouter.sellExactIn(key, 1_000_000e18, 0). Expected: either 0 BMC burned (nothing was sold) or the call reverts. Actual: the call succeeds, ethOut = 0, token.balanceOf(manager) unchanged, totalSupply drops by 10_000e18 and the holder's balance drops by 10_000e18. Variant: after one buyExactIn{value: 0.1 ether}, sellExactIn(key, 4*bought, 0) is filled for 9_004_553e18 BMC (all the pool's ETH, 0.0977625 ETH) but burns 355_679e18, 3.95% of what was sold instead of 1% (90_045e18). Both are the tests in the attached proof file; they fail on the current code (forge test --match-path test/scratch/PartialFillOvercharge.t.sol).","severity":"medium","snippet":"        uint256 amountIn = uint256(-params.amountSpecified);\n        uint256 fee;\n        if (params.zeroForOne) {\n            // Buy: ETH is the specified input.\n            fee = (amountIn * buyFeeBps) / BPS;\n            if (fee > 0) _takeEthFee(fee);\n        } else {\n            // Sell: BMC is the specified input.\n            fee = (amountIn * sellBurnBps) / BPS;\n            if (fee > 0) _takeAndBurn(fee);\n        }\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(fee.toInt128(), 0), 0);","title":"Exact-input fee/burn is charged on the requested amount before the fill, so a partially filled or unfilled sale still burns 1% of the whole request"},{"citation":"resolved","description":"Once the PoolManager holds at least the fee in ETH (true after the first buy), every buy's fee is pushed to feeRecipient with a plain call inside the swap; CurrencyLibrary.transfer reverts with NativeTransferFailed if the call fails, so the whole buy reverts, and collect() uses the same take, so the claim path cannot pay either. The README justifies this with 'The community wallet is an externally owned account'. On Ethereum mainnet 0x83027AD9d0D5374335086c539dADD1db983441B0 currently carries the code 0xef0100000000005c84f8fd50b21cac312528a64437030e, an EIP-7702 delegation to 0x000000005c84f8fd50b21cac312528a64437030e (checked 2026-10-11 via eth_getCode on a public RPC). A simulated 1 wei call with empty calldata to it succeeds today (eth_estimateGas 21223), so there is no live DoS, but the wallet's owner can re-delegate at any time to code whose receive reverts or burns gas, and from that moment every buy on the pool (and collect) reverts until they change it back; the hook has no fallback and nobody can change the recipient. This is a trust assumption on the recipient rather than an attacker path, recorded because the stated assumption (EOA) is false for the actual launch address and the consequence is a halt of all buys. Fix: never let the push decide the swap: mint the claim when the push would fail (always mint and let collect push with a low-level call that leaves the claim in place on failure), or document that the recipient must keep accepting plain ETH.","line":225,"path":"src/BMCHook.sol","reproduction":"State: same pool as finding 1 but feeRecipient = a contract whose receive() reverts (stand-in for the delegated wallet after a re-delegation). Call 1: buyExactIn{value: 0.1 ether}(key, 0) with the manager holding no ETH: succeeds, pendingEthFees = 0.001 ether. Call 2: buyExactIn{value: 0.1 ether}(key, 0): manager holds 0.1 ether >= fee, hook calls take(ETH, feeRecipient, 0.001 ether), the recipient reverts, the swap reverts with NativeTransferFailed. Call 3: hook.collect(): reverts the same way. Expected: the trade completes and the fee is booked for later; actual: no buy can complete while the manager holds ETH. Reproduced in test/scratch/RecipientRejectsEth.t.sol (expectRevert demonstration).","severity":"low","snippet":"        if (address(poolManager).balance >= fee) {\n            poolManager.take(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, fee);","title":"Direct ETH fee push makes every buy depend on the community wallet accepting plain ETH; the launch recipient is an EIP-7702 delegated account, not the EOA the README assumes"},{"citation":"resolved","description":"sellExactOut promises 'Receive exactly ethOut ETH' and buyExactOut 'Buy exactly tokensOut tokens', but neither checks the delivered side of the delta; only the input side is bounded. When the pool cannot fill the request (no ETH-side liquidity before buys, or liquidity exhausted) the swap stops at the price limit, the caller receives less than requested (down to nothing), the function returns normally, and Swapped is emitted with the requested amount as if delivered. On the launch pool before any buy, sellExactOut(key, 0.001 ether, 1_000_000e18) succeeds with 0 tokens charged and 0 ETH delivered, and because the swap walked through empty liquidity to MAX_SQRT_PRICE-1, slot0.tick goes from 184216 to 887271 at no cost to the caller. That is v4's behaviour for one-sided liquidity, but it means anyone can park the pool's recorded price at the maximum (any price-reading integration, or an LP adding at the current price, sees a nonsensical price) and the next buy pays the gas to walk the bitmap back down. No funds are lost by the caller. Fix: in buyExactOut require delta.amount1() == int256(tokensOut) and in sellExactOut require delta.amount0() == int256(ethOut) (revert on a short fill), and emit the delivered amounts.","line":86,"path":"src/BMCSwapRouter.sol","reproduction":"State: same launch-shaped pool as finding 1, no buy yet, holder approved. Call: sellExactOut(key, 0.001 ether, 1_000_000e18). Expected: revert (the pool has no ETH to deliver) or 0.001 ether delivered. Actual: returns tokensIn = 0, holder.balance stays 0, Swapped(holder, false, 0.001 ether, 0) is emitted, slot0 tick moves 184216 -> 887271. Reproduced in test/scratch/RouterExactOutPartial.t.sol.","severity":"low","snippet":"        BalanceDelta delta = _swap(key, false, int256(ethOut));\n        tokensIn = uint256(uint128(-delta.amount1()));\n        if (tokensIn > maxTokensIn) revert TooMuchRequested(tokensIn, maxTokensIn);","title":"Router exact-output functions do not enforce the exact output: a partial fill succeeds and emits the requested amount, and sellExactOut on an empty ETH side moves the pool tick to the maximum for free"},{"citation":"resolved","description":"deploy() mines the hook salt with HookDeployer.mine(address(this), ...) and then deploys with new BMCHook{salt: salt}, expecting the script contract to be the CREATE2 deployer. Under vm.startBroadcast forge rejects any use of address(this) in a script contract ('Script contracts are ephemeral and their addresses should not be relied upon'), so run() reverts right after deploying the token. Even without that guard, a broadcast CREATE2 goes through the default CREATE2 deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C), so the predicted address and its flag bits would not match and the hook constructor's validateHookPermissions would revert. The README calls the script a fork/local rehearsal, and the test calls deploy() directly, but run() as written can never rehearse against a live or local node. Fix: compute the salt against the CREATE2 deployer the broadcast uses (or deploy through a small on-chain factory contract whose address is known), and avoid address(this) in the broadcast path.","line":43,"path":"script/Deploy.s.sol","reproduction":"Command on a local anvil (port 8599, default funded key): POOL_MANAGER=0x000000000004444c5dc75cB358380D2e3dE08A90 FEE_RECIPIENT=0x83027AD9d0D5374335086c539dADD1db983441B0 forge script script/Deploy.s.sol:DeployBMC --rpc-url http://127.0.0.1:8599 --broadcast --private-key <anvil key 0>. Expected: token, hook and router deployed and logged. Actual: BMCToken is deployed, then run() reverts with 'Usage of `address(this)` detected in script contract. Script contracts are ephemeral and their addresses should not be relied upon.' and the script fails.","severity":"low","snippet":"        (bytes32 salt, address predicted) = HookDeployer.mine(address(this), creationCode, flags, 500_000);\n        d.hookSalt = salt;\n        d.hook = new BMCHook{salt: salt}(\n            config.poolManager, address(d.token), config.feeRecipient, config.buyFeeBps, config.sellBurnBps\n        );\n        require(address(d.hook) == predicted, \"hook landed on an unexpected address\");","title":"DeployBMC.run() cannot broadcast: the salt is mined against address(this) of the script contract, which forge refuses in broadcast mode and which is not the CREATE2 deployer anyway"}],"hash":"5051eed0f5cdebc992412cd5acb1f4b83d146bf6f32f18cb80bc1bd5442f411f","nodeId":"48cacbf1-083b-4cd2-a0d6-5a8ba0601626","outcome":"completed","summary":"Review complete. The findings file is written, validated against the tree, and the working tree is clean.\n\n**Outcome:** four substantiated findings in `.imd-findings.json`, one medium with a failing Foundry proof, three low. Coverage records all 16 listed entry points plus three invariant rows, none unreached.\n\n**Findings**\n\n- **Medium, exact-input fee charged before the fill** (`src/BMCHook.sol:175`). The hook takes 1% of the requested amount in `beforeSwap`, then the pool fills. On the launch pool, which holds no ETH until buys happen, a sale the pool cannot absorb still burns 1% of the full request. With no buy yet, a 1,000,000 BMC sale returns 0 ETH, sells nothing, and burns 10,000 BMC. After one 0.1 ETH buy, a larger sale burns about 4% of what was actually sold. The site's sell form reaches this with minOut 0. The attached proof test fails on the current code and tolerates either fix style (re-price on the filled amount, or refuse short fills).\n- **Low, ETH push has no fallback and the recipient is not an EOA** (`src/BMCHook.sol:225`). Every direct-pay buy and `collect` revert if the community wallet rejects plain ETH. On mainnet that address carries an EIP-7702 delegation. It accepts ETH today, which I confirmed by simulation, but a re-delegation would halt all buys with nobody able to change the recipient.\n- **Low, router exact-output functions do not enforce the output** (`src/BMCSwapRouter.sol:86`). A short fill succeeds and emits the requested amount. On an empty ETH side, `sellExactOut` delivers nothing and moves the pool tick to the maximum for free.\n- **Low, deploy script cannot broadcast** (`script/Deploy.s.sol:43`). Confirmed on a local anvil: forge rejects the script's use of its own address, and the salt is mined for the wrong CREATE2 deployer anyway.\n\n**Verified as holding:** delta accounting on all four swap shapes against the v4 `Hooks` library sign and side mapping, hook net delta zero on every path, callback and `unlockCallback` access control, one-pool initialization, the router's settlement and refunds, and the plain OpenZeppelin token.\n\n**Not covered:** the Slither sync-before-swap router pattern was assessed only by reasoning, not tested, since no mainstream router does it. The website was read for its contract calls only.","treeHash":null,"usage":{"cachedInputTokens":2227064,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":59544,"runtime":"claude","turns":41,"wallClockMs":921152}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9ab27edcfd62be02","findings":[{"citation":"resolved","description":"Asymmetry between the two fee branches. Exact-output swaps are charged in afterSwap from the delta the pool actually produced (src/BMCHook.sol:204-213), so the fee is 1% of what really changed hands. Exact-input swaps are charged in beforeSwap from params.amountSpecified, before the pool runs, and the hook takes/burns that amount at once; the returned BeforeSwapDelta makes the swapper pay it whatever the pool then fills. Uniswap v4 fills an exact-input order only up to the price limit or the end of liquidity and leaves the rest unconsumed, so when liquidity runs out the swapper pays 1% of the requested amount on a trade that moved far less. The launch pool is exactly this shape: the factory seeds BMC only, below the opening tick, so the pool never holds more ETH than buyers have put in, and any sale larger than that fills partially; before the first buy it fills nothing. The brief promises 'burns 1% of the BMC tokens being sold'; here the burn can be 10%, 50% or 100% of what was sold. With the router (price limit at MAX_SQRT_PRICE-1) and the site's quote flow (minEthOut derived from a staticcall that already reflects the partial fill), the seller sees a small ETH quote, confirms, and loses 1% of the full order. Any other router with a tight sqrtPriceLimitX96 reaches the same state on any pool. A side effect: an oversized sale parks the pool price at MAX_SQRT_PRICE-1 (tick 887271), after which BMCSwapRouter.sellExactIn reverts with PriceLimitAlreadyExceeded until the next buy. Fix: charge exact-input orders in afterSwap from the delta the pool actually produced (fee = 1% of -delta.amount0 for buys / -delta.amount1 for sells, returned via afterSwapReturnDelta), the same way exact-output orders are charged, and drop the beforeSwap charge; or, if the fee must stay in beforeSwap, re-check the fill in afterSwap and refund the unused part of the fee via a negative unspecified delta.","line":175,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookDeployer} from \"src/HookDeployer.sol\";\n\n/// @notice Exact-input swaps are charged in `beforeSwap` as 1% of the amount *requested*, before the pool\n/// fills. On the launch geometry (BMC-only liquidity below the opening tick) the pool only ever holds the\n/// ETH buyers put in, so a sale larger than that fills partially (or not at all), yet the full 1% of the\n/// requested amount is burned. Exact-output sales, charged in `afterSwap` on the pool's real amount, do not\n/// have this problem. Both tests fail on the current code and pass once the exact-input fee is computed\n/// from the amount actually filled.\nabstract contract LaunchPoolFixture is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96; // 1e8 BMC per ETH (10 ETH market cap)\n    int24 constant MIN_USABLE_TICK = -887_220;\n\n    address feeRecipient = makeAddr(\"communityWallet\");\n    address buyer = makeAddr(\"buyer\");\n    address seller = makeAddr(\"seller\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    receive() external payable {}\n\n    function deployLaunchPool() internal {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode, abi.encode(address(manager), address(token), feeRecipient, uint256(100), uint256(100))\n        );\n        hook = BMCHook(HookDeployer.deploy(creationCode, BMC_HOOK_FLAGS));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n\n        // The launch seeds tokens only, below the opening price: no ETH is in the pool until someone buys.\n        token.approve(address(lpRouter), type(uint256).max);\n        int24 upper = (TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE) / TICK_SPACING) * TICK_SPACING;\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(MIN_USABLE_TICK, upper, int256(1e22), 0), \"\");\n\n        // A holder (airdrop / launcher allocation) who will sell through the project's router.\n        token.transfer(seller, 100_000_000 ether);\n        vm.prank(seller);\n        token.approve(address(router), type(uint256).max);\n    }\n}\n\ncontract PartialFillSellTest is LaunchPoolFixture {\n    function setUp() public {\n        deployLaunchPool();\n        // One buyer puts 0.1 ETH in; that is all the ETH the pool holds.\n        vm.deal(buyer, 1 ether);\n        vm.prank(buyer);\n        router.buyExactIn{value: 0.1 ether}(key, 0);\n    }\n\n    function test_exactInputSellBurnsOnePercentOfTheRequestedAmountNotOfWhatWasSold() public {\n        uint256 tokensIn = 100_000_000 ether;\n        uint256 supplyBefore = token.totalSupply();\n        uint256 sellerBefore = token.balanceOf(seller);\n        uint256 managerBefore = token.balanceOf(address(manager));\n\n        vm.prank(seller);\n        uint256 ethOut = router.sellExactIn(key, tokensIn, 0);\n\n        uint256 burned = supplyBefore - token.totalSupply();\n        uint256 leftWallet = sellerBefore - token.balanceOf(seller); // what the sale actually cost the seller\n        uint256 soldToPool = token.balanceOf(address(manager)) - managerBefore; // what the pool actually bought\n\n        emit log_named_uint(\"eth received\", ethOut);\n        emit log_named_uint(\"tokens the pool bought\", soldToPool);\n        emit log_named_uint(\"tokens burned\", burned);\n        emit log_named_uint(\"tokens that left the seller\", leftWallet);\n\n        assertGt(ethOut, 0, \"the sale went through (partially)\");\n        assertLt(soldToPool, tokensIn / 2, \"the pool could absorb only a small part of the order\");\n\n        // Brief: \"burns 1% of the BMC tokens being sold\". The burn must be at most 1% of what the seller\n        // actually parted with. On the current code it is 1% of the 100M requested: about 10% of the sale.\n        assertLe(burned * 100, leftWallet, \"burn exceeds 1% of the tokens actually sold\");\n    }\n}\n\ncontract ZeroFillSellTest is LaunchPoolFixture {\n    function setUp() public {\n        deployLaunchPool(); // nobody has bought yet: the pool holds no ETH at all\n    }\n\n    function test_firstSaleOnTheLaunchPoolBurnsOnePercentAndPaysNothing() public {\n        uint256 supplyBefore = token.totalSupply();\n        uint256 walletBefore = token.balanceOf(seller);\n\n        vm.prank(seller);\n        uint256 ethOut = router.sellExactIn(key, 10_000_000 ether, 0);\n\n        uint256 burned = supplyBefore - token.totalSupply();\n        uint256 lost = walletBefore - token.balanceOf(seller);\n        emit log_named_uint(\"eth received\", ethOut);\n        emit log_named_uint(\"tokens burned\", burned);\n        emit log_named_uint(\"tokens lost by the seller\", lost);\n\n        assertEq(ethOut, 0, \"the pool had no ETH: nothing was sold\");\n        assertEq(lost, burned, \"the only thing the seller lost was the burn\");\n        assertEq(burned, 0, \"a sale that sold nothing must burn nothing\");\n    }\n}","reproduction":"State: ETH/BMC pool at the launch price (sqrtPriceX96 = 10000<<96), hook rates 100/100, BMC-only liquidity 1e22 in [-887220, opening tick]. Buyer calls router.buyExactIn{value: 0.1 ether}(key, 0) so the pool holds 0.099 ETH. Seller holding 100,000,000 BMC approves the router and calls router.sellExactIn(key, 100_000_000e18, 0). Expected: burn <= 1% of the BMC that actually left the seller. Actual: ethOut = 0.0977 ETH, the pool bought 9,004,553 BMC, 1,000,000 BMC were burned (1% of the requested 100M), 10,004,553 BMC left the seller's wallet: the burn is ~10% of the sale. Second state: the same pool before any buy (0 ETH). router.sellExactIn(key, 10_000_000e18, 0) returns ethOut = 0, nothing is sold, 100,000 BMC are burned from the seller, and the pool tick is left at 887271. Run: forge test --match-path test/scratch/PartialFillFee.t.sol","severity":"medium","snippet":"        uint256 amountIn = uint256(-params.amountSpecified);\n        uint256 fee;\n        if (params.zeroForOne) {\n            // Buy: ETH is the specified input.\n            fee = (amountIn * buyFeeBps) / BPS;\n            if (fee > 0) _takeEthFee(fee);\n        } else {\n            // Sell: BMC is the specified input.\n            fee = (amountIn * sellBurnBps) / BPS;\n            if (fee > 0) _takeAndBurn(fee);\n        }\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(fee.toInt128(), 0), 0);","title":"Exact-input swaps are charged 1% of the amount requested, not of the amount filled: a partially filled or unfilled sale still burns 1% of the whole order (and a buy pays 1% of the whole input)"},{"citation":"resolved","description":"The brief requires 'Approve (exact amount) then buy/sell'. The sell handler first calls quoteSell, which does routerRead.sellExactIn.staticCall(key, tokensIn, 0, {from: account}) (site/app.js:163). BMCSwapRouter.sellExactIn begins with IERC20Minimal(token).transferFrom(msg.sender, address(this), tokensIn) (src/BMCSwapRouter.sol:70), and BMCToken is OpenZeppelin ERC20, whose _spendAllowance reverts with ERC20InsufficientAllowance when the allowance is below tokensIn. The eth_call therefore reverts for every wallet that has not already approved at least tokensIn, the exception is caught, 'Sell failed' is shown, and lines 209-214 (the approve step) are never reached. The quote shown while typing (line 179) fails the same way. The approve-then-sell flow only works for a wallet that approved the router by other means. Fix: approve first (or check the allowance first), then quote; or quote through a function that does not pull tokens (e.g. simulate with a state override, or add a view quoter).","line":206,"path":"site/app.js","reproduction":"Launched config (token/hook/router set). Connect a wallet holding 1,000 BMC with allowance(wallet, router) == 0. Type 1000 in the sell box: the quote line shows 'Could not quote: ... ERC20InsufficientAllowance'. Click Sell. Expected: 'Step 1/2: approve exactly the amount you are selling' followed by an approve transaction. Actual: quoteSell at line 206 throws before line 209 runs, 'Sell failed: ...' is shown, no approval is requested and no sale is possible from the site.","severity":"medium","snippet":"      const expected = await quoteSell(tokensIn);\n      const minOut = withSlippage(expected, $(\"sell-slip\").value || \"0\");\n      const tokenW = new ethers.Contract(cfg.token, TOKEN_ABI, signer);\n      const allowance = await tokenW.allowance(account, cfg.router);\n      if (allowance < tokensIn) {\n        status($(\"trade-status\"), \"Step 1/2: approve exactly the amount you are selling…\");\n        const a = await tokenW.approve(cfg.router, tokensIn);\n        await a.wait();\n      }","title":"Site sell flow quotes with a staticcall that executes transferFrom before approving, so a wallet without a prior allowance can never sell from the site"},{"citation":"resolved","description":"Asymmetry between the exact-input and exact-output entry points: buyExactIn and sellExactIn check the output against a minimum (lines 51 and 73), but sellExactOut only checks tokensIn <= maxTokensIn and buyExactOut checks nothing about the delivered amount (line 60-61). When liquidity runs out before the requested output is reached, the v4 swap stops at the router's fixed price limit (line 124) with amountSpecifiedRemaining > 0 and the router returns as if the order had been filled: the caller receives less than ethOut / tokensOut, is charged for what was delivered plus the hook fee, and the Swapped event reports the requested amount, not the delivered one. The launch pool reaches this state whenever a sale exceeds the ETH buyers have put in. No funds are lost beyond the trade being smaller than asked, but the 'exactly' guarantee in the NatSpec and in the site copy is not enforced. Fix: require delta.amount0() == ethOut in sellExactOut and delta.amount1() == tokensOut in buyExactOut (or revert with a PartialFill error), and emit the delivered amounts.","line":86,"path":"src/BMCSwapRouter.sol","reproduction":"Pool as in finding 1 after one 0.1 ETH buy (0.099 ETH in the pool). Seller calls router.sellExactOut(key, 1 ether, 100_000_000e18). Expected: revert, or the seller's balance rises by exactly 1 ETH. Actual: the call succeeds, returns tokensIn = 9,094,598 BMC, the seller receives 0.0977 ETH, and Swapped(seller, false, 1e18, tokensIn) is emitted with the requested ethOut. See test/scratch/ExactOutUnderDelivery.t.sol.","severity":"low","snippet":"        BalanceDelta delta = _swap(key, false, int256(ethOut));\n        tokensIn = uint256(uint128(-delta.amount1()));\n        if (tokensIn > maxTokensIn) revert TooMuchRequested(tokensIn, maxTokensIn);","title":"sellExactOut / buyExactOut return normally when the pool delivers less than the exact amount requested"},{"citation":"resolved","description":"Access-control note. The one-time attachment is first-come-first-served: the sender argument is ignored, and the only checks are that currency0 is native ETH and currency1 is the launch token. The launch factory deploys the hook and calls PoolManager.initialize in the same transaction, and the hook's CREATE2 address is known in advance, so there is no window in the documented flow. If the two steps are ever separated (a rehearsal with script/Deploy.s.sol followed by a later initialize, a redeploy, or a factory change), anyone who sees the hook's code land can call PoolManager.initialize with the same currencies but their own fee (e.g. 3000 or the dynamic-fee flag), tick spacing and opening price; poolInitialized becomes true, the factory's own initialize at fee 12500 / spacing 60 reverts with AlreadyInitialized, and the only pool carrying the launch hook is the stranger's. The launch guide anticipates this and asks a hook that checks its initializer to take the factory as a constructor argument written '$factory'. Fix (hardening, no behaviour change for the factory): add an immutable factory argument ('$factory' in launch.json) and require sender == factory in beforeInitialize, and/or pin the manifest fee and tick spacing.","line":151,"path":"src/BMCHook.sol","reproduction":"Deploy BMCHook at a flagged address (HookDeployer.deploy) without initializing. From any EOA call manager.initialize({currency0: 0x0, currency1: token, fee: 3000, tickSpacing: 1, hooks: hook}, 1<<96). Expected: refused. Actual: succeeds and hook.poolInitialized() == true. Then from the launch factory call manager.initialize({..., fee: 12500, tickSpacing: 60, hooks: hook}, 10000<<96): reverts (AlreadyInitialized). See test/scratch/OpenInitializer.t.sol. Precondition: deployment and initialization in separate transactions, which the documented factory flow does not do.","severity":"low","snippet":"        if (poolInitialized) revert AlreadyInitialized();\n        if (!key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != token) revert WrongPoolCurrencies();\n        poolInitialized = true;\n        poolId = key.toId();","title":"beforeInitialize accepts any initializer, fee, tick spacing and price and locks the hook to the first pool it sees"}],"hash":"578cc99f7b9ea23347eaef1b60b3ad2bc49cce7124f0a1982b4e406e4ee163c2","nodeId":"91342188-91f4-436f-87a7-f9c3a2a16003","outcome":"completed","summary":"The review is complete. The findings file is written at `.imd-findings.json` with 4 findings and 18 coverage rows, and no repository file was changed (only `test/scratch/` and the findings file, both harness-owned).\n\n**What I found**\n\n- **Medium, with proof (hook, `beforeSwap`)**: exact-input swaps are charged 1% of the amount *requested*, while exact-output swaps are charged 1% of what the pool *actually* moved. When liquidity runs out the order fills partially but the full fee is still taken. On the launch geometry (BMC-only liquidity below the opening tick), the pool never holds more ETH than buyers put in, so a large sale burns far more than 1% of what was sold. In the proof test a 100M BMC order sold about 9M and burned 1M, roughly a 10% burn. On the fresh pool before any buy, a sale sells nothing, returns 0 ETH and still burns 1%. The proof file `test/scratch/PartialFillFee.t.sol` fails on the current code for exactly this reason.\n- **Medium (site)**: the sell flow quotes with a staticcall to `sellExactIn` before approving. The router's `transferFrom` reverts on a wallet without allowance, so the handler throws before it reaches the approve step. A first-time seller cannot sell from the site at all.\n- **Low (router)**: `sellExactOut` and `buyExactOut` never check that the exact amount was delivered. When liquidity runs out they return normally with less, and the event reports the requested amount.\n- **Low (hook, `beforeInitialize`)**: the initializer, fee, tick spacing and price are unchecked, so the first pool wins. Safe under the factory's single-transaction flow, but a non-atomic deploy lets a stranger attach the hook to their own pool and brick the factory's initialize.\n\n**What holds**: every callback and both `unlockCallback`s refuse non-PoolManager callers, there is no owner, setter or upgrade path anywhere, the hook's delta accounting nets to zero on all four swap shapes, `collect` is safely permissionless, and the token is plain OpenZeppelin ERC20. The existing 48-test suite passes.\n\n**Not covered**: the IMD launch factory's actual seeding position and the LP-fee claim contract are outside this tree, so I could not verify the launcher's 1% share path or the exact launch liquidity range.","treeHash":null,"usage":{"cachedInputTokens":2319477,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":49357,"runtime":"claude","turns":37,"wallClockMs":776575}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b0b4e7bbc84f9d80","findings":[{"citation":"resolved","description":"Round-2 settlement note, not a code defect. The fix for the earlier medium finding (c3980bb5...) changed BMCHook so that a rejected or gas-exhausting ETH payment leaves an ERC-6909 claim instead of reverting the buy (src/BMCHook.sol:239-257 _takeEthFee/_tryPayEth) and collect() keeps the claim on a failed payment (src/BMCHook.sol:291-301). README was updated accordingly ('A recipient that rejects ETH or exhausts the payment attempt's 100,000 gas budget also leaves an ETH claim'). The manifest notes, which admission reviewers read as the explanatory text of the accepted tree, still carry the old sentence that a rejecting recipient 'would revert direct-pay buys and could not be paid by collect() either' and that the wallet 'is expected to be an EOA' (the brief's wallet 0x83027AD9d0D5374335086c539dADD1db983441B0 carries EIP-7702 delegation code on mainnet). Nothing deploys differently; only the description is stale. Fix: replace the sentence with the README's wording (claims on failed payment, collect retries, recipient must eventually accept ETH within 100,000 gas).","line":32,"path":"launch.json","reproduction":"Read launch.json line 32 (notes) and compare with src/BMCHook.sol:239-257 and test/BMCRevision.t.sol test_rejectedBuyFeeBecomesAClaimAndLaterPaysExactlyOnce: with feeRecipient etched to a contract whose receive() reverts, router.buyExactIn{value: 0.1 ether}(key, 1) succeeds and hook.pendingEthFees() == 0.001 ether; hook.collect() returns normally and keeps the claim. Expected: notes describe this. Actual: notes say such a recipient 'would revert direct-pay buys and could not be paid by collect()'. forge test (110 tests incl. the proofs) confirms the code behaviour.","severity":"info","snippet":"The community wallet is expected to be an EOA: a contract recipient that rejects ETH would revert direct-pay buys and could not be paid by collect() either, and the address is immutable.","title":"launch.json notes still state the pre-fix recipient behaviour (EOA assumed, rejected ETH reverts buys and collect), contradicting the revised hook and README"}],"hash":"57937dbacf17e2c31dd355a8443afd39109ffe058492ceec13848637aa3b5d44","nodeId":"e0e12860-ed75-450d-a3f2-7033a47cd818","outcome":"completed","summary":"The review is complete. The findings file is written and the working tree is unchanged apart from it.\n\n**Outcome: all earlier blocking findings are fixed. No new code defect reproduces.**\n\nWhat I settled, each run against the revised code:\n\n- **Exact-input fee on partial fills (high):** fixed. The hook's afterSwap now compares the consumed input with the input less the fee and reverts PartialFill. The five specialist proofs now pass or fail only because the trade itself reverts with that error, which is the fix option I offered. Full fills through the project router and a third-party router with open limits still pass.\n- **Buy fee pushed to a rejecting recipient (medium):** fixed. Payment runs under a 100,000 gas budget inside try/catch, a failure mints an ERC-6909 claim, and collect keeps the claim when payment fails. Both attached proofs pass. The author's reentry and gas-exhaustion tests also pass.\n- **Site sell flow quoting before approval (medium):** fixed. The handler checks allowance, approves the exact amount, waits for the receipt, then quotes. The six Node site tests pass.\n- **Exact-output under-delivery, fee base asymmetry (low):** both fixed, with regression and fuzz tests passing.\n- **Deploy script broadcast (low):** fixed. I rehearsed it on a local anvil in broadcast mode, and all three contracts deployed with the hook at a correctly flagged address.\n- **Open initializer and the 2^96 manifest price (low, info):** the author's disputes hold. The factory deploys and initializes atomically, and pre-deployment initialization of the predicted address reverts. The manifest price is the fixed value the deployer overrides. Dropped.\n\n**One informational note kept.** The manifest notes in launch.json still say the community wallet is expected to be an EOA and that a rejecting recipient would halt buys and collect. That sentence describes the pre-fix behaviour and contradicts the revised hook and README. It changes nothing about deployment.\n\nThe full test suite reports 108 project tests passing, and the coverage record answers all 16 entry points as holding, plus the deploy script and manifest rows.","treeHash":null,"usage":{"cachedInputTokens":1077558,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":21295,"runtime":"claude","turns":20,"wallClockMs":699665}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c0fc4ea4f50e3380","findings":[{"citation":"resolved","description":"beforeSwap charges the sell burn on `amountIn = uint256(-params.amountSpecified)` and burns it immediately (`_takeAndBurn`), before the pool runs. Uniswap v4 only consumes as much of an exact input as liquidity between the current price and the price limit allows; the rest is simply not swapped and the swapper's delta covers only the consumed part. The launch seeds its pool with BMC only, below the opening price (no ETH is deposited; see README 'Launch parameters' and the fixture's addTokenOnlyLiquidity, 'as the launch seeds its pool'). So there is never ETH liquidity above the opening price: any sell that would push the price back past it fills only up to the ceiling, and a sell while the pool holds no ETH fills nothing. In both cases the hook has already burned 1% of the whole specified amount, and BMCSwapRouter refunds the unfilled BMC to the seller - minus the 1% that was burned on it. Seam (Numerical Gap, boundary x invariant): the invariant 'burn == 1% of the BMC sold' holds only on full fills; at the liquidity boundary the hook's fee base (specified input) and the pool's fill (consumed input) diverge. Concrete numbers from the attached test (token-only position of liquidity 1e22 from MIN tick to the opening tick, rates 100/100): (a) pool at the ceiling, seller sells 1,000,000 BMC: ethOut = 0, 10,000 BMC burned, pool sqrtPrice left at MAX_SQRT_PRICE-1 (1461446703485210103287273052203988822378723970341). (b) after one 1 ETH buy (bought = 49,374,720,659,463,950,491,652,993 BMC), seller sells 2*bought = 98,749,441,318,927,900,983,305,986: the pool took 49,999,717,123,507,797,966,230,880, 47,762,229,782,230,824,007,242,047 were refunded, 987,494,413,189,279,009,833,059 were burned (1% of specified) where 1% of what was sold is 505,047,647,712,199,979,456,877; the seller lost 482,446,765,477,079,030,376,182 BMC (~0.0096 ETH at the fill price) beyond the stated rule; ethOut 0.977624999999999999 ETH. The website's sell flow (site/app.js: quoteSell via staticCall then sellExactIn with minOut from that quote) accepts partial fills silently, so a user following the UI pays this overcharge without warning. The README lists this as known behaviour 'with full-range or deep liquidity this does not arise', but the launch's own liquidity is one-sided and shallow on the ETH side, where it arises on every sell that reaches the ceiling. Exact-output swaps are not affected (afterSwap uses the pool's actual delta). Fix (minimal, keeps the design): in afterSwap for exact-input swaps compare the pool's consumed input (-delta.amount1() for sells, -delta.amount0() for buys) with amountIn - fee and revert on a shortfall (PartialFill), so a trade that cannot fill is refused instead of overcharged; or defer the burn to afterSwap, burn only consumed * sellBurnBps / (BPS - sellBurnBps) and send the surplus BMC back to `sender` (the router, which refunds it). Either makes the attached proof pass.","line":182,"path":"src/BMCHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {BMCToken} from \"src/BMCToken.sol\";\nimport {BMCHook, BMC_HOOK_FLAGS} from \"src/BMCHook.sol\";\nimport {BMCSwapRouter} from \"src/BMCSwapRouter.sol\";\nimport {HookFlags} from \"src/HookFlags.sol\";\n\n/// @notice BMCHook.beforeSwap burns 1% of the *specified* input of an exact-input sell before the pool\n/// fills it. The launch pool is seeded with BMC only, below the opening price, so there is never ETH\n/// liquidity above that price: a sell that would push the price past it only partly fills, the unfilled\n/// BMC is refunded to the seller, and 1% of that refunded BMC has already been burned. At the ceiling\n/// (no ETH in the pool) a sell fills nothing and still burns 1% of the whole input.\n///\n/// Fails on the current code. Passes once the hook either refuses a partly-filled exact-input swap or\n/// burns only 1% of the BMC the pool actually took (the test accepts a revert or a corrected burn).\ncontract ProofPartialFillBurnTest is Test {\n    uint24 constant LP_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant OPENING_SQRT_PRICE = uint160(10_000) << 96; // 1e8 BMC per ETH: 1e9 BMC = 10 ETH\n    int24 constant MIN_USABLE_TICK = -887_220;\n\n    address feeRecipient = makeAddr(\"communityWallet\");\n    address buyer = makeAddr(\"buyer\");\n    address seller = makeAddr(\"seller\");\n\n    PoolManager manager;\n    BMCToken token;\n    BMCHook hook;\n    BMCSwapRouter router;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new BMCToken();\n        router = new BMCSwapRouter(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        hook = BMCHook(_deployHook());\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: LP_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        manager.initialize(key, OPENING_SQRT_PRICE);\n\n        // Token-only liquidity below the opening price, the way the launch seeds its pool (no ETH).\n        token.approve(address(lpRouter), type(uint256).max);\n        int24 upper = (TickMath.getTickAtSqrtPrice(OPENING_SQRT_PRICE) / TICK_SPACING) * TICK_SPACING;\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(MIN_USABLE_TICK, upper, int256(1e22), 0), \"\");\n\n        vm.deal(buyer, 10 ether);\n        token.transfer(seller, 200_000_000 ether);\n        vm.prank(seller);\n        token.approve(address(router), type(uint256).max);\n    }\n\n    /// Pool at the ceiling (no ETH at all): selling 1,000,000 BMC fills nothing. Expected: nothing burned,\n    /// nothing paid (or the swap refused). Actual: 10,000 BMC burned, 0 ETH received.\n    function test_sellWithNoEthLiquidity_mustNotBurnForZeroOutput() public {\n        uint256 tokensIn = 1_000_000 ether;\n        uint256 supplyBefore = token.totalSupply();\n        uint256 sellerBefore = token.balanceOf(seller);\n        uint256 ethBefore = seller.balance;\n\n        vm.prank(seller);\n        try router.sellExactIn(key, tokensIn, 0) {} catch {}\n\n        uint256 burned = supplyBefore - token.totalSupply();\n        uint256 sellerPaid = sellerBefore - token.balanceOf(seller);\n        assertEq(seller.balance - ethBefore, 0, \"the pool had no ETH: nothing could be filled\");\n        assertEq(burned, 0, \"1% of a sale that filled nothing was burned\");\n        assertEq(sellerPaid, 0, \"the seller paid BMC for zero ETH\");\n    }\n\n    /// After one 1 ETH buy, a sell of twice the BMC that buy produced can only fill back to the opening\n    /// price. Expected: the burn is 1% of the BMC the pool actually took (fee = filled / 99 when the fee\n    /// comes off the input). Actual: 1% of the whole specified amount, so ~half the burn is on BMC that\n    /// was refunded to the seller.\n    function test_partialFillSell_mustBurnOnlyOnePercentOfWhatWasSold() public {\n        vm.prank(buyer);\n        uint256 bought = router.buyExactIn{value: 1 ether}(key, 0);\n\n        uint256 tokensIn = 2 * bought;\n        uint256 supplyBefore = token.totalSupply();\n        uint256 sellerBefore = token.balanceOf(seller);\n\n        vm.prank(seller);\n        try router.sellExactIn(key, tokensIn, 0) {} catch {}\n\n        uint256 burned = supplyBefore - token.totalSupply();\n        uint256 sellerPaid = sellerBefore - token.balanceOf(seller);\n        uint256 filled = sellerPaid - burned; // what the pool actually took from the seller\n        assertLt(filled, tokensIn - burned, \"sanity: the sale only partly filled (price ceiling)\");\n        // 1 BMC of tolerance for rounding in a corrected implementation; the current code is off by ~480,000 BMC.\n        assertLe(burned, filled / 99 + 1 ether, \"burn exceeds 1% of the BMC actually sold\");\n    }\n\n    function _deployHook() internal returns (address at) {\n        bytes memory creationCode = abi.encodePacked(\n            type(BMCHook).creationCode,\n            abi.encode(address(manager), address(token), feeRecipient, uint256(100), uint256(100))\n        );\n        bytes32 initCodeHash = keccak256(creationCode);\n        for (uint256 i = 0; i < 500_000; i++) {\n            address predicted = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initCodeHash))))\n            );\n            if (!HookFlags.matches(predicted, BMC_HOOK_FLAGS) || predicted.code.length != 0) continue;\n            bytes32 salt = bytes32(i);\n            assembly (\"memory-safe\") {\n                at := create2(0, add(creationCode, 0x20), mload(creationCode), salt)\n            }\n            require(at == predicted, \"hook landed elsewhere\");\n            return at;\n        }\n        revert(\"no salt\");\n    }\n}","reproduction":"State: BMCHook(manager, BMC, recipient, 100, 100) attached to ETH/BMC pool (fee 12500, spacing 60) at sqrtPrice 10_000<<96, one position of liquidity 1e22 on [-887220, openingTick] (BMC only, no ETH). Call 1: seller (holding BMC, approved) calls BMCSwapRouter.sellExactIn(key, 1_000_000e18, 0). Expected: either revert, or 0 burned and 0 BMC taken since no ETH can be paid. Actual: returns 0 ETH, totalSupply drops by 10_000e18, seller's balance drops by 10_000e18, pool price moved to MAX_SQRT_PRICE-1. Call 2 (fresh state): buyer calls buyExactIn{value: 1 ether}(key, 0) -> bought BMC. Seller calls sellExactIn(key, 2*bought, 0). Expected burn = 1% of BMC the pool took (~505,047e18). Actual burn = 1% of 2*bought = 987,494e18; ~47.76M BMC refunded unfilled, 1% of which (~482,447e18 BMC) was burned. Run: forge test --match-path test/scratch/Proof_PartialFillBurn.t.sol (both tests fail on the current code).","severity":"high","snippet":"            // Sell: BMC is the specified input.\n            fee = (amountIn * sellBurnBps) / BPS;\n            if (fee > 0) _takeAndBurn(fee);","title":"Exact-input sells burn 1% of the specified amount, not of the BMC actually sold: partial fills on the launch's one-sided pool burn the seller's refunded tokens too (zero-fill sell burns 1% for 0 ETH)"},{"citation":"resolved","description":"For exact-input buys the fee is 1% of the total ETH the swapper sends (beforeSwap: amountIn * 100 / 10000, pool gets the other 99%). For exact-output buys afterSwap computes the fee on `ethIn`, the pool's own charge, and adds it on top, so the swapper's total ETH is ethIn + ethIn/100 and the fee is ethIn/(101*ethIn/100) = 0.9901% of the ETH side. The same asymmetry applies to the sell burn (exact-output sells burn 1/101 of the BMC paid). Two routes to the same economic trade produce different fee totals (Invariant/Numerical Gap: path divergence, precision x invariant). Numbers: a buy whose ETH side totals 1.01 ETH pays 0.0101 ETH through buyExactIn but 0.01 ETH through buyExactOut (test_buyExactOutput_feeIsOnePercentOfTheEthThePoolCharged asserts fee == poolEth / 100). Difference ~0.01% of volume, not compounding, so low. Fix: in afterSwap compute fee = ethIn * buyFeeBps / (BPS - buyFeeBps) (and the same for tokenIn) so the fee is 1% of the total paid, or document that exact-output trades pay 1/101.","line":206,"path":"src/BMCHook.sol","reproduction":"Funded pool (fixture: full-range liquidity 1e22, 1.1 ETH). Call buyExactOut{value: 1 ether}(key, 1_000_000e18): recipient receives fee, pool receives poolEth, fee == poolEth/100, total = 1.01*poolEth -> fee/total = 0.990%. Call buyExactIn{value: total}(key, 0) with the same total: recipient receives total/100 = 1.000%. Expected: equal rates for the same ETH side; actual: 0.990% vs 1.000%.","severity":"low","snippet":"            fee = (ethIn * buyFeeBps) / BPS;","title":"Buy fee base differs by swap shape: exact-output buys pay 1/101 (0.990%) of the ETH side, exact-input buys pay 1/100"},{"citation":"resolved","description":"sellExactOut (and buyExactOut) bound only the input (tokensIn <= maxTokensIn / msg.value); neither compares the pool's delivered output with the amount the caller asked for. With the router's unbounded price limit (MAX_SQRT_PRICE - 1) an exact-output swap that runs out of liquidity partially fills: the swapper receives less than `ethOut`, pays for what was delivered plus the 1% burn, and the Swapped event reports `ethOut` rather than the amount received. On the launch's BMC-only pool this is reachable on every sell that reaches the opening price. Numbers from test/scratch/PartialFillBurn.t.sol (ExactOutPartialFillTest): pool at ceiling, sellExactOut(key, 0.01 ether, 10_000_000e18) returns tokensIn = 0, seller receives 0 ETH, event says 0.01 ETH, pool price moved to MAX_SQRT_PRICE-1; after a 0.5 ETH buy, sellExactOut(key, 1 ether, 200_000_000e18) delivers 0.488812499999999999 ETH for 33,535,526,184,514,667,540,857,656 BMC and does not revert. Boundary (Step 2.4, return-value handling): the caller's requested amount is never compared with the delta. The website only uses the exact-input entry points, so impact is limited to direct integrators; no funds are lost beyond the exact-input issue in finding 1. Fix: in sellExactOut require uint256(uint128(delta.amount0())) == ethOut (and in buyExactOut uint256(uint128(delta.amount1())) == tokensOut), or emit the delivered amounts.","line":87,"path":"src/BMCSwapRouter.sol","reproduction":"State as in finding 1 (BMC-only position, no ETH in the pool). seller calls BMCSwapRouter.sellExactOut(key, 0.01 ether, 10_000_000e18). Expected: revert (nothing can be delivered) or the requested 0.01 ETH. Actual: returns 0, seller's ETH balance unchanged, Swapped(seller, false, 0.01 ether, 0) emitted. After buyExactIn{value: 0.5 ether}: sellExactOut(key, 1 ether, 200_000_000e18) returns 33.5M BMC charged and the seller receives 0.4888 ETH instead of 1 ETH, no revert.","severity":"low","snippet":"        tokensIn = uint256(uint128(-delta.amount1()));\n        if (tokensIn > maxTokensIn) revert TooMuchRequested(tokensIn, maxTokensIn);","title":"BMCSwapRouter exact-output swaps do not check that the requested output was delivered: at the pool's price ceiling sellExactOut returns less ETH than asked without reverting and emits the requested am"}],"hash":"5fced49f988441c64b64f809f60ae641799468ec352e867374bb5316643d4959","nodeId":"e0e4bde8-856a-4e40-8be4-6a2539787679","outcome":"completed","summary":"The review is complete. The findings file is written, the working tree is unchanged apart from my scratch tests, and the proof fails on the current code and passes under a candidate fix.\n\n**Outcome:** `.imd-findings.json` holds 3 findings and 18 coverage rows (all 16 listed entry points plus two invariants).\n\n**Findings**\n\n- **High, `src/BMCHook.sol:182` (with proof).** Exact-input sells burn 1% of the *specified* BMC in `beforeSwap`, before the pool fills. The launch pool is seeded with BMC only, so there is never ETH liquidity above the opening price. Any sell that reaches that ceiling only partly fills, the unfilled BMC is refunded to the seller, and 1% of that refund has already been burned. At the ceiling a sell fills nothing and still burns 1% for zero ETH. Reproduced numbers: after one 1 ETH buy, selling twice the bought amount burned 987,494 BMC where 1% of what was actually sold is 505,047 BMC. The website's sell flow accepts such partial fills silently. The proof test under `test/scratch/Proof_PartialFillBurn.t.sol` fails now and passed when I temporarily made `afterSwap` refuse partially filled exact-input swaps (the source was restored and `git status` is clean).\n- **Low, `src/BMCHook.sol:206`.** Exact-output trades pay the fee on the pool's charge and add it on top, so the rate is 1/101 (0.990%) of the ETH side, while exact-input trades pay 1/100. Same for the sell burn.\n- **Low, `src/BMCSwapRouter.sol:87`.** The exact-output entry points never compare the delivered amount with the requested one. At the ceiling, `sellExactOut` for 0.01 ETH returns 0 and emits 0.01; after a 0.5 ETH buy, a 1 ETH request delivered 0.4888 ETH without reverting.\n\n**Coverage.** Hook fee math otherwise holds: delta placement nets to zero on all four swap shapes, overflow and int128 casts only fail for inputs the pool rejects anyway, dust under 100 wei pays no fee. Token, `collect`, both `unlockCallback`s, `receive`, and the exact-input router paths hold. Not reached in depth: the site's JavaScript beyond the quote and slippage math, and the deploy script's rehearsal path, both outside the Solidity entry points.","treeHash":null,"usage":{"cachedInputTokens":1811740,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":41478,"runtime":"claude","turns":35,"wallClockMs":757365}},{"artifacts":[],"attempt":1,"bundleHash":"bcb7ebc88aeb226985bbd983260351e63e685068b5f48e5d90741b66996006b8","device":"5d667e4b0751bcb5","findings":[],"hash":"cb85b1500f8fef679b7a1908074524f5b2e469b1d3db1657952263923cae22b0","nodeId":"3de2415d-8de5-4ee7-beb3-5f34a629a4a1","outcome":"completed","summary":"The revision is done. The one advisory finding did not hold, so the manifest's values are unchanged.\n\n**Finding on pool.initialPrice (disputed).** The reviewer's arithmetic is right: the manifest value is 2^96, and the tests open their local pool at the 10 ETH-cap price. But the launch reference fixes pool.initialPrice at exactly that value and states the deployer derives the real opening price from the 10 ETH market cap and the token's address, which decides currency ordering. Changing the value would be refused by the manifest check. I added one sentence to the manifest notes confirming that reading, as the finding asked, and recorded the verdict in .imd-responses.json.\n\n**Verification.**\n- Permissions in launch.json match what `getHookPermissions` in src/BMCHook.sol returns.\n- Constructor arguments still match the five declared parameters in order.\n- `forge build` succeeds and `forge test` reports 65 passed, 0 failed.\n- Notes are 3,227 characters, under the 4,000 cap, and `\"kind\": \"univ4_hook\"` remains the first field.\n\nOnly launch.json changed, plus the response file the revision requires.","treeHash":"0ddd52dea68de6b8ab625fb3d0c13e16e7df0fad","usage":{"cachedInputTokens":288979,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3046,"runtime":"claude","turns":7,"wallClockMs":171758}},{"artifacts":[],"attempt":1,"bundleHash":"53cc785d5e5b909afe4932cc886d27ccabe8519e04e580f0bc31ce19ea356d56","device":"21173e2f9a010d0b","findings":[],"hash":"d7cae8101078af6609b85815bccbd8decd4183bb4af2c736c21b87d79d6096b9","nodeId":"a6ad88fa-89b8-46bf-9198-fa2d39c73654","outcome":"completed","summary":"Fixed six findings and documented the initializer advisory dispute in [.imd-responses.json](.imd-responses.json).\n\nValidation passed: `forge build`, 65 Foundry tests, `forge fmt --check`, six site tests, all protected checks, and the reviewer proof. Local Anvil deployment also succeeded.\n\nNo mainnet deployment or public-site publication was performed.","treeHash":"c4c88a9cd21e44343f23ae35cc8bf32bf0a709ba","usage":{"cachedInputTokens":3290752,"inputTokens":149705,"model":"gpt-6-astra","outputTokens":26590,"runtime":"codex","turns":8,"wallClockMs":1045211}},{"artifacts":[],"attempt":1,"bundleHash":"e6435ba313ace77a72e0864a72c21bfc13bd8218bbb004e8cfd9b09f9693aef3","device":"584bd638e7e022f0","findings":[{"description":"The revised afterSwap reverts an exact-input swap with PartialFill whenever the pool consumed less than the input minus the hook fee. This fixes the previous round's defect (a fee charged on ETH or BMC that was never traded) but changes the hook from fee-only to swap-gating: an exact-input trade that hits its sqrtPriceLimitX96 or exhausts liquidity reverts instead of partially filling. On the launch pool, seeded with BMC only below the opening price, every sale larger than the ETH the pool has accumulated reverts outright, and any router that passes a slippage-derived price limit on an exact-input swap (as aggregators do) will see reverts where a hookless pool would fill partially. The alternative that keeps swaps unblocked is to charge the exact-input fee in afterSwap on the amount the pool actually consumed (as the exact-output path already does), which also removes the need for the beforeSwapReturnDelta permission. The README documents the current choice; this entry is so the requester decides knowingly. The submitted tests assert the current behaviour (test/BMCHookEdge.t.sol test_shortFilled*, test/BMCHookInvariant.t.sol invariant_noSuccessfulSwapWasShortFilled) and will need updating if the design changes.","line":211,"path":"src/BMCHook.sol","reproduction":"Full-range pool, 1e22 liquidity, 1.1 ETH + BMC. Exact-input buy of 1 ETH with sqrtPriceLimitX96 = openingPrice * 999/1000 via v4-core PoolSwapTest. Expected under a fee-only hook: partial fill, fee 1% of the ETH actually traded. Actual: revert WrappedError(hook, afterSwap, PartialFill(0.99e18, consumed)) with consumed > 0. Same for an exact-input sale with limit openingPrice * 1001/1000. Shown by test_shortFilledBuyRevertsWithPartialFillNamingBothAmounts and test_shortFilledSaleRevertsWithPartialFillNamingBothAmounts in test/BMCHookEdge.t.sol.","severity":"low","title":"Exact-input swaps now revert on any short fill, which the brief's \"does not block or alter swaps otherwise\" may not intend"},{"description":"mine() allocates a fresh abi.encodePacked buffer every iteration, so memory grows linearly and its gas cost quadratically. Under a 300M gas block the loop runs out of memory (EvmError: MemoryOOG) after roughly 100,000 iterations, far short of maxTries = 500_000. Matching 5 exact flag bits of 14 succeeds with probability 1/16384 per try, so about e^-6, 0.25% of init-code hashes (i.e. of constructor-argument sets) have no salt within reach, and deploy(Config) in script/Deploy.s.sol reverts for them. Not a launch issue, since the IMD deployer mines the salt off chain, and the launch's own argument set succeeds (every test using the fixture's deployHook passes). Observed while fuzzing hook rates in test/BMCHookEdge.t.sol, where the fuzzed constructor arguments produced such hashes; those tests now place hooks by running the constructor in place instead of mining.","line":26,"path":"src/HookDeployer.sol","reproduction":"Call HookDeployer.deploy(abi.encodePacked(type(BMCHook).creationCode, abi.encode(manager, token, recipient, 5277, 1000000000000)), BMC_HOOK_FLAGS) from a test contract (the counterexample forge reported: buyBps 5277 is outside the cap but the OOG happens inside mine before the constructor runs; any argument set whose hash sequence needs over ~100k tries behaves the same). Expected: a salt within 500,000 tries or NoSaltFound. Actual: EvmError: MemoryOOG.","severity":"info","title":"HookDeployer.mine cannot reach its 500,000-try bound on chain; the rehearsal script's deploy(Config) fails for roughly 0.2% of init-code hashes"}],"hash":"f3cfe08be463d78c725d1aa029943eae24db6804ce3a4c093bc7feaa5e3568ec","nodeId":"d0b011ab-49e1-4d59-922d-ebc7805f8c58","outcome":"completed","summary":"Done. The suite builds and passes: 100 submitted tests plus my scratch probe, green under three fuzz seeds.\n\n**What this revision changed and why.** The implementer's revision changed the hook's rules in response to the last round, and five of my tests encoded the old rules. Two exact-output fuzz tests in `test/BMCHookEdge.t.sol` asserted the fee as 1% of the pool's input, where the hook now grosses it up to 1% of the total paid. The invariant handler in `test/BMCHookInvariant.t.sol` had the same stale expectation for exact-output buys and sells. Both files now assert the revised rule in two equivalent forms, the gross-up the hook computes and the brief's \"1% of the ETH side\", which I checked are identical under floor rounding for every rate below 100%.\n\n**New coverage of the revision's behaviour, in the edge file:**\n- Short-filled exact-input buys and sells revert with the wrapped `PartialFill` error, decoded and checked field by field: expected equals input minus fee, consumed is strictly less and nonzero, and no fee, burn or claim survives. A zero-fill case and the exact-output counterpart, which legitimately fills short and pays 1% of what was actually paid, sit beside them.\n- The payment-window reentrancy guard: a recipient re-entering any of the four swap shapes while being paid is refused with `ReentrantSwap`, the buy still pays it, trading resumes afterwards, and the guard also holds during a retried `collect`.\n- The 100,000-gas payout budget: a wallet doing real work on receipt is paid directly, a wallet that spins past the budget leaves a claim without exhausting the buyer's gas, and earlier claims stay intact and fully backed through later direct payments.\n- A fuzz over every rate up to the 50% cap confirming exact output charges the rate on total input, with the fee equal to the pool's input at the cap.\n\n**Invariant handler additions:** a price-limited exact-input action through a third-party router that must either fill completely under the rule or revert leaving no trace, and a recipient that toggles between accepting and refusing ETH. Four new invariants assert no successful short fill, no leftovers from reverts, no payment to a refusing wallet, and whole claims kept on refusal. The former short-fill counters are now asserted zero instead of merely tallied. A deterministic probe confirmed both branches of every new action fire.\n\n**Harness fix.** Per-rate hooks are now placed by running the constructor in place at a flagged address rather than mining a CREATE2 salt on chain. Fuzzed rates change the init code, and an unlucky search ran out of memory, which made two fuzz tests flaky.\n\n**Findings file** (`.imd-findings.json`, two entries, no proofs since neither is high severity):\n- Low: exact-input swaps now revert on any short fill, which trades the brief's \"does not block swaps\" for correct fee accounting. The requester should decide knowingly, and my tests assert the current behaviour.\n- Info: the on-chain salt miner cannot reach its 500,000-try bound and fails for roughly 0.2% of argument sets. This affects only the rehearsal script, not the launch.\n\nI did not commit, since nothing was installed and the harness has been making the commits. Files changed: `test/BMCHookEdge.t.sol` and `test/BMCHookInvariant.t.sol` only.","treeHash":"92c04f8a5fceed88a2bbef985a415897602f97f6","usage":{"cachedInputTokens":4471209,"inputTokens":1026,"model":"claude-fable-5-1","outputTokens":51385,"runtime":"claude","turns":54,"wallClockMs":1054990}}],"verification":[{"checks":[{"durationMs":25537,"exitCode":0,"name":"build","output":"Compiling 92 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.42s\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/BMCSwapRouter.sol:53:9\n   │\n53 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:49:48\n   │\n49 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:50:21\n   │\n50 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:50:29\n   │\n50 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:63:9\n   │\n63 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:60:47\n   │\n60 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:61:17\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:61:25\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:75:9\n   │\n75 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:70:9\n   │\n70 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:71:49\n   │\n71 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:72:18\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:72:26\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCHook.sol:175:28\n    │\n175 │         uint256 amountIn = 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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:90:9\n   │\n90 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:85:9\n   │\n85 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:86:48\n   │\n86 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:87:20\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:87:28\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:103:13\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:103:39\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:103:47\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:105:62\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:105:70\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:109:28\n    │\n109 │             uint256 owed = 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/BMCSwapRouter.sol:109:36\n    │\n109 │             uint256 owed = 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/BMCHook.sol:205:44\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:205:52\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCSwapRouter.sol:112:13\n    │\n112 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:114:62\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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/BMCHook.sol:211:48\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:211:56\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCSwapRouter.sol:114:70\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:228:13\n    │\n228 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:231:13\n    │\n231 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:243:13\n    │\n243 │             emit SellBurn(amount, true);\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/BMCHook.sol:246:13\n    │\n246 │             emit SellBurn(amount, false);\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/BMCHook.sol:263:9\n    │\n263 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:280:9\n    │\n280 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":130,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 762.14µs (1.06ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 14153509)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.43ms (1.04ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 346491)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 397883)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 13.35ms (575.53µs CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 174720)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 202356)\n[PASS] test_exactAllowanceIsEnough() (gas: 302645)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 641632)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 219512)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 252430)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 14.13ms (2.42ms CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 323337)\n[PASS] test_exactInputFeeIsOnTheSpecifiedInputBeforeTheFill() (gas: 259011)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 370160)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 340279)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 492616)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 22.95ms (1.47ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 220753, ~: 220494)\nLogs:\n  Bound result 1000000000\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 206464, ~: 209358)\nLogs:\n  Bound result 1000000\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 266811)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTheEthThePoolCharged() (gas: 238170)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71189)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10562)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10706)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8619)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331442)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326129)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64266)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 36810173)\n[PASS] test_initializeRefusesANonEthPair() (gas: 37524440)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 37530048)\n[PASS] test_initializeRefusesASecondPool() (gas: 45178)\n[PASS] test_noOwnerOrSetters() (gas: 155526)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22780)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 384848)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 284356)\n[PASS] test_sellExactOutput_burnsOnePercentOfTheTokensThePoolCharged() (gas: 248831)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36075)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 42.80ms (84.60ms CPU time)\n\nRan 6 test suites in 43.74ms (95.43ms CPU time): 48 tests passed, 0 failed, 0 skipped (48 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":207,\"foundry.toml\":20,\"launch.json\":33,\"remappings.txt\":4,\"script/Deploy.s.sol\":67,\"site/app.js\":246,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":367,\"src/BMCSwapRouter.sol\":147,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":417,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"01c449fa88c92d49d0413a00d2329b3ecd37588b300cede455073699b1df26db","verifiedTreeHash":"6aab93cb18fc0cb397e548d7dfc9ac522020099c","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":26528,"exitCode":0,"name":"build","output":"Compiling 92 files with Solc 0.8.26\nSolc 0.8.26 finished in 26.41s\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/BMCSwapRouter.sol:53:9\n   │\n53 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:49:48\n   │\n49 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:50:21\n   │\n50 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:50:29\n   │\n50 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:63:9\n   │\n63 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCHook.sol:175:28\n    │\n175 │         uint256 amountIn = 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/BMCSwapRouter.sol:60:47\n   │\n60 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:61:17\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:61:25\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:75:9\n   │\n75 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:70:9\n   │\n70 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:71:49\n   │\n71 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:72:18\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:72:26\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCHook.sol:205:44\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:205:52\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:211:48\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:211:56\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:90:9\n   │\n90 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:85:9\n   │\n85 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:86:48\n   │\n86 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:87:20\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:87:28\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:103:13\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:103:39\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:103:47\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:105:62\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:105:70\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:109:28\n    │\n109 │             uint256 owed = 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/BMCSwapRouter.sol:109:36\n    │\n109 │             uint256 owed = 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/BMCSwapRouter.sol:112:13\n    │\n112 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:114:62\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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/BMCSwapRouter.sol:114:70\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:228:13\n    │\n228 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:231:13\n    │\n231 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:243:13\n    │\n243 │             emit SellBurn(amount, true);\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/BMCHook.sol:246:13\n    │\n246 │             emit SellBurn(amount, false);\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/BMCHook.sol:263:9\n    │\n263 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:280:9\n    │\n280 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":130,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 891.81µs (994.13µs CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 14153509)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.18ms (818.82µs CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 323337)\n[PASS] test_exactInputFeeIsOnTheSpecifiedInputBeforeTheFill() (gas: 259011)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 370160)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 340279)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 492616)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 14.08ms (1.57ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 346491)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 397883)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 22.76ms (690.28µs CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 174720)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 202356)\n[PASS] test_exactAllowanceIsEnough() (gas: 302645)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 641632)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 219512)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 252430)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 23.19ms (2.75ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 221206, ~: 220464)\nLogs:\n  Bound result 5000000000000000000\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 206957, ~: 209358)\nLogs:\n  Bound result 10000000000000000000000000\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 266811)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTheEthThePoolCharged() (gas: 238170)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71189)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10562)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10706)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8619)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331442)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326129)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64266)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 36810173)\n[PASS] test_initializeRefusesANonEthPair() (gas: 37524440)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 37530048)\n[PASS] test_initializeRefusesASecondPool() (gas: 45178)\n[PASS] test_noOwnerOrSetters() (gas: 155526)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22780)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 384848)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 284356)\n[PASS] test_sellExactOutput_burnsOnePercentOfTheTokensThePoolCharged() (gas: 248831)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36075)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 39.52ms (92.71ms CPU time)\n\nRan 6 test suites in 40.40ms (101.62ms CPU time): 48 tests passed, 0 failed, 0 skipped (48 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":207,\"foundry.toml\":20,\"remappings.txt\":4,\"script/Deploy.s.sol\":67,\"site/app.js\":246,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":367,\"src/BMCSwapRouter.sol\":147,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":417,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2272,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/BMCSwapRouter.sol:135: BMCSwapRouter._refundEth() (src/BMCSwapRouter.sol#135-141) sends eth to arbitrary user\n[high/medium] unchecked-transfer at src/BMCSwapRouter.sol:79: BMCSwapRouter.sellExactOut(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#79-91) ignores return value by IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender,address(this),maxTokensIn) (src/BMCSwapRouter.sol#85)\n[high/medium] unchecked-transfer at src/BMCSwapRouter.sol:67: BMCSwapRouter.sellExactIn(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#67-76) ignores return value by IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender,address(this),tokensIn) (src/BMCSwapRouter.sol#70)\n[medium/medium] unused-return at src/BMCHook.sol:262: BMCHook.collect() (src/BMCHook.sol#262-264) ignores return value by poolManager.unlock() (src/BMCHook.sol#263)\n[medium/medium] unused-return at src/BMCSwapRouter.sol:94: BMCSwapRouter.unlockCallback(bytes) (src/BMCSwapRouter.sol#94-118) ignores return value by poolManager.settle{value: uint256(uint128(- amount0))}() (src/BMCSwapRouter.sol#103)\n[medium/medium] unused-return at src/BMCSwapRouter.sol:94: BMCSwapRouter.unlockCallback(bytes) (src/BMCSwapRouter.sol#94-118) ignores return value by poolManager.settle() (src/BMCSwapRouter.sol#112)\n[low/medium] reentrancy-benign at src/BMCHook.sol:224: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#224-233):\n[low/medium] reentrancy-benign at src/BMCHook.sol:267: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#267-282):\n[low/medium] reentrancy-benign at src/BMCHook.sol:267: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#267-282):\n[low/medium] reentrancy-benign at src/BMCHook.sol:237: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#237-248):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:79: Reentrancy in BMCSwapRouter.sellExactOut(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#79-91):\n[low/medium] reentrancy-events at src/BMCHook.sol:237: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#237-248):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:67: Reentrancy in BMCSwapRouter.sellExactIn(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#67-76):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:46: Reentrancy in BMCSwapRouter.buyExactIn(PoolKey,uint256) (src/BMCSwapRouter.sol#46-54):\n[low/medium] reentrancy-events at src/BMCHook.sol:237: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#237-248):\n[low/medium] reentrancy-events at src/BMCHook.sol:224: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#224-233):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:57: Reentrancy in BMCSwapRouter.buyExactOut(PoolKey,uint256) (src/BMCSwapRouter.sol#57-64):\n[low/medium] reentrancy-events at src/BMCHook.sol:267: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#267-282):\n[low/medium] reentrancy-events at src/BMCHook.sol:224: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#224-233):","passed":true},{"durationMs":398,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/BMCHook.sol:270: Reentrancy: State change after external call (4 places)\n[low] internal-function-used-once at src/HookDeployer.sol:14: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/BMCHook.sol:47: Large Numeric Literal (3 places)\n[low] unchecked-return at src/BMCHook.sol:263: Unchecked Return (4 places)\n[low] unsafe-erc20-operation at src/BMCSwapRouter.sol:70: Unsafe ERC20 Operation (4 places)\n[low] unused-public-function at src/BMCHook.sol:251: Public Function Not Used Internally (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1132d833b0f014522c7cce6fdb37780f08f7374ac8c745ea72fcce1e993e0dfb","verifiedTreeHash":"4cb15601c32930bb0f4abfc705f6ef8ab0f90920","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":48624,"exitCode":0,"name":"build","output":"Compiling 95 files with Solc 0.8.26\nSolc 0.8.26 finished in 48.48s\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/BMCSwapRouter.sol:53:9\n   │\n53 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:49:48\n   │\n49 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:50:21\n   │\n50 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:50:29\n   │\n50 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:63:9\n   │\n63 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:60:47\n   │\n60 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:61:17\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:61:25\n   │\n61 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCHook.sol:175:28\n    │\n175 │         uint256 amountIn = 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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:75:9\n   │\n75 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:70:9\n   │\n70 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:71:49\n   │\n71 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:72:18\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:72:26\n   │\n72 │         ethOut = uint256(uint128(delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:90:9\n   │\n90 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:85:9\n   │\n85 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCHook.sol:205:44\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:205:52\n    │\n205 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:211:48\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:211:56\n    │\n211 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCSwapRouter.sol:86:48\n   │\n86 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:87:20\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:87:28\n   │\n87 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:103:13\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:103:39\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:103:47\n    │\n103 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:105:62\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:105:70\n    │\n105 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:109:28\n    │\n109 │             uint256 owed = 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/BMCSwapRouter.sol:109:36\n    │\n109 │             uint256 owed = 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/BMCSwapRouter.sol:112:13\n    │\n112 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:114:62\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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/BMCSwapRouter.sol:114:70\n    │\n114 │             poolManager.take(data.key.currency1, data.payer, 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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:228:13\n    │\n228 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:231:13\n    │\n231 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:243:13\n    │\n243 │             emit SellBurn(amount, true);\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/BMCHook.sol:246:13\n    │\n246 │             emit SellBurn(amount, false);\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/BMCHook.sol:263:9\n    │\n263 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:280:9\n    │\n280 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":3363,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 14153509)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.14ms (809.11µs CPU time)\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 2.74ms (956.43µs CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 323337)\n[PASS] test_exactInputFeeIsOnTheSpecifiedInputBeforeTheFill() (gas: 259011)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 370160)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 340279)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 492616)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 15.11ms (1.93ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 346491)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 397883)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.88ms (661.30µs CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 174720)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 202356)\n[PASS] test_exactAllowanceIsEnough() (gas: 302645)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 641632)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 219512)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 252430)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 17.22ms (2.35ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 220039, ~: 220412)\nLogs:\n  Bound result 4999999999003935599\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 207544, ~: 209358)\nLogs:\n  Bound result 3935598\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 266811)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTheEthThePoolCharged() (gas: 238170)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71189)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10562)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10706)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8619)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331442)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326129)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64266)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 36810173)\n[PASS] test_initializeRefusesANonEthPair() (gas: 37524440)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 37530048)\n[PASS] test_initializeRefusesASecondPool() (gas: 45178)\n[PASS] test_noOwnerOrSetters() (gas: 155526)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22780)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 384848)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 284356)\n[PASS] test_sellExactOutput_burnsOnePercentOfTheTokensThePoolCharged() (gas: 248831)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36075)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 55.02ms (124.04ms CPU time)\n\nRan 21 tests for test/BMCHookEdge.t.sol:BMCHookEdgeTest\n[PASS] testFuzz_buyExactOutput_feeIsOnePercentOfThePoolsEth(uint256) (runs: 64, μ: 223711, ~: 223896)\nLogs:\n  Bound result 19999999999999000003935599\n\n[PASS] testFuzz_constructorRejectsAnyRateAboveTheCap(uint256,uint256) (runs: 64, μ: 441877, ~: 441816)\nLogs:\n  Bound result 2616616032\n  Bound result 15\n\n[PASS] testFuzz_ratesAreAppliedAsConfigured(uint256,uint256,uint256) (runs: 64, μ: 15679444, ~: 7589122)\nLogs:\n  Bound result 3478\n  Bound result 4470\n  Bound result 245134469373803697\n\n[PASS] testFuzz_sellExactOutput_burnIsOnePercentOfThePoolsTokens(uint256) (runs: 64, μ: 232975, ~: 234274)\nLogs:\n  Bound result 49999999003935599\n\n[PASS] test_aHooklessBmcPoolIsUntouched() (gas: 600941)\n[PASS] test_buyFeeReachesAContractWalletThatAcceptsEth() (gas: 2767788)\n[PASS] test_buyFeeReachesTheBriefsCommunityWallet() (gas: 7143749)\n[PASS] test_collectCannotBeCalledWhileTheManagerIsUnlocked() (gas: 308085)\n[PASS] test_collectTwiceIsIdempotent() (gas: 284320)\n[PASS] test_constructorAcceptsTheCapExactly() (gas: 84515551)\n[PASS] test_dustBuyBelowOneHundredWeiPaysNoFeeAndDoesNotRevert() (gas: 152429)\n[PASS] test_dustSellBelowOneHundredWeiBurnsNothingAndDoesNotRevert() (gas: 149785)\n[PASS] test_feeAppliesThroughAnyRouterNotOnlyOurs() (gas: 253633)\n[PASS] test_hookBuiltForAnotherManagerRefusesThisOne() (gas: 11124060)\n[PASS] test_hookCannotBeAttachedToAnyOtherPoolAfterItsOwn() (gas: 77531)\n[PASS] test_hookDataCannotSwitchTheFeeOff() (gas: 210659)\n[PASS] test_hookRejectsPlainEth() (gas: 36491)\n[PASS] test_smallestChargedBuyPaysOneWei() (gas: 200258)\n[PASS] test_strangerCannotBurnTheHooksClaims() (gas: 38421)\n[PASS] test_swapperCanSettleWithClaimsAndStillPaysTheFee() (gas: 235629)\n[PASS] test_zeroRatesChargeNothingAndDoNotAlterTheSwap() (gas: 12067611)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 592.88ms (681.40ms CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantFundedPool\n[PASS]\nBMCHookInvariantFundedPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_someSwapsHappen\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantFundedPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+--------------+-------+---------+----------╮\n| Contract       | Selector     | Calls | Reverts | Discards |\n+============================================================+\n| BMCHookHandler | buyExactIn   | 1131  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut  | 1036  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | collect      | 1078  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn  | 1161  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut | 1120  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | transfer     | 1099  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | warp         | 1055  | 0       | 0        |\n╰----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 500000000000000000\n  Bound result 2048000\n  Bound result 2592000\n  Bound result 29867046890294527\n  Bound result 1935319960534402014\n  Bound result 613300456341021678840440\n  Bound result 5001\n  Bound result 11224\n  Bound result 2230\n  Bound result 2186436638481590677\n  Bound result 60\n  Bound result 5001\n  Bound result 14110\n  Bound result 3574\n  Bound result 887270\n  Bound result 500000000000000000\n  Bound result 1543730369157697068069989\n  Bound result 4840\n  Bound result 100000000000000000000\n  Bound result 4832\n  Bound result 17877\n  Bound result 2865\n  Bound result 8833\n  Bound result 3000000000000000000\n  Bound result 205083049737423647155080\n  Bound result 2600402852146055393\n  Bound result 1061083606226317802\n  Bound result 3000000000000000000\n  Bound result 5277\n  Bound result 12345000000000000000000\n  Bound result 15000000000000000\n  Bound result 856\n  Bound result 1000000000000000000\n  Bound result 321417271504213099\n  Bound result 2048000\n  Bound result 100000000000000\n  Bound result 59445\n  Bound result 1248000\n  Bound result 1856000\n  Bound result 3404369434286774459140652\n  Bound result 311155063104864255\n  Bound result 1\n  Bound result 3573\n  Bound result 9007199254740991\n  Bound result 500000000000000000\n  Bound result 10000\n  Bound result 1191124463843360770\n  Bound result 1555712\n  Bound result 1000000000\n  Bound result 9038\n  Bound result 3\n  Bound result 7985813153161979262845760\n  Bound result 309249\n  Bound result 37660389623361299919626239\n  Bound result 158228663557991727\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.24s (2.22s CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantTokenOnlyPool\n[PASS]\nBMCHookInvariantTokenOnlyPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantTokenOnlyPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+--------------+-------+---------+----------╮\n| Contract       | Selector     | Calls | Reverts | Discards |\n+============================================================+\n| BMCHookHandler | buyExactIn   | 1131  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut  | 1036  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | collect      | 1078  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn  | 1161  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut | 1120  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | transfer     | 1099  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | warp         | 1055  | 0       | 0        |\n╰----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 500000000000000000\n  Bound result 2048000\n  Bound result 2592000\n  Bound result 29867046890294527\n  Bound result 1935319960534402014\n  Bound result 613300456341021671064440\n  Bound result 5001\n  Bound result 9030\n  Bound result 1791999\n  Bound result 4739\n  Bound result 60\n  Bound result 5001\n  Bound result 13645\n  Bound result 3512197888\n  Bound result 887270\n  Bound result 500000000000000000\n  Bound result 4352439797075739106461263\n  Bound result 4340\n  Bound result 100000000000000000000\n  Bound result 2879\n  Bound result 17175\n  Bound result 10886\n  Bound result 4688\n  Bound result 3000000000000000000\n  Bound result 1213345599495259380215853\n  Bound result 2600402852146055393\n  Bound result 1061083606226317802\n  Bound result 3000000000000000000\n  Bound result 1560\n  Bound result 12345000000000000000000\n  Bound result 15000000000000000\n  Bound result 2256\n  Bound result 1000000000000000000\n  Bound result 321417271504213099\n  Bound result 2048000\n  Bound result 100000000000000\n  Bound result 59445\n  Bound result 1248000\n  Bound result 1856000\n  Bound result 6524219139312857454318938\n  Bound result 3608\n  Bound result 1\n  Bound result 440\n  Bound result 16954\n  Bound result 500000000000000000\n  Bound result 10000\n  Bound result 14786\n  Bound result 1902\n  Bound result 1000000000\n  Bound result 8812\n  Bound result 3\n  Bound result 7985813153161979262845760\n  Bound result 16955\n  Bound result 37660389623361299919626239\n  Bound result 158228663557991727\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.50s (2.48s CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantEthOnlyPool\n[PASS]\nBMCHookInvariantEthOnlyPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantEthOnlyPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+--------------+-------+---------+----------╮\n| Contract       | Selector     | Calls | Reverts | Discards |\n+============================================================+\n| BMCHookHandler | buyExactIn   | 1131  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut  | 1036  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | collect      | 1078  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn  | 1161  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut | 1120  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | transfer     | 1099  | 0       | 0        |\n|----------------+--------------+-------+---------+----------|\n| BMCHookHandler | warp         | 1055  | 0       | 0        |\n╰----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 500000000000000000\n  Bound result 2048000\n  Bound result 2592000\n  Bound result 29867046890294527\n  Bound result 1935319960534402014\n  Bound result 613300456341021671064440\n  Bound result 5001\n  Bound result 5246\n  Bound result 8106\n  Bound result 4413\n  Bound result 60\n  Bound result 5001\n  Bound result 13717\n  Bound result 21917386103515624534704127\n  Bound result 887270\n  Bound result 500000000000000000\n  Bound result 369152627191754711572622\n  Bound result 12792\n  Bound result 100000000000000000000\n  Bound result 21440\n  Bound result 8339\n  Bound result 4508378477925045080212898\n  Bound result 2302811999\n  Bound result 3000000000000000000\n  Bound result 114348259486864076486918\n  Bound result 2600402852146055393\n  Bound result 1061083606226317802\n  Bound result 3000000000000000000\n  Bound result 12923\n  Bound result 12345000000000000000000\n  Bound result 15000000000000000\n  Bound result 673\n  Bound result 1000000000000000000\n  Bound result 321417271504213099\n  Bound result 2048000\n  Bound result 100000000000000\n  Bound result 59445\n  Bound result 1248000\n  Bound result 1856000\n  Bound result 4986277387287597570723876\n  Bound result 1539\n  Bound result 1\n  Bound result 14346\n  Bound result 8329\n  Bound result 500000000000000000\n  Bound result 10000\n  Bound result 5001\n  Bound result 4261\n  Bound result 1000000000\n  Bound result 2209400481361428480\n  Bound result 3\n  Bound result 7985813153161979262845760\n  Bound result 7444\n  Bound result 37660389623361299919626239\n  Bound result 158228663557991727\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.26s (3.23s CPU time)\n\nRan 10 test suites in 3.26s (8.69s CPU time): 72 tests passed, 0 failed, 0 skipped (72 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":207,\"foundry.toml\":20,\"remappings.txt\":4,\"script/Deploy.s.sol\":67,\"site/app.js\":246,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":367,\"src/BMCSwapRouter.sol\":147,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":417,\"test/BMCHookEdge.t.sol\":334,\"test/BMCHookInvariant.t.sol\":327,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2b2d5cb4583f4c9cb9aff03731cdcaa3b9a0d1fb97709d2b85918c4fabda68c2","verifiedTreeHash":"d8068fb33ff12226e0344062848c15c91c84fb2a","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":41737,"exitCode":0,"name":"build","output":"Compiling 94 files with Solc 0.8.26\nSolc 0.8.26 finished in 41.61s\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/BMCSwapRouter.sol:54:9\n   │\n54 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:50:48\n   │\n50 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:51:21\n   │\n51 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:51:29\n   │\n51 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:66:9\n   │\n66 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:61:47\n   │\n61 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:62:28\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:62:36\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:64:17\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:64:25\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:78:9\n   │\n78 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:73:9\n   │\n73 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:74:49\n   │\n74 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:75:18\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:75:26\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:95:9\n   │\n95 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:88:9\n   │\n88 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:89:48\n   │\n89 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:90:28\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:90:36\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:92:20\n   │\n92 │         tokensIn = uint256(uint128(-delta.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[reentrancy-no-eth]: external call can be reentered before `payingEth` is updated\n    ╭▸ src/BMCHook.sol:253:13\n    │\n253 │         try poolManager.take{gas: ETH_PAYOUT_GAS}(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, amount) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:92:28\n   │\n92 │         tokensIn = uint256(uint128(-delta.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/BMCHook.sol:182:28\n    │\n182 │         uint256 amountIn = 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[unused-return]: return value of an external call is not used\n    ╭▸ src/BMCSwapRouter.sol:108:13\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:108:39\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:108:47\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:110:62\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:110:70\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:114:28\n    │\n114 │             uint256 owed = 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/BMCSwapRouter.sol:114:36\n    │\n114 │             uint256 owed = 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/BMCSwapRouter.sol:117:13\n    │\n117 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:119:62\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCSwapRouter.sol:119:70\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCHook.sol:206:32\n    │\n206 │             uint256 amountIn = 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/BMCHook.sol:210:54\n    │\n210 │             uint256 consumedInput = inputDelta < 0 ? uint256(-int256(inputDelta)) : 0;\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/BMCHook.sol:221:44\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:221:52\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:227:48\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:227:56\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:242:13\n    │\n242 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:245:13\n    │\n245 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:267:13\n    │\n267 │             emit SellBurn(amount, true);\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/BMCHook.sol:270:13\n    │\n270 │             emit SellBurn(amount, false);\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/BMCHook.sol:287:9\n    │\n287 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:309:9\n    │\n309 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":140,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 747.08µs (1.13ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 15830827)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.19ms (2.83ms CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 328497)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 375408)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 345200)\n[PASS] test_largeFullyFilledBuyPaysOnePercent() (gas: 261709)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 508236)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 11.39ms (1.70ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCCollectionFailureTest\n[PASS] test_rejectedEthCollectionStillProcessesPendingBurns() (gas: 20543569)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.50ms (11.06ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 348733)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 400591)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.51ms (559.70µs CPU time)\n\nRan 9 tests for test/BMCRevision.t.sol:BMCPartialFillTest\n[PASS] test_buyAtTightPriceLimitRollsBackThePrepaidEthFee() (gas: 2286775)\n[PASS] test_buyBeyondTokenLiquidityRollsBackTheFeeClaim() (gas: 987271)\n[PASS] test_exactOutputBuyBeyondLiquidityReverts() (gas: 1001674)\n[PASS] test_exactOutputSaleBeyondLiquidityReverts() (gas: 1182103)\n[PASS] test_exactOutputSaleWithNoEthReverts() (gas: 828785)\n[PASS] test_saleAtTightPriceLimitRollsBackThePrepaidBurn() (gas: 2311382)\n[PASS] test_saleBeyondAvailableEthRevertsWithoutBurning() (gas: 1170823)\n[PASS] test_successfulLaunchBuyThenSaleStillFills() (gas: 677411)\n[PASS] test_zeroFillSaleRevertsWithoutBurningOrMovingThePrice() (gas: 901366)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 13.71ms (8.14ms CPU time)\n\nRan 6 tests for test/BMCRevision.t.sol:BMCRecipientFailureTest\n[PASS] testFuzz_exactOutputBuyChargesOnePercentOfTotal(uint256) (runs: 64, μ: 222685, ~: 232091)\nLogs:\n  Bound result 9847801438850557441628196\n\n[PASS] testFuzz_exactOutputSellBurnsOnePercentOfTotal(uint256) (runs: 64, μ: 236264, ~: 238035)\nLogs:\n  Bound result 2442474906697134\n\n[PASS] test_exactOutputBuyRetainsRejectedFeeAndSellsStillWork() (gas: 529606)\n[PASS] test_gasExhaustingRecipientCannotHaltBuyOrCollection() (gas: 531048)\n[PASS] test_recipientCannotTradeAheadInsideTheBuyFeePayment() (gas: 588606)\n[PASS] test_rejectedBuyFeeBecomesAClaimAndLaterPaysExactlyOnce() (gas: 664678)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 24.00ms (21.88ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 223911, ~: 223581)\nLogs:\n  Bound result 1574820052238683693\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 210147, ~: 211956)\nLogs:\n  Bound result 9847801438850557441628196\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 269944)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTotalEthPaid() (gas: 241330)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71244)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10606)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10728)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8641)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331662)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326151)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64420)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 83150381)\n[PASS] test_initializeRefusesANonEthPair() (gas: 83892872)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 83898502)\n[PASS] test_initializeRefusesASecondPool() (gas: 45222)\n[PASS] test_noOwnerOrSetters() (gas: 155702)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22802)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 390557)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 287020)\n[PASS] test_sellExactOutput_burnsOnePercentOfTotalTokensPaid() (gas: 251887)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36141)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 45.55ms (121.41ms CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 197396)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 225345)\n[PASS] test_exactAllowanceIsEnough() (gas: 304724)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 651992)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 221774)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 255028)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 45.54ms (3.98ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCAtomicInitializationTest\n[PASS] test_predictedPoolCannotInitializeBeforeHookDeployment() (gas: 140339071)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 49.28ms (48.79ms CPU time)\n\nRan 10 test suites in 50.00ms (217.42ms CPU time): 65 tests passed, 0 failed, 0 skipped (65 total tests)\n","passed":true},{"durationMs":47,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":236,\"foundry.toml\":20,\"launch.json\":33,\"remappings.txt\":4,\"script/Deploy.s.sol\":72,\"site/app.js\":251,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":396,\"src/BMCSwapRouter.sol\":152,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":419,\"test/BMCRevision.t.sol\":330,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/site/app.test.cjs\":117,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cb85b1500f8fef679b7a1908074524f5b2e469b1d3db1657952263923cae22b0","verifiedTreeHash":"0ddd52dea68de6b8ab625fb3d0c13e16e7df0fad","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":42590,"exitCode":0,"name":"build","output":"Compiling 94 files with Solc 0.8.26\nSolc 0.8.26 finished in 42.46s\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/BMCSwapRouter.sol:54:9\n   │\n54 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:50:48\n   │\n50 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:51:21\n   │\n51 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:51:29\n   │\n51 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:66:9\n   │\n66 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:61:47\n   │\n61 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:62:28\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:62:36\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:64:17\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:64:25\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:78:9\n   │\n78 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:73:9\n   │\n73 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:74:49\n   │\n74 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:75:18\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:75:26\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:95:9\n   │\n95 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:88:9\n   │\n88 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:89:48\n   │\n89 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:90:28\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:90:36\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:92:20\n   │\n92 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:92:28\n   │\n92 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:108:13\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:108:39\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:108:47\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:110:62\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:110:70\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:114:28\n    │\n114 │             uint256 owed = 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/BMCSwapRouter.sol:114:36\n    │\n114 │             uint256 owed = 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[reentrancy-no-eth]: external call can be reentered before `payingEth` is updated\n    ╭▸ src/BMCHook.sol:253:13\n    │\n253 │         try poolManager.take{gas: ETH_PAYOUT_GAS}(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, amount) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/BMCSwapRouter.sol:117:13\n    │\n117 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:119:62\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCSwapRouter.sol:119:70\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCHook.sol:182:28\n    │\n182 │         uint256 amountIn = 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/BMCHook.sol:206:32\n    │\n206 │             uint256 amountIn = 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/BMCHook.sol:210:54\n    │\n210 │             uint256 consumedInput = inputDelta < 0 ? uint256(-int256(inputDelta)) : 0;\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/BMCHook.sol:221:44\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:221:52\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:227:48\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:227:56\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:242:13\n    │\n242 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:245:13\n    │\n245 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:267:13\n    │\n267 │             emit SellBurn(amount, true);\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/BMCHook.sol:270:13\n    │\n270 │             emit SellBurn(amount, false);\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/BMCHook.sol:287:9\n    │\n287 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:309:9\n    │\n309 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":149,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 745.44µs (1.09ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 15830827)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.27ms (2.95ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 348733)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 400591)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.96ms (540.90µs CPU time)\n\nRan 9 tests for test/BMCRevision.t.sol:BMCPartialFillTest\n[PASS] test_buyAtTightPriceLimitRollsBackThePrepaidEthFee() (gas: 2286775)\n[PASS] test_buyBeyondTokenLiquidityRollsBackTheFeeClaim() (gas: 987271)\n[PASS] test_exactOutputBuyBeyondLiquidityReverts() (gas: 1001674)\n[PASS] test_exactOutputSaleBeyondLiquidityReverts() (gas: 1182103)\n[PASS] test_exactOutputSaleWithNoEthReverts() (gas: 828785)\n[PASS] test_saleAtTightPriceLimitRollsBackThePrepaidBurn() (gas: 2311382)\n[PASS] test_saleBeyondAvailableEthRevertsWithoutBurning() (gas: 1170823)\n[PASS] test_successfulLaunchBuyThenSaleStillFills() (gas: 677411)\n[PASS] test_zeroFillSaleRevertsWithoutBurningOrMovingThePrice() (gas: 901366)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 13.22ms (8.84ms CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 328497)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 375408)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 345200)\n[PASS] test_largeFullyFilledBuyPaysOnePercent() (gas: 261709)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 508236)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 13.19ms (1.85ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCCollectionFailureTest\n[PASS] test_rejectedEthCollectionStillProcessesPendingBurns() (gas: 20543569)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.50ms (14.06ms CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 197396)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 225345)\n[PASS] test_exactAllowanceIsEnough() (gas: 304724)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 651992)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 221774)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 255028)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 14.96ms (2.25ms CPU time)\n\nRan 6 tests for test/BMCRevision.t.sol:BMCRecipientFailureTest\n[PASS] testFuzz_exactOutputBuyChargesOnePercentOfTotal(uint256) (runs: 64, μ: 224508, ~: 232071)\nLogs:\n  Bound result 15581125122683\n\n[PASS] testFuzz_exactOutputSellBurnsOnePercentOfTotal(uint256) (runs: 64, μ: 236226, ~: 238005)\nLogs:\n  Bound result 15581125122683\n\n[PASS] test_exactOutputBuyRetainsRejectedFeeAndSellsStillWork() (gas: 529606)\n[PASS] test_gasExhaustingRecipientCannotHaltBuyOrCollection() (gas: 531048)\n[PASS] test_recipientCannotTradeAheadInsideTheBuyFeePayment() (gas: 588606)\n[PASS] test_rejectedBuyFeeBecomesAClaimAndLaterPaysExactlyOnce() (gas: 664678)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 30.07ms (22.84ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 223842, ~: 223579)\nLogs:\n  Bound result 15581125122683\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 209614, ~: 211956)\nLogs:\n  Bound result 15581125122683\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 269944)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTotalEthPaid() (gas: 241330)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71244)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10606)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10728)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8641)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331662)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326151)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64420)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 83150381)\n[PASS] test_initializeRefusesANonEthPair() (gas: 83892872)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 83898502)\n[PASS] test_initializeRefusesASecondPool() (gas: 45222)\n[PASS] test_noOwnerOrSetters() (gas: 155702)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22802)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 390557)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 287020)\n[PASS] test_sellExactOutput_burnsOnePercentOfTotalTokensPaid() (gas: 251887)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36141)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 55.76ms (140.54ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCAtomicInitializationTest\n[PASS] test_predictedPoolCannotInitializeBeforeHookDeployment() (gas: 140339071)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 56.81ms (56.28ms CPU time)\n\nRan 10 test suites in 57.74ms (215.47ms CPU time): 65 tests passed, 0 failed, 0 skipped (65 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":236,\"foundry.toml\":20,\"remappings.txt\":4,\"script/Deploy.s.sol\":72,\"site/app.js\":251,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":396,\"src/BMCSwapRouter.sol\":152,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":419,\"test/BMCRevision.t.sol\":330,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/site/app.test.cjs\":117,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2349,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/BMCSwapRouter.sol:140: BMCSwapRouter._refundEth() (src/BMCSwapRouter.sol#140-146) sends eth to arbitrary user\n[high/medium] unchecked-transfer at src/BMCSwapRouter.sol:70: BMCSwapRouter.sellExactIn(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#70-79) ignores return value by IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender,address(this),tokensIn) (src/BMCSwapRouter.sol#73)\n[high/medium] unchecked-transfer at src/BMCSwapRouter.sol:82: BMCSwapRouter.sellExactOut(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#82-96) ignores return value by IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender,address(this),maxTokensIn) (src/BMCSwapRouter.sol#88)\n[medium/medium] unused-return at src/BMCSwapRouter.sol:99: BMCSwapRouter.unlockCallback(bytes) (src/BMCSwapRouter.sol#99-123) ignores return value by poolManager.settle{value: uint256(uint128(- amount0))}() (src/BMCSwapRouter.sol#108)\n[medium/medium] unused-return at src/BMCHook.sol:286: BMCHook.collect() (src/BMCHook.sol#286-288) ignores return value by poolManager.unlock() (src/BMCHook.sol#287)\n[medium/medium] unused-return at src/BMCSwapRouter.sol:99: BMCSwapRouter.unlockCallback(bytes) (src/BMCSwapRouter.sol#99-123) ignores return value by poolManager.settle() (src/BMCSwapRouter.sol#117)\n[low/medium] reentrancy-benign at src/BMCHook.sol:261: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#261-272):\n[low/medium] reentrancy-benign at src/BMCHook.sol:291: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#291-311):\n[low/medium] reentrancy-benign at src/BMCHook.sol:239: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#239-247):\n[low/medium] reentrancy-benign at src/BMCHook.sol:291: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#291-311):\n[low/medium] reentrancy-benign at src/BMCHook.sol:251: Reentrancy in BMCHook._tryPayEth(uint256) (src/BMCHook.sol#251-257):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:47: Reentrancy in BMCSwapRouter.buyExactIn(PoolKey,uint256) (src/BMCSwapRouter.sol#47-55):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:82: Reentrancy in BMCSwapRouter.sellExactOut(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#82-96):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:58: Reentrancy in BMCSwapRouter.buyExactOut(PoolKey,uint256) (src/BMCSwapRouter.sol#58-67):\n[low/medium] reentrancy-events at src/BMCHook.sol:291: Reentrancy in BMCHook.unlockCallback(bytes) (src/BMCHook.sol#291-311):\n[low/medium] reentrancy-events at src/BMCHook.sol:239: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#239-247):\n[low/medium] reentrancy-events at src/BMCSwapRouter.sol:70: Reentrancy in BMCSwapRouter.sellExactIn(PoolKey,uint256,uint256) (src/BMCSwapRouter.sol#70-79):\n[low/medium] reentrancy-events at src/BMCHook.sol:261: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#261-272):\n[low/medium] reentrancy-events at src/BMCHook.sol:261: Reentrancy in BMCHook._takeAndBurn(uint256) (src/BMCHook.sol#261-272):\n[low/medium] reentrancy-events at src/BMCHook.sol:239: Reentrancy in BMCHook._takeEthFee(uint256) (src/BMCHook.sol#239-247):","passed":true},{"durationMs":434,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/BMCHook.sol:298: Reentrancy: State change after external call (4 places)\n[low] internal-function-used-once at src/HookDeployer.sol:14: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/BMCHook.sol:48: Large Numeric Literal (4 places)\n[low] unchecked-return at src/BMCHook.sol:287: Unchecked Return (4 places)\n[low] unsafe-erc20-operation at src/BMCSwapRouter.sol:73: Unsafe ERC20 Operation (4 places)\n[low] unused-public-function at src/BMCHook.sol:275: Public Function Not Used Internally (2 places)","passed":true},{"durationMs":8135,"exitCode":0,"name":"proof c3980bb50008","output":"Compiling 87 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.56s\nCompiler run successful!\n\nRan 2 tests for test/imd-proof-c44910e1/Proof_c3980bb50008.t.sol:RecipientRejectsEthTest\n[PASS] test_buyStillCompletesWhenTheWalletRejectsEth() (gas: 209030)\n[PASS] test_exactOutputBuyStillCompletesWhenTheWalletRejectsEth() (gas: 214920)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.96ms (659.21µs CPU time)\n\nRan 1 test suite in 3.19ms (1.96ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d7cae8101078af6609b85815bccbd8decd4183bb4af2c736c21b87d79d6096b9","verifiedTreeHash":"c4c88a9cd21e44343f23ae35cc8bf32bf0a709ba","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":67678,"exitCode":0,"name":"build","output":"Compiling 96 files with Solc 0.8.26\nSolc 0.8.26 finished in 67.53s\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/BMCSwapRouter.sol:54:9\n   │\n54 │         emit Swapped(msg.sender, true, msg.value, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:50:48\n   │\n50 │         BalanceDelta delta = _swap(key, true, -int256(msg.value));\n   │                                                ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:51:21\n   │\n51 │         tokensOut = uint256(uint128(delta.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/BMCSwapRouter.sol:51:29\n   │\n51 │         tokensOut = uint256(uint128(delta.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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:66:9\n   │\n66 │         emit Swapped(msg.sender, true, ethIn, tokensOut);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:61:47\n   │\n61 │         BalanceDelta delta = _swap(key, true, int256(tokensOut));\n   │                                               ━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:62:28\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:62:36\n   │\n62 │         uint256 received = uint256(uint128(delta.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/BMCSwapRouter.sol:64:17\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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/BMCSwapRouter.sol:64:25\n   │\n64 │         ethIn = uint256(uint128(-delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:78:9\n   │\n78 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:73:9\n   │\n73 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:74:49\n   │\n74 │         BalanceDelta delta = _swap(key, false, -int256(tokensIn));\n   │                                                 ━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:75:18\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:75:26\n   │\n75 │         ethOut = uint256(uint128(delta.amount0()));\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/BMCSwapRouter.sol:95:9\n   │\n95 │         emit Swapped(msg.sender, false, ethOut, tokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[erc20-unchecked-transfer]: ERC20 `transfer` or `transferFrom` call does not check the return value\n   ╭▸ src/BMCSwapRouter.sol:88:9\n   │\n88 │         IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), maxTokensIn);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/BMCSwapRouter.sol:89:48\n   │\n89 │         BalanceDelta delta = _swap(key, false, int256(ethOut));\n   │                                                ━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'int256' 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/BMCSwapRouter.sol:90:28\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:90:36\n   │\n90 │         uint256 received = uint256(uint128(delta.amount0()));\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/BMCSwapRouter.sol:92:20\n   │\n92 │         tokensIn = uint256(uint128(-delta.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/BMCSwapRouter.sol:92:28\n   │\n92 │         tokensIn = uint256(uint128(-delta.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[reentrancy-no-eth]: external call can be reentered before `payingEth` is updated\n    ╭▸ src/BMCHook.sol:253:13\n    │\n253 │         try poolManager.take{gas: ETH_PAYOUT_GAS}(CurrencyLibrary.ADDRESS_ZERO, feeRecipient, amount) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/BMCSwapRouter.sol:108:13\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:108:39\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:108:47\n    │\n108 │             poolManager.settle{value: uint256(uint128(-amount0))}();\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/BMCSwapRouter.sol:110:62\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:110:70\n    │\n110 │             poolManager.take(data.key.currency0, data.payer, uint256(uint128(amount0)));\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/BMCSwapRouter.sol:114:28\n    │\n114 │             uint256 owed = 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/BMCHook.sol:182:28\n    │\n182 │         uint256 amountIn = 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/BMCSwapRouter.sol:114:36\n    │\n114 │             uint256 owed = 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/BMCSwapRouter.sol:117:13\n    │\n117 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/BMCSwapRouter.sol:119:62\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCSwapRouter.sol:119:70\n    │\n119 │             poolManager.take(data.key.currency1, data.payer, 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/BMCHook.sol:206:32\n    │\n206 │             uint256 amountIn = 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/BMCHook.sol:210:54\n    │\n210 │             uint256 consumedInput = inputDelta < 0 ? uint256(-int256(inputDelta)) : 0;\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/BMCHook.sol:221:44\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:221:52\n    │\n221 │             uint256 ethIn = ethDelta < 0 ? uint256(uint128(-ethDelta)) : 0;\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/BMCHook.sol:227:48\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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/BMCHook.sol:227:56\n    │\n227 │             uint256 tokenIn = tokenDelta < 0 ? uint256(uint128(-tokenDelta)) : 0;\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[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:242:13\n    │\n242 │             emit BuyFeeTaken(fee, true);\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/BMCHook.sol:245:13\n    │\n245 │             emit BuyFeeTaken(fee, false);\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/BMCHook.sol:267:13\n    │\n267 │             emit SellBurn(amount, true);\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/BMCHook.sol:270:13\n    │\n270 │             emit SellBurn(amount, false);\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/BMCHook.sol:287:9\n    │\n287 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/BMCHook.sol:309:9\n    │\n309 │         emit FeesCollected(ethPaid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":3081,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/BMCToken.t.sol:BMCTokenTest\n[PASS] test_burnFromNeedsAllowance() (gas: 174677)\n[PASS] test_burnMoreThanBalanceReverts() (gas: 34709)\n[PASS] test_burnReducesSupply() (gas: 110331)\n[PASS] test_metadataAndSupply() (gas: 57492)\n[PASS] test_noMintPath() (gas: 48960)\n[PASS] test_noOwnerOrPause() (gas: 100315)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 35057)\n[PASS] test_transferMovesExactly() (gas: 84691)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.90ms (1.24ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployPlacesTheHookOnAFlaggedAddressAndWiresIt() (gas: 15830827)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.24ms (3.79ms CPU time)\n\nRan 11 tests for test/BMCSwapRouter.t.sol:BMCSwapRouterTest\n[PASS] test_buyExactOutRevertsWhenEthIsShort() (gas: 197396)\n[PASS] test_buyRevertsBelowMinimumOutput() (gas: 225345)\n[PASS] test_exactAllowanceIsEnough() (gas: 304724)\n[PASS] test_hookOnlyAppliesToItsPool() (gas: 7811)\n[PASS] test_refusesPoolsNotPairedWithEth() (gas: 799746)\n[PASS] test_routerHoldsNothingAfterSwaps() (gas: 651992)\n[PASS] test_sellExactOutRevertsWhenMoreTokensAreNeededThanAllowed() (gas: 221774)\n[PASS] test_sellRequiresAllowance() (gas: 101389)\n[PASS] test_sellRevertsBelowMinimumOutput() (gas: 255028)\n[PASS] test_unlockCallbackRefusesCallersOtherThanThePoolManager() (gas: 31678)\n[PASS] test_zeroAmountsRevert() (gas: 64613)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 12.41ms (2.76ms CPU time)\n\nRan 5 tests for test/BMCHook.t.sol:BMCHookTokenOnlyPoolTest\n[PASS] test_anyoneCanCollect() (gas: 328497)\n[PASS] test_firstBuyBooksTheFeeAsAClaim_collectPaysIt() (gas: 375408)\n[PASS] test_firstExactOutputBuyBooksTheFeeAsAClaim() (gas: 345200)\n[PASS] test_largeFullyFilledBuyPaysOnePercent() (gas: 261709)\n[PASS] test_onceTheManagerHoldsEthLaterBuysPayDirectly() (gas: 508236)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 15.33ms (1.74ms CPU time)\n\nRan 9 tests for test/BMCRevision.t.sol:BMCPartialFillTest\n[PASS] test_buyAtTightPriceLimitRollsBackThePrepaidEthFee() (gas: 2286775)\n[PASS] test_buyBeyondTokenLiquidityRollsBackTheFeeClaim() (gas: 987271)\n[PASS] test_exactOutputBuyBeyondLiquidityReverts() (gas: 1001674)\n[PASS] test_exactOutputSaleBeyondLiquidityReverts() (gas: 1182103)\n[PASS] test_exactOutputSaleWithNoEthReverts() (gas: 828785)\n[PASS] test_saleAtTightPriceLimitRollsBackThePrepaidBurn() (gas: 2311382)\n[PASS] test_saleBeyondAvailableEthRevertsWithoutBurning() (gas: 1170823)\n[PASS] test_successfulLaunchBuyThenSaleStillFills() (gas: 677411)\n[PASS] test_zeroFillSaleRevertsWithoutBurningOrMovingThePrice() (gas: 901366)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 16.66ms (9.68ms CPU time)\n\nRan 2 tests for test/BMCHook.t.sol:BMCHookEthOnlyPoolTest\n[PASS] test_firstExactOutputSaleBooksTheBurnAsAClaim() (gas: 348733)\n[PASS] test_firstSaleBooksTheBurnAsAClaim_collectBurnsIt() (gas: 400591)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 25.36ms (527.72µs CPU time)\n\nRan 6 tests for test/BMCRevision.t.sol:BMCRecipientFailureTest\n[PASS] testFuzz_exactOutputBuyChargesOnePercentOfTotal(uint256) (runs: 64, μ: 227307, ~: 232057)\nLogs:\n  Bound result 166538335344337390748946\n\n[PASS] testFuzz_exactOutputSellBurnsOnePercentOfTotal(uint256) (runs: 64, μ: 236638, ~: 238021)\nLogs:\n  Bound result 35347668153418180\n\n[PASS] test_exactOutputBuyRetainsRejectedFeeAndSellsStillWork() (gas: 529606)\n[PASS] test_gasExhaustingRecipientCannotHaltBuyOrCollection() (gas: 531048)\n[PASS] test_recipientCannotTradeAheadInsideTheBuyFeePayment() (gas: 588606)\n[PASS] test_rejectedBuyFeeBecomesAClaimAndLaterPaysExactlyOnce() (gas: 664678)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 25.72ms (23.87ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCCollectionFailureTest\n[PASS] test_rejectedEthCollectionStillProcessesPendingBurns() (gas: 20543569)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 27.16ms (21.65ms CPU time)\n\nRan 1 test for test/BMCRevision.t.sol:BMCAtomicInitializationTest\n[PASS] test_predictedPoolCannotInitializeBeforeHookDeployment() (gas: 140339071)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 54.37ms (52.70ms CPU time)\n\nRan 21 tests for test/BMCHook.t.sol:BMCHookTest\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 64, μ: 224000, ~: 223597)\nLogs:\n  Bound result 3335377644390715639\n\n[PASS] testFuzz_sellExactInput_burnIsExactlyOnePercent(uint256) (runs: 64, μ: 209831, ~: 211956)\nLogs:\n  Bound result 166538335344337390748946\n\n[PASS] test_buyExactInput_sendsOnePercentOfEthToTheCommunityWallet() (gas: 269944)\n[PASS] test_buyExactOutput_feeIsOnePercentOfTotalEthPaid() (gas: 241330)\n[PASS] test_collectWithNothingPendingChangesNothing() (gas: 71244)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 10606)\n[PASS] test_constructorRejectsRatesAboveTheCap() (gas: 10728)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8641)\n[PASS] test_everyCallbackRefusesCallersOtherThanThePoolManager() (gas: 331662)\n[PASS] test_hookDoesNotBlockLiquidityChanges() (gas: 326151)\n[PASS] test_immutablesAreTheConstructorArguments() (gas: 64420)\n[PASS] test_initializeFromAnyFactoryAddress() (gas: 83150381)\n[PASS] test_initializeRefusesANonEthPair() (gas: 83892872)\n[PASS] test_initializeRefusesAPoolWithoutTheToken() (gas: 83898502)\n[PASS] test_initializeRefusesASecondPool() (gas: 45222)\n[PASS] test_noOwnerOrSetters() (gas: 155702)\n[PASS] test_permissionsMatchAddressFlags() (gas: 22802)\n[PASS] test_roundTripReducesSupplyAndPaysTheWallet() (gas: 390557)\n[PASS] test_sellExactInput_burnsOnePercentOfTheTokensSold() (gas: 287020)\n[PASS] test_sellExactOutput_burnsOnePercentOfTotalTokensPaid() (gas: 251887)\n[PASS] test_unusedCallbacksRevertEvenForThePoolManager() (gas: 36141)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 68.06ms (162.23ms CPU time)\n\nRan 32 tests for test/BMCHookEdge.t.sol:BMCHookEdgeTest\n[PASS] testFuzz_buyExactOutput_feeIsOnePercentOfTotalEthPaid(uint256) (runs: 64, μ: 228736, ~: 228897)\nLogs:\n  Bound result 166538335344337390748946\n\n[PASS] testFuzz_constructorRejectsAnyRateAboveTheCap(uint256,uint256) (runs: 64, μ: 463287, ~: 463219)\nLogs:\n  Bound result 5004\n  Bound result 1606\n\n[PASS] testFuzz_exactOutputChargesTheRateOnTotalInputForAnyRate(uint256,uint256,uint256) (runs: 64, μ: 923930, ~: 923933)\nLogs:\n  Bound result 1473\n  Bound result 1983\n  Bound result 501784166321\n\n[PASS] testFuzz_ratesAreAppliedAsConfigured(uint256,uint256,uint256) (runs: 64, μ: 916122, ~: 914446)\nLogs:\n  Bound result 960\n  Bound result 1977\n  Bound result 110936465758698778\n\n[PASS] testFuzz_sellExactOutput_burnIsOnePercentOfTotalTokensPaid(uint256) (runs: 64, μ: 236447, ~: 237961)\nLogs:\n  Bound result 38675103387418180\n\n[PASS] test_aHooklessBmcPoolIsUntouched() (gas: 600985)\n[PASS] test_buyFeeReachesAContractWalletThatAcceptsEth() (gas: 815238)\n[PASS] test_buyFeeReachesTheBriefsCommunityWallet() (gas: 686544)\n[PASS] test_claimsSurviveLaterDirectPaymentsIntact() (gas: 1129890)\n[PASS] test_collectCannotBeCalledWhileTheManagerIsUnlocked() (gas: 308107)\n[PASS] test_collectTwiceIsIdempotent() (gas: 287748)\n[PASS] test_constructorAcceptsTheCapExactly() (gas: 33454444)\n[PASS] test_dustBuyBelowOneHundredWeiPaysNoFeeAndDoesNotRevert() (gas: 155177)\n[PASS] test_dustSellBelowOneHundredWeiBurnsNothingAndDoesNotRevert() (gas: 152449)\n[PASS] test_feeAppliesThroughAnyRouterNotOnlyOurs() (gas: 256451)\n[PASS] test_hookBuiltForAnotherManagerRefusesThisOne() (gas: 16225826)\n[PASS] test_hookCannotBeAttachedToAnyOtherPoolAfterItsOwn() (gas: 77808)\n[PASS] test_hookDataCannotSwitchTheFeeOff() (gas: 213990)\n[PASS] test_hookRejectsPlainEth() (gas: 36569)\n[PASS] test_recipientReenteringAnySwapShapeIsRefusedWithReentrantSwap() (gas: 1877333)\n[PASS] test_recipientReenteringDuringCollectIsRefused() (gas: 1430016)\n[PASS] test_recipientUsingMostOfTheBudgetIsStillPaidDirectly() (gas: 906543)\n[PASS] test_shortFilledBuyRevertsWithPartialFillNamingBothAmounts() (gas: 320444)\n[PASS] test_shortFilledExactOutputBuyPaysTheRateOnWhatWasPaid() (gas: 224747)\n[PASS] test_shortFilledSaleRevertsWithPartialFillNamingBothAmounts() (gas: 357416)\n[PASS] test_smallestChargedBuyPaysOneWei() (gas: 203545)\n[PASS] test_spinningRecipientLeavesAClaimAndDoesNotExhaustTheBuyersGas() (gas: 1135044)\n[PASS] test_strangerCannotBurnTheHooksClaims() (gas: 38575)\n[PASS] test_swapperCanSettleWithClaimsAndStillPaysTheFee() (gas: 238850)\n[PASS] test_swapsResumeAfterARefusedReentry() (gas: 1571269)\n[PASS] test_zeroFillReportsZeroConsumed() (gas: 175126)\n[PASS] test_zeroRatesChargeNothingAndDoNotAlterTheSwap() (gas: 1057191)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 68.00ms (189.41ms CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantFundedPool\n[PASS]\nBMCHookInvariantFundedPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_noSuccessfulSwapWasShortFilled\n[PASS] invariant_paymentsRespectTheRecipient\n[PASS] invariant_revertedSwapsLeaveNoTrace\n[PASS] invariant_someSwapsHappen\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantFundedPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+----------------------+-------+---------+----------╮\n| Contract       | Selector             | Calls | Reverts | Discards |\n+====================================================================+\n| BMCHookHandler | buyExactIn           | 834   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut          | 885   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | collect              | 866   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn          | 881   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut         | 794   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | swapExactInWithLimit | 843   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | toggleRecipient      | 883   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | transfer             | 854   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | warp                 | 840   | 0       | 0        |\n╰----------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 18894\n  Bound result 1000000000000000000000\n  Bound result 14088\n  Bound result 200000000000000000\n  Bound result 2664\n  Bound result 255\n  Bound result 23934\n  Bound result 1289\n  Bound result 1502280168\n  Bound result 157011381\n  Bound result 8137423578348929310742035\n  Bound result 17341\n  Bound result 500000\n  Bound result 8126\n  Bound result 19634\n  Bound result 999\n  Bound result 4390\n  Bound result 10278\n  Bound result 2\n  Bound result 7995\n  Bound result 50000000000000000\n  Bound result 9041677255679773147752249\n  Bound result 10178\n  Bound result 7356\n  Bound result 8649\n  Bound result 3\n  Bound result 989\n  Bound result 100000000000000000\n  Bound result 60941\n  Bound result 408486167187706221832329\n  Bound result 12501\n  Bound result 387270\n  Bound result 4800950049794451883109602\n  Bound result 1\n  Bound result 16882\n  Bound result 1618\n  Bound result 557\n  Bound result 1000000000\n  Bound result 500000\n  Bound result 3\n  Bound result 2373\n  Bound result 49099\n  Bound result 6069\n  Bound result 18\n  Bound result 1000000000000000000\n  Bound result 767022\n  Bound result 2585\n  Bound result 500000000000000000\n  Bound result 10000000000000000000\n  Bound result 4691\n  Bound result 1560\n  Bound result 5888\n  Bound result 6787163012403684700\n  Bound result 14200\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.59s (2.57s CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantTokenOnlyPool\n[PASS]\nBMCHookInvariantTokenOnlyPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_noSuccessfulSwapWasShortFilled\n[PASS] invariant_paymentsRespectTheRecipient\n[PASS] invariant_revertedSwapsLeaveNoTrace\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantTokenOnlyPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+----------------------+-------+---------+----------╮\n| Contract       | Selector             | Calls | Reverts | Discards |\n+====================================================================+\n| BMCHookHandler | buyExactIn           | 834   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut          | 885   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | collect              | 866   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn          | 881   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut         | 794   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | swapExactInWithLimit | 843   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | toggleRecipient      | 883   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | transfer             | 854   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | warp                 | 840   | 0       | 0        |\n╰----------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 19132\n  Bound result 1000000000000000000000\n  Bound result 14208\n  Bound result 200000000000000000\n  Bound result 2327\n  Bound result 255\n  Bound result 313\n  Bound result 2557\n  Bound result 3040\n  Bound result 9808\n  Bound result 8137423578348929310742035\n  Bound result 17153\n  Bound result 500000\n  Bound result 914\n  Bound result 19805\n  Bound result 999\n  Bound result 4071\n  Bound result 11436\n  Bound result 2\n  Bound result 8965\n  Bound result 50000000000000000\n  Bound result 9042174258764123732382249\n  Bound result 13220\n  Bound result 8788\n  Bound result 9014\n  Bound result 3\n  Bound result 1551\n  Bound result 100000000000000000\n  Bound result 60941\n  Bound result 4951734340795348654599055\n  Bound result 13079\n  Bound result 387270\n  Bound result 4999960000248548719522486\n  Bound result 1\n  Bound result 16819\n  Bound result 51\n  Bound result 877\n  Bound result 1000000000\n  Bound result 500000\n  Bound result 3\n  Bound result 3495\n  Bound result 49099\n  Bound result 6024\n  Bound result 18\n  Bound result 1000000000000000000\n  Bound result 767022\n  Bound result 2261\n  Bound result 500000000000000000\n  Bound result 10000000000000000000\n  Bound result 5657\n  Bound result 1963468718374350338978509\n  Bound result 15121\n  Bound result 6787163012403684700\n  Bound result 14619\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.67s (2.66s CPU time)\n\nRan 1 test for test/BMCHookInvariant.t.sol:BMCHookInvariantEthOnlyPool\n[PASS]\nBMCHookInvariantEthOnlyPool invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_burnsMatchTheRule\n[PASS] invariant_claimsAreBacked\n[PASS] invariant_collectLeavesOnlyUncoverableClaims\n[PASS] invariant_ethFeesMatchTheRule\n[PASS] invariant_everySwapFollowedTheRule\n[PASS] invariant_hookAndRouterHoldNothing\n[PASS] invariant_noSuccessfulSwapWasShortFilled\n[PASS] invariant_paymentsRespectTheRecipient\n[PASS] invariant_revertedSwapsLeaveNoTrace\n[PASS] invariant_supplyFallsOnlyByBurns\n[PASS] invariant_termsAreImmutable\n[PASS] invariant_walletReceivedExactlyWhatTheHookPaid\n BMCHookInvariantEthOnlyPool invariants (runs: 128, calls: 7680, reverts: 0)\n\n╭----------------+----------------------+-------+---------+----------╮\n| Contract       | Selector             | Calls | Reverts | Discards |\n+====================================================================+\n| BMCHookHandler | buyExactIn           | 834   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | buyExactOut          | 885   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | collect              | 866   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactIn          | 881   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | sellExactOut         | 794   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | swapExactInWithLimit | 843   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | toggleRecipient      | 883   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | transfer             | 854   | 0       | 0        |\n|----------------+----------------------+-------+---------+----------|\n| BMCHookHandler | warp                 | 840   | 0       | 0        |\n╰----------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 445952\n  Bound result 1000000000000000000000\n  Bound result 1789716565375540911506123\n  Bound result 200000000000000000\n  Bound result 2315\n  Bound result 255\n  Bound result 20300\n  Bound result 3157\n  Bound result 1067\n  Bound result 2680\n  Bound result 8137423578348929310742035\n  Bound result 16934\n  Bound result 500000\n  Bound result 10148\n  Bound result 15006\n  Bound result 999\n  Bound result 9716\n  Bound result 5438\n  Bound result 2\n  Bound result 4351\n  Bound result 50000000000000000\n  Bound result 33351994035561836116103\n  Bound result 20645\n  Bound result 18419\n  Bound result 5119\n  Bound result 3\n  Bound result 3162989339686076468405743\n  Bound result 100000000000000000\n  Bound result 60941\n  Bound result 240842083037098675033279\n  Bound result 20504\n  Bound result 387270\n  Bound result 4115248358570423314727266\n  Bound result 1\n  Bound result 13290\n  Bound result 2170665077\n  Bound result 12803\n  Bound result 1000000000\n  Bound result 500000\n  Bound result 3\n  Bound result 1571\n  Bound result 49099\n  Bound result 3944371764532007315087554\n  Bound result 18\n  Bound result 1000000000000000000\n  Bound result 767022\n  Bound result 2342\n  Bound result 500000000000000000\n  Bound result 10000000000000000000\n  Bound result 9669\n  Bound result 2882\n  Bound result 17361\n  Bound result 6787163012403684700\n  Bound result 6922\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.97s (2.95s CPU time)\n\nRan 14 test suites in 2.97s (8.55s CPU time): 100 tests passed, 0 failed, 0 skipped (100 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"BMCHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"BMCHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"BMCHook.collect()\",\"BMCHook.unlockCallback(bytes)\",\"BMCSwapRouter.buyExactIn((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.buyExactOut((address,address,uint24,int24,address),uint256)\",\"BMCSwapRouter.receive()\",\"BMCSwapRouter.sellExactIn((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.sellExactOut((address,address,uint24,int24,address),uint256,uint256)\",\"BMCSwapRouter.unlockCallback(bytes)\",\"BMCToken.approve(address,uint256)\",\"BMCToken.burn(uint256)\",\"BMCToken.burnFrom(address,uint256)\",\"BMCToken.transfer(address,uint256)\",\"BMCToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"README.md\":236,\"foundry.toml\":20,\"remappings.txt\":4,\"script/Deploy.s.sol\":72,\"site/app.js\":251,\"site/config.json\":31,\"site/index.html\":120,\"site/styles.css\":43,\"src/BMCHook.sol\":396,\"src/BMCSwapRouter.sol\":152,\"src/BMCToken.sol\":23,\"src/HookDeployer.sol\":41,\"src/HookFlags.sol\":53,\"test/BMCHook.t.sol\":419,\"test/BMCHookEdge.t.sol\":688,\"test/BMCHookInvariant.t.sol\":463,\"test/BMCRevision.t.sol\":330,\"test/BMCSwapRouter.t.sol\":107,\"test/BMCToken.t.sol\":77,\"test/Deploy.t.sol\":48,\"test/mocks/MockERC20.sol\":15,\"test/site/app.test.cjs\":117,\"test/utils/BMCFixture.sol\":107},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f3cfe08be463d78c725d1aa029943eae24db6804ce3a4c093bc7feaa5e3568ec","verifiedTreeHash":"92c04f8a5fceed88a2bbef985a415897602f97f6","verifierVersion":"0.1.0+fb8278c5"}]}