{"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":"da4c0e74-7790-4c26-a7af-49f5062bd7e3","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"fe3858bf058246146d6cfe80fda8cfd805aa2476cac2d6e61e4cfd48326f8bc3","dependsOn":["implement_contract","write_foundry_tests","manifest"],"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":"f7ac62ca1be493a8e01dce07d63428ba8e0ce8d918227f5d725c076f3383fac4","dependsOn":["implement_contract","write_foundry_tests","manifest"],"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":"5e162eb79771b815d16199bf595af7a281ade37fa90f9dba5e2a9157beb478b3","dependsOn":["implement_contract","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"5ab2448b13a3d06d13922c0359175f5aea8606cc95e854524ba6ae35a864d7c9","dependsOn":["implement_contract","write_foundry_tests","manifest"],"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":"530083c94f229ea45ac5bbe9d84bbae910e1dda6964854fc7c31758ba6549732","dependsOn":["implement_contract","write_foundry_tests","manifest"],"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":"b85c138a3a41a28f2e45a6ee7b24e1be0c123dd3fa2a69e42df049d3c08ce235","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d37537abb20af46832b24602788d7895021ecaac20c6934313dc4f8aba4ad047","skillId":"implement-contract","tools":[]},"key":"implement_contract","kind":"code","role":"implement","skillHash":"d37537abb20af46832b24602788d7895021ecaac20c6934313dc4f8aba4ad047","skillId":"implement-contract","state":"accepted"},{"acceptedSubmissionHash":"4eca8c13b5fcdc763fd547ee3c875434e035accd7d22ff5ad2e8415ac856f600","dependsOn":["implement_contract","write_foundry_tests"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"05f5e32e299b6280db783fe130fe80f7220bfff5331a2544ed24ebc069f9c024","dependsOn":["implement_contract"],"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":"Token name: IMD Offsets. Token symbol: IMDO. Chain id 11155111, paired with ETH.\n\nPURPOSE. IMDO funds regenerative contributions: ecological credits bought and retired on Regen Network by a public treasury. Holders get NO payouts, rewards, yield or staking; do not add any.\n\nTOKEN (ERC-20). Plain ERC-20 with plain transfers: NO transfer tax, NO fee-on-transfer, NO owner, NO mint after deployment, NO blacklist, NO pause, NO trading gate, NO upgradeability. Fixed supply minted once at launch. A public burn(uint256) is allowed (holders may burn their own tokens). It must behave normally with wallets, routers and bridges and must not look like a honeypot to scanners.\nHOOK. Build the pool hook as an IMMUTABLE contract attached to the launch pool. Behaviour:\n- Sell-only fee: buys cost nothing. A sell is a swap where the token is paid in (use the settled BalanceDelta in afterSwap so exact-in and exact-out sells are sized correctly).\n- Graduated by sell size relative to the pool's token reserve: a sell of less than 1% of the reserve (under 100 bps) pays 0%; from 1% up to 3% of the reserve (100 to 299 bps) pays 0.5%; from 3% up to 5% (300 to 499 bps) pays 1%; 5% of the reserve or more (500 bps+) pays 2% (fee ppm 0 / 5,000 / 10,000 / 20,000). Hard cap in code: 2% (20,000 ppm); no bracket may ever exceed it.\n- Anti-splitting: accumulate each tx.origin's sells within one transaction (transient storage) and bill the cumulative size.\n- Anti-manipulation: size against a reserve snapshot LAGGED BY ONE BLOCK so a same-transaction reserve inflation cannot lower the bracket.\n- Fee is taken in the pool's quote asset (native ETH) and sent DIRECTLY to the immutable treasury 0xb1eC9d1C36974d05eb9889eBf8A150b05791E559. Any token-side fee (exact-out sells) is burned. A permissionless harvest() may only move already-accrued claims to the treasury / burn.\n- No owner, no setters, no upgradeability: brackets, cap, treasury and burn behaviour are constructor constants.\n- COMPATIBILITY WITH THE LAUNCH FACTORY (critical): the pool is initialized by the launch factory with this hook attached. The factory's own liquidity position, its pool-fee accounting and distribution (to whoever the factory pays today) and the swarm's Merkle distributor must keep working UNCHANGED; the hook only adds its own fee on large sells and must never block, revert or re-route the factory's accounting. Prove this in tests with a pool that has a factory-style liquidity position and show the pool-fee accounting and distribution are unaffected by the hook. State in the README exactly where the pool's own LP fee goes and where the hook's fee goes.\n- Use OpenZeppelin uniswap-hooks BaseHookFee patterns; the hook address must satisfy v4 permission-bit mining (afterSwap, afterSwapReturnDelta).\nTESTS (Foundry; a different worker writes them). Cover: buys free; exact fee per bracket for exact-in AND exact-out sells; treasury receives exactly the fee in ETH; the pool-fee distribution and swarm claims are unaffected by the hook; split sells in one tx are billed cumulatively; same-tx reserve inflation does not lower the bracket; no function can change brackets/cap/treasury; sells are never blocked; invariant that the hook never holds user funds beyond accrued fee claims; fixed supply, burn works, plain transfers untaxed, no privileged roles, deploy script runs from configuration.\nDELIVER a README stating every number and who can call what, a deploy script, and the launch attestation. Do not claim an audit was performed.","parentJobId":null,"planHash":"57ea6f159aed8b6b44302f3010961dc3d8553430600ebe4ab207a65b85ddc044","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"da4c0e74-7790-4c26-a7af-49f5062bd7e3","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-661-imd-offsets-token-symbol-imdo-chain-id-1"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51018","feedbackHash":"8b21cbacb8b335a7367364bb145e0ae7a4101bb31b0750c3bdeb99e43f4ca101","nodeKey":"audit_economics","submissionHash":"fe3858bf058246146d6cfe80fda8cfd805aa2476cac2d6e61e4cfd48326f8bc3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"b974cb7072f9686dfd36c485a8721e9c1c3389ab4d06b17d05f7083e45c56b16","nodeKey":"audit_flow","submissionHash":"f7ac62ca1be493a8e01dce07d63428ba8e0ce8d918227f5d725c076f3383fac4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51023","feedbackHash":"6c104f5610c1c0b12b5ea4d4cf3281f1bf9ff866a655d91f8c7e0da627d09e65","nodeKey":"audit_judge","submissionHash":"5e162eb79771b815d16199bf595af7a281ade37fa90f9dba5e2a9157beb478b3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50974","feedbackHash":"fc847632d95f6956eb74607b062df7c283a797b9bafd5319b02b377540b03633","nodeKey":"audit_math","submissionHash":"5ab2448b13a3d06d13922c0359175f5aea8606cc95e854524ba6ae35a864d7c9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50939","feedbackHash":"e8f7cc83f6735a7a0df73bc3172ce8005487e7fc3dd28435a00bcc2046841613","nodeKey":"audit_permissions","submissionHash":"530083c94f229ea45ac5bbe9d84bbae910e1dda6964854fc7c31758ba6549732","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50957","feedbackHash":"c049f8f1ec5bbf4f7d9b62c546c27a8ef6a19e17607ac2013402c74f2c734876","nodeKey":"implement_contract","submissionHash":"ef76871838fa835775f2297b779539cbeff61fd69c6a6a841bfe480bead5edea","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"63d4dceb671255ba461d32cd10d8b374a61f0801c04736dbc7572de307331087","nodeKey":"implement_contract","submissionHash":"b85c138a3a41a28f2e45a6ee7b24e1be0c123dd3fa2a69e42df049d3c08ce235","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50957","feedbackHash":"c6105660a7d95f899106f213d19c03fca192b0695a378836e641a361fee5c144","nodeKey":"manifest","submissionHash":"d9886eac6ae830a0fd720eb3c500489cea52dc781aeb5ee15a42b2325c01ec4f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"d2a8f90e1f622305e6be046282e4eb4cb9dce6d1d6aed6a0452b9e05b49a3a0a","nodeKey":"manifest","submissionHash":"4eca8c13b5fcdc763fd547ee3c875434e035accd7d22ff5ad2e8415ac856f600","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"139b0888793d1fbd47a2aaddfe589d55c892f25629540b10a2849180c02c025b","nodeKey":"write_foundry_tests","submissionHash":"05f5e32e299b6280db783fe130fe80f7220bfff5331a2544ed24ebc069f9c024","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"c78f55c6a8efbcfa1e5fbfc4ba3c81c21b0d9518e7a6a1eb5b088d3be024e349","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"7674c81e4d4aba83abe371a29d17ab8afb44860827b23f5cbf08a6685a986af6","device":"468e82a89b9bfe18","findings":[{"description":"The brief asks that each tx.origin's sells within one transaction be accumulated and billed at the cumulative size. The revised _bill() still selects the bracket from the cumulative token input, but it then bounds every leg's fee to MAX_FEE_PPM (2%) of that leg's own gross ETH output (exact input) or settled token input (exact output). When a later leg crosses into a higher bracket, the catch-up owed on the earlier legs' proceeds can exceed 2% of the crossing leg, and the excess is only carried to a further sell leg of the same transaction; if no such leg follows, it is never collected. The README records this as a deliberate trade-off (it protects a principal whose sell is bundled behind a stranger's under a shared tx.origin from forfeiting a whole leg), and it is no cheaper than splitting the same sells across separate transactions, which the brief leaves unbilled. It is reported here, not asserted as correct, because the suite's split-sell tests (test_threeLegSplitCrossesTwoBrackets, test_dustCrossingLegAsTheLastLeg..., test_aLegNeverPaysMoreThanTwoPercentOfItsOwnSizeUnderASharedOrigin) now encode the capped amounts rather than the full cumulative bill.","line":351,"path":"src/IMDOFeeHook.sol","reproduction":"Fixture: full-range launch position, lagged reserve 9999999999999999999457 IMDO, price 1:1. One transaction with three exact-input sells of ceil(1%) = 99999999999999999995 IMDO each (cumulative 3% -> 1% bracket). Gross ETH per leg: 98715803439706129880 / 96785892738500431280 / 94912029292951112796. Fees charged: 493579017198530649 (0.5% of leg 1), 483929463692502156 (cumulative 0.5% less leg 1), 1898240585859022255 (= exactly 2% of leg 3, the cap). Expected under 'bill the cumulative size': 1% of all proceeds = 2904137254711576739 wei. Actual: 2875749066750055060 wei; 28388187961521679 wei of the catch-up is never collected because no further sell leg follows. Larger gap with fewer legs (README example): two exact-input legs of 3% each pay 1% then 2% of the second leg, about 1.5% of the proceeds instead of the 2% bracket the 6% cumulative size selects. Same-origin bundler case from the same fixture: legs of 4.9% then 0.2% owe 1% of leg 1 plus 2% of leg 2 on the second leg (about 28% of its output); the hook collects exactly 2% of leg 2 and the remaining 26% is uncollected.","severity":"low","title":"Per-leg 2% hard cap lets a sell split across legs of one transaction pay less than the cumulative bracket"}],"hash":"05f5e32e299b6280db783fe130fe80f7220bfff5331a2544ed24ebc069f9c024","nodeId":"08e855db-0d4c-4033-b1f7-7e198ec393cf","outcome":"completed","summary":"Done. The test revision is complete and verified.\n\n**What was wrong.** The accepted suite in `test/IMDO.t.sol` encoded the pre-revision hook. The revised hook caps every leg's fee at 2% of its own size, records the exact-output token fee as a claim that `harvest()` burns instead of burning it inside the swap, and no longer reverts `harvest()` when the treasury rejects ETH. Nine tests failed against the current code for those reasons. The earlier attempt at this task never produced a tree, so I redid the work from the accepted file.\n\n**What changed in the suite.**\n- The exact-output helper now checks that the token fee is a claim, that nothing burns inside the swap, and that a permissionless harvest burns exactly that claim. Four tests depended on it.\n- The three-leg, mixed-mode, and three dust-leg tests now assert the documented per-leg cap: a crossing leg pays exactly 2% of its own output or input, a following leg carries the remainder, and a trailing leg leaves it uncollected.\n- The treasury-rejects test now expects `harvest()` to succeed, skip the ETH leg, and keep the claim intact.\n- New tests: a shared-origin bundler case in both swap modes, a mixed-mode catch-up below the cap collected exactly, and a harvest that burns the token claim while the ETH claim stays because the treasury rejects ETH.\n- The split-sell fuzz test now checks each leg against a reference model of the README's bill formula. The invariant handler calls `harvest()` unguarded and asserts the two-leg independence on every call.\n\n**Results.** All 51 tests pass offline, including 5 invariants over 48 runs with zero reverts. The file is `forge fmt` clean. Only `test/IMDO.t.sol` is modified.\n\n**Reported rather than asserted.** `.imd-findings.json` holds one low-severity finding: the per-leg cap means a sell split across legs of one transaction can pay less than the cumulative bracket, with the exact wei amounts from the three-leg scenario. The README records it as an intended trade-off, so no proof file is attached.","treeHash":"a5816a540ab798e3955558dc631b2ee92401479e","usage":{"cachedInputTokens":3612734,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":41373,"runtime":"claude","turns":38,"wallClockMs":477152}},{"artifacts":[],"attempt":2,"bundleHash":"6834891657cd96ff4c3162fc2675f604115a46151aca935c2e5379ca878c027e","device":"468e82a89b9bfe18","findings":[{"description":"run() reverts with InvalidConfiguration() on any chain other than 11155111 (line 129) before reading configuration, so the network's standard offline check `EXPECTED_CHAIN_ID=0 forge script script/Deploy.s.sol:Deploy --offline` (local chain 31337) fails. Even on Sepolia the script does not deploy IMDOToken or IMDOFeeHook itself: it reads POOL_MANAGER, LAUNCH_FACTORY, TOKEN_ADDRESS and a raw FACTORY_CALLDATA (line 133) and forwards that blob to the factory between the broadcast markers, so the deployment is whatever the blob encodes and cannot be reviewed from the script. The accepted pattern on this network is a script that reads only EXPECTED_CHAIN_ID (31337 or the target chain) and performs exactly one reviewable deployment between vm.startBroadcast/stopBroadcast. The test suite covers what is testable without vm.setEnv (constants, mine(), CREATE2 reproduction, the chain guard) and cannot exercise prepare()/run() end to end.","line":129,"path":"script/Deploy.s.sol","reproduction":"In the repository root run `EXPECTED_CHAIN_ID=0 forge script script/Deploy.s.sol:Deploy --offline`. Expected: a dry run that deploys the token and hook on the local chain. Actual: `[Revert] InvalidConfiguration()` from Deploy::run (Gas used: 21715; `Error: script failed: InvalidConfiguration()`). With `--chain-id 11155111` the next failure is the unset POOL_MANAGER environment variable; there is no path that simulates a deployment without an externally built FACTORY_CALLDATA.","severity":"medium","title":"Deploy script cannot be simulated from configuration offline: it hard-requires chain 11155111, four unset environment variables and an opaque FACTORY_CALLDATA blob"},{"description":"The anti-splitting bill reprices all of a tx.origin's earlier ETH proceeds at the new bracket (line 337, due = quote * rate - paid) and charges the whole catch-up on the leg that crosses the bracket. If that leg is tiny, the hook's returned delta exceeds the leg's own ETH output and the swapper's currency0 delta for that swap becomes negative. The PoolManager accepts this and a router that settles net deltas (as the suite's router does) completes the transaction, but a router that assumes every sell leg has a non-negative output (take-only settlement, exact-output routers with a zero-value call) reverts on that leg. The README documents this as a router constraint; it is recorded here because the brief says sells must never be blocked and this is the one case where a sell inside the hook's own rules can fail at the router.","line":337,"path":"src/IMDOFeeHook.sol","reproduction":"Reserve R = lagged snapshot (1e22 in the suite). Transaction by one tx.origin: leg 1 exact-input sell of ceil(R/100) - 1 tokens (0 ppm, gross ETH g1 ~ 9.9e19), leg 2 exact-input sell of 1 wei of token (gross ETH 0). Cumulative size reaches 1% so rate = 5,000 ppm and the leg-2 fee is floor(g1 * 5000 / 1e6) ~ 4.9e17 wei while leg 2's output is 0: the leg-2 swap delta is (-4.9e17 ETH, -1 token). Expected by output-only routers: a non-negative ETH delta per sell. Actual: the swapper owes ETH on that leg. test_splitExactInputSellsAreBilledCumulatively and test_mixedModeSplitIsBilledCumulatively in test/IMDO.t.sol show the catch-up amounts with a net-settling router.","severity":"low","title":"Cumulative catch-up billing can make a later sell leg's ETH delta negative, which output-only routers cannot settle"},{"description":"feeRate() returns MAX_FEE_PPM whenever the lagged reserve is zero (line 276). The reserve snapshot is taken one block late, so in the block in which the factory initialises the pool and seeds liquidity the snapshot is 0 and a sell of any size, including 1 wei, pays 2% rather than the 0% the bracket table promises for sells under 1% of the reserve. From the next block on the brackets apply as specified. This is a documented, deliberate conservative branch (README: 'In the launch block ... a positive sell uses the conservative 2% bracket') and arguably desirable against launch-block sniping, but it is a visible deviation from the stated schedule for one block and is therefore recorded rather than silently asserted. The suite asserts the current behaviour in IMDOLaunchBlockTest and IMDOTokenOnlyPoolTest.","line":276,"path":"src/IMDOFeeHook.sol","reproduction":"Factory launches (initialize + seed 1e22 tokens) in block N. Still in block N, sell 1e18 tokens (0.01% of the reserve) exact-input. Expected per bracket table: 0 ppm. Actual: HookFeeCharged rate = 20000 and the treasury receives floor(grossETH * 0.02). Rolling to block N+1 and repeating the same sell charges 0 (test_launchBlockSellsPayTheCapThenTheLagExpires).","severity":"info","title":"Every sell in the launch block (and after any block that closed with an empty reserve) is billed at the 2% cap regardless of size"},{"description":"The brief asks for per-tx.origin accumulation and the hook does exactly that (line 325). The consequence, documented in the README, is that a bundler or relayer that submits several users' sells in one transaction pushes all of them into the cumulative bracket, and the user whose leg crosses the bracket pays the catch-up for everyone before them. No fix is proposed because the keying is the specified design; it is listed so the launch team can state it to integrators.","line":325,"path":"src/IMDOFeeHook.sol","reproduction":"Single transaction from origin O containing two unrelated sells of ceil(R/100)/2 tokens each. Expected if billed per user: 0 fee on both (each is under 1%). Actual: the second leg pays floor((g1 + g2) * 5000 / 1e6) ETH (test_splitExactInputSellsAreBilledCumulatively).","severity":"info","title":"Billing is keyed by tx.origin, so unrelated sellers whose trades share one transaction origin (ERC-4337 bundlers, relayers, batchers) are billed as one seller"}],"hash":"2769ae20a41b791928cd811acda5b47aac673be737309e99b5ef6f11911db9d2","nodeId":"08e855db-0d4c-4033-b1f7-7e198ec393cf","outcome":"completed","summary":"Done. The deliverable is `test/IMDO.t.sol`, plus `.imd-findings.json` for what could not be tested around.\n\n**Result**\n\n| Check | Outcome |\n| --- | --- |\n| `forge build --offline` | passes |\n| `forge test --offline` | 41 tests, 0 failures (7 suites) |\n| Invariants | 48 runs × 40 depth, 1920 calls, 0 reverts, fail-on-revert on |\n| `forge fmt --check test/IMDO.t.sol` | clean |\n\n**How the suite works.** The repository has no forge-std and no v4-core, and this task may only write one file. So the test file declares its own cheatcode interface and carries the real Uniswap v4 PoolManager as creation bytecode, compiled from the vendored v4-core. Every fixture deploys that manager, launches through a factory-style contract that creates the token, mines and CREATE2-deploys the hook, initialises the pool and seeds the launch position in one transaction, and funds a Merkle distributor stub. A hookless mirror pool with identical state supplies reference amounts, so every fee assertion compares against genuine v4 math rather than a re-implementation.\n\n**What is covered**\n\n- Buys free in both modes, identical deltas to the hookless pool, no treasury movement.\n- Exact fee at every bracket boundary for exact-in sells (ETH to treasury) and exact-out sells (tokens burned), including the `ceil` edges one wei either side of 1%, 3% and 5%.\n- Split sells billed cumulatively in one transaction, across two brackets, and in mixed exact-in then exact-out mode. The same legs as separate transactions pay nothing.\n- Same-transaction liquidity inflation and donation do not lower the bracket. The lag expires exactly one block later.\n- Factory LP fee growth, collection, payout split and position withdrawal are byte-for-byte equal to the hookless pool. Merkle claims are untaxed and unaffected.\n- Treasury rejecting ETH or burning gas never blocks a sell. Fees become ERC-6909 claims that a permissionless harvest later delivers only to the treasury.\n- ETH-only pool exercises the token-claim fallback and harvest burn. Token-only pool on a fresh manager exercises the buy path.\n- Protocol fees are excluded from the reserve, and the reserve reconciles with the manager's inventory.\n- Fuzzed random calldata and a curated admin selector list change nothing. Fuzzed sells of any size never revert.\n- Token: fixed supply, exact transfers, allowance paths, burn, no admin calls, no escape opcodes. Deploy script constants, salt mining reproduction, and the chain guard.\n- Invariant handler: random swaps, liquidity, donations, protocol-fee changes, treasury toggling, block rolls, harvests and burns. Invariants: hook holds nothing beyond accrued claims, every fee is at the treasury, burned or claimable, reserve reconciles, configuration immutable, factory position intact.\n\n**Reported rather than asserted** in `.imd-findings.json`\n\n- Medium: the deploy script reverts with `InvalidConfiguration` under the network's standard offline check, needs four environment variables, and forwards an opaque factory calldata blob instead of performing a reviewable deployment.\n- Low: cumulative catch-up billing can make a tiny later sell leg's ETH delta negative, which output-only routers cannot settle.\n- Info: all sells in the launch block pay the 2% cap, and tx.origin keying groups unrelated sellers under one bundler.\n\n**Not covered.** The script's `prepare()` and `run()` paths need environment variables, and `vm.setEnv` is forbidden, so only their guard paths are tested.","treeHash":"e30a7c78f44912c7f5aa4c939e2a433d2477c39f","usage":{"cachedInputTokens":6333810,"inputTokens":1188,"model":"claude-fable-5-1","outputTokens":178029,"runtime":"claude","turns":39,"wallClockMs":2210692}},{"artifacts":[],"attempt":1,"bundleHash":"fa54437e0fd7d216c88bd6688d60f68b1e0a756da2355e4d0c3c764a667c12c1","device":"468e82a89b9bfe18","findings":[],"hash":"4eca8c13b5fcdc763fd547ee3c875434e035accd7d22ff5ad2e8415ac856f600","nodeId":"e3550fb2-2556-4523-94fb-99781b777314","outcome":"completed","summary":"`launch.json` has been re-derived from the revised tree. Only that file changed.\n\n**What changed in the manifest.** The structural fields were already correct and stay as they were: hook `IMDOFeeHook` with constructor arguments `$poolManager` then `$token`, the same seven permissions, token IMD Offsets / IMDO / 18 decimals, native ETH pairing, LP fee 3000, tick spacing 60, initial sqrtPriceX96 of 2^96. The notes were rewritten where the revised source contradicted them:\n\n- **Pinned pool key.** beforeInitialize now accepts only fee 3000 and tick spacing 60, not any of the three tiers. The notes say so and that 3000 is the only tier the hook initializes with.\n- **Per-leg hard cap.** A leg never pays more than 2% of its own size, even when carrying catch-up for earlier legs under a shared tx.origin. The old text said catch-up could exceed 2% of a leg.\n- **Exact-output fee.** The IMDO fee is now always an ERC-6909 claim, never taken inside afterSwap, and is burned by harvest. The old text described an immediate burn.\n- **Harvest independence.** A treasury that rejects ETH leaves the ETH claim recorded while the IMDO claim still burns, and harvest no longer reverts. The old text said harvest could revert.\n- **Deploy script.** Noted that the dry-run script attests the pinned key and that the factory must deploy and initialize in one transaction.\n\n**Checks run.**\n\n| Check | Result |\n| --- | --- |\n| Manifest schema and refinements (first key `kind`, enum permissions, ETH pairing, listed fee tier, price below 2^256, no extra fields) | pass |\n| Notes length | 3887 of 4000 chars |\n| Flags implied by declared permissions | 0x25d4, equal to the hook's constant |\n| Scratch test reading getHookPermissions from a mined hook against the manifest list | pass |\n| `forge build --offline`, `forge test --offline` | 52 tests pass, 0 fail |\n| `EXPECTED_CHAIN_ID=0 forge script script/Deploy.s.sol:Deploy --offline` | succeeds, hook address ends in bits 0x25d4 |\n\nThe initial price remains an explicit assumption carried over from the previous round, since neither the brief nor the deploy script sets one. The scratch test lives in test/scratch and is not part of the deliverable.","treeHash":"91d504f95e3cc0d008993df35380b9466866b1a9","usage":{"cachedInputTokens":744460,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":11247,"runtime":"claude","turns":16,"wallClockMs":196493}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0256823ae36e7900","findings":[],"hash":"4ff5bc961317767bb9faeb797b34f44ea7579f91c2b9aa91ce5a06b41ea12248","nodeId":"08e855db-0d4c-4033-b1f7-7e198ec393cf","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":5,"wallClockMs":1034291}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"Access control / unprotected initialization. beforeInitialize is the one-shot binding of the hook to its pool. It checks only the currencies, the hook address and the fee tier. It ignores the `sender` argument, accepts any tickSpacing (the manifest fixes 60) and any starting sqrtPriceX96, and PoolManager.initialize is permissionless. The delivered deployment flow is two transactions: script/Deploy.s.sol deploys the token and the mined hook and explicitly does not initialize the pool (README: 'The launch factory then initializes the ETH/IMDO pool with this hook attached'). Between those transactions any account can call PoolManager.initialize with a key that passes the checks but is not the launch pool, e.g. fee 500, tickSpacing 1, hooks = this, at MIN_SQRT_PRICE+1. The hook sets initialized=true and poolId to that key forever. The factory's initialize with {0x0, token, 3000, 60, hook} then reverts with InvalidPool. Initializing the correct key at a wrong price first is equally fatal: the factory's call reverts with PoolAlreadyInitialized while the hook stays bound to the attacker-priced pool. There is no recovery path (no function can rebind), so the attested hook address and salt are dead and the launch must mine, deploy and re-attest a new hook. If the network's factory deploys the hook and initializes in the same transaction the window does not exist; the defect is that the hook offers no protection of its own and the delivered script and README prescribe the vulnerable ordering. Minimal fix that keeps the design: refuse any key whose tickSpacing is not the launch value and any sqrtPriceX96 outside an accepted band, and make the README/script require the factory to deploy and initialize atomically (or take the expected initializer as an immutable constructor argument the deployer fills in).","line":207,"path":"src/IMDOFeeHook.sol","reproduction":"1. Deploy IMDOToken and IMDOFeeHook exactly as Deploy.run does (mined salt, constructor (poolManager, token)); hook.initialized() == false. 2. From any address call poolManager.initialize(PoolKey{currency0: 0x0, currency1: token, fee: 500, tickSpacing: 1, hooks: hook}, 4295128740). Expected: refused (not the launch pool, not the launch factory). Actual: succeeds; hook.initialized() == true and hook.poolId() == keccak256(abi.encode(that key)). 3. The launch factory calls poolManager.initialize(PoolKey{0x0, token, 3000, 60, hook}, 79228162514264337593543950336). Expected: launch pool created. Actual: reverts (beforeInitialize -> InvalidPool, wrapped by PoolManager). Verified on the real PoolManager bytecode embedded in test/IMDO.t.sol with test/scratch/InitFrontRun.t.sol: the test asserts exactly this sequence and passes.","severity":"medium","snippet":"        if (\n            initialized || key.currency0 != address(0) || key.currency1 != token || key.hooks != address(this)\n                || (key.fee != 500 && key.fee != 3_000 && key.fee != 10_000)\n        ) revert InvalidPool();\n        initialized = true;\n        poolId = keccak256(abi.encode(key));","title":"beforeInitialize binds the hook to the first pool anyone initializes: no sender, tickSpacing or price check, so the two-step deploy flow in Deploy.s.sol/README can be front-run and the attested hook b"},{"citation":"resolved","description":"Economics x asymmetry (trust-gap seam 2). The brief requires that sells by one tx.origin in one transaction are accumulated and the cumulative size is billed. _bill computes the cumulative bill correctly but caps each leg's charge at that leg's own output (fee <= quoteOut) and lets the remainder wait for a later leg. A seller who orders their legs as [just under a bracket boundary, dust leg that crosses it] therefore pays the lower bracket on almost everything: the catch-up for the crossing is confined to the dust leg's output and the remainder is never collected because the transaction ends. The outcome depends only on input shape, and the seller picks the favorable shape. The README discloses the behaviour ('a final tiny leg may leave a remainder uncollected') and argues the seller never pays less than the earlier legs alone would cost, but the brief's guarantee is the cumulative bill. Bounded: the saving is at most one bracket step on the pre-boundary legs, and cross-transaction splitting (not accumulated at all, by the brief) already yields the lower bracket, which is why this is low rather than medium.","line":344,"path":"src/IMDOFeeHook.sol","reproduction":"Fixture test/IMDO.t.sol IMDOHookTest (full-range launch position, next block, lagged reserve R = 9,999,999,999,999,999,999,457). total = ceil(R*500/10000) = 499,999,999,999,999,999,973 tokens (exactly the 5% boundary, 20,000 ppm bracket). (a) alice sells `total` in one exact-input swap: treasury receives 19,999 ppm of gross ETH output. (b) carol, in one Router.execute (one transaction), sells [total - 1e18, 1e18] exact-input: treasury receives 11,890 ppm of her gross ETH output. Expected per the brief: both pay the 20,000 ppm bracket on the full cumulative size. Actual: the split pays 8,109 ppm less. test/scratch/Split.t.sol reproduces these numbers on the real PoolManager (logs 'single: fee ppm of gross: 19999', 'split : fee ppm of gross: 11890').","severity":"low","snippet":"        if (exactInput) {\n            fee = due / PPM;\n            if (fee > quoteOut) fee = quoteOut;\n            paid += fee * PPM;","title":"Per-leg fee bound lets a boundary-hugging split in one transaction pay less than the unsplit sale of the same total (5% sold: 11,890 ppm split vs 19,999 ppm single)"},{"citation":"resolved","description":"Branch asymmetry between first-block and later blocks. beforeInitialize sets reserveBlock = block.number but leaves laggedReserve = 0; _rollReserve only promotes liveReserve at the first operation of a later block. So for every sell in the block in which the factory initializes and seeds the pool, feeRate is called with reserve == 0 and returns MAX_FEE_PPM. The bracket table in the brief and README says a sell under 100 bps of the reserve pays 0 ppm; in the launch block a sell of 0.01% of the seeded reserve pays 20,000 ppm. The README and manifest notes disclose this ('a zero mature reserve uses the cap'), and it is within the hard cap, but it is a concrete state in which the stated brackets do not apply and early sellers pay 2% on sells the table says are free. Any sell-side router or aggregator that quotes a launch-block sell from the bracket table gets a wrong quote. If the design intent is 'no snapshot, no discount', the README table should state the launch-block rule as a bracket row; otherwise seed laggedReserve with the inventory present when the first liquidity is added in the launch block.","line":276,"path":"src/IMDOFeeHook.sol","reproduction":"Fixture test/IMDO.t.sol IMDOLaunchBlockTest (launch not followed by vm.roll). hook.reserveSnapshot() == 0 and hook.liveReserve() == seeded inventory. alice sells liveReserve/10000 (0.01% of the reserve) exact-input in the launch block. Expected per bracket table: 0 ppm, treasury unchanged. Actual: HookFeeCharged rate 20,000 and the treasury receives gross*20000/1e6 (the repository's own test_launchBlockSellsPayTheCapThenTheLagExpires at test/IMDO.t.sol:1312 asserts exactly this). After vm.roll(+1) the same 0.01% sell pays 0.","severity":"low","snippet":"        if (reserve == 0) return MAX_FEE_PPM; // No mature reserve: conservative, never divide by zero.","title":"Every sell in the launch block (lagged reserve still 0) is billed the 2% cap regardless of size, contradicting the bracket table for sells under 1% of the reserve"}],"hash":"530083c94f229ea45ac5bbe9d84bbae910e1dda6964854fc7c31758ba6549732","nodeId":"d14906fc-3afc-4f54-93b7-50d4f81c38f0","outcome":"completed","summary":"The findings file is in place at the repository root and the task's own exclude rules keep it and the scratch directory out of git, so no tracked file changed. The review is complete.\n\n**Outcome.** `.imd-findings.json` holds 3 findings and a 17-row coverage record answering all 12 listed entry points plus 5 invariants. No source, script, README or manifest file was modified. Scratch tests live under `test/scratch/` and their build output was removed.\n\n**Findings (all reproduced on the real PoolManager bytecode the repository's suite embeds):**\n\n1. **Medium, access control.** `beforeInitialize` binds the hook to the first pool anyone initializes. It ignores the sender, the tick spacing and the starting price. The delivered flow is two transactions: the deploy script creates the hook and the README says the factory initializes later. In that window an outsider initialized a fee-500, tick-spacing-1 pool at the minimum price, and the factory's intended initialization then reverted with `InvalidPool`. No function can rebind, so the attested hook is dead and must be re-mined and re-attested. If the production factory deploys and initializes atomically the window closes, but the hook itself offers no defence and the script and README prescribe the vulnerable order.\n2. **Low, economics × asymmetry.** The per-leg fee bound in `_bill` lets a boundary-hugging split underpay. Selling exactly 5% of the lagged reserve in one swap paid 19,999 ppm. Selling the same total as a 4.99% leg plus a dust leg in one transaction paid 11,890 ppm. The README discloses the behaviour, and cross-transaction splitting is unaccumulated by the brief anyway, so the impact is bounded.\n3. **Low, branch asymmetry.** In the launch block the lagged reserve is zero, so every sell pays the 2% cap, including sells under 1% of the reserve that the bracket table says are free. The repository's own launch-block test asserts this behaviour.\n\n**What held.** All seven callbacks are manager-only, reentrancy-guarded and pinned to the bound pool. `harvest` and `unlockCallback` can only move recorded claims to the constant treasury or burn them, and the callback is gated by a transient flag only the guarded `harvest` sets. Hook deltas net to zero on the direct-pay and claim paths. No setter, owner, delegatecall or upgrade path exists for brackets, cap, treasury, token or manager. The token's four entry points are plain ERC-20 with a self-burn. The treasury address has no code on Sepolia, so the 80,000-gas payment cap is sufficient for it.\n\n**Verification notes.** The baseline suite passes, 48 tests, when compiled without via-ir. With via-ir, as the README's attested build command specifies, solc 0.8.26 segfaults on the test file's embedded PoolManager literal, so I could not run the suite under the attested compiler settings. Aderyn's three high-severity lines were checked and dismissed: the caret is an intentional XOR in `_mulDiv`, the state writes after external calls in `unlockCallback` are reachable only by the trusted treasury inside the hook's own reentrancy guard, and the `int128` casts intentionally extract `BalanceDelta` halves. No Slither run was performed beyond the supplied output, and no live factory rehearsal exists in the workspace.","treeHash":null,"usage":{"cachedInputTokens":2122156,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":54130,"runtime":"claude","turns":38,"wallClockMs":736153}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"98b4506bef931d13","findings":[{"citation":"resolved","description":"afterAddLiquidity adds the full settled token amount of any position to liveReserve, including positions whose tick range lies entirely below the current price and therefore hold only IMDO and never trade. The next block's laggedReserve (the bracket denominator used in _bill, line 335) is then inflated by that deposit. Because such a position is out of range it carries no price exposure and no ETH requirement, and it can be withdrawn in the same transaction as the sell. The one-block lag only forces the deposit to be in place at the close of the previous block; the README acknowledges that manipulation across a block boundary is outside the protection, but the cost of that manipulation is effectively zero (gas plus holding tokens the seller already holds for one block), which makes the 0.5% / 1% / 2% brackets avoidable by anyone with roughly 4x the pool's token inventory in hand. Seam: boundary (block boundary, out-of-range tick) x invariant (bracket is sized against the pool's tradeable reserve). This is a design limitation within the brief's stated one-block lag rather than an arithmetic error; reported so the author can decide whether the denominator should count only in-range liquidity or whether the limitation should be stated as the expected cost of bypass.","line":227,"path":"src/IMDOFeeHook.sol","reproduction":"Fixture: factory full-range position L=1e22 at sqrtPrice 2^96 (pool token inventory 9_999_999_999_999_999_999_457 wei IMDO), reserve snapshot mature (one block after launch). Block N: alice (holding 1e25 IMDO) calls modifyLiquidity on the hooked pool with tickLower=-660, tickUpper=-600, liquidityDelta=1.5e25. Settled token amount 43_602_497_974_678_082_870_800, ETH 0; hook.liveReserve() becomes 53_602_497_974_678_082_870_257. Block N+1, one transaction: (1) swap zeroForOne=false exact-in amountSpecified=-499_999_999_999_999_999_973 (exactly ceil(5%) of the real 9.999e21 reserve, i.e. the 2% bracket); (2) modifyLiquidity same range with liquidityDelta=-1.5e25. Expected per the bracket table: 500 bps of the pool reserve -> 20_000 ppm fee, roughly 9.9e18 wei ETH to the treasury (the same sell without the deposit paid 10_082_445_610_628_068_328 wei in a sibling run). Actual: TREASURY.balance delta = 0; HookFeeCharged not emitted; alice ends with 9_999_500_000_000_000_000_026 IMDO, i.e. her full deposit back minus the sale. Run in test/scratch (IMDOFixture from test/IMDO.t.sol, _launch(TICK_LOWER, TICK_UPPER, true, true)): deposited tokens 43602497974678082870800, sell 499999999999999999973, fee paid 0.","severity":"low","snippet":"        _updateReserve(int128(delta));","title":"Bracket denominator counts out-of-range (token-only) liquidity, so a one-block, price-risk-free deposit lowers the sell bracket to 0%"},{"citation":"resolved","description":"The anti-splitting accumulator lives in transient storage keyed by tx.origin and is therefore empty at the start of every transaction, exactly as the brief specifies (\"within one transaction\"). The denominator laggedReserve, however, does not change inside a block (every sell in the block uses the previous block's closing inventory), so N consecutive transactions from the same origin in one block, each selling just under 1% of the snapshot, are each billed at 0 ppm while the same total in one transaction is billed at 20_000 ppm. Seam: boundary (transaction boundary) x invariant (cumulative size determines the bracket). The README states that a new transaction starts with empty counters; this entry records the concrete cost of that rule so the author can weigh a per-block accumulator (persistent storage keyed by origin and block.number) against the brief's wording.","line":325,"path":"src/IMDOFeeHook.sol","reproduction":"Same fixture as finding 1 (snapshot 9_999_999_999_999_999_999_457). In one block alice sends 6 separate transactions, each a sell swap zeroForOne=false exact-in amountSpecified=-99_999_999_999_999_999_994 (= ceil(1%) - 1). Cumulative sold 599_999_999_999_999_999_964 = 599 bps of the snapshot. Expected if sized cumulatively: 20_000 ppm bracket, about 1.0e19 wei to the treasury. Actual: TREASURY.balance delta 0 across all six (each transaction sees sold < _threshold(reserve, 100) and feeRate returns 0). Control: next block, bob sells the same total 599_999_999_999_999_999_964 in one transaction and the treasury receives 10_082_445_610_628_068_328 wei.","severity":"info","snippet":"        bytes32 base = keccak256(abi.encode(\"IMDO.transaction.billing\", tx.origin));","title":"Billing counters are per transaction while the reserve snapshot is constant for the whole block, so splitting one sell across several transactions in one block pays 0 instead of 2%"},{"citation":"resolved","description":"feeRate guards the division by returning the cap when the denominator is zero. laggedReserve is zero during the launch block (beforeInitialize sets reserveBlock = block.number with liveReserve = 0, and _rollReserve only copies liveReserve in a later block) and during the first active block after any block that closed with no token inventory. In those blocks the bracket table in the brief (\"a sell of less than 1% of the reserve pays 0%\") is not applied: a 1e18 IMDO sell against a 1e22 reserve (1 bps) is charged 20_000 ppm. Seam: boundary (reserve == 0) x precision (ratio undefined) x invariant (bracket table). The README documents the behaviour as conservative; it is recorded here because launch-block traders, including swarm recipients who sell in the launch block, pay the maximum rate, and because the alternative (treat a zero snapshot as the live inventory for that block only) would keep the table intact without creating a manipulation window inside the launch transaction.","line":276,"path":"src/IMDOFeeHook.sol","reproduction":"Fixture _launch(TICK_LOWER, TICK_UPPER, true, false) (stay in the launch block; liveReserve = 10_000_999_999_999_999_999_457 after seeding, laggedReserve = 0). alice sells exact-in amountSpecified=-1e18 (about 1 bps of the inventory). Expected per the bracket table: 0 ppm, treasury unchanged. Actual: swapper nets 976_962_596_829_096_139 wei ETH, treasury receives 19_938_012_180_185_635 wei; gross output = 996_900_609_009_281_774 wei and floor(996900609009281774 * 20_000 / 1_000_000) = 19_938_012_180_185_635, i.e. exactly the 2% bracket; hook.laggedReserve() == 0 and hook.liveReserve() == 10_000_999_999_999_999_999_457 at that moment.","severity":"info","snippet":"        if (reserve == 0) return MAX_FEE_PPM; // No mature reserve: conservative, never divide by zero.","title":"A zero lagged reserve maps every sell size to the 2% cap, so launch-block sells (and the first block after the pool is emptied) pay 20_000 ppm even far below the 1% threshold"}],"hash":"5ab2448b13a3d06d13922c0359175f5aea8606cc95e854524ba6ae35a864d7c9","nodeId":"b8cd1fde-0467-4dff-adf6-6cbd7c7961c1","outcome":"completed","summary":"The review is complete. The findings file is written at `.imd-findings.json`, no repository file was changed, and the working tree is clean apart from ignored build output.\n\n**Outcome: no arithmetic defect found in the hook or token.** I traced every value-moving formula against the Math Precision, Boundary and Numerical Gap checklists and confirmed each by hand and, where it mattered, empirically against the real v4 PoolManager embedded in the existing suite. Specifically:\n\n- **Bracket thresholds** are exact `ceil(reserve * bps / 10000)` without a large product, and map to the brief's bps table at the 10000 and 10001 reserve edges.\n- **Cumulative bill** `Q*r - P` cannot underflow because both the rate and the quote total are monotone within a transaction. Exact-in legs credit `paid` exactly; exact-out legs round the credit down by under a millionth of a wei. The 512-bit `mulDiv` is the standard OpenZeppelin construction with the correct inverse seed and six Newton steps, so the aderyn \"caret\" lead is a false positive.\n- **Casts** `int128(delta)` and `int128(delta >> 128)` correctly extract amount1 and amount0 for a pool that beforeInitialize forces to be ETH/IMDO. The headroom check is unreachable below roughly 2^126 token units. The aderyn casting and reentrancy leads do not reach a defect.\n- **Reserve tracking** reconciles with the manager's pool-attributable balance. Protocol fees are subtracted from the input side only, and v4's pool-favouring rounding means no outflow can underflow `liveReserve`, so the factory's liquidity and fee collection cannot be blocked.\n- **Boundaries probed live:** a 9.9e21 wei exact-out sell burned exactly 2% of its gross token input, a 1 wei exact-out cost 3 wei of token and no fee, and the treasury address on Sepolia is an empty account, so direct ETH payment will not fall back to claims.\n\n**Three recorded items, all design limitations rather than math errors, each with reproduced numbers:**\n\n1. **Low.** Out-of-range token-only liquidity counts in the bracket denominator. A deposit at ticks -660 to -600 one block earlier let a 5% sell pay zero instead of about 10 ETH, with the position withdrawn in the same transaction and no price exposure.\n2. **Info.** Per-transaction counters plus a block-constant snapshot mean six transactions in one block sold 5.99% of the reserve for zero fee, where one transaction paid 10.08 ETH. This follows the brief's \"within one transaction\" wording.\n3. **Info.** A zero lagged reserve maps every size to the 2% cap, so a 1 bps sell in the launch block paid 20,000 ppm.\n\nThe coverage record answers all 12 listed entry points plus four invariants. Two entry points carry `finding` references for items 1 and 2. None are `unreached`. No proof files were attached since nothing reached high or critical severity.","treeHash":null,"usage":{"cachedInputTokens":2837922,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":57014,"runtime":"claude","turns":42,"wallClockMs":767725}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca080fd306399669","findings":[],"hash":"5e162eb79771b815d16199bf595af7a281ade37fa90f9dba5e2a9157beb478b3","nodeId":"964d4b4f-ee3c-4738-9213-08819cec2d83","outcome":"completed","summary":"All five earlier findings are settled and I found nothing new, so `.imd-findings.json` holds an empty findings list plus a coverage row for each of the 12 entry points, all `holds`.\n\nI ran the four attached proofs against the revised code on a real v4 PoolManager, and the repository's own suite; all pass (4 of 4 proofs, 51 of 51 tests, invariant run with 0 reverts).\n\n- **Shared-origin catch-up billing (medium): fixed.** `_bill` now caps each leg at 20,000 ppm of its own size in both swap modes, and the shared-origin proof passes. The weaker retroactive catch-up on split sells is stated in the README as the requester's trade-off.\n- **Exact-output fee charged twice with a pay-first router (medium): fixed.** The token fee is now always an ERC-6909 claim, with no `take` inside `afterSwap`, and the pay-first proof passes. `harvest()` burns the token claim even while the treasury rejects ETH.\n- **Pool squatting via `beforeInitialize` (low): fixed.** The full launch key (fee 3000, tick spacing 60) is pinned and the squat proof passes. Initializing the exact key at a wrong price is still possible in a two-step deploy; the README limits that flow to dry runs and requires atomic deploy-and-initialize.\n- **Launch-block sells pay the 2% cap (low): settled by documentation.** The code is unchanged and the rule is now an explicit row of the README bracket table, which was one of the two options I offered.\n- **Out-of-range liquidity inflating the reserve across a block (info): dropped.** The author's dispute holds: the brief asks only for a one-block lag, and the README now states this exact scenario as residual exposure.\n\nI checked the fix for new problems and found none: `quote * rate - paid` cannot underflow because each leg adds at most the outstanding amount to `paid`, and the mint and harvest deltas net to zero.\n\nThe repository suite only compiled without `--via-ir` here (solc 0.8.26 segfaulted on `test/IMDO.t.sol` with it); the proofs ran with `--via-ir`. The scratch copies and build output are removed, and no tracked file changed.","treeHash":null,"usage":{"cachedInputTokens":987766,"inputTokens":21,"model":"claude-fable-5-1","outputTokens":7283,"runtime":"claude","turns":11,"wallClockMs":165815}},{"artifacts":[],"attempt":1,"bundleHash":"415d597ed349a43f082a8c03570554a9aa007cb111a394d55212abf775fabd29","device":"468e82a89b9bfe18","findings":[],"hash":"629cb62b80687fd1db9e7ce21a4d11a72975e0dfd12fd0d5d237c7dbd2258cca","nodeId":"08e855db-0d4c-4033-b1f7-7e198ec393cf","outcome":"completed","summary":"Work is complete. The suite builds offline, all 48 tests pass, `forge fmt --check` is clean, and the only modified tracked file is `test/IMDO.t.sol`. The untracked dotfiles in `git status` are the sandbox's read-only device nodes that were present before I started, not files I created.\n\n**What failed and why.** The implementation node revised the deploy script to accept chain 31337 as a local dry run and added a per-leg bound to the hook's cumulative sell bill. My accepted test `test_runRefusesTheWrongChain` asserted that `run()` reverts on the default test chain, which is 31337, so it failed against the revised script. The previous repair attempt was rejected for a model-side content flag, so none of it survived.\n\n**Changes in `test/IMDO.t.sol`:**\n\n- **Deploy-script tests rewritten for the revised script.** `run()` now executes from a test on chain 31337: it asserts the fixed supply minted to one account, the hook at an address carrying exactly the permission bits, the stand-in manager with code, the returned salt equal to what `mine` finds for Foundry's CREATE2 deployer, the full `LaunchAttested` event payload, and that the real PoolManager cannot initialize a pool against the locally built hook. A fuzz test rejects every other chain id, and a Sepolia run without a configured PoolManager is refused. The suite reads the two environment variables with `envOr` only to detect a foreign configuration; it never sets them.\n- **Per-leg bound tests.** A dust leg crossing a bracket surrenders exactly its own output and never leaves a negative ETH delta, the next leg pays the carried remainder, and the bill is complete by the end of the transaction. The exact-output analogue bounds the token fee to the leg's own input. A dust leg as the last leg leaves only a bounded remainder that does not carry into a later transaction and is never cheaper than separate sells.\n- **Fuzz property over three-leg splits.** Total paid in one transaction is at least the sum of each leg billed at its own bracket and at most the final bracket on all proceeds, with every leg's ETH delta non-negative.\n- **Invariant handler.** A new `splitSell` action runs two sell legs in one transaction, optionally mixing modes, so the random call sequences now exercise the bound path. Runs show 128 calls to it with zero reverts.\n\n**Findings.** None. I checked the per-leg bound for an exploitable split and the carried-remainder accounting; the documented trade-off holds and the fuzz property confirms it, so no `.imd-findings.json` was written.","treeHash":"efd1580bcb4ecc49f8f4494cc89aaf05e802ab5e","usage":{"cachedInputTokens":2758031,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":43008,"runtime":"claude","turns":28,"wallClockMs":709964}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca080fd306399669","findings":[{"citation":"resolved","description":"Merged from audit_economics 10fe0b1a and audit_flow dd71dc25 (same root cause). _bill keeps one running bill per tx.origin (line 325) and collects the whole outstanding cumulative amount `quote * rate - paid` (line 337) from the current leg, bounded only by that leg's own gross output (line 346, and line 350 for exact-output). When one tx.origin executes sells for several principals (ERC-4337 bundler, relayer, batch settler), the later principal's leg pays the catch-up owed on the earlier principal's proceeds. A seller whose own sale is in the 0% bracket loses 27.7% of their ETH, or all of it for a smaller leg; the ETH goes to the treasury and is not recoverable. The brief states a hard 2% cap; the README says bundled origins share a bracket but not that one party pays another's bill. The earlier seller pays only their own bracket, so placing a large sell ahead of a victim inside a shared origin costs nothing. Fix options that keep the design: bound each leg's fee to MAX_FEE_PPM of that leg's own size (ETH fee <= quoteOut*20_000/1e6, token fee <= tokenIn*20_000/1e6) while still using the cumulative bracket, which keeps the hard cap but weakens the retroactive catch-up; or keep the catch-up and state in the README that a leg under a shared origin can forfeit its whole output. This is a requester trade-off between the 2% cap and full retroactive billing, so no proof is attached that would force one choice.","line":346,"path":"src/IMDOFeeHook.sol","reproduction":"Reproduced by running the specialist test (forge test --match-path test/scratch/Proof_10fe0b1a4036.t.sol, real v4 PoolManager): pool ETH/IMDO fee 3000 tickSpacing 60 at sqrtPrice 2^96, full-range liquidity 1e22, one block after launch, lagged reserve 9,999,999,999,999,999,999,457. A Bundler contract called with tx.origin = bundlerEOA sells for two users through PoolSwapTest. Leg 1: alice exact-in reserve*490/10000 IMDO, fee = 9,999 ppm of her gross (correct 1% bracket). Leg 2: bob exact-in reserve*20/10000 IMDO (0.20%, 0% bracket alone). Expected: bob pays 0, and never more than 20,000 ppm of his gross. Actual: bob pays 277,457 ppm of his gross (about 5.02 ETH of 18.09 ETH); the test fails with \"bob's leg billed above the 2% hard cap: 5019581763196613256000000 > 361827054004762729540000\". With bob's leg reduced to 0.05% of the reserve the whole output is taken (fee capped at quoteOut).","severity":"medium","snippet":"if (fee > quoteOut) fee = quoteOut;","title":"Catch-up billing keyed only on tx.origin charges a later seller for an earlier seller's bracket, up to 100% of that leg's output (2% cap broken per seller)"},{"citation":"resolved","description":"Merged from audit_economics 64c1fa35 and audit_flow e332f61c (same root cause; kept at medium because a seller loses funds under a specific but valid router ordering). For exact-output sells _payOrAccrue calls poolManager.take(token, address(this), fee) during afterSwap and burns. PoolManager.settle() credits balanceOf(manager) - reservesAtSync. If the swapper's router called sync(IMDO) and transferred its maximum input before swap (sync -> transfer -> swap -> settle -> refund, a valid v4 order for exact-output trades), the hook's take lowers the manager balance between sync and settle, so settle credits `transferred - fee` while the swap delta already contains `+fee`. The seller pays the fee twice: one copy is burned, the other stays in the PoolManager with no delta, pool or claim attached. With a tight maximum input the unlock reverts instead, so the sell is blocked. Routers that settle after the swap (PoolSwapTest, V4Router) are not affected, and the ETH path is not affected because native settlement uses msg.value. OpenZeppelin's BaseHookFee avoids this by taking the fee as an ERC-6909 claim. Fix: in the token branch always mint the claim (accruedToken += fee; poolManager.mint) and burn in harvest(); when doing so, also make harvest's token burn independent of the ETH leg, which currently reverts the whole harvest if the treasury rejects ETH.","line":373,"path":"src/IMDOFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {IMDOToken, IMDOFeeHook} from \"src/IMDOFeeHook.sol\";\n\n/// @dev A router that funds the input before the swap: sync -> transfer(max) -> swap -> settle,\n/// then takes the ETH output and the unused token credit back to the user. This is a valid v4\n/// settlement order (sync must precede the transfer, settle must follow it).\ncontract PayFirstRouter {\n    IPoolManager immutable manager;\n    IMDOToken immutable token;\n\n    constructor(IPoolManager m, IMDOToken t) {\n        manager = m;\n        token = t;\n    }\n\n    receive() external payable {}\n\n    function sellExactOut(PoolKey calldata key, uint256 ethOut, uint256 maxTokenIn) external returns (BalanceDelta) {\n        token.transferFrom(msg.sender, address(this), maxTokenIn);\n        bytes memory r = manager.unlock(abi.encode(msg.sender, key, ethOut, maxTokenIn));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (address user, PoolKey memory key, uint256 ethOut, uint256 maxTokenIn) =\n            abi.decode(data, (address, PoolKey, uint256, uint256));\n        manager.sync(key.currency1);\n        token.transfer(address(manager), maxTokenIn);\n        BalanceDelta delta = manager.swap(key, SwapParams(false, int256(ethOut), TickMath.MAX_SQRT_PRICE - 1), \"\");\n        uint256 credited = manager.settle();\n        manager.take(key.currency0, user, ethOut);\n        int256 tokenCredit = int256(credited) + int256(delta.amount1());\n        require(tokenCredit >= 0, \"input exceeded max\");\n        if (tokenCredit > 0) manager.take(key.currency1, user, uint256(tokenCredit));\n        return abi.encode(delta);\n    }\n}\n\ncontract ProofPayFirstRouterTest is Test {\n    uint160 constant SQRT_1_1 = 79228162514264337593543950336;\n    PoolManager manager;\n    IMDOToken token;\n    IMDOFeeHook hook;\n    PoolModifyLiquidityTest lpRouter;\n    PayFirstRouter router;\n    PoolKey key;\n    address alice = makeAddr(\"alice\");\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new IMDOToken();\n        bytes memory initCode =\n            abi.encodePacked(type(IMDOFeeHook).creationCode, abi.encode(address(manager), address(token)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i; i < 500_000; ++i) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & 0x3fff == 0x25d4) {\n                hook = new IMDOFeeHook{salt: bytes32(i)}(address(manager), address(token));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        router = new PayFirstRouter(manager, token);\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 3000, 60, IHooks(address(hook)));\n        manager.initialize(key, SQRT_1_1);\n        vm.deal(address(this), 1e24);\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity{value: 2e22}(key, ModifyLiquidityParams(-887220, 887220, 1e22, 0), \"\");\n        token.transfer(alice, 1e25);\n        vm.prank(alice);\n        token.approve(address(router), type(uint256).max);\n        vm.roll(block.number + 1);\n    }\n\n    /// An exact-output sell of ~5% of the reserve (2% bracket). The hook's `take(token)` runs\n    /// between the router's `sync` and `settle`, so `settle` credits `max - fee` while the swap\n    /// delta already includes `+fee`: the seller pays the hook fee twice, and the second copy is\n    /// stranded in the PoolManager, owned by nobody.\n    function test_payFirstRouterSellerPaysExactlyTheSettledSwapDelta() public {\n        uint256 before = token.balanceOf(alice);\n        vm.prank(alice, alice);\n        BalanceDelta d = router.sellExactOut(key, 480e18, 600e18);\n        uint256 swapIn = uint256(-int256(d.amount1()));\n        uint256 paid = before - token.balanceOf(alice);\n        emit log_named_uint(\"settled token input incl. hook fee\", swapIn);\n        emit log_named_uint(\"tokens the seller actually lost\", paid);\n        assertEq(paid, swapIn, \"seller charged more than the settled swap delta\");\n    }\n}","reproduction":"Reproduced by running the attached test (forge test --match-path test/scratch/Proof_64c1fa35bf5a.t.sol, real v4 PoolManager): same pool fixture, one block after launch. PayFirstRouter.unlockCallback: manager.sync(IMDO); token.transfer(manager, 600e18); manager.swap(key, SwapParams(false, +480e18, MAX_SQRT_PRICE-1)); manager.settle(); take 480 ETH to alice; take leftover token credit to alice. Settled swap delta for alice (includes the hook fee once): 515,833,213,927,496,776,044 IMDO. Expected: alice's IMDO balance falls by exactly that. Actual: it falls by 525,947,590,671,173,183,417, i.e. 10,114,376,743,676,407,373 IMDO (the hook fee) more, left ownerless in the PoolManager. Test fails with \"seller charged more than the settled swap delta: 525947590671173183417 != 515833213927496776044\".","severity":"medium","snippet":"poolManager.take(token, address(this), fee);","title":"Exact-output sell fee is physically taken from PoolManager inside afterSwap, so a router that synced IMDO before the swap is charged the hook fee twice and the duplicate is stranded"},{"citation":"resolved","description":"Merged from audit_permissions 8c66ec22 and audit_economics 8061c671 (same root cause; low because no funds are lost and the window does not exist when the launch factory deploys the hook and initializes in one transaction, which is the manifest flow). beforeInitialize checks only currencies, hook address and fee tier, then sets initialized = true and poolId forever. It ignores the sender, the tick spacing (manifest: 60) and sqrtPriceX96. script/Deploy.s.sol deploys the hook without initializing, and the README tells the operator the factory initializes later. In that window any account can bind the hook to a different key (fee 500, tickSpacing 1) or to the intended key at an extreme price. The factory's initialize then reverts and the mined hook address and salt are dead; there is no rebind path. Fix that keeps the design: pin the launch fee and tickSpacing (constructor constants or literal constructor arguments) and compare the full key, and make the README and script require deploy and initialize in the same transaction.","line":208,"path":"src/IMDOFeeHook.sol","reproduction":"Reproduced by running the specialist test (forge test --match-path test/scratch/Proof_8061c671525d.t.sol, real v4 PoolManager): 1) deploy IMDOToken and IMDOFeeHook(manager, token) at a mined 0x25d4 address, no pool. 2) Any address calls manager.initialize(PoolKey(0x0, token, 500, 1, hook), 4295128740): succeeds, hook.initialized() == true, poolId bound to that key. 3) manager.initialize(PoolKey(0x0, token, 3000, 60, hook), 79228162514264337593543950336). Expected: launch pool initializes. Actual: reverts WrappedError(hook, 0xdc98354e beforeInitialize, 0x2083cd40 InvalidPool, 0xa9e35b2f HookCallFailed). Variant: initializing the exact launch key first at MIN_SQRT_PRICE+1 makes the factory's call revert with PoolAlreadyInitialized while the hook stays bound to the mispriced pool.","severity":"low","snippet":"initialized || key.currency0 != address(0) || key.currency1 != token || key.hooks != address(this)","title":"beforeInitialize binds the hook to the first ETH/IMDO key anyone initializes (any tickSpacing, any price, any sender), so the two-step flow in Deploy.s.sol and the README can be squatted"},{"citation":"resolved","description":"Merged from audit_math 6837e80a, audit_permissions 4069f918 and audit_economics b824b17e (same root cause). beforeInitialize sets reserveBlock = block.number and leaves laggedReserve = 0; _rollReserve only copies liveReserve in a later block. feeRate returns MAX_FEE_PPM when the reserve is 0, so every sell in the block the factory initializes and seeds the pool, and in the first active block after a block that closed with zero inventory, pays 2% even at 1 bps of the live reserve. The brief's table says under 1% pays 0%. The README and manifest notes disclose the rule and the charge stays within the cap, so this is a specification deviation, not a loss beyond the cap. Either add the launch-block rule as an explicit row of the bracket table the requester accepts, or seed the snapshot from the live inventory when no lagged snapshot exists.","line":276,"path":"src/IMDOFeeHook.sol","reproduction":"Reproduced with my own Foundry test on a real v4 PoolManager: initialize ETH/IMDO 3000/60 at sqrtPrice 2^96 and add full-range liquidity 1e22 in the same block (hook.laggedReserve() == 0, hook.liveReserve() == 9,999,999,999,999,999,999,457). In that block alice sells exact-in 1e18 IMDO (1 bps of the live reserve). Expected per the bracket table: fee 0. Actual: alice receives 976,962,596,829,096,139 wei and the treasury receives 19,938,012,180,185,635 wei = floor(996,900,609,009,281,774 * 20,000 / 1e6), the 2% bracket. After vm.roll to the next block the same 1e18 sell pays 0. The repository's own test_launchBlockSellsPayTheCapThenTheLagExpires asserts the same behaviour.","severity":"low","snippet":"if (reserve == 0) return MAX_FEE_PPM; // No mature reserve: conservative, never divide by zero.","title":"Zero lagged reserve maps every sell to the 2% cap, so launch-block sells of any size pay 20,000 ppm instead of the bracket table's 0%"},{"citation":"resolved","description":"Merged from audit_math bc9b18f5 and audit_economics 1d676383 (same root cause). afterAddLiquidity adds the settled token amount of any position to liveReserve, including positions entirely below the current price that hold only IMDO and never trade. The next block's laggedReserve (the denominator in _bill, line 335) is inflated by that deposit, and the position can be withdrawn in the same transaction as the sell. The brief asked only for a one-block lag against same-transaction inflation and the README says manipulation across a block boundary is out of scope, so this conforms to the brief; it is recorded as the residual exposure because the cost is only gas and one block of holding tokens the seller already owns. If the requester wants it closed, count only in-range liquidity or lag by more than one block; no change is required to meet the brief.","line":227,"path":"src/IMDOFeeHook.sol","reproduction":"Reproduced with my own Foundry test on a real v4 PoolManager: pool as above, block 100, lagged reserve R = 9,999,999,999,999,999,999,457. alice adds liquidity 1.5e24 in ticks [-887220, -60]: costs 1,495,506,967,566,133,828,511,959 IMDO and 0 ETH. Block 101: hook.reserveSnapshot() == 1,505,506,967,566,133,828,511,416. alice sells exact-in ceil(R*500/10000) = 499,999,999,999,999,999,973 IMDO (hook.feeRate(amount, R) == 20000). Expected if sized against the tradeable reserve: about 9.50 ETH to the treasury (the same sell without the deposit paid 9,496,594,751,631,185,406 wei). Actual: treasury receives 0, alice receives 474,829,737,581,559,270,347 wei; she then removes the position and her net IMDO change is the sale amount plus 1 wei of rounding.","severity":"info","snippet":"_updateReserve(int128(delta));","title":"Bracket denominator counts out-of-range token-only liquidity, so a position parked across one block boundary drops a 5% sell from the 2% bracket to 0% at no price risk"}],"hash":"a6ba271e6453f8b631e1ef918c9bf2f35d07c56fb6eca13537a4de683c7e7b3c","nodeId":"964d4b4f-ee3c-4738-9213-08819cec2d83","outcome":"completed","summary":"The review is written to `.imd-findings.json`: five findings kept, all reproduced on a real v4 PoolManager, and all 12 entry points answered. No critical or high defects were found, and no repository file was changed.\n\n**Findings kept, after merging duplicates**\n\n1. **Medium, shared-origin over-billing.** The cumulative bill is keyed only on `tx.origin` and collected from the current leg, bounded only by that leg's output. In the economics specialist's test, a seller in the 0% bracket lost 277,457 ppm of their ETH after another user's sell in the same bundle. The fix is a requester trade-off between the 2% cap and full retroactive billing, so I attached no proof that would force one choice.\n2. **Medium, exact-output fee double-charged.** The token fee is physically taken from the PoolManager inside `afterSwap`. A router that synced IMDO before the swap pays the fee twice, and the second copy is stranded. The economics specialist's proof fails for this reason and is attached.\n3. **Low, pool binding can be squatted.** `beforeInitialize` accepts any tick spacing, price and sender. In the two-step flow of the deploy script, anyone can bind the hook to a wrong pool and kill the mined address. The window does not exist if the factory deploys and initializes atomically.\n4. **Low, launch-block sells pay 2%.** A zero lagged reserve maps every sell to the cap. A 1 bps sell in the launch block paid 20,000 ppm, against the bracket table's 0%.\n5. **Info, out-of-range liquidity inflates the bracket denominator.** A token-only position parked across one block boundary dropped a 5% sell from the 2% bracket to a zero fee. This conforms to the brief's one-block lag and is recorded as residual exposure only.\n\n**Specialist findings dropped**\n\n- **Boundary-hugging split:** the seller gains nothing. They pay what selling just under the boundary alone would cost, and the dust leg forfeits its whole output.\n- **Cross-transaction splitting:** this is the brief's own rule, which accumulates within one transaction only.\n- **Manifest initial price of 1 IMDO per ETH:** no intended price is supplied to check it against, and the manifest notes already disclose it as an assumption. It still deserves the requester's confirmation before launch.\n\n**Other checks**\n\n- The repository's own suite passes: 48 tests.\n- The deploy script dry run succeeds offline.\n- The static-analysis leads did not reproduce: the caret operator is the standard modular-inverse seed, the casts select the token half of the delta, and the flagged state change follows a zeroed counter.\n\nMy own read of the remaining entry points found nothing the specialists missed. Delta accounting balances on every path, the reserve tracker cannot underflow, and harvest moves only recorded claims to the fixed destinations.","treeHash":null,"usage":{"cachedInputTokens":1487660,"inputTokens":28,"model":"claude-fable-5-1","outputTokens":38472,"runtime":"claude","turns":16,"wallClockMs":673915}},{"artifacts":[],"attempt":1,"bundleHash":"d10f121c81fd76e90bd0dfcc2c4b7a6e3495513e345fb05fb1042bc8c40a6192","device":"468e82a89b9bfe18","findings":[],"hash":"b85c138a3a41a28f2e45a6ee7b24e1be0c123dd3fa2a69e42df049d3c08ce235","nodeId":"0357c5d4-1818-410f-987f-f17577b94624","outcome":"completed","summary":"Revision complete. All five findings are answered in `.imd-responses.json`, four fixed and one disputed, and only the three allowed files changed.\n\n**What reproduced and what changed**\n\n- **Shared-origin catch-up billing (medium), fixed.** A bundler running two principals' sells under one origin charged the second leg 27.7% of its gross on the accepted code. The per-leg bound in `_bill` is now 2% of the leg's own size for both exact-in and exact-out, while the bracket stays cumulative. I took the reviewer's first option and recorded the trade-off in the README: catch-up is weaker within one transaction, which matches what cross-transaction splitting already allowed.\n- **Exact-output take between sync and settle (medium), fixed.** The attached proof failed on the starting tree and passes now. The token fee is always an ERC-6909 claim, never a `take` inside `afterSwap`. The permissionless `harvest()` burns it, and its ETH leg is wrapped in try/catch so a treasury that rejects ETH no longer blocks the token burn.\n- **Pool squatting (low), fixed.** The hook now pins the full launch key: fee 3000 and tick spacing 60 as constants, compared along with currencies and hook address. The deploy script attests those constants. The README states that deploy and initialize must happen in one factory transaction, because pinning cannot stop someone initializing the exact key at a bad price in a two-step window.\n- **Launch-block 2% rule (low), fixed by documentation.** Code unchanged. The README bracket table now has an explicit row for sells while the lagged reserve is zero. Seeding from live inventory would reopen same-block inflation in the launch block, and the existing test suite asserts the current behaviour.\n- **Out-of-range liquidity in the denominator (info), disputed.** The brief asks only for a one-block lag, which the hook provides. The README now names this residual exposure explicitly.\n\n**Verification**\n\n- Default and attested offline builds succeed, `forge fmt --check` passes, and the deploy dry run on chain 31337 succeeds.\n- A 13-test scratch suite on a real v4 PoolManager passes, including the reviewer's proof, both bundler scenarios, the squat case, brackets, free buys, claim-then-burn, independent harvest legs, split billing, and same-transaction inflation.\n- Attested hook runtime is 7,105 bytes with no forbidden opcodes. The README's code hashes were recomputed from the attested build.\n\nOne note: the repository has no `foundry.toml` or `lib/`, so scratch tests were compiled against the vendored forge-std and v4-core via remappings on the command line. Nothing outside `src/`, `script/` and `README.md` was changed.","treeHash":"e1eb5b8f0a89cf4dd6aea49dbe907cdf83af6706","usage":{"cachedInputTokens":2233493,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":47904,"runtime":"claude","turns":50,"wallClockMs":888356}},{"artifacts":[],"attempt":1,"bundleHash":"8f11e349da4d8594615b0f23c7a1919768ae5c65e860e8780717b251db89df9e","device":"468e82a89b9bfe18","findings":[],"hash":"d65c5fb1f5690556cc271187e00f1b7c82de74031f71d7139a052fae6b01cf34","nodeId":"0357c5d4-1818-410f-987f-f17577b94624","outcome":"completed","summary":"Final README sentence fixed. All work is complete and verified; here is the recap.\n\n**Outcome.** The medium finding reproduced and is fixed. The low finding reproduced against a real v4 PoolManager and is fixed with a small change in the hook's billing. The two info findings are confirmed behaviour that the brief specifies, so they are answered as disputed with no code change. All four answers are in `.imd-responses.json`.\n\n**Deploy script (medium, fixed).** The old `run()` reverted on any chain other than Sepolia before reading configuration, and even there only forwarded an opaque factory calldata blob. The rewritten script reads only `EXPECTED_CHAIN_ID` plus an optional `POOL_MANAGER`, accepts chains 31337 and 11155111, and between the broadcast markers deploys the token with CREATE and the hook with CREATE2 through Foundry's default deterministic deployer, mining the salt for permission bits 0x25d4 in the script. On the local chain without a manager it first deploys an empty stand-in so the hook constructor's code check passes. After deployment it re-reads bindings, treasury, cap, supply and not-initialized state and emits an attestation event. Pool initialization stays with the launch factory.\n\nVerified:\n- `EXPECTED_CHAIN_ID=0 forge script script/Deploy.s.sol:Deploy --offline` succeeds and returns token, hook and salt.\n- Wrong expected chain, missing `POOL_MANAGER` on Sepolia, and a codeless manager each revert with `InvalidConfiguration()` before deploying.\n- `run()` also works when called from a forge test, with and without a real PoolManager, followed by a factory-style pool initialization.\n\n**Per-leg fee bound (low, fixed).** Using Uniswap's own output-only settlement rule in a batching router, a sell of just under 1% followed by a 1 wei sell reverted on the second leg with a negative ETH delta under the accepted code. Now a leg is never billed more than its own size: the ETH fee is capped at the leg's gross ETH output and the IMDO fee at the leg's settled input. The uncollected remainder stays in the cumulative bill and is charged on the next leg of the same transaction. Single sells and ordinary splits bill exactly as before, and a transaction that ends on a tiny crossing leg never pays less than selling the already-billed earlier legs alone.\n\n**Checks run.** Offline build, size check and `forge fmt --check` pass. Both protected floor suites pass against the revised creation code, 9 tests. A 10-test scratch suite on a real PoolManager passes, and the same tiny-leg tests fail against the old hook code, confirming the reproduction. The README's billing section, script section, attestation hashes and verification results are updated. Nothing outside `src/IMDOFeeHook.sol`, `script/Deploy.s.sol` and `README.md` was changed, and `foundry.toml` was left absent as before.","treeHash":"b0488633d6726b0c50139a29d9c132da70a30b2a","usage":{"cachedInputTokens":4020030,"inputTokens":1026,"model":"claude-fable-5-1","outputTokens":63265,"runtime":"claude","turns":42,"wallClockMs":1295659}},{"artifacts":[],"attempt":1,"bundleHash":"321ff3453fcf4db54e1fff98c9337eb4e1d29ff9cec0993308aad00e2bf95d58","device":"0256823ae36e7900","findings":[],"hash":"d9886eac6ae830a0fd720eb3c500489cea52dc781aeb5ee15a42b2325c01ec4f","nodeId":"e3550fb2-2556-4523-94fb-99781b777314","outcome":"completed","summary":"Created [launch.json](/home/imd-worker/.identitymd/work/da4c0e74-7790-4c26-a7af-49f5062bd7e3/e3550fb2-2556-4523-94fb-99781b777314/launch.json) with matching constructors and permissions, native ETH pairing, and documented price assumption.\n\nSchema validation and `forge build` passed. `forge test`: 48 passed, zero failures.\n\nOnly `launch.json` changed outside exempt scratch artifacts.","treeHash":"97a59b09917427abb5ea4929d0cd4f60cc5eb3e6","usage":{"cachedInputTokens":660224,"inputTokens":56884,"model":"gpt-6-astra","outputTokens":4516,"runtime":"codex","turns":4,"wallClockMs":238118}},{"artifacts":[],"attempt":1,"bundleHash":"8a77e728c523138b22f1b181bfd60935c4c86cb48c80fb4e8046a3aaaf8b9909","device":"0256823ae36e7900","findings":[],"hash":"ef76871838fa835775f2297b779539cbeff61fd69c6a6a841bfe480bead5edea","nodeId":"0357c5d4-1818-410f-987f-f17577b94624","outcome":"completed","summary":"Implemented the token, immutable hook, deployment script, and README launch attestation. No configuration or dependency files changed.\n\nVerified: offline build passed; **36 tests passed**, including 512 fuzz cases and 1,024 invariant calls.\n\nDocumented limitation: cumulative bracket catch-up can require extra ETH from a tiny final sell, so output-only routers cannot be guaranteed compatible. Production factory rehearsal remains pending its actual configuration and calldata.","treeHash":"780866418488258e805da92ade47462a214d189a","usage":{"cachedInputTokens":2108544,"inputTokens":74340,"model":"gpt-6-astra","outputTokens":20390,"runtime":"codex","turns":5,"wallClockMs":1828413}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"The per-transaction bill is keyed only by tx.origin and is retroactive: on every sell leg the hook recomputes due = cumulativeGrossETH * rate(cumulativeSold) - paid (line 337) and collects it from the current leg, bounded only by that leg's own ETH output (lines 344-347). The code assumes every sell leg under one tx.origin belongs to the same principal. That identity assumption is false whenever one EOA submits a transaction that executes sells for several principals: an ERC-4337 bundler (its EOA is tx.origin for every userOp in the bundle), a batch-auction / intent settlement contract (one solver EOA settles many users' orders), or any multicall relayer. In that case the later principal's leg is charged the catch-up on the earlier principal's proceeds. The charge on that leg is not bounded by MAX_FEE_PPM but by the leg's entire output, so a seller whose own sale is below 1% (0% bracket) can be billed 12.5% of their proceeds or, for a smaller leg, all of them. The README discloses that bundlers share the bracket but not that a leg can lose up to 100% of its output; the brief's '2% hard cap' guarantee is broken for that seller. Root cause is the combination of (a) the tx.origin key the brief specifies and (b) the retroactive catch-up being collected from a single leg. A fix that keeps the specified key and cumulative bracket: bill each leg at the cumulative bracket reached so far, i.e. fee_leg = quoteOut_leg * feeRate(cumulativeSold, laggedReserve) / PPM (and the exact-output analogue), so no leg is ever charged more than MAX_FEE_PPM of its own size; this trades the retroactive re-billing of earlier legs for a hard per-leg cap, which the brief states as a hard cap in code. If the retroactive catch-up is kept, the README must state that a leg can forfeit 100% of its output when the origin is shared.","line":325,"path":"src/IMDOFeeHook.sol","reproduction":"Setup: factory launch exactly as test/IMDO.t.sol IMDOFixture._launch(TICK_LOWER, TICK_UPPER, withMirror=true, leaveLaunchBlock=true): full-range position with liquidity 1e22 at price 1, laggedReserve R = hook.reserveSnapshot(). A BatchSettler contract (one unlock) performs two exact-input sells on the hooked pool, each settled from and paid to a DIFFERENT trader: order 1 = alice sells ceil(5% R) - 1 tokens; order 2 = bob sells ceil(0.5% R) tokens. The settler is invoked via vm.prank(bundler, bundler) so tx.origin = bundler for both legs. Standalone brackets: hook.feeRate(aliceSize, R) == 10_000, hook.feeRate(bobSize, R) == 0. Observed (same sizes on the hookless mirror pool give gross outputs gA = 474829737581559270346 wei, gB = 45014598262435288017 wei): alice receives gA - 1% = correct; bob's leg returns amount0 = gB - 5648589341064298464, i.e. bob pays 5.6486 ETH on 45.01 ETH of proceeds = 125,483 ppm (12.5%) although his own sale is in the 0% bracket; the amount equals exactly (gA+gB)*20_000/PPM - gA*10_000/PPM, the catch-up on alice's proceeds. With bob's order reduced to ceil(0.05% R) tokens (gB = 4520687539245871921 wei on the mirror) bob's hooked leg returns amount0 == 0 and bob's ETH balance does not change: 100% of his output is taken. Expected: a seller whose cumulative own sale is below 1% pays 0, and no leg is ever charged more than MAX_FEE_PPM = 20_000 ppm of its size. Scratch test: test/scratch/SharedOrigin.t.sol (BatchSettler + SharedOriginTest, both cases pass as written, demonstrating the behaviour).","severity":"medium","snippet":"        bytes32 base = keccak256(abi.encode(\"IMDO.transaction.billing\", tx.origin));","title":"Shared tx.origin bills a later, unrelated seller for an earlier seller's bracket catch-up (up to 100% of the later leg's ETH)"},{"citation":"resolved","description":"For exact-output sells the hook pulls its token fee with an immediate ERC-20 poolManager.take(token, hook, fee) inside afterSwap (line 373) instead of minting an ERC-6909 claim as OpenZeppelin's BaseHookFee pattern does. PoolManager.settle() for an ERC-20 credits the caller with balanceOfSelf() - syncedReserves. If the swapper's router has already called sync(token) and transferred its (maximum) input before calling swap, the hook's take lowers the manager's token balance between sync and settle, so settle credits the router `fee` less than it transferred. The router is then debited tokenIn + fee by the swap and credited only transferred - fee, so the user pays tokenIn + 2*fee; the extra `fee` tokens stay in the PoolManager with no delta, no pool and no claim attached (permanently stranded). The hookless mirror pool is unaffected under the same router, so the double charge is hook-induced. This ordering (pre-fund, swap, settle, refund) is a legitimate v4 settlement pattern for exact-output routers that do not know the input in advance. The ETH path (exact-input fee) is not affected because native settlement uses msg.value, not sync. Fix: in the token branch always mint the claim (poolManager.mint(address(this), token.toId(), fee)) and burn it during harvest, as the fallback already does, or take with claims (ERC-6909) rather than ERC-20 transfer during the swap.","line":372,"path":"src/IMDOFeeHook.sol","reproduction":"Setup as test/IMDO.t.sol IMDOFixture._launch(TICK_LOWER, TICK_UPPER, true, true), laggedReserve R. PrepayRouter.unlockCallback does: manager.sync(token); token.transferFrom(payer, manager, maxIn = R/5); d = manager.swap(key, SwapParams(zeroForOne=false, amountSpecified=+R*8/100 (exact ETH out, 2% bracket), MAX_SQRT-1)); manager.settle(); take(token, payer, positive token delta); take(ETH, payer, amount0(d)). Observed on the hooked pool with alice: settled token input (amount1 of d, which already includes the hook fee) = 889625397932929222398; hook fee = 17443635253586847498 tokens (burned, correct); alice's token balance decreases by 907069033186516069896 = settled input + one more fee; the PoolManager's token balance rises by 889625397932929222398, so 17443635253586847498 tokens are inside the manager with no owner. Same router on the hookless mirror pool with bob: paid == settled input exactly. Expected: alice pays exactly the settled input including the fee once (889625397932929222398). Scratch test: test/scratch/SyncOrder.t.sol (fails on the assertion 'alice pays exactly the settled input including the hook fee').","severity":"low","snippet":"            if (IMDOToken(token).balanceOf(address(poolManager)) >= fee) {\n                poolManager.take(token, address(this), fee);\n                IMDOToken(token).burn(fee);\n                return;","title":"Exact-output sell with a sync-then-swap-then-settle router charges the token fee twice and strands the duplicate in the PoolManager"},{"citation":"resolved","description":"sqrtPriceX96 = 2^96 encodes price(currency1/currency0) = 1.0 in raw units: 1 wei of IMDO per 1 wei of ETH, i.e. 1 IMDO = 1 ETH, so the 1,000,000,000 IMDO fixed supply is priced at 10^9 ETH. The notes say this copies the repository's test fixture because the brief gave no price. The deployer passes pool.initialPrice to the factory's initialize, so the pool opens at this price; a full-range two-sided factory position at this ratio needs ETH equal in wei to the token amount seeded (e.g. 5e26 wei = 500,000,000 ETH for half the supply), which cannot be funded, and a one-sided token position above the price leaves a pool nobody can buy into at a sane price. This is outside the control-flow area and is not a code defect, but it is a concrete launch input that the manifest owner should confirm or replace with the policy's intended valuation before admission.","line":26,"path":"launch.json","reproduction":"Decode launch.json pool.initialPrice: (79228162514264337593543950336 / 2^96)^2 = 1.0 → 1 IMDO (18 decimals) costs 1 ETH at the first block. With the token's INITIAL_SUPPLY = 1e27 units, implied fully diluted valuation = 1e9 ETH. Expected: an initial price derived from the launch policy's intended seed ratio; if 1:1 is intended, state it as a decision rather than as an inherited test-fixture value.","severity":"info","snippet":"    \"initialPrice\": \"79228162514264337593543950336\"","title":"Manifest initial price is exactly 1 IMDO per 1 ETH, which with a 10^27-unit supply is almost certainly not the intended launch valuation"}],"hash":"f7ac62ca1be493a8e01dce07d63428ba8e0ce8d918227f5d725c076f3383fac4","nodeId":"9b4ed019-328d-48b6-9d42-2cfaffbd33a0","outcome":"completed","summary":"The review is complete. No tracked file was changed; the findings file and scratch tests are untracked, as required.\n\n**Outcome.** `.imd-findings.json` holds 3 substantiated findings and 19 coverage rows, one for each of the 12 compiled entry points plus invariants, the attestation hashes, the manifest and the deploy script. The repository's own 48-test suite passes on this tree, and the README's attested creation and runtime code hashes reproduce byte-for-byte under the attested compiler flags.\n\n**Findings, most severe first:**\n\n- **Medium, `src/IMDOFeeHook.sol:325`.** The sell bill is keyed only by `tx.origin` and is collected retroactively from whichever leg crosses a bracket. When one origin settles sells for several principals in one transaction (ERC-4337 bundle, batch-auction solver, multicall relayer), a later unrelated seller pays the earlier seller's catch-up. Reproduced with a batch settler: Alice sells just under 5% and pays her correct 1%; Bob, selling 0.5% (0% bracket on his own), pays 12.5% of his proceeds, and with a 0.05% sale receives zero ETH. The brief's 2% hard cap is broken for that seller. The key is the brief's choice, so I state the design trade-off and a cap-preserving alternative rather than a one-line fix.\n- **Low, `src/IMDOFeeHook.sol:372`.** For exact-output sells the hook pulls its token fee with an immediate ERC-20 `take` instead of a claim. A router that calls `sync`, pre-funds its maximum input, swaps, then settles sees its credit reduced by the fee, so the seller pays the fee twice and the duplicate tokens are stranded in the PoolManager with no owner. Reproduced; the hookless mirror pool is unaffected under the same router. Minting the claim, as the fallback path already does, removes it.\n- **Info, `launch.json:26`.** The manifest's `initialPrice` decodes to exactly 1 IMDO per 1 ETH, which prices the fixed supply at 10^9 ETH. Outside my area and disclosed as an inherited fixture value, but it is a concrete launch input to confirm.\n\n**What held.** Caller gating, pool binding, delta decoding and netting, headroom bounds, the before/after protocol-fee reconciliation, the one-block lag, the ETH direct-pay with claim fallback, harvest and its callback gate, the token's four entry points, and the manifest's permission and constructor agreement. The one-block lag can be worked around by adding liquidity in the previous block, but that is exactly the protection the brief specifies and the README says so, so I did not report it.\n\n**Not covered.** The protected floor suites need forge-std and v4-core, which are not on disk, so I did not rerun them. No fork or live-factory rehearsal was performed, and no audit is claimed.","treeHash":null,"usage":{"cachedInputTokens":2996268,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":72631,"runtime":"claude","turns":36,"wallClockMs":1165722}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"_bill() keys the running sell total on tx.origin and charges each leg the whole outstanding cumulative bill `quote*rate - paid`, bounded only by the leg's own gross output (line 346). When two unrelated principals sell in the same transaction with one tx.origin (an ERC-4337 bundler, a meta-transaction relayer, a batch executor), the second principal's leg absorbs the catch-up owed on the first principal's volume. The brief states a hard cap of 2% that no bracket may exceed; here a principal who alone would be in the 0% bracket pays 27.7% of their output, and with a slightly larger first leg loses 100% of it. The README discloses that bundled transactions share a bracket but not that one party pays another's bill. The payer gets nothing back; the ETH goes to the treasury. The first seller pays exactly what they would pay alone, so this is a zero-cost griefing vector for anyone who can place a large sell ahead of a victim inside one origin, and an accidental loss in any shared bundle. Minimal fix that keeps the design: bound every leg's fee by MAX_FEE_PPM of its own gross (ETH fee <= quoteOut*MAX_FEE_PPM/PPM, token fee <= tokenIn*MAX_FEE_PPM/PPM) so the stated cap holds per leg; or key the transient accumulator on (tx.origin, swap sender/payer) in addition to tx.origin.","line":337,"path":"src/IMDOFeeHook.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 {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {IMDOToken, IMDOFeeHook} from \"src/IMDOFeeHook.sol\";\n\n/// @dev Executes several principals' sells inside one transaction, the way an ERC-4337 bundler\n/// or a meta-transaction relayer does: one tx.origin, many unrelated sellers.\ncontract Bundler {\n    PoolSwapTest immutable router;\n    IMDOToken immutable token;\n    address constant TREASURY = 0xb1eC9d1C36974d05eb9889eBf8A150b05791E559;\n\n    constructor(PoolSwapTest r, IMDOToken t) {\n        router = r;\n        token = t;\n    }\n\n    receive() external payable {}\n\n    function run(PoolKey calldata key, address[] calldata users, uint256[] calldata amounts)\n        external\n        returns (uint256[] memory ethOut, uint256[] memory fee)\n    {\n        ethOut = new uint256[](users.length);\n        fee = new uint256[](users.length);\n        for (uint256 i; i < users.length; ++i) {\n            token.transferFrom(users[i], address(this), amounts[i]);\n            token.approve(address(router), amounts[i]);\n            uint256 before = address(this).balance;\n            uint256 tBefore = TREASURY.balance;\n            router.swap(\n                key,\n                SwapParams(false, -int256(amounts[i]), TickMath.MAX_SQRT_PRICE - 1),\n                PoolSwapTest.TestSettings(false, false),\n                \"\"\n            );\n            ethOut[i] = address(this).balance - before;\n            fee[i] = TREASURY.balance - tBefore;\n            (bool ok,) = users[i].call{value: ethOut[i]}(\"\");\n            require(ok);\n        }\n    }\n}\n\ncontract ProofSharedOriginTest is Test {\n    uint160 constant SQRT_1_1 = 79228162514264337593543950336;\n\n    PoolManager manager;\n    IMDOToken token;\n    IMDOFeeHook hook;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    address bundlerEOA = makeAddr(\"bundlerEOA\");\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new IMDOToken();\n        bytes memory initCode =\n            abi.encodePacked(type(IMDOFeeHook).creationCode, abi.encode(address(manager), address(token)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i; i < 500_000; ++i) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & 0x3fff == 0x25d4) {\n                hook = new IMDOFeeHook{salt: bytes32(i)}(address(manager), address(token));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 3000, 60, IHooks(address(hook)));\n        manager.initialize(key, SQRT_1_1);\n        vm.deal(address(this), 1e24);\n        token.approve(address(lpRouter), type(uint256).max);\n        // Factory-style full-range launch position: ~1e22 IMDO and ~1e22 wei at price 1.\n        lpRouter.modifyLiquidity{value: 2e22}(key, ModifyLiquidityParams(-887220, 887220, 1e22, 0), \"\");\n        token.transfer(alice, 1e25);\n        token.transfer(bob, 1e25);\n        vm.roll(block.number + 1); // leave the launch block so the lagged reserve is the seeded one\n    }\n\n    /// Alice sells 4.90% of the lagged reserve (1% bracket) and Bob sells 0.20% (0% bracket on its\n    /// own) in the same transaction, with one tx.origin. Bob's leg is billed the catch-up on\n    /// Alice's volume: far more than 2% of Bob's own output.\n    function test_laterPrincipalInSharedOriginTransactionIsBilledAboveTheCap() public {\n        uint256 reserve = hook.reserveSnapshot();\n        Bundler bundler = new Bundler(swapRouter, token);\n        vm.prank(alice);\n        token.approve(address(bundler), type(uint256).max);\n        vm.prank(bob);\n        token.approve(address(bundler), type(uint256).max);\n        address[] memory users = new address[](2);\n        users[0] = alice;\n        users[1] = bob;\n        uint256[] memory amounts = new uint256[](2);\n        amounts[0] = reserve * 490 / 10_000;\n        amounts[1] = reserve * 20 / 10_000;\n        vm.prank(bundlerEOA, bundlerEOA);\n        (uint256[] memory ethOut, uint256[] memory fee) = bundler.run(key, users, amounts);\n        uint256 bobGross = ethOut[1] + fee[1];\n        emit log_named_uint(\"alice fee ppm of her gross\", fee[0] * 1_000_000 / (ethOut[0] + fee[0]));\n        emit log_named_uint(\"bob fee ppm of his gross\", fee[1] * 1_000_000 / bobGross);\n        assertLe(fee[1] * 1_000_000, bobGross * hook.MAX_FEE_PPM(), \"bob's leg billed above the 2% hard cap\");\n    }\n}","reproduction":"Pool: ETH/IMDO 3000/60 at price 1 with a full-range position of liquidity 1e22 (lagged reserve 9,999,999,999,999,999,999,457 wei IMDO), one block after launch. A Bundler contract (receives tokens from each user, swaps through PoolSwapTest, forwards ETH) is called by EOA `bundlerEOA` (tx.origin). Leg 1: alice sells exact-in 4.90% of the lagged reserve = 489,999,999,999,999,999,973 IMDO -> gross 465.775 ETH, fee 4.658 ETH (9,999 ppm, correct 1% bracket). Leg 2: bob sells exact-in 0.20% = 19,999,999,999,999,999,998 IMDO -> gross 18.091 ETH, fee charged 5.020 ETH = 277,457 ppm of bob's gross. Expected: bob's leg (0.20% of reserve, or at most 2% under the hard cap) pays <= 0.362 ETH; bob selling the same amount in his own transaction pays 0. Actual: bob loses 5.02 ETH of 18.09 ETH to the treasury. Run `forge test --match-path test/scratch/ProofSharedOrigin.t.sol`: fails with `bob's leg billed above the 2% hard cap: 5019581763196613256000000 > 361827054004762729540000`.","severity":"medium","snippet":"        uint256 due = quote * rate - paid;","title":"Cumulative catch-up billing lets a later seller under a shared tx.origin be charged up to 100% of their ETH output, far above the 2% hard cap"},{"citation":"resolved","description":"For exact-output sells the hook moves `fee` IMDO out of the PoolManager with `poolManager.take(token, address(this), fee)` while the swapper's settlement is still pending. Uniswap v4 settlement is `sync -> transfer -> settle`; the order relative to the swap is the router's choice, and a pay-first router (sync, transfer the maximum input, swap, settle, take the refund) is a legitimate pattern for exact-output trades whose input is not known up front. Because the hook's take lowers the manager's token balance between that router's sync and settle, `settle()` credits `max - fee` while the swap delta already contains `+fee`: the seller is debited the hook fee twice. One copy is burned as the fee, the other sits in the PoolManager with no delta or claim referencing it and is unrecoverable by anyone (the next sync of IMDO absorbs it into reserves). If the router's maximum input is tight (max < tokenIn + 2*fee) the unlock reverts with CurrencyNotSettled instead, so the sell is blocked, contrary to the brief's 'sells are never blocked'. OpenZeppelin's BaseHookFee avoids exactly this by taking the fee as an ERC-6909 claim (`take(..., claims=true)` / mint) inside the callback and withdrawing later; the local adaptation diverges on this point. The ETH (exact-input) path is unaffected because native settlement uses msg.value. Minimal fix: in `_payOrAccrue` always `poolManager.mint(address(this), id, fee)` during the swap (accruedToken += fee) and let harvest() take and burn; do the same for ETH or keep the direct take for ETH only.","line":372,"path":"src/IMDOFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {IMDOToken, IMDOFeeHook} from \"src/IMDOFeeHook.sol\";\n\n/// @dev A router that funds the input before the swap: sync -> transfer(max) -> swap -> settle,\n/// then takes the ETH output and the unused token credit back to the user. This is a valid v4\n/// settlement order (sync must precede the transfer, settle must follow it).\ncontract PayFirstRouter {\n    IPoolManager immutable manager;\n    IMDOToken immutable token;\n\n    constructor(IPoolManager m, IMDOToken t) {\n        manager = m;\n        token = t;\n    }\n\n    receive() external payable {}\n\n    function sellExactOut(PoolKey calldata key, uint256 ethOut, uint256 maxTokenIn) external returns (BalanceDelta) {\n        token.transferFrom(msg.sender, address(this), maxTokenIn);\n        bytes memory r = manager.unlock(abi.encode(msg.sender, key, ethOut, maxTokenIn));\n        return abi.decode(r, (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (address user, PoolKey memory key, uint256 ethOut, uint256 maxTokenIn) =\n            abi.decode(data, (address, PoolKey, uint256, uint256));\n        manager.sync(key.currency1);\n        token.transfer(address(manager), maxTokenIn);\n        BalanceDelta delta = manager.swap(key, SwapParams(false, int256(ethOut), TickMath.MAX_SQRT_PRICE - 1), \"\");\n        uint256 credited = manager.settle();\n        manager.take(key.currency0, user, ethOut);\n        int256 tokenCredit = int256(credited) + int256(delta.amount1());\n        require(tokenCredit >= 0, \"input exceeded max\");\n        if (tokenCredit > 0) manager.take(key.currency1, user, uint256(tokenCredit));\n        return abi.encode(delta);\n    }\n}\n\ncontract ProofPayFirstRouterTest is Test {\n    uint160 constant SQRT_1_1 = 79228162514264337593543950336;\n    PoolManager manager;\n    IMDOToken token;\n    IMDOFeeHook hook;\n    PoolModifyLiquidityTest lpRouter;\n    PayFirstRouter router;\n    PoolKey key;\n    address alice = makeAddr(\"alice\");\n\n    receive() external payable {}\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        token = new IMDOToken();\n        bytes memory initCode =\n            abi.encodePacked(type(IMDOFeeHook).creationCode, abi.encode(address(manager), address(token)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i; i < 500_000; ++i) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & 0x3fff == 0x25d4) {\n                hook = new IMDOFeeHook{salt: bytes32(i)}(address(manager), address(token));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        router = new PayFirstRouter(manager, token);\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 3000, 60, IHooks(address(hook)));\n        manager.initialize(key, SQRT_1_1);\n        vm.deal(address(this), 1e24);\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity{value: 2e22}(key, ModifyLiquidityParams(-887220, 887220, 1e22, 0), \"\");\n        token.transfer(alice, 1e25);\n        vm.prank(alice);\n        token.approve(address(router), type(uint256).max);\n        vm.roll(block.number + 1);\n    }\n\n    /// An exact-output sell of ~5% of the reserve (2% bracket). The hook's `take(token)` runs\n    /// between the router's `sync` and `settle`, so `settle` credits `max - fee` while the swap\n    /// delta already includes `+fee`: the seller pays the hook fee twice, and the second copy is\n    /// stranded in the PoolManager, owned by nobody.\n    function test_payFirstRouterSellerPaysExactlyTheSettledSwapDelta() public {\n        uint256 before = token.balanceOf(alice);\n        vm.prank(alice, alice);\n        BalanceDelta d = router.sellExactOut(key, 480e18, 600e18);\n        uint256 swapIn = uint256(-int256(d.amount1()));\n        uint256 paid = before - token.balanceOf(alice);\n        emit log_named_uint(\"settled token input incl. hook fee\", swapIn);\n        emit log_named_uint(\"tokens the seller actually lost\", paid);\n        assertEq(paid, swapIn, \"seller charged more than the settled swap delta\");\n    }\n}","reproduction":"Same pool fixture (liquidity 1e22 full range, price 1, one block after launch). Router: sync(IMDO), transfer 600e18 IMDO to the manager, swap(zeroForOne=false, amountSpecified=+480e18 ETH out, no price limit), settle(), take 480 ETH to the user, take the leftover token credit back to the user. Settled swap delta for alice: -515,833,213,927,496,776,044 IMDO (505.7 IMDO swap input + 10.114 IMDO hook fee, 2% bracket). Expected: alice's IMDO balance falls by exactly 515,833,213,927,496,776,044 and the fee (10,114,376,743,676,407,373) is burned once. Actual: alice's balance falls by 525,947,590,671,173,183,417, i.e. the hook fee is paid twice; 10,114,376,743,676,407,373 IMDO remain in the PoolManager owned by nobody. With maxTokenIn = 520e18 the same call reverts (router credit short by the fee). Run `forge test --match-path test/scratch/ProofPayFirstRouter.t.sol`: fails with `seller charged more than the settled swap delta: 525947590671173183417 != 515833213927496776044`.","severity":"medium","snippet":"            if (IMDOToken(token).balanceOf(address(poolManager)) >= fee) {\n                poolManager.take(token, address(this), fee);\n                IMDOToken(token).burn(fee);","title":"Exact-output sell fee is taken physically inside afterSwap, so a router that syncs the token before the swap double-charges the seller (second copy stranded in PoolManager) or reverts"},{"citation":"resolved","description":"beforeInitialize accepts the first ETH/IMDO key with any of the three fee tiers, any tick spacing and any initial price, from any initializer, and binds the hook to it forever (`initialized = true`, single pool). The delivered deploy script and README flow deploy the hook in one transaction and leave pool initialization to the launch factory later. In that window an unprivileged account can call PoolManager.initialize with (ETH, IMDO, fee 500, tickSpacing 1, hooks = hook, sqrtPrice = MIN+1). The factory's later initialize of (ETH, IMDO, 3000, 60, hook) reverts (InvalidPool wrapped as a hook call failure) and the hook must be redeployed at a newly mined address; if the squatter instead uses the exact intended key with an extreme price, the factory's initialize reverts with PoolAlreadyInitialized. No funds are lost; the launch is delayed and the attested hook address/salt become invalid. This does not apply if the factory deploys the hook and initializes in the same transaction (the manifest flow). Fix options: pin the expected fee/tickSpacing in the constructor (constants, like the treasury) and compare the full key, or have beforeInitialize accept only the launch factory / token-holder as `sender`, or let the deploy script initialize the pool itself in the same broadcast.","line":207,"path":"src/IMDOFeeHook.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 {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {IMDOToken, IMDOFeeHook} from \"src/IMDOFeeHook.sol\";\n\ncontract ProofPoolSquatTest is Test {\n    uint160 constant SQRT_1_1 = 79228162514264337593543950336;\n    PoolManager manager;\n    IMDOToken token;\n    IMDOFeeHook hook;\n\n    /// script/Deploy.s.sol deploys the token and the hook but not the pool; the factory initializes\n    /// later. In between, anyone can bind the hook to a pool of their choosing, after which the\n    /// intended ETH/IMDO 3000/60 launch pool can never be initialized with this hook.\n    function test_intendedLaunchPoolCanStillBeInitializedAfterAThirdPartyTriesFirst() public {\n        manager = new PoolManager(address(this));\n        token = new IMDOToken();\n        bytes memory initCode =\n            abi.encodePacked(type(IMDOFeeHook).creationCode, abi.encode(address(manager), address(token)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i; i < 500_000; ++i) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & 0x3fff == 0x25d4) {\n                hook = new IMDOFeeHook{salt: bytes32(i)}(address(manager), address(token));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n\n        address attacker = makeAddr(\"attacker\");\n        PoolKey memory squat =\n            PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 500, 1, IHooks(address(hook)));\n        vm.prank(attacker);\n        // Whether or not the squat is refused is up to the fix; what must hold is that the factory's\n        // intended pool can still be initialized afterwards.\n        try manager.initialize(squat, TickMath.MIN_SQRT_PRICE + 1) {} catch {}\n\n        PoolKey memory intended =\n            PoolKey(Currency.wrap(address(0)), Currency.wrap(address(token)), 3000, 60, IHooks(address(hook)));\n        manager.initialize(intended, SQRT_1_1);\n        assertEq(hook.poolId(), keccak256(abi.encode(intended)), \"hook bound to the intended launch pool\");\n    }\n}","reproduction":"1) Deploy IMDOToken and IMDOFeeHook(manager, token) at a mined 0x25d4 address (as script/Deploy.s.sol does), no pool yet. 2) From any EOA: manager.initialize(PoolKey(0x0, token, 500, 1, hook), 4295128740). Result: hook.initialized() == true, hook.poolId() == keccak(that key). 3) Factory: manager.initialize(PoolKey(0x0, token, 3000, 60, hook), 2^96). Expected: the launch pool initializes and the hook binds to it. Actual: revert WrappedError(hook, beforeInitialize selector, InvalidPool 0x2083cd40, HookCallFailed). Run `forge test --match-path test/scratch/ProofPoolSquat.t.sol`: fails at step 3.","severity":"low","snippet":"        if (\n            initialized || key.currency0 != address(0) || key.currency1 != token || key.hooks != address(this)\n                || (key.fee != 500 && key.fee != 3_000 && key.fee != 10_000)\n        ) revert InvalidPool();","title":"Anyone can bind the hook to a pool of their choosing between script/Deploy.s.sol and the factory's initialization, permanently blocking the intended launch pool for that hook instance"},{"citation":"resolved","description":"laggedReserve is 0 until the first hook callback in a block after initialization, and feeRate() returns MAX_FEE_PPM whenever the reserve is 0. The brief's table says a sell below 1% of the reserve pays 0%; during the launch block (the factory's seeding transaction and every public trade in that block) a sell of any size, even dust, pays 2%, and the same happens in the block after any block whose closing inventory was zero. A buyer who buys and sells inside the launch block is taxed on a trade the brackets define as free. The README documents this as a conservative choice, so this is a specification deviation rather than a loss to the protocol; the simplest alignment is to use the live reserve when no lagged snapshot exists yet (the launch block has no earlier state to manipulate) or to treat reserve == 0 as the 0% bracket for sells smaller than the live reserve.","line":276,"path":"src/IMDOFeeHook.sol","reproduction":"Fresh PoolManager, token, hook, pool at price 1 with a 1e22 full-range position seeded in the same block (live reserve ~1e22 IMDO, lagged 0). In the same block alice sells exact-in 1e18 IMDO (0.01% of the live reserve). Expected per the bracket table: fee 0. Actual: ETH out 0.976962596829096139, fee 0.019938012180185635 ETH to the treasury = 199 bps of gross (rate 20,000 ppm). One block later the same sell pays 0.","severity":"low","snippet":"        if (reserve == 0) return MAX_FEE_PPM; // No mature reserve: conservative, never divide by zero.","title":"Every sell in the launch block (or in the first block after the reserve was zero) pays the 2% cap regardless of size, contradicting the 'under 1% pays 0%' bracket"},{"citation":"resolved","description":"liveReserve counts every IMDO the pool holds, including positions entirely below the current price, which hold IMDO only and never trade until the price falls into them. A seller who holds enough IMDO parks it in such a position in block N (no ETH required, no price exposure, no impermanent loss), sells against the inflated laggedReserve in block N+1, and withdraws the position in the same transaction. The only cost is gas and one block of latency (12 s on Sepolia). The brief asked for a one-block lag and the README says manipulation sustained across a block boundary is out of scope, so this is reported as the residual economic exposure rather than a specification breach: the lag defeats same-transaction inflation only, and the 'reserve' definition makes cross-block inflation free for large holders (one needs about 100x the sale size in IMDO to reach the 0% bracket, 33x for 0.5%, 20x for 1%). Mitigations that keep the design: count only in-range (active) liquidity, or compute the snapshot from liquidity inside a tick band around the price, or lag by more than one block / use a minimum of the last k closes.","line":335,"path":"src/IMDOFeeHook.sol","reproduction":"Pool as above, lagged reserve R = 9,999,999,999,999,999,999,457 IMDO. Block N: alice (holding 1e25 IMDO) adds liquidity 1.5e24 in ticks [-887220, -60] through PoolModifyLiquidityTest; this costs 1,495,506,967,566,133,828,511,959 IMDO and 0 ETH. Block N+1: reserveSnapshot() = 1,505,506,967,566,133,828,511,416. alice sells exact-in 5% of the real reserve (499,999,999,999,999,999,972 IMDO): ETH out 474.83 ETH, fee 0 (sold/lagged = 33 bps -> 0% bracket). She then removes the position and gets back 1,495,506,967,566,133,828,511,958 IMDO (1 wei rounding). Expected if sized against the tradeable reserve: 2% bracket, fee ~9.5 ETH. Actual: 0.","severity":"low","snippet":"        rate = feeRate(sold, laggedReserve);","title":"A token-only out-of-range position held across one block boundary inflates the lagged reserve at zero cost and drops a 5% sell to the 0% bracket"},{"citation":"resolved","description":"This is the design the brief specifies (accumulation per tx.origin within one transaction, reserve lagged one block) and it is stated here only so the economic picture is complete: laggedReserve is constant for the whole block, and each new transaction starts with empty transient counters, so a seller who sends N transactions in one block (or one per block) each below ceil(reserve/100) never enters a paying bracket. On Sepolia this costs N gas payments and no latency. The hook therefore raises revenue only from sellers who choose to sell more than 1% of the reserve in a single transaction; it is not an effective brake on large dumps. If that is acceptable, no change is needed; otherwise the accumulator would have to be keyed per block (e.g. per origin per block, in regular storage) which the brief did not ask for.","line":325,"path":"src/IMDOFeeHook.sol","reproduction":"Pool as above (lagged reserve 9,999,999,999,999,999,999,457). alice sends 10 separate transactions in one block, each selling exact-in 98,999,999,999,999,999,994 IMDO (0.99% of the lagged reserve): total sold 989 bps of the lagged reserve, total ETH out 898.36 ETH, total fee 0. A single 9.89% sale in one transaction would pay 2% (about 18 ETH).","severity":"info","snippet":"        bytes32 base = keccak256(abi.encode(\"IMDO.transaction.billing\", tx.origin));","title":"Graduated brackets bind only inside one transaction: separate transactions in the same block each stay under 1% and sell 9.9% of the reserve for no fee"}],"hash":"fe3858bf058246146d6cfe80fda8cfd805aa2476cac2d6e61e4cfd48326f8bc3","nodeId":"4a5862c0-7494-4c04-9713-ee840caeb1a5","outcome":"completed","summary":"The review is complete. The findings file is written and validated, no tracked files were changed (the findings file and test/scratch/ are excluded by the repository's own git exclude list, which is expected).\n\n## Summary\n\nI audited the IMDO token and `IMDOFeeHook` against the Pashov Economic Security, Invariant and Flow Gap guides, running the shipped suite (48 tests pass) and my own scenarios on a real v4 PoolManager. Results are in `.imd-findings.json`: 6 findings, 3 with failing Foundry proofs under `test/scratch/`, and 17 coverage rows (all 12 listed entry points plus 5 invariants).\n\n**Findings**\n\n1. **Medium.** The cumulative catch-up bill in `_bill` is bounded only by a leg's own output, so when two unrelated sellers share one `tx.origin` (ERC-4337 bundler, relayer) the later seller pays the earlier seller's catch-up. Reproduced: a 0.20% sell that pays 0 alone pays 27.7% of its ETH output when preceded by a 4.90% sell in the same transaction, breaking the 2% hard cap per principal. Proof: `ProofSharedOrigin.t.sol`.\n2. **Medium.** Exact-output sells take the token fee physically inside `afterSwap`. A router that syncs the token before the swap (a valid v4 order) settles short by the fee: the seller pays the fee twice, the second copy is stranded in the PoolManager, and with a tight max input the sell reverts. OpenZeppelin's BaseHookFee mints claims instead. Proof: `ProofPayFirstRouter.t.sol`.\n3. **Low.** `beforeInitialize` accepts any initializer, tick spacing and price, so in the window between `script/Deploy.s.sol` and the factory's initialization anyone can bind the hook to a different pool, permanently blocking the intended launch pool for that hook address. Proof: `ProofPoolSquat.t.sol`.\n4. **Low.** With a zero lagged reserve (launch block), every sell pays 2%, including dust, contradicting the \"under 1% pays 0%\" bracket.\n5. **Low.** A token-only out-of-range position held across one block inflates the lagged reserve at zero cost. A 5% sale paid 0 after parking tokens for one block. This is within the brief's stated one-block scope, reported as residual exposure.\n6. **Info.** Brackets bind only per transaction: ten same-block transactions sold 9.89% of the reserve for no fee. By design per the brief.\n\n**Verified as holding:** reserve accounting (no undercount or underflow path), protocol fee exclusion, hook custody equals recorded claims, harvest and unlockCallback guards, factory LP fee accounting and Merkle claims unaffected, and all four token entry points. Each proof was confirmed to fail on the current code and pass against a patched copy with a candidate fix.","treeHash":null,"usage":{"cachedInputTokens":4518124,"inputTokens":962,"model":"claude-fable-5-1","outputTokens":84032,"runtime":"claude","turns":52,"wallClockMs":1072401}}],"verification":[{"checks":[{"durationMs":1429,"exitCode":0,"name":"build","output":"Compiling 3 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.01s\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:434:13\n    │\n434 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:186:34\n    │\n186 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:207:9\n    │\n207 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:218:9\n    │\n218 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:229:32\n    │\n229 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:231:24\n    │\n231 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:242:32\n    │\n242 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:244:24\n    │\n244 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:251:9\n    │\n251 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:263:9\n    │\n263 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:289:16\n    │\n289 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:317:13\n    │\n317 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:298:29\n    │\n298 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:299:29\n    │\n299 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:307:27\n    │\n307 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:308:45\n    │\n308 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:311:57\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:311:65\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:319:16\n    │\n319 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:319:23\n    │\n319 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:355:21\n    │\n355 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:394:9\n    │\n394 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedETH` is updated\n    ╭▸ src/IMDOFeeHook.sol:411:17\n    │\n411 │             try poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount) {\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:413:17\n    │\n413 │                 poolManager.burn(address(this), 0, ethAmount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:37\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:24\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:424:9\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:441:39\n    │\n441 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:442:29\n    │\n442 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":983,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/IMDO.t.sol:IMDOTokenTest\n[PASS] testFuzz_burnReducesSupplyAndOnlyTheCallersBalance(uint256,uint256) (runs: 500, μ: 171875, ~: 172404)\n[PASS] testFuzz_transfersMoveExactlyTheAmount(uint256) (runs: 500, μ: 159934, ~: 160140)\n[PASS] test_allowancesAndTransferFrom() (gas: 339607)\n[PASS] test_metadataAndFixedSupplyMintedToDeployer() (gas: 47999)\n[PASS] test_noAdminCallCanMintOrSeizeEvenFromTheDeployer() (gas: 632322)\n[PASS] test_runtimeCodeHasNoDelegatecallOrSelfdestruct() (gas: 1369268)\n[PASS] test_transferFailurePaths() (gas: 122618)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 38.98ms (66.54ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOEthOnlyPoolTest\n[PASS] test_exactOutputSellWithoutManagerTokensAccruesAClaimThatHarvestBurns() (gas: 817933)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 103.61ms (1.14ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOLaunchBlockTest\n[PASS] test_launchBlockSellsPayTheCapThenTheLagExpires() (gas: 1038756)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 107.35ms (2.03ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOTokenOnlyPoolTest\n[PASS] test_buyOnTokenOnlyPoolThenSellInLaunchBlock() (gas: 688685)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 109.74ms (11.01ms CPU time)\n\nRan 8 tests for test/IMDO.t.sol:IMDODeployScriptTest\n[PASS] testFuzz_runRefusesEveryChainButLocalAndSepolia(uint64) (runs: 64, μ: 35127, ~: 35104)\n[PASS] test_constantsAgreeWithTheHook() (gas: 50930)\n[PASS] test_hookCreationCodeIsTheFactorysInitCode() (gas: 57669)\n[PASS] test_hookRefusesAnAddressWithoutTheFlags() (gas: 8791)\n[PASS] test_mineReproducesTheFactoryDeployment() (gas: 51361093)\n[PASS] test_mineRespectsSaltStartAndAlwaysReturnsFlaggedAddresses() (gas: 5533659)\n[PASS] test_runOnSepoliaWithoutAConfiguredPoolManagerIsRefused() (gas: 40581)\n[PASS] test_runOnTheLocalChainDeploysTheTokenAndTheMinedHookAndAttests() (gas: 31646063)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 190.73ms (210.25ms CPU time)\n\nRan 32 tests for test/IMDO.t.sol:IMDOHookTest\n[PASS] testFuzz_feeRateFollowsTheBracketTable(uint256,uint256) (runs: 2000, μ: 11851, ~: 12551)\n[PASS] testFuzz_noCallChangesBracketsCapOrTreasury(bytes4,bytes,address) (runs: 300, μ: 155058, ~: 130635)\n[PASS] testFuzz_oneTransactionSplitNeverPaysLessThanPerLegBracketsNorMoreThanTheFinalBracket(uint256,uint256,uint256) (runs: 96, μ: 684392, ~: 694327)\n[PASS] testFuzz_sellsAreNeverBlocked(uint256,bool,bool) (runs: 200, μ: 530071, ~: 508861)\n[PASS] test_aLegNeverPaysMoreThanTwoPercentOfItsOwnSizeUnderASharedOrigin() (gas: 1049417)\n[PASS] test_buysAreFreeInBothModes() (gas: 810553)\n[PASS] test_callbacksRefuseCallersOtherThanTheManager() (gas: 307160)\n[PASS] test_curatedAdminCallsAllFail() (gas: 323982)\n[PASS] test_dustCrossingLegAsTheLastLegLeavesABoundedRemainderThatDoesNotCarryAcrossTransactions() (gas: 1397698)\n[PASS] test_dustCrossingLegIsBoundedToItsOwnOutputAndTheNextLegPaysTheRest() (gas: 670104)\n[PASS] test_exactInputSellsPayExactlyTheBracketFee() (gas: 2614867)\n[PASS] test_exactOutputDustCrossingLegIsBoundedToItsOwnInputAndTheNextLegPaysTheRest() (gas: 877666)\n[PASS] test_exactOutputSellsPayExactlyTheBracketFeeInBurnedTokens() (gas: 2348461)\n[PASS] test_factoryPoolFeeAccountingAndDistributionUnaffectedByTheHook() (gas: 5304435)\n[PASS] test_gasBurningTreasuryCannotBlockSells() (gas: 590066)\n[PASS] test_harvestBurnsTheTokenClaimEvenWhileTheTreasuryRejectsEth() (gas: 1546995)\n[PASS] test_harvestWithNothingAccruedIsANoOp() (gas: 55951)\n[PASS] test_launchBindsOnePoolAndTracksSeededReserve() (gas: 126536)\n[PASS] test_mixedModeSplitCatchUpUnderTheCapIsConvertedExactly() (gas: 680060)\n[PASS] test_mixedModeSplitIsBilledCumulativelyWithinTheLegCap() (gas: 743482)\n[PASS] test_permissionsMatchAddressBits() (gas: 13770)\n[PASS] test_protocolFeesAreExcludedFromTheReserve() (gas: 757707)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6074563)\n[PASS] test_sameBlockEarlierTransactionsDoNotChangeTheSnapshot() (gas: 1196992)\n[PASS] test_sameTransactionDonationDoesNotLowerTheBracket() (gas: 512270)\n[PASS] test_sameTransactionLiquidityInflationDoesNotLowerTheBracket() (gas: 985888)\n[PASS] test_secondPoolWithThisHookIsRejected() (gas: 93451)\n[PASS] test_splitExactInputSellsAreBilledCumulatively() (gas: 1115783)\n[PASS] test_swarmMerkleClaimsAreUntaxedAndUnaffected() (gas: 5230791)\n[PASS] test_threeLegSplitCrossesTwoBrackets() (gas: 687115)\n[PASS] test_treasuryReceivesExactlyTheFeeAndTheSellerTheRest() (gas: 499773)\n[PASS] test_treasuryRejectingEthFallsBackToClaimsAndNeverBlocksTheSell() (gas: 761616)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 232.92ms (343.12ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOInvariantTest\n[PASS]\nIMDOInvariantTest invariants:\n[PASS] invariant_configurationIsImmutable\n[PASS] invariant_everyFeeChargedIsAtTheTreasuryBurnedOrClaimable\n[PASS] invariant_factoryPositionIsIntact\n[PASS] invariant_hookNeverHoldsUserFundsBeyondAccruedClaims\n[PASS] invariant_reserveReconcilesWithManagerInventory\n IMDOInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭-------------+---------------------+-------+---------+----------╮\n| Contract    | Selector            | Calls | Reverts | Discards |\n+================================================================+\n| IMDOHandler | addLiquidity        | 138   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | burn                | 146   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactIn          | 133   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactOut         | 110   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | collectProtocolFees | 149   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | donate              | 172   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | harvest             | 124   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | removeLiquidity     | 143   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | roll                | 125   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactIn         | 121   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactOut        | 130   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | setProtocolFee      | 134   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | splitSell           | 143   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | toggleTreasury      | 152   | 0       | 0        |\n╰-------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 875.84ms (754.39ms CPU time)\n\nRan 7 test suites in 889.84ms (1.66s CPU time): 51 tests passed, 0 failed, 0 skipped (51 total tests)\n","passed":true},{"durationMs":49,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":217,\"script/Deploy.s.sol\":137,\"src/IMDOFeeHook.sol\":481,\"test/IMDO.t.sol\":2116},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"05f5e32e299b6280db783fe130fe80f7220bfff5331a2544ed24ebc069f9c024","verifiedTreeHash":"a5816a540ab798e3955558dc631b2ee92401479e","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":1409,"exitCode":0,"name":"build","output":"Compiling 3 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.05s\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:416:13\n    │\n416 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:183:34\n    │\n183 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:204:9\n    │\n204 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:214:9\n    │\n214 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:225:32\n    │\n225 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:227:24\n    │\n227 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:238:32\n    │\n238 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:240:24\n    │\n240 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:247:9\n    │\n247 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:259:9\n    │\n259 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:285:16\n    │\n285 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:372:9\n    │\n372 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:313:13\n    │\n313 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:294:29\n    │\n294 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:295:29\n    │\n295 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:303:27\n    │\n303 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:304:45\n    │\n304 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:307:57\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:307:65\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:315:16\n    │\n315 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:315:23\n    │\n315 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:340:21\n    │\n340 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:379:9\n    │\n379 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:395:13\n    │\n395 │             poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:396:13\n    │\n396 │             poolManager.burn(address(this), 0, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:406:24\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:406:37\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:406:9\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:423:39\n    │\n423 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:424:29\n    │\n424 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":1000,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/IMDO.t.sol:IMDOTokenTest\n[PASS] testFuzz_burnReducesSupplyAndOnlyTheCallersBalance(uint256,uint256) (runs: 500, μ: 172046, ~: 172404)\n[PASS] testFuzz_transfersMoveExactlyTheAmount(uint256) (runs: 500, μ: 159650, ~: 160092)\n[PASS] test_allowancesAndTransferFrom() (gas: 339607)\n[PASS] test_metadataAndFixedSupplyMintedToDeployer() (gas: 47999)\n[PASS] test_noAdminCallCanMintOrSeizeEvenFromTheDeployer() (gas: 632322)\n[PASS] test_runtimeCodeHasNoDelegatecallOrSelfdestruct() (gas: 1369268)\n[PASS] test_transferFailurePaths() (gas: 122618)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 40.54ms (66.13ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOLaunchBlockTest\n[PASS] test_launchBlockSellsPayTheCapThenTheLagExpires() (gas: 1037706)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 54.34ms (1.92ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOEthOnlyPoolTest\n[PASS] test_exactOutputSellWithoutManagerTokensAccruesAClaimThatHarvestBurns() (gas: 818464)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 55.27ms (1.28ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOTokenOnlyPoolTest\n[PASS] test_buyOnTokenOnlyPoolThenSellInLaunchBlock() (gas: 688104)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 62.09ms (2.44ms CPU time)\n\nRan 5 tests for test/IMDO.t.sol:IMDODeployScriptTest\n[PASS] test_constantsAgreeWithTheHook() (gas: 33834)\n[PASS] test_hookRefusesAnAddressWithoutTheFlags() (gas: 8813)\n[PASS] test_mineReproducesTheFactoryDeployment() (gas: 9940020)\n[PASS] test_mineRespectsSaltStartAndAlwaysReturnsFlaggedAddresses() (gas: 5313744)\n[PASS] test_runRefusesTheWrongChain() (gas: 36230)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 86.44ms (54.58ms CPU time)\n\nRan 25 tests for test/IMDO.t.sol:IMDOHookTest\n[PASS] testFuzz_feeRateFollowsTheBracketTable(uint256,uint256) (runs: 2000, μ: 11972, ~: 12551)\n[PASS] testFuzz_noCallChangesBracketsCapOrTreasury(bytes4,bytes,address) (runs: 300, μ: 179161, ~: 130483)\n[PASS] testFuzz_sellsAreNeverBlocked(uint256,bool,bool) (runs: 200, μ: 523521, ~: 508356)\n[PASS] test_buysAreFreeInBothModes() (gas: 810462)\n[PASS] test_callbacksRefuseCallersOtherThanTheManager() (gas: 307119)\n[PASS] test_curatedAdminCallsAllFail() (gas: 323982)\n[PASS] test_exactInputSellsPayExactlyTheBracketFee() (gas: 2611774)\n[PASS] test_exactOutputSellsPayExactlyTheBracketFeeInBurnedTokens() (gas: 1627851)\n[PASS] test_factoryPoolFeeAccountingAndDistributionUnaffectedByTheHook() (gas: 4715255)\n[PASS] test_gasBurningTreasuryCannotBlockSells() (gas: 589617)\n[PASS] test_harvestWithNothingAccruedIsANoOp() (gas: 55839)\n[PASS] test_launchBindsOnePoolAndTracksSeededReserve() (gas: 126511)\n[PASS] test_mixedModeSplitIsBilledCumulatively() (gas: 608981)\n[PASS] test_permissionsMatchAddressBits() (gas: 13858)\n[PASS] test_protocolFeesAreExcludedFromTheReserve() (gas: 757192)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6104964)\n[PASS] test_sameBlockEarlierTransactionsDoNotChangeTheSnapshot() (gas: 1196478)\n[PASS] test_sameTransactionDonationDoesNotLowerTheBracket() (gas: 511821)\n[PASS] test_sameTransactionLiquidityInflationDoesNotLowerTheBracket() (gas: 984926)\n[PASS] test_secondPoolWithThisHookIsRejected() (gas: 93438)\n[PASS] test_splitExactInputSellsAreBilledCumulatively() (gas: 1113836)\n[PASS] test_swarmMerkleClaimsAreUntaxedAndUnaffected() (gas: 4638528)\n[PASS] test_threeLegSplitCrossesTwoBrackets() (gas: 612394)\n[PASS] test_treasuryReceivesExactlyTheFeeAndTheSellerTheRest() (gas: 499279)\n[PASS] test_treasuryRejectingEthFallsBackToClaimsAndNeverBlocksTheSell() (gas: 744720)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 121.10ms (251.46ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOInvariantTest\n[PASS]\nIMDOInvariantTest invariants:\n[PASS] invariant_configurationIsImmutable\n[PASS] invariant_everyFeeChargedIsAtTheTreasuryBurnedOrClaimable\n[PASS] invariant_factoryPositionIsIntact\n[PASS] invariant_hookNeverHoldsUserFundsBeyondAccruedClaims\n[PASS] invariant_reserveReconcilesWithManagerInventory\n IMDOInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭-------------+---------------------+-------+---------+----------╮\n| Contract    | Selector            | Calls | Reverts | Discards |\n+================================================================+\n| IMDOHandler | addLiquidity        | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | burn                | 154   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactIn          | 153   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactOut         | 150   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | collectProtocolFees | 156   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | donate              | 147   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | harvest             | 120   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | removeLiquidity     | 159   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | roll                | 175   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactIn         | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactOut        | 131   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | setProtocolFee      | 148   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | toggleTreasury      | 139   | 0       | 0        |\n╰-------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 910.26ms (846.72ms CPU time)\n\nRan 7 test suites in 921.35ms (1.33s CPU time): 41 tests passed, 0 failed, 0 skipped (41 total tests)\n","passed":true},{"durationMs":32,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":200,\"script/Deploy.s.sol\":188,\"src/IMDOFeeHook.sol\":463,\"test/IMDO.t.sol\":1626},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2769ae20a41b791928cd811acda5b47aac673be737309e99b5ef6f11911db9d2","verifiedTreeHash":"e30a7c78f44912c7f5aa4c939e2a433d2477c39f","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":701,"exitCode":0,"name":"build","output":"Compiling 3 files with Solc 0.8.26\nSolc 0.8.26 finished in 503.71ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:434:13\n    │\n434 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:186:34\n    │\n186 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:207:9\n    │\n207 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:218:9\n    │\n218 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:229:32\n    │\n229 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:231:24\n    │\n231 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:242:32\n    │\n242 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:244:24\n    │\n244 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:251:9\n    │\n251 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:263:9\n    │\n263 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:289:16\n    │\n289 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:317:13\n    │\n317 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:298:29\n    │\n298 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:299:29\n    │\n299 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:307:27\n    │\n307 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:308:45\n    │\n308 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:311:57\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:311:65\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:319:16\n    │\n319 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:319:23\n    │\n319 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:355:21\n    │\n355 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:394:9\n    │\n394 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedETH` is updated\n    ╭▸ src/IMDOFeeHook.sol:411:17\n    │\n411 │             try poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount) {\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:413:17\n    │\n413 │                 poolManager.burn(address(this), 0, ethAmount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:37\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:24\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:424:9\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:441:39\n    │\n441 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:442:29\n    │\n442 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":711,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/IMDO.t.sol:IMDOTokenTest\n[PASS] testFuzz_burnReducesSupplyAndOnlyTheCallersBalance(uint256,uint256) (runs: 500, μ: 171854, ~: 172356)\n[PASS] testFuzz_transfersMoveExactlyTheAmount(uint256) (runs: 500, μ: 160245, ~: 160140)\n[PASS] test_allowancesAndTransferFrom() (gas: 339607)\n[PASS] test_metadataAndFixedSupplyMintedToDeployer() (gas: 47999)\n[PASS] test_noAdminCallCanMintOrSeizeEvenFromTheDeployer() (gas: 632322)\n[PASS] test_runtimeCodeHasNoDelegatecallOrSelfdestruct() (gas: 1369268)\n[PASS] test_transferFailurePaths() (gas: 122618)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 14.26ms (33.25ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOEthOnlyPoolTest\n[PASS] test_exactOutputSellWithoutManagerTokensAccruesAClaimThatHarvestBurns() (gas: 817933)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 65.45ms (863.41µs CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOLaunchBlockTest\n[PASS] test_launchBlockSellsPayTheCapThenTheLagExpires() (gas: 1038756)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 66.28ms (1.58ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOTokenOnlyPoolTest\n[PASS] test_buyOnTokenOnlyPoolThenSellInLaunchBlock() (gas: 688685)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 75.73ms (704.53µs CPU time)\n\nRan 32 tests for test/IMDO.t.sol:IMDOHookTest\n[PASS] testFuzz_feeRateFollowsTheBracketTable(uint256,uint256) (runs: 2000, μ: 11941, ~: 12551)\n[PASS] testFuzz_noCallChangesBracketsCapOrTreasury(bytes4,bytes,address) (runs: 300, μ: 167071, ~: 130500)\n[PASS] testFuzz_oneTransactionSplitNeverPaysLessThanPerLegBracketsNorMoreThanTheFinalBracket(uint256,uint256,uint256) (runs: 96, μ: 698490, ~: 711860)\n[PASS] testFuzz_sellsAreNeverBlocked(uint256,bool,bool) (runs: 200, μ: 535667, ~: 548558)\n[PASS] test_aLegNeverPaysMoreThanTwoPercentOfItsOwnSizeUnderASharedOrigin() (gas: 1049417)\n[PASS] test_buysAreFreeInBothModes() (gas: 810553)\n[PASS] test_callbacksRefuseCallersOtherThanTheManager() (gas: 307160)\n[PASS] test_curatedAdminCallsAllFail() (gas: 323982)\n[PASS] test_dustCrossingLegAsTheLastLegLeavesABoundedRemainderThatDoesNotCarryAcrossTransactions() (gas: 1397698)\n[PASS] test_dustCrossingLegIsBoundedToItsOwnOutputAndTheNextLegPaysTheRest() (gas: 670104)\n[PASS] test_exactInputSellsPayExactlyTheBracketFee() (gas: 2614867)\n[PASS] test_exactOutputDustCrossingLegIsBoundedToItsOwnInputAndTheNextLegPaysTheRest() (gas: 877666)\n[PASS] test_exactOutputSellsPayExactlyTheBracketFeeInBurnedTokens() (gas: 2348461)\n[PASS] test_factoryPoolFeeAccountingAndDistributionUnaffectedByTheHook() (gas: 5304435)\n[PASS] test_gasBurningTreasuryCannotBlockSells() (gas: 590066)\n[PASS] test_harvestBurnsTheTokenClaimEvenWhileTheTreasuryRejectsEth() (gas: 1546995)\n[PASS] test_harvestWithNothingAccruedIsANoOp() (gas: 55951)\n[PASS] test_launchBindsOnePoolAndTracksSeededReserve() (gas: 126536)\n[PASS] test_mixedModeSplitCatchUpUnderTheCapIsConvertedExactly() (gas: 680060)\n[PASS] test_mixedModeSplitIsBilledCumulativelyWithinTheLegCap() (gas: 743482)\n[PASS] test_permissionsMatchAddressBits() (gas: 13770)\n[PASS] test_protocolFeesAreExcludedFromTheReserve() (gas: 757707)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6074563)\n[PASS] test_sameBlockEarlierTransactionsDoNotChangeTheSnapshot() (gas: 1196992)\n[PASS] test_sameTransactionDonationDoesNotLowerTheBracket() (gas: 512270)\n[PASS] test_sameTransactionLiquidityInflationDoesNotLowerTheBracket() (gas: 985888)\n[PASS] test_secondPoolWithThisHookIsRejected() (gas: 93451)\n[PASS] test_splitExactInputSellsAreBilledCumulatively() (gas: 1115783)\n[PASS] test_swarmMerkleClaimsAreUntaxedAndUnaffected() (gas: 5230791)\n[PASS] test_threeLegSplitCrossesTwoBrackets() (gas: 687115)\n[PASS] test_treasuryReceivesExactlyTheFeeAndTheSellerTheRest() (gas: 499773)\n[PASS] test_treasuryRejectingEthFallsBackToClaimsAndNeverBlocksTheSell() (gas: 761616)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 156.50ms (255.54ms CPU time)\n\nRan 8 tests for test/IMDO.t.sol:IMDODeployScriptTest\n[PASS] testFuzz_runRefusesEveryChainButLocalAndSepolia(uint64) (runs: 64, μ: 35115, ~: 35104)\n[PASS] test_constantsAgreeWithTheHook() (gas: 50930)\n[PASS] test_hookCreationCodeIsTheFactorysInitCode() (gas: 57669)\n[PASS] test_hookRefusesAnAddressWithoutTheFlags() (gas: 8791)\n[PASS] test_mineReproducesTheFactoryDeployment() (gas: 51361093)\n[PASS] test_mineRespectsSaltStartAndAlwaysReturnsFlaggedAddresses() (gas: 5533659)\n[PASS] test_runOnSepoliaWithoutAConfiguredPoolManagerIsRefused() (gas: 40581)\n[PASS] test_runOnTheLocalChainDeploysTheTokenAndTheMinedHookAndAttests() (gas: 31646063)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 156.53ms (149.97ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOInvariantTest\n[PASS]\nIMDOInvariantTest invariants:\n[PASS] invariant_configurationIsImmutable\n[PASS] invariant_everyFeeChargedIsAtTheTreasuryBurnedOrClaimable\n[PASS] invariant_factoryPositionIsIntact\n[PASS] invariant_hookNeverHoldsUserFundsBeyondAccruedClaims\n[PASS] invariant_reserveReconcilesWithManagerInventory\n IMDOInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭-------------+---------------------+-------+---------+----------╮\n| Contract    | Selector            | Calls | Reverts | Discards |\n+================================================================+\n| IMDOHandler | addLiquidity        | 125   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | burn                | 137   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactIn          | 124   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactOut         | 148   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | collectProtocolFees | 136   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | donate              | 136   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | harvest             | 134   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | removeLiquidity     | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | roll                | 145   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactIn         | 154   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactOut        | 133   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | setProtocolFee      | 123   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | splitSell           | 142   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | toggleTreasury      | 139   | 0       | 0        |\n╰-------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 651.67ms (588.31ms CPU time)\n\nRan 7 test suites in 653.56ms (1.19s CPU time): 51 tests passed, 0 failed, 0 skipped (51 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":217,\"launch.json\":29,\"script/Deploy.s.sol\":137,\"src/IMDOFeeHook.sol\":481,\"test/IMDO.t.sol\":2116},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4eca8c13b5fcdc763fd547ee3c875434e035accd7d22ff5ad2e8415ac856f600","verifiedTreeHash":"91d504f95e3cc0d008993df35380b9466866b1a9","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":700,"exitCode":0,"name":"build","output":"Compiling 3 files with Solc 0.8.26\nSolc 0.8.26 finished in 512.39ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:424:13\n    │\n424 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:183:34\n    │\n183 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:204:9\n    │\n204 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:214:9\n    │\n214 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:225:32\n    │\n225 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:227:24\n    │\n227 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:238:32\n    │\n238 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:240:24\n    │\n240 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:247:9\n    │\n247 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:259:9\n    │\n259 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:285:16\n    │\n285 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:380:9\n    │\n380 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:313:13\n    │\n313 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:294:29\n    │\n294 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:295:29\n    │\n295 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:303:27\n    │\n303 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:304:45\n    │\n304 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:307:57\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:307:65\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:315:16\n    │\n315 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:315:23\n    │\n315 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:347:21\n    │\n347 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:403:13\n    │\n403 │             poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:404:13\n    │\n404 │             poolManager.burn(address(this), 0, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:37\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:24\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:414:9\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:431:39\n    │\n431 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:432:29\n    │\n432 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":697,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/IMDO.t.sol:IMDOTokenTest\n[PASS] testFuzz_burnReducesSupplyAndOnlyTheCallersBalance(uint256,uint256) (runs: 500, μ: 171566, ~: 172368)\n[PASS] testFuzz_transfersMoveExactlyTheAmount(uint256) (runs: 500, μ: 159999, ~: 160116)\n[PASS] test_allowancesAndTransferFrom() (gas: 339607)\n[PASS] test_metadataAndFixedSupplyMintedToDeployer() (gas: 47999)\n[PASS] test_noAdminCallCanMintOrSeizeEvenFromTheDeployer() (gas: 632322)\n[PASS] test_runtimeCodeHasNoDelegatecallOrSelfdestruct() (gas: 1369268)\n[PASS] test_transferFailurePaths() (gas: 122618)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 15.52ms (36.94ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOTokenOnlyPoolTest\n[PASS] test_buyOnTokenOnlyPoolThenSellInLaunchBlock() (gas: 688130)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.10ms (738.11µs CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOLaunchBlockTest\n[PASS] test_launchBlockSellsPayTheCapThenTheLagExpires() (gas: 1037758)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.46ms (1.17ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOEthOnlyPoolTest\n[PASS] test_exactOutputSellWithoutManagerTokensAccruesAClaimThatHarvestBurns() (gas: 818516)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 25.86ms (697.83µs CPU time)\n\nRan 29 tests for test/IMDO.t.sol:IMDOHookTest\n[PASS] testFuzz_feeRateFollowsTheBracketTable(uint256,uint256) (runs: 2000, μ: 11971, ~: 12573)\n[PASS] testFuzz_noCallChangesBracketsCapOrTreasury(bytes4,bytes,address) (runs: 300, μ: 155011, ~: 130378)\n[PASS] testFuzz_oneTransactionSplitNeverPaysLessThanPerLegBracketsNorMoreThanTheFinalBracket(uint256,uint256,uint256) (runs: 96, μ: 649804, ~: 659778)\n[PASS] testFuzz_sellsAreNeverBlocked(uint256,bool,bool) (runs: 200, μ: 523952, ~: 508382)\n[PASS] test_buysAreFreeInBothModes() (gas: 810507)\n[PASS] test_callbacksRefuseCallersOtherThanTheManager() (gas: 307119)\n[PASS] test_curatedAdminCallsAllFail() (gas: 323982)\n[PASS] test_dustCrossingLegAsTheLastLegLeavesABoundedRemainderThatDoesNotCarryAcrossTransactions() (gas: 1394239)\n[PASS] test_dustCrossingLegIsBoundedToItsOwnOutputAndTheNextLegPaysTheRest() (gas: 666920)\n[PASS] test_exactInputSellsPayExactlyTheBracketFee() (gas: 2611956)\n[PASS] test_exactOutputDustCrossingLegIsBoundedToItsOwnInputAndTheNextLegPaysTheRest() (gas: 697266)\n[PASS] test_exactOutputSellsPayExactlyTheBracketFeeInBurnedTokens() (gas: 1627933)\n[PASS] test_factoryPoolFeeAccountingAndDistributionUnaffectedByTheHook() (gas: 4715411)\n[PASS] test_gasBurningTreasuryCannotBlockSells() (gas: 589643)\n[PASS] test_harvestWithNothingAccruedIsANoOp() (gas: 55839)\n[PASS] test_launchBindsOnePoolAndTracksSeededReserve() (gas: 126489)\n[PASS] test_mixedModeSplitIsBilledCumulatively() (gas: 609011)\n[PASS] test_permissionsMatchAddressBits() (gas: 13858)\n[PASS] test_protocolFeesAreExcludedFromTheReserve() (gas: 757241)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6123287)\n[PASS] test_sameBlockEarlierTransactionsDoNotChangeTheSnapshot() (gas: 1196526)\n[PASS] test_sameTransactionDonationDoesNotLowerTheBracket() (gas: 511782)\n[PASS] test_sameTransactionLiquidityInflationDoesNotLowerTheBracket() (gas: 984978)\n[PASS] test_secondPoolWithThisHookIsRejected() (gas: 93416)\n[PASS] test_splitExactInputSellsAreBilledCumulatively() (gas: 1113852)\n[PASS] test_swarmMerkleClaimsAreUntaxedAndUnaffected() (gas: 4638688)\n[PASS] test_threeLegSplitCrossesTwoBrackets() (gas: 612472)\n[PASS] test_treasuryReceivesExactlyTheFeeAndTheSellerTheRest() (gas: 499372)\n[PASS] test_treasuryRejectingEthFallsBackToClaimsAndNeverBlocksTheSell() (gas: 744768)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 121.07ms (262.75ms CPU time)\n\nRan 8 tests for test/IMDO.t.sol:IMDODeployScriptTest\n[PASS] testFuzz_runRefusesEveryChainButLocalAndSepolia(uint64) (runs: 64, μ: 35171, ~: 35148)\n[PASS] test_constantsAgreeWithTheHook() (gas: 50883)\n[PASS] test_hookCreationCodeIsTheFactorysInitCode() (gas: 58725)\n[PASS] test_hookRefusesAnAddressWithoutTheFlags() (gas: 8791)\n[PASS] test_mineReproducesTheFactoryDeployment() (gas: 9963671)\n[PASS] test_mineRespectsSaltStartAndAlwaysReturnsFlaggedAddresses() (gas: 25750131)\n[PASS] test_runOnSepoliaWithoutAConfiguredPoolManagerIsRefused() (gas: 40602)\n[PASS] test_runOnTheLocalChainDeploysTheTokenAndTheMinedHookAndAttests() (gas: 47420881)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 124.31ms (162.12ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOInvariantTest\n[PASS]\nIMDOInvariantTest invariants:\n[PASS] invariant_configurationIsImmutable\n[PASS] invariant_everyFeeChargedIsAtTheTreasuryBurnedOrClaimable\n[PASS] invariant_factoryPositionIsIntact\n[PASS] invariant_hookNeverHoldsUserFundsBeyondAccruedClaims\n[PASS] invariant_reserveReconcilesWithManagerInventory\n IMDOInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭-------------+---------------------+-------+---------+----------╮\n| Contract    | Selector            | Calls | Reverts | Discards |\n+================================================================+\n| IMDOHandler | addLiquidity        | 158   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | burn                | 143   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactIn          | 142   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactOut         | 126   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | collectProtocolFees | 138   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | donate              | 126   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | harvest             | 155   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | removeLiquidity     | 131   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | roll                | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactIn         | 118   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactOut        | 150   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | setProtocolFee      | 128   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | splitSell           | 127   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | toggleTreasury      | 134   | 0       | 0        |\n╰-------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 618.98ms (596.03ms CPU time)\n\nRan 7 test suites in 621.59ms (946.30ms CPU time): 48 tests passed, 0 failed, 0 skipped (48 total tests)\n","passed":true},{"durationMs":37,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":208,\"script/Deploy.s.sol\":131,\"src/IMDOFeeHook.sol\":471,\"test/IMDO.t.sol\":1919},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"629cb62b80687fd1db9e7ce21a4d11a72975e0dfd12fd0d5d237c7dbd2258cca","verifiedTreeHash":"efd1580bcb4ecc49f8f4494cc89aaf05e802ab5e","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":197,"exitCode":0,"name":"build","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 95.61ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:434:13\n    │\n434 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:186:34\n    │\n186 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:207:9\n    │\n207 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:218:9\n    │\n218 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:229:32\n    │\n229 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:231:24\n    │\n231 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:242:32\n    │\n242 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:244:24\n    │\n244 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:251:9\n    │\n251 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:263:9\n    │\n263 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:289:16\n    │\n289 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:317:13\n    │\n317 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:298:29\n    │\n298 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:299:29\n    │\n299 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:307:27\n    │\n307 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:308:45\n    │\n308 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:311:57\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:311:65\n    │\n311 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:319:16\n    │\n319 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:319:23\n    │\n319 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:355:21\n    │\n355 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:394:9\n    │\n394 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedETH` is updated\n    ╭▸ src/IMDOFeeHook.sol:411:17\n    │\n411 │             try poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount) {\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:413:17\n    │\n413 │                 poolManager.burn(address(this), 0, ethAmount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:37\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:424:24\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:424:9\n    │\n424 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:441:39\n    │\n441 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:442:29\n    │\n442 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":51,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\nWarning: No tests found in project! Forge looks for functions that start with `test`\n","passed":true},{"durationMs":32,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":217,\"script/Deploy.s.sol\":137,\"src/IMDOFeeHook.sol\":481},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":417,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":350,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/IMDOFeeHook.sol:471: Incorrect use of caret operator\n[high] reentrancy-state-change at src/IMDOFeeHook.sol:411: Reentrancy: State change after external call (2 places)\n[high] unsafe-casting at src/IMDOFeeHook.sol:231: Unsafe Casting of integers (2 places)\n[low] large-numeric-literal at src/IMDOFeeHook.sol:9: Large Numeric Literal (10 places)\n[low] literal-instead-of-constant at src/IMDOFeeHook.sol:191: Literal Instead of Constant (7 places)\n[low] non-reentrant-not-first at src/IMDOFeeHook.sol:142: `nonReentrant` is Not the First Modifier (6 places)\n[low] state-change-without-event at src/IMDOFeeHook.sol:222: State Change Without Event (3 places)\n[low] unchecked-return at src/IMDOFeeHook.sol:394: Unchecked Return\n[low] unused-public-function at src/IMDOFeeHook.sol:274: Public Function Not Used Internally","passed":true},{"durationMs":138,"exitCode":0,"name":"proof 3d7317556ab7","output":"Owed nothing: on the tree this revision started from the proof did not compile or run, so it never reproduced the finding.\n\nError: Compiler run failed:\nError (6275): Source \"forge-std/Test.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"forge-std/Test.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:4:1:\n  |\n4 | import {Test} from \"forge-std/Test.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/PoolManager.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/PoolManager.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:5:1:\n  |\n5 | import {PoolManager} from \"v4-core/src/PoolManager.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/interfaces/IPoolManager.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/interfaces/IPoolManager.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:6:1:\n  |\n6 | import {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/interfaces/IHooks.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/interfaces/IHooks.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:7:1:\n  |\n7 | import {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/types/PoolKey.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/types/PoolKey.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:8:1:\n  |\n8 | import {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/types/Currency.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/types/Currency.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:9:1:\n  |\n9 | import {Currency} from \"v4-core/src/types/Currency.sol\";\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/types/PoolOperation.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/types/PoolOperation.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n  --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:10:1:\n   |\n10 | import {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\n   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/types/BalanceDelta.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/types/BalanceDelta.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n  --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:11:1:\n   |\n11 | import {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\n   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/libraries/TickMath.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/libraries/TickMath.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n  --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:12:1:\n   |\n12 | import {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\n   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nError (6275): Source \"v4-core/src/test/PoolModifyLiquidityTest.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\nParserError: Source \"v4-core/src/test/PoolModifyLiquidityTest.sol\" not found: File not found. Searched the following locations: \"/tmp/imd-verify-ccamU9/repo\".\n  --> test/imd-proof-9df86465/Proof_3d7317556ab7.t.sol:13:1:\n   |\n13 | import {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\n   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b85c138a3a41a28f2e45a6ee7b24e1be0c123dd3fa2a69e42df049d3c08ce235","verifiedTreeHash":"e1eb5b8f0a89cf4dd6aea49dbe907cdf83af6706","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":119,"exitCode":0,"name":"build","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 63.76ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:424:13\n    │\n424 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:183:34\n    │\n183 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:204:9\n    │\n204 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:214:9\n    │\n214 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:225:32\n    │\n225 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:227:24\n    │\n227 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:238:32\n    │\n238 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:240:24\n    │\n240 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:247:9\n    │\n247 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:259:9\n    │\n259 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:285:16\n    │\n285 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:380:9\n    │\n380 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:313:13\n    │\n313 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:294:29\n    │\n294 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:295:29\n    │\n295 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:303:27\n    │\n303 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:304:45\n    │\n304 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:307:57\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:307:65\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:315:16\n    │\n315 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:315:23\n    │\n315 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:347:21\n    │\n347 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:403:13\n    │\n403 │             poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:404:13\n    │\n404 │             poolManager.burn(address(this), 0, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:24\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:37\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:414:9\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:431:39\n    │\n431 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:432:29\n    │\n432 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":36,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\nWarning: No tests found in project! Forge looks for functions that start with `test`\n","passed":true},{"durationMs":17,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":208,\"script/Deploy.s.sol\":131,\"src/IMDOFeeHook.sol\":471},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":162,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":236,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/IMDOFeeHook.sol:461: Incorrect use of caret operator\n[high] reentrancy-state-change at src/IMDOFeeHook.sol:403: Reentrancy: State change after external call (3 places)\n[high] unsafe-casting at src/IMDOFeeHook.sol:227: Unsafe Casting of integers (2 places)\n[low] large-numeric-literal at src/IMDOFeeHook.sol:9: Large Numeric Literal (11 places)\n[low] literal-instead-of-constant at src/IMDOFeeHook.sol:188: Literal Instead of Constant (10 places)\n[low] non-reentrant-not-first at src/IMDOFeeHook.sol:142: `nonReentrant` is Not the First Modifier (6 places)\n[low] state-change-without-event at src/IMDOFeeHook.sol:218: State Change Without Event (3 places)\n[low] unchecked-return at src/IMDOFeeHook.sol:387: Unchecked Return\n[low] unused-public-function at src/IMDOFeeHook.sol:270: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d65c5fb1f5690556cc271187e00f1b7c82de74031f71d7139a052fae6b01cf34","verifiedTreeHash":"b0488633d6726b0c50139a29d9c132da70a30b2a","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":648,"exitCode":0,"name":"build","output":"Compiling 3 files with Solc 0.8.26\nSolc 0.8.26 finished in 489.34ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:424:13\n    │\n424 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:183:34\n    │\n183 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:204:9\n    │\n204 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:214:9\n    │\n214 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:225:32\n    │\n225 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:227:24\n    │\n227 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:238:32\n    │\n238 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:240:24\n    │\n240 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:247:9\n    │\n247 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:259:9\n    │\n259 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:285:16\n    │\n285 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:380:9\n    │\n380 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:313:13\n    │\n313 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:294:29\n    │\n294 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:295:29\n    │\n295 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:303:27\n    │\n303 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:304:45\n    │\n304 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:307:57\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:307:65\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:315:16\n    │\n315 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:315:23\n    │\n315 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:347:21\n    │\n347 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:387:9\n    │\n387 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:403:13\n    │\n403 │             poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:404:13\n    │\n404 │             poolManager.burn(address(this), 0, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:37\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:414:24\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:414:9\n    │\n414 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:431:39\n    │\n431 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:432:29\n    │\n432 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":620,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/IMDO.t.sol:IMDOTokenTest\n[PASS] testFuzz_burnReducesSupplyAndOnlyTheCallersBalance(uint256,uint256) (runs: 500, μ: 171682, ~: 172404)\n[PASS] testFuzz_transfersMoveExactlyTheAmount(uint256) (runs: 500, μ: 159460, ~: 160128)\n[PASS] test_allowancesAndTransferFrom() (gas: 339607)\n[PASS] test_metadataAndFixedSupplyMintedToDeployer() (gas: 47999)\n[PASS] test_noAdminCallCanMintOrSeizeEvenFromTheDeployer() (gas: 632322)\n[PASS] test_runtimeCodeHasNoDelegatecallOrSelfdestruct() (gas: 1369268)\n[PASS] test_transferFailurePaths() (gas: 122618)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 13.46ms (31.38ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOTokenOnlyPoolTest\n[PASS] test_buyOnTokenOnlyPoolThenSellInLaunchBlock() (gas: 688130)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 17.88ms (691.13µs CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOLaunchBlockTest\n[PASS] test_launchBlockSellsPayTheCapThenTheLagExpires() (gas: 1037758)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.65ms (1.11ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOEthOnlyPoolTest\n[PASS] test_exactOutputSellWithoutManagerTokensAccruesAClaimThatHarvestBurns() (gas: 818516)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.33ms (615.38µs CPU time)\n\nRan 8 tests for test/IMDO.t.sol:IMDODeployScriptTest\n[PASS] testFuzz_runRefusesEveryChainButLocalAndSepolia(uint64) (runs: 64, μ: 35148, ~: 35148)\n[PASS] test_constantsAgreeWithTheHook() (gas: 50883)\n[PASS] test_hookCreationCodeIsTheFactorysInitCode() (gas: 58725)\n[PASS] test_hookRefusesAnAddressWithoutTheFlags() (gas: 8791)\n[PASS] test_mineReproducesTheFactoryDeployment() (gas: 9963671)\n[PASS] test_mineRespectsSaltStartAndAlwaysReturnsFlaggedAddresses() (gas: 25750131)\n[PASS] test_runOnSepoliaWithoutAConfiguredPoolManagerIsRefused() (gas: 40602)\n[PASS] test_runOnTheLocalChainDeploysTheTokenAndTheMinedHookAndAttests() (gas: 47420881)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 110.92ms (150.63ms CPU time)\n\nRan 29 tests for test/IMDO.t.sol:IMDOHookTest\n[PASS] testFuzz_feeRateFollowsTheBracketTable(uint256,uint256) (runs: 2000, μ: 11908, ~: 12573)\n[PASS] testFuzz_noCallChangesBracketsCapOrTreasury(bytes4,bytes,address) (runs: 300, μ: 160958, ~: 130278)\n[PASS] testFuzz_oneTransactionSplitNeverPaysLessThanPerLegBracketsNorMoreThanTheFinalBracket(uint256,uint256,uint256) (runs: 96, μ: 651540, ~: 659562)\n[PASS] testFuzz_sellsAreNeverBlocked(uint256,bool,bool) (runs: 200, μ: 527721, ~: 532021)\n[PASS] test_buysAreFreeInBothModes() (gas: 810507)\n[PASS] test_callbacksRefuseCallersOtherThanTheManager() (gas: 307119)\n[PASS] test_curatedAdminCallsAllFail() (gas: 323982)\n[PASS] test_dustCrossingLegAsTheLastLegLeavesABoundedRemainderThatDoesNotCarryAcrossTransactions() (gas: 1394239)\n[PASS] test_dustCrossingLegIsBoundedToItsOwnOutputAndTheNextLegPaysTheRest() (gas: 666920)\n[PASS] test_exactInputSellsPayExactlyTheBracketFee() (gas: 2611956)\n[PASS] test_exactOutputDustCrossingLegIsBoundedToItsOwnInputAndTheNextLegPaysTheRest() (gas: 697266)\n[PASS] test_exactOutputSellsPayExactlyTheBracketFeeInBurnedTokens() (gas: 1627933)\n[PASS] test_factoryPoolFeeAccountingAndDistributionUnaffectedByTheHook() (gas: 4715411)\n[PASS] test_gasBurningTreasuryCannotBlockSells() (gas: 589643)\n[PASS] test_harvestWithNothingAccruedIsANoOp() (gas: 55839)\n[PASS] test_launchBindsOnePoolAndTracksSeededReserve() (gas: 126489)\n[PASS] test_mixedModeSplitIsBilledCumulatively() (gas: 609011)\n[PASS] test_permissionsMatchAddressBits() (gas: 13858)\n[PASS] test_protocolFeesAreExcludedFromTheReserve() (gas: 757241)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6123287)\n[PASS] test_sameBlockEarlierTransactionsDoNotChangeTheSnapshot() (gas: 1196526)\n[PASS] test_sameTransactionDonationDoesNotLowerTheBracket() (gas: 511782)\n[PASS] test_sameTransactionLiquidityInflationDoesNotLowerTheBracket() (gas: 984978)\n[PASS] test_secondPoolWithThisHookIsRejected() (gas: 93416)\n[PASS] test_splitExactInputSellsAreBilledCumulatively() (gas: 1113852)\n[PASS] test_swarmMerkleClaimsAreUntaxedAndUnaffected() (gas: 4638688)\n[PASS] test_threeLegSplitCrossesTwoBrackets() (gas: 612472)\n[PASS] test_treasuryReceivesExactlyTheFeeAndTheSellerTheRest() (gas: 499372)\n[PASS] test_treasuryRejectingEthFallsBackToClaimsAndNeverBlocksTheSell() (gas: 744768)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 113.50ms (229.21ms CPU time)\n\nRan 1 test for test/IMDO.t.sol:IMDOInvariantTest\n[PASS]\nIMDOInvariantTest invariants:\n[PASS] invariant_configurationIsImmutable\n[PASS] invariant_everyFeeChargedIsAtTheTreasuryBurnedOrClaimable\n[PASS] invariant_factoryPositionIsIntact\n[PASS] invariant_hookNeverHoldsUserFundsBeyondAccruedClaims\n[PASS] invariant_reserveReconcilesWithManagerInventory\n IMDOInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭-------------+---------------------+-------+---------+----------╮\n| Contract    | Selector            | Calls | Reverts | Discards |\n+================================================================+\n| IMDOHandler | addLiquidity        | 146   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | burn                | 122   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactIn          | 115   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | buyExactOut         | 127   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | collectProtocolFees | 136   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | donate              | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | harvest             | 137   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | removeLiquidity     | 144   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | roll                | 145   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactIn         | 162   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | sellExactOut        | 139   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | setProtocolFee      | 129   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | splitSell           | 139   | 0       | 0        |\n|-------------+---------------------+-------+---------+----------|\n| IMDOHandler | toggleTreasury      | 135   | 0       | 0        |\n╰-------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 566.81ms (545.08ms CPU time)\n\nRan 7 test suites in 568.76ms (868.55ms CPU time): 48 tests passed, 0 failed, 0 skipped (48 total tests)\n","passed":true},{"durationMs":33,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":208,\"launch.json\":29,\"script/Deploy.s.sol\":131,\"src/IMDOFeeHook.sol\":471,\"test/IMDO.t.sol\":1919},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d9886eac6ae830a0fd720eb3c500489cea52dc781aeb5ee15a42b2325c01ec4f","verifiedTreeHash":"97a59b09917427abb5ea4929d0cd4f60cc5eb3e6","verifierVersion":"0.1.0+b537d296"},{"checks":[{"durationMs":129,"exitCode":0,"name":"build","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 71.12ms\nCompiler run successful!\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:142:9\n    │\n142 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-events-access-control]: `laggedReserve` is changed without an event but is used for access control\n    ╭▸ src/IMDOFeeHook.sol:416:13\n    │\n416 │             laggedReserve = liveReserve;\n    │             ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDOFeeHook.sol:183:34\n    │\n183 │     constructor(address manager, address launchToken) BaseHookFee(manager) {\n    │                                  ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:204:9\n    │\n204 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:214:9\n    │\n214 │         emit PoolBound(poolId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:225:32\n    │\n225 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:227:24\n    │\n227 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:238:32\n    │\n238 │     ) external onlyPoolManager nonReentrant returns (bytes4, int256) {\n    │                                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:240:24\n    │\n240 │         _updateReserve(int128(delta));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:247:9\n    │\n247 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMDOFeeHook.sol:259:9\n    │\n259 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:285:16\n    │\n285 │         return (reserve / 10_000) * bps + ((reserve % 10_000) * bps + 9_999) / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:372:9\n    │\n372 │         emit FeeClaimAccrued(currency, fee);\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/IMDOFeeHook.sol:313:13\n    │\n313 │             emit HookFeeCharged(tx.origin, rate, sold, currency, fee);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:294:29\n    │\n294 │         int128 tokenDelta = int128(delta);\n    │                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:295:29\n    │\n295 │         int128 quoteDelta = int128(delta >> 128);\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:303:27\n    │\n303 │         uint256 tokenIn = uint256(-int256(tokenDelta));\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/IMDOFeeHook.sol:304:45\n    │\n304 │         uint256 quoteOut = quoteDelta > 0 ? uint256(int256(quoteDelta)) : 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/IMDOFeeHook.sol:307:57\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:307:65\n    │\n307 │         uint256 headroom = params.amountSpecified < 0 ? uint256(uint128(type(int128).max)) : uint256(1) << 127;\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/IMDOFeeHook.sol:315:16\n    │\n315 │         return int128(int256(fee));\n    │                ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/IMDOFeeHook.sol:315:23\n    │\n315 │         return int128(int256(fee));\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[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/IMDOFeeHook.sol:340:21\n    │\n340 │             paid += fee * PPM;\n    │                     ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMDOFeeHook.sol:379:9\n    │\n379 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:395:13\n    │\n395 │             poolManager.take{gas: DIRECT_TAKE_GAS}(address(0), TREASURY, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `accruedToken` is updated\n    ╭▸ src/IMDOFeeHook.sol:396:13\n    │\n396 │             poolManager.burn(address(this), 0, ethAmount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:406:37\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMDOFeeHook.sol:406:24\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │                        ━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDOFeeHook.sol:406:9\n    │\n406 │         emit Harvested(redeemedETH, redeemedToken);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMDOFeeHook.sol:423:39\n    │\n423 │         if (delta < 0) liveReserve += uint256(-int256(delta));\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/IMDOFeeHook.sol:424:29\n    │\n424 │         else liveReserve -= uint256(int256(delta));\n    │                             ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":44,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\nWarning: No tests found in project! Forge looks for functions that start with `test`\n","passed":true},{"durationMs":21,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDOFeeHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMDOFeeHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMDOFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMDOFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMDOFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMDOFeeHook.harvest()\",\"IMDOFeeHook.unlockCallback(bytes)\",\"IMDOToken.approve(address,uint256)\",\"IMDOToken.burn(uint256)\",\"IMDOToken.transfer(address,uint256)\",\"IMDOToken.transferFrom(address,address,uint256)\"],\"files\":{\"README.md\":200,\"script/Deploy.s.sol\":188,\"src/IMDOFeeHook.sol\":463},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":165,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":228,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/IMDOFeeHook.sol:453: Incorrect use of caret operator\n[high] reentrancy-state-change at src/IMDOFeeHook.sol:395: Reentrancy: State change after external call (3 places)\n[high] unsafe-casting at src/IMDOFeeHook.sol:227: Unsafe Casting of integers (2 places)\n[low] large-numeric-literal at src/IMDOFeeHook.sol:9: Large Numeric Literal (11 places)\n[low] literal-instead-of-constant at src/IMDOFeeHook.sol:188: Literal Instead of Constant (10 places)\n[low] non-reentrant-not-first at src/IMDOFeeHook.sol:142: `nonReentrant` is Not the First Modifier (6 places)\n[low] state-change-without-event at src/IMDOFeeHook.sol:218: State Change Without Event (3 places)\n[low] unchecked-return at src/IMDOFeeHook.sol:379: Unchecked Return\n[low] unused-public-function at src/IMDOFeeHook.sol:270: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"ef76871838fa835775f2297b779539cbeff61fd69c6a6a841bfe480bead5edea","verifiedTreeHash":"780866418488258e805da92ade47462a214d189a","verifierVersion":"0.1.0+b537d296"}]}