{"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":"50fae95a-207c-4ecc-93ba-f0d863d885d7","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"0432945690eb3a4f4348ca4ec10ebd246c76b604e60b4375fadf4966707fb303","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"1d71e6e82220f18ee5e05a2654293d4fd0cf04a9b5a699ff01aa6f67865a95d8","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"8b7e722ac888be7d9cfb144a943d9104d8ea17cb1db3f22add79b40f4730c4e3","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"a2959e84a22c3b81a0250a58482acb9acc36634ba29209f2a714437b55b8ea83","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0270830da15191f0ee088540b48c72716caf7f7e1583bcb0682b69af80a86b34","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"47b4c613a1305f87c058b27fa851354d58e371d6e6a7ea8b7caabeabb9421831","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"38710a950a484f01afe4de839516ed2af6dd0d8d2a2a3604615cfc9a4aa2e864","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"9dbc42eaf86f4e916e426f2cf5a4a4696ab16f9c3c37a93797bbe3ee1254f59e","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Deploy one contract, LilyGuardDeployer, on Robinhood Chain: its constructor takes the Robinhood Chain PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951, $owner and the fee recipient 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1, takes no salt argument, starts at salt 0 and increments until the CREATE2 address carries the hook's permission flags, then creates LilyGuard in the launch transaction, passing the PoolManager, $owner and fee recipient unchanged to the hook constructor; neither constructor calls any other contract (apart from the deployer creating the hook). There is no deploy function to call afterwards; hook() returns the hook's address. Enable only the callbacks the behavior below needs.\nLilyGuard is a Uniswap v4 hook for the existing token 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 (IMD); only pools of native ETH and 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 may use it. No new token and no pool created by the launch; the creator initializes the pool afterwards.\nOnly pools whose currency0 is native ETH may be initialized with this hook; any other initialization reverts.\nIn this hook, the token is the pool's non-ETH currency and its total supply is that token's totalSupply().\nEvery pool-dependent view, state, window and limit is keyed by PoolId and isolated per pool; pass the pool id to each pool-dependent function.\nThe hook does exactly the following, and nothing else:\n1. Take 1.0% of the ETH side of every buy and sell as a hook fee, settled in ETH inside the swap that paid it (mint a claim when the PoolManager cannot pay it directly), and send it to 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1. Expose claimFees(PoolId poolId) that anyone may call to redeem that pool's claims using the same distribution. Fees are computed on the amount actually traded; when the swap specifies its ETH amount and the pool fills only part of it, the swap reverts rather than overpaying.\n2. The owner may pause and unpause swaps separately by PoolId. Liquidity can always be removed while paused. Expose pause(PoolId poolId) and unpause(PoolId poolId) for the owner and paused(PoolId poolId) as a view.\nOwner 0x5bff2a2fecd1d6152676813b21346238733beab1 may only: pause and unpause swaps, through the functions named above. No other admin function, no backend signer, no upgrade path.\nFees are always taken from the ETH (pair currency) side, never from IMD; a fee the PoolManager cannot pay directly is minted as a claim the recipient can redeem. Fee recipient: 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1.\nThe hook uses no outside pool, token, price feed or service; every price it needs comes from its own pool.\nUse mulDiv for every percentage calculation, including fees, shares, limits and price moves, to avoid intermediate multiplication overflow.\nKeep the function names exactly as written so the storefront can show them. Composed with The Hooks Store block builder.","parentJobId":null,"planHash":"66c16beffe2b1d58f4b26502a946f2a70c910f00cadbf53081a18fcb67b3138d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"50fae95a-207c-4ecc-93ba-f0d863d885d7","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2450-deploy-one-contract-lilyguarddeployer"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51452","feedbackHash":"520c35d182f674dc553d16a3a10e2615ca1e7328e6d0e2ed2c5ef4b01d4a9c0e","nodeKey":"audit_economics","submissionHash":"0432945690eb3a4f4348ca4ec10ebd246c76b604e60b4375fadf4966707fb303","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"53916","feedbackHash":"c89cd5772e044b843cf759e22ea91ad629abddf517ae5826d7218dc174070d48","nodeKey":"audit_flow","submissionHash":"1d71e6e82220f18ee5e05a2654293d4fd0cf04a9b5a699ff01aa6f67865a95d8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52289","feedbackHash":"52171f71e88bb963ac99fa976947d7f11cb109cdafe35419d74eeeb802fba5a2","nodeKey":"audit_judge","submissionHash":"8b7e722ac888be7d9cfb144a943d9104d8ea17cb1db3f22add79b40f4730c4e3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52159","feedbackHash":"a54195abbb27cb24509132c0ff8119252f8a40e4f4f31cca30ae51d33b45280c","nodeKey":"audit_math","submissionHash":"a2959e84a22c3b81a0250a58482acb9acc36634ba29209f2a714437b55b8ea83","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52669","feedbackHash":"08dc3deb3e167bd29e650cb803560d32f6ef585c88f3c3249718fb89096e0569","nodeKey":"audit_permissions","submissionHash":"0270830da15191f0ee088540b48c72716caf7f7e1583bcb0682b69af80a86b34","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52276","feedbackHash":"db65a9d4e8674422b9fbf6ad8e4c262115402ecb4df410a6d020b840b5c2fe3b","nodeKey":"build_contract_project","submissionHash":"47b4c613a1305f87c058b27fa851354d58e371d6e6a7ea8b7caabeabb9421831","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51221","feedbackHash":"1b377e9ee01f128e6cd7de51f816c09f2d681bfa8bb3c7e539e3dc65b704e7eb","nodeKey":"manifest","submissionHash":"38710a950a484f01afe4de839516ed2af6dd0d8d2a2a3604615cfc9a4aa2e864","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52278","feedbackHash":"961e4cff33188b75036f72115bd0769d9a4909104085c9a90d567313c3a373fa","nodeKey":"write_foundry_tests","submissionHash":"9dbc42eaf86f4e916e426f2cf5a4a4696ab16f9c3c37a93797bbe3ee1254f59e","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"5509ec1e73132de5cc79cc533939471408534dfd806005dca261c0034e07fe0d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7cb215928f3c9473","findings":[{"citation":"resolved","description":"Asymmetry between the two branches of beforeSwap/afterSwap. When ETH is the specified currency (exact ETH in buy, exact ETH out sell), fee = 1% of |amountSpecified|, which is the user's gross budget (buy) or net receipt (sell), not the ETH the pool traded (|amountSpecified| - fee on buys, |amountSpecified| + fee on sells). When ETH is unspecified (afterSwap line 172, `fee = _fee(_abs(int256(delta.amount0())));`), the fee is 1% of the pool's executed ETH. The brief says fees are computed on the amount actually traded. So for an identical pool trade, exact-input buys pay 1.0101% of traded ETH and exact-ETH-output sells pay 0.990%, while the other two modes pay exactly 1%. Any trader can pick the cheaper mode: sells via exact ETH output underpay the recipient by about 1 bp of volume, and buys via exact ETH input overcharge users by about 1 bp. Low impact, but it breaks the stated fee basis and the two branches are not symmetric. Fix: for ETH-specified swaps, charge fee = mulDiv(|amountSpecified|, 100, 10_100) on exact-ETH-input (so fee is 1% of the traded remainder), and fee = mulDiv(|amountSpecified|, 100, 9_900) on exact-ETH-output, or the equivalent; then update the expected check at line 166 to match.","line":145,"path":"src/LilyGuard.sol","reproduction":"Test base setup (pool at 1:1, liquidity 1000e18 on [-600,600]). Buys: (a) swap zeroForOne, amountSpecified=-1e18 gives pool ETH delta -0.99e18, IMD out 986056732423396135, hook fee 0.01e18. (b) From the same state, swap zeroForOne, amountSpecified=+986056732423396135 (exact IMD out) gives the identical pool ETH delta -0.99e18 and IMD out, but hook fee is 0.0099e18. Sells: (c) oneForZero, amountSpecified=+1e18 (exact ETH out) gives pool ETH +1.01e18, IMD in 1014063321306575811, fee 0.01e18. (d) oneForZero, amountSpecified=-1014063321306575811 (exact IMD in) gives the identical pool ETH +1.01e18, but fee is 0.0101e18. Expected: the same fee (1% of the 0.99e18 or 1.01e18 traded) in both modes. Actual: fees differ by 1%. Measured with forge using LilyGuardTestBase._assertSwap, with vm.snapshotState/revertToState between modes.","severity":"low","snippet":"        int128 fee = _ethSpecified(params) ? _fee(_abs(params.amountSpecified)) : int128(0);","title":"Fee base differs by swap mode: ETH-specified swaps are charged on the user's gross amount, not the ETH actually traded, so the same pool trade pays 0.0099 or 0.0101 ETH depending on mode"}],"hash":"0270830da15191f0ee088540b48c72716caf7f7e1583bcb0682b69af80a86b34","nodeId":"c5efee79-185d-4656-b738-c59cf06e89c0","outcome":"completed","summary":"The review is done: one low-severity finding, and nothing more serious in the permissions area. The results are in `.imd-findings.json` with 1 finding and 10 coverage rows. No source files were changed, and the scratch test I used has been deleted. The existing suite passes (47 tests).\n\n**Finding 1 (low): the fee is charged on a different base depending on how the swap is specified** (`src/LilyGuard.sol:145`, and `:172` for the other case)\n- **What happens:** when the trader specifies the ETH amount, the 1% fee is taken from the amount they named. When they specify the IMD amount instead, it is taken from the ETH the pool actually traded. The brief says fees are computed on the amount actually traded.\n- **Measured from the same pool state, with the same trade through the pool each time:**\n\n| Trade | Pool ETH traded | Swap mode | Fee |\n|---|---|---|---|\n| Buy | 0.99 ETH | ETH amount specified | 0.01 ETH |\n| Buy | 0.99 ETH | IMD amount specified | 0.0099 ETH |\n| Sell | 1.01 ETH | ETH amount specified | 0.01 ETH |\n| Sell | 1.01 ETH | IMD amount specified | 0.0101 ETH |\n\n- **Effect:** about 0.01% of volume. Sellers who specify their ETH output underpay the fee recipient, and buyers who specify their ETH input overpay.\n- **Fix:** scale the fee in the two ETH-specified modes so it comes to 1% of the ETH traded, then update the expected-fill check at line 166 to match.\n\n**Permissions: all of these hold up.**\n- **Owner:** the owner, fee recipient and PoolManager are fixed at construction. The owner can only call `pause`/`unpause`. There is no setter, ownership transfer, ETH receive function or upgrade path.\n- **Pause:** it blocks swaps on that one pool only. The hook has no liquidity callbacks, so liquidity can always be removed while paused.\n- **Callbacks:** every enabled callback accepts calls only from the PoolManager. `unlockCallback` additionally only runs during the hook's own `claimFees`.\n- **`claimFees`:** anyone may call it, but it pays only the fixed recipient.\n\n**Static-analysis leads:**\n- The aderyn \"incorrect shift order\" warning on `LilyGuardDeployer.sol:29` is a false positive: the shift arguments are in the right order and the address-mining loop is correct.\n- The two slither reentrancy warnings are covered by the hook's reentrancy guard. A failed payment falls back to recording a claim, so a recipient that rejects ETH cannot block swaps.\n\nEvery one of the 7 entry points has a coverage row: `afterSwap` and `beforeSwap` point to Finding 1, and the other five hold. Nothing was left unreached.","treeHash":null,"usage":{"cachedInputTokens":613359,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":12814,"runtime":"claude","turns":11,"wallClockMs":157898}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d63ea36a2b809080","findings":[{"citation":"resolved","description":"Area: Flow Gap (execution x first principles) and Invariant (interface guarantee). _validateKey only checks the two currencies and the hook address; it does not reject key.fee == 0x800000 (LPFeeLibrary.DYNAMIC_FEE_FLAG). v4 initializes a dynamic-fee pool with lpFee = 0 (PoolManager.initialize -> getInitialLPFee) and expects the hook to call updateDynamicLPFee or return an override fee from beforeSwap. LilyGuard never calls updateDynamicLPFee and beforeSwap always returns lpFeeOverride = 0 (line 146), which has no OVERRIDE_FEE_FLAG, so the stored 0 fee is used on every swap forever. Every ETH/IMD pool opened on this hook with the dynamic flag therefore pays liquidity providers nothing while the hook still takes its 1% ETH fee and the README promises the pool's 'chosen valid v4 fee' is honoured untouched. The brief says the hook does 'exactly the following, and nothing else' and that the creator initializes with a valid fee; a fee tier the hook can never service is not a valid configuration for this hook, and the hook is the only party able to refuse it at initialization. Impact is bounded to the LPs of a pool whose creator (or anyone, since initialization is permissionless and the ID is public) opens it with that flag; a creator who then seeds liquidity under that ID earns no swap fees. Fix: in _validateKey (or beforeInitialize) add `|| key.fee.isDynamicFee()` to the revert condition (LPFeeLibrary), so only static-fee ETH/IMD pools register.","line":217,"path":"src/LilyGuard.sol","reproduction":"State: LilyGuard deployed, PoolManager fresh. Call manager.initialize(PoolKey(ETH, IMD, 0x800000, 60, hook), 2^96). Expected: revert in beforeInitialize (UnsupportedPool), because the hook has no dynamic-fee logic. Actual: succeeds; getSlot0(poolId).lpFee == 0. Then add liquidity [-600, 600] of 1000e18 and perform a buy of exact 1 ether ETH input and a sell of exact 1e18 IMD input. getFeeGrowthGlobals(poolId) stays (0, 0) before and after both swaps, while the identical two swaps on a static fee=3000 ETH/IMD pool with the same liquidity move feeGrowthGlobal0 by 1010638629755187236486222584072351 and feeGrowthGlobal1 by 1020847100762815390390123822295304. The hook fee of 0.01 ETH per buy is still collected on the dynamic pool. Reproduced in test/scratch/Probe.t.sol::test_dynamicFeePoolHasZeroLPFee (logs: 'lpFee stored for dynamic pool: 0', 'feeGrowth0 delta: 0', 'feeGrowth1 delta: 0').","severity":"low","snippet":"    function _validateKey(PoolKey calldata key) private view {\n        if (\n            !key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != IMD\n                || address(key.hooks) != address(this)\n        ) revert UnsupportedPool();","title":"beforeInitialize accepts v4's dynamic-fee flag although the hook never manages an LP fee, so such a pool trades with a permanent 0% LP fee"},{"citation":"resolved","description":"Area: Economic Security / Invariant (path divergence: two routes to the same AMM execution produce different fees). The brief says 'Fees are computed on the amount actually traded'. For an exact-ETH-input buy the hook computes the fee on params.amountSpecified (line 145 in beforeSwap, again at line 163 in afterSwap), returns it as the specified beforeSwap delta, and the AMM then trades amountSpecified minus fee. So the fee is 1% of (traded + fee) = 1.0101% of the ETH the pool actually traded. For an exact-IMD-output buy that makes the pool trade the same gross ETH, afterSwap computes the fee on delta.amount0() (line 172), i.e. exactly 1% of the traded ETH. Symmetrically, an exact-ETH-output sell is charged 1% of the net output while an exact-IMD-input sell is charged 1% of the gross AMM output. Two swap modes that produce byte-identical AMM executions (same raw Swap event amounts, same IMD out, same price impact) therefore charge different hook fees, so '1.0% of the ETH side' is not one number and the exact-ETH mode overcharges relative to the brief's 'amount actually traded' by fee/100. Loss per trader is 1 basis point of the trade (0.0001 ETH per 1 ETH), does not compound, and is documented in the README table, so this is a specification deviation rather than an exploit; a router can already pick the cheaper mode. Fix: for ETH-specified swaps compute the fee on the amount the AMM will actually trade, e.g. fee = mulDiv(A, 100, 10100) for exact input (so traded = A - fee and fee == 1% of traded, rounding down) and fee = mulDiv(A, 100, 10000) is already 1% of the net output for exact output if net is the intended base; or state in the brief that the base is the trader-side amount and leave the code.","line":145,"path":"src/LilyGuard.sol","reproduction":"Pool ETH/IMD fee=3000, spacing 60, liquidity 1000e18 in [-600, 600], price 1:1. (1) Buy exact ETH input: swap(zeroForOne=true, amountSpecified=-1e18). Raw Swap event amount0 = -0.99e18 (AMM traded 0.99 ETH), hook fee = 0.01e18, trader pays 1.0e18 and receives 986056732423396135 IMD. (2) On an identical fresh pool (fee=3000, spacing 10 to get a new PoolId) buy exact IMD output: swap(zeroForOne=true, amountSpecified=+986056732423396135). Raw Swap event amount0 = -0.99e18 (same AMM trade), hook fee = 0.0099e18, trader pays 0.9999e18 for the same 986056732423396135 IMD. Expected per brief: same fee for the same traded ETH amount (0.0099e18 in both). Actual: 0.01e18 vs 0.0099e18; the exact-ETH-input trader pays 0.0001 ETH more. Reproduced in test/scratch/Probe.t.sol::test_feeBaseDiffersByMode.","severity":"low","snippet":"        int128 fee = _ethSpecified(params) ? _fee(_abs(params.amountSpecified)) : int128(0);","title":"Fee base differs by swap mode: ETH-specified swaps are charged on the specified amount (which includes the fee), token-specified swaps on the ETH the pool actually traded"}],"hash":"0432945690eb3a4f4348ca4ec10ebd246c76b604e60b4375fadf4966707fb303","nodeId":"8d7a4276-a2a4-43a9-a553-c92a870295ab","outcome":"completed","summary":"The review is complete and the findings file is written. Nothing more is needed from the machine.\n\n**Outcome.** The economics of LilyGuard hold up. I traced every fee path against the vendored v4-core delta conventions and found no way to lose, misdirect, or strand funds. Two low-severity defects are recorded in `.imd-findings.json`, each with a reproduction confirmed by a scratch test, plus a coverage row for all seven listed entry points and six invariants.\n\n**Findings (both low).**\n- **Dynamic-fee pools are accepted but never serviced.** `beforeInitialize` does not reject the v4 dynamic-fee flag. The hook never sets an LP fee and always returns no override, so such a pool trades with a permanent zero LP fee while the hook still takes its 1% ETH cut. Reproduced: fee growth stays at zero after a buy and a sell, versus nonzero on an identical static-fee pool.\n- **Fee base differs by swap mode.** ETH-specified swaps are charged on the specified amount, which includes the fee itself, while token-specified swaps are charged on the ETH the pool actually traded. Two swaps with byte-identical AMM execution pay different hook fees. The gap is one basis point of the trade and is documented in the README, so it is a specification deviation rather than an exploit.\n\n**What holds.** Hook ETH delta nets to zero on the direct-pay, deferred-claim, and redemption paths. Partial ETH-specified fills revert before any payout. Claims are isolated per pool, always backed by ETH the swap settled, and redeemable while paused. Pause touches only swaps, since no liquidity callbacks exist. No IMD is ever taken. The dust-level fee bypass under 100 wei and the hook self-call exemption are unreachable in practice. The static-analysis leads were checked: the reentrancy lines are covered by the guard and the authenticated redemption context, and the aderyn shift-order line is a false positive.\n\n**Limits.** The deployer's unbounded salt search is noted in coverage as a liveness consideration, not a finding. Forge's gas meter does not account creation gas inside tests, so I did not get a reliable measurement of it. I could not check the live Robinhood Chain PoolManager or IMD token, since no endpoint for that chain was provided.","treeHash":null,"usage":{"cachedInputTokens":1972753,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":44531,"runtime":"claude","turns":34,"wallClockMs":643091}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"505a745a77612e11","findings":[{"citation":"resolved","description":"_validateKey checks currency0, currency1 and hooks but not key.fee. v4 accepts the sentinel fee 0x800000 (LPFeeLibrary.DYNAMIC_FEE_FLAG) for any pool whose hook address has at least one flag, and PoolManager.initialize stores lpFee = 0 for it (getInitialLPFee). LilyGuard implements no dynamic-fee logic: beforeSwap returns lpFeeOverride 0 without the override bit, and nothing ever calls updateDynamicLPFee. Because initialization is permissionless (any sender, any fee tier), an ETH/IMD pool keyed with the dynamic flag is registered by the hook, gets its own PoolId, pause state and fee claims, and then executes every swap with a zero LP fee while still charging the 1% hook fee. LPs who provide to that pool (for example because a storefront or integrator lists it as the hook's pool) earn nothing, and the pool is not distinguishable from a static-fee pool through the hook's own views (token/paused/pendingFees). The brief leaves the LP tier to the creator, so this is a missing guard rather than a loss path; the fix is to reject key.fee.isDynamicFee() in _validateKey (or implement the dynamic fee). Verified with a scratch test: initialize(PoolKey(ETH, IMD, 0x800000, 60, hook), 2**96) succeeds, getSlot0 returns lpFee == 0, and a 1 ETH exact-input buy on it returns more IMD (989020869339354039) than the same buy on the 3000-fee pool (986056732423396135) while still paying the 0.01 ETH hook fee.","line":218,"path":"src/LilyGuard.sol","reproduction":"State: LilyGuard deployed via LilyGuardDeployer against a PoolManager; IMD code at 0x5F7B...7127. Call manager.initialize(PoolKey{currency0: address(0), currency1: IMD, fee: 0x800000, tickSpacing: 60, hooks: hook}, 79228162514264337593543950336). Expected: beforeInitialize reverts UnsupportedPool (hook has no dynamic-fee support). Actual: succeeds, hook.token(poolId) == IMD, manager.getSlot0(poolId).lpFee == 0; add liquidity [-600,600] 1000e18, swap zeroForOne exact input -1e18: amount1 = 989020869339354039 (vs 986056732423396135 on the 3000 tier), hook fee 0.01 ETH still charged, LP fee 0.","severity":"low","snippet":"        if (\n            !key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != IMD\n                || address(key.hooks) != address(this)\n        ) revert UnsupportedPool();","title":"beforeInitialize accepts the dynamic-fee flag but the hook never sets an LP fee, so such a pool trades at a 0 LP fee forever"},{"citation":"resolved","description":"The brief says the fee is 1.0% of the ETH side and is computed on the amount actually traded. For the two token-specified modes afterSwap uses the executed AMM delta (fee = E/100, exactly 1% of the pool leg). For the two ETH-specified modes beforeSwap and afterSwap use |amountSpecified| instead: an exact-input buy of A pays fee A/100 while the AMM only trades A - A/100, i.e. 1.0101% of the ETH actually traded; an exact-output sell of A pays A/100 while the AMM supplies A + A/100, i.e. 0.990% of the ETH actually traded. The same ETH leg through the pool is therefore charged three different rates depending on which side the router specifies, which lets a router pick the cheaper mode for sells (exact-ETH-output) and makes the recipient's income depend on routing rather than on volume. The README documents this convention, so it is a deviation from the brief's wording rather than a loss; the fix is to base the specified-ETH fee on the executed delta (fee = |delta.amount0()| * 100 / 10_000 of the AMM leg, i.e. charge mulDiv(A, 100, 10_100) on exact input and mulDiv(A, 100, 9_900) on exact output, or document the chosen basis as accepted).","line":145,"path":"src/LilyGuard.sol","reproduction":"Pool ETH/IMD fee 3000 spacing 60 at price 1:1 with 1000e18 liquidity in [-600,600]. (1) swap zeroForOne=true amountSpecified=-1e18: trader amount0 = -1e18, hook fee paid = 0.01e18, AMM ETH input = 0.99e18 -> fee/ammETH = 10101 ppm. (2) swap zeroForOne=false amountSpecified=+1e18: trader receives 1e18, fee 0.01e18, AMM ETH output 1.01e18 -> 9900 ppm. (3) swap zeroForOne=false amountSpecified=-1e18 (IMD in): fee/ammETH = 9999 ppm. Expected per brief: 1.0% of the ETH actually traded in every mode.","severity":"low","snippet":"        int128 fee = _ethSpecified(params) ? _fee(_abs(params.amountSpecified)) : int128(0);","title":"Specified-ETH swaps compute the 1% fee on the specified amount, not on the ETH the pool actually traded, so the effective rate differs by swap mode (1.0101% / 0.990% / 1%)"},{"citation":"resolved","description":"The search is correct (CREATE2 preimage 0xff ++ deployer ++ salt ++ keccak(initcode), 85 bytes; the shl(248, 0xff) / shl(96, address()) operands are in the right Yul order, so the aderyn incorrect-shift-order lead is a false positive) and the salt is the first match from zero, but it is unbounded by design and the number of candidates is 2^14 on average. Measured by deploying the exact launch init code (manager 0x8366..., owner and recipient 0x5Bff...) through a CREATE2 factory at five factory salts: hook salts 55178 / 5980 / 4247 / 36480 / 51201 cost 8,395,946 / 2,196,998 / 1,978,640 / 6,039,998 / 7,894,844 gas for the whole deployer constructor including the hook creation. The deployment service checks the simulated cost against gasCeilingWei, so a launch whose factory address and salt happen to need >55k candidates (about 3%) is refused or runs out of gas rather than losing funds; the salt depends only on the deployer's address and the constructor arguments, so the simulation is exact. Not a defect against the brief, which mandates the increment-from-zero search; recorded so the gas ceiling is set with this distribution in mind.","line":32,"path":"src/LilyGuardDeployer.sol","reproduction":"Deploy bytes.concat(type(LilyGuardDeployer).creationCode, abi.encode(0x8366a39CC670B4001A1121B8F6A443A643e40951, 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1, 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1)) via create2 from a factory at chainId 4663; with factory salt 0 the deployer mines hook salt 55178 and the create2 consumes 8,395,946 gas; with factory salt 2 it mines 4247 and consumes 1,978,640 gas.","severity":"info","snippet":"            for {} 1 {} {\n                mstore(add(ptr, 21), candidate)\n                let predicted := keccak256(ptr, 85)\n                if eq(and(predicted, mask), flags) { break }\n                candidate := add(candidate, 1)\n            }","title":"On-chain salt mining costs about 150 gas per candidate with no cap; the launch constructor needs 2-4M gas typically and above 8M in a few percent of deployer addresses"}],"hash":"1d71e6e82220f18ee5e05a2654293d4fd0cf04a9b5a699ff01aa6f67865a95d8","nodeId":"f86f0ec6-9077-4e43-9913-9df32a20811a","outcome":"completed","summary":"Review complete. The findings file is written and validated against the tree.\n\n**Outcome.** No critical, high or medium defect in the control flow. The hook's fee accounting, pause gating, claim redemption and deployer mining all behave as the brief specifies. I recorded two low findings and one informational note in `.imd-findings.json`, with a coverage row for each of the seven listed entry points plus the deployer constructor and three invariants.\n\n**Findings.**\n- **Low. Dynamic-fee flag accepted.** `_validateKey` checks currencies and hook but not the fee. A pool keyed with the v4 dynamic-fee sentinel initializes successfully, is registered by the hook, and trades at a zero LP fee forever because the hook never calls the dynamic fee updater. Reproduced with a scratch test showing the slot0 LP fee is zero and the swap output exceeds the 3000-tier pool's.\n- **Low. Fee basis differs by swap mode.** Specified-ETH swaps charge 1% of the specified amount rather than of the ETH the pool actually traded, giving effective rates of 1.0101%, 0.990% and 1% across modes. Measured on a 1:1 pool with concrete numbers. The README documents this convention, so it is a wording deviation from the brief, not a loss path.\n- **Info. Mining gas.** The constructor's salt search costs about 150 gas per candidate and is uncapped by design. Deploying the exact launch init code through a CREATE2 factory at five factory salts cost between 2.0M and 8.4M gas. This matters only for setting the launch gas ceiling. The aderyn \"incorrect shift order\" lead is a false positive. The shift operands are in the correct Yul order.\n\n**What I verified and found sound.** All four swap modes net the hook's delta to zero on the direct-take path, the deferred-mint path and redemption, including with protocol fees enabled. Partial fills of specified-ETH swaps revert before any payment. Per-pool claims draw from the shared ETH claim id without cross-pool leakage. The reentrancy lock blocks claim, pause and nested swaps from a reentering recipient. Both slither leads are benign given the lock. The suite of 46 tests passes as shipped.\n\n**Not reached.** I did not run the 64-run stateful invariant suite to completion, and I did not probe a recipient that griefs by consuming gas inside the direct-take path, since the recipient is the trusted owner wallet.","treeHash":null,"usage":{"cachedInputTokens":1819569,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":44486,"runtime":"claude","turns":39,"wallClockMs":620361}},{"artifacts":[],"attempt":1,"bundleHash":"e5f1c9e2c1233cfea4cc9d5fc4f771fd7e1c1f7afe82be48c14a70286a141e43","device":"9d2479a5270b8df1","findings":[],"hash":"38710a950a484f01afe4de839516ed2af6dd0d8d2a2a3604615cfc9a4aa2e864","nodeId":"7ed25fa5-7d39-4520-b50d-c17c129c4915","outcome":"completed","summary":"Created [launch.json](/home/imd2/.identitymd/work/50fae95a-207c-4ecc-93ba-f0d863d885d7/7ed25fa5-7d39-4520-b50d-c17c129c4915/launch.json) with only `LilyGuardDeployer` and the required constructor arguments.\n\nManifest structure and compiled ABI checks passed. `forge build` succeeded; `forge test` passed all 47 tests using the required fuzz seed. Only `launch.json` changed.","treeHash":"9f63a56a81e633e9cca3c58bfe0f94f1e0ba4648","usage":{"cachedInputTokens":294971,"inputTokens":58383,"model":"gpt-6.1-sol","outputTokens":2686,"runtime":"codex","turns":3,"wallClockMs":55293}},{"artifacts":[],"attempt":1,"bundleHash":"b42cca1aa8cabe16bbc7c0a5828b8ddbd62d7e91880d2893dad70a55591cfa18","device":"41822d343cdb63ba","findings":[],"hash":"47b4c613a1305f87c058b27fa851354d58e371d6e6a7ea8b7caabeabb9421831","nodeId":"384cfc8d-2e81-4af1-a03d-3b44995c25da","outcome":"completed","summary":"Implemented LilyGuard and its constructor-only CREATE2 deployer, with ETH fees, per-pool claims, and pause controls.\n\nVerified Solidity 0.8.26 build, 47 passing tests with the supplied fuzz seed, and `forge fmt --check`. Dependencies are vendored for offline builds.\n\nDeployment parameters, assumptions, and operational responsibilities are documented in [README.md](./README.md).","treeHash":"744a2e3e24e27eb44cd08018bf80f731df5fb36a","usage":{"cachedInputTokens":2258821,"inputTokens":166020,"model":"gpt-6.1-sol","outputTokens":50966,"runtime":"codex","turns":6,"wallClockMs":1026635}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee9fbaf2480d1034","findings":[{"citation":"resolved","description":"Merged from audit_flow, write_foundry_tests, audit_permissions, audit_economics and audit_math (same root cause). beforeSwap (line 145) and afterSwap (line 163) compute the fee on |params.amountSpecified| whenever ETH is the specified currency. The positive beforeSwap specified delta then changes the ETH leg the AMM executes: an exact-ETH-input buy of A pays fee A/100 while the pool trades A - A/100 (fee = 1.0101% of the executed ETH), and an exact-ETH-output sell of A pays A/100 while the pool supplies A + A/100 (0.990%). The two token-specified modes (line 172) use delta.amount0(), i.e. exactly 1% of the ETH the AMM traded. The brief states one rate, '1.0% of the ETH side', 'computed on the amount actually traded'; the hook produces three effective rates depending on which side the router specifies, so a router can pick the cheaper mode on sells (exact ETH output) and an exact-ETH-input buyer pays about 1 bp of volume more than a buyer requesting the identical IMD output. The README documents the trader-side convention, and the discrepancy is bounded to 1% of the fee and never compounds, so this is a specification deviation rather than a loss path: low. Fix preserving the design: for exact ETH input use fee = mulDiv(A, 100, 10_100) (so fee == 1% of A - fee), for exact ETH output fee = mulDiv(A, 100, 9_900) (so fee == 1% of A + fee); the afterSwap expected = amountSpecified + fee check stays valid.","line":163,"path":"src/LilyGuard.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Vm} from \"forge-std/Vm.sol\";\nimport {LilyGuard} from \"src/LilyGuard.sol\";\nimport {LilyGuardDeployer} from \"src/LilyGuardDeployer.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\n\ncontract ExecutedFeeToken {\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n        totalSupply += amount;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ExecutedFeeRouter is IUnlockCallback {\n    IPoolManager private immutable manager;\n    address private immutable controller;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n        controller = msg.sender;\n    }\n\n    function seed(PoolKey memory key) external {\n        require(msg.sender == controller, \"controller\");\n        manager.unlock(abi.encode(key, false, SwapParams(false, 0, 0)));\n    }\n\n    function swap(PoolKey memory key, bool buy, int256 amount) external {\n        require(msg.sender == controller, \"controller\");\n        manager.unlock(\n            abi.encode(\n                key,\n                true,\n                SwapParams(buy, amount, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1)\n            )\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"manager\");\n        (PoolKey memory key, bool swapping, SwapParams memory params) =\n            abi.decode(data, (PoolKey, bool, SwapParams));\n        BalanceDelta delta;\n        if (swapping) {\n            delta = manager.swap(key, params, \"\");\n        } else {\n            (delta,) = manager.modifyLiquidity(\n                key, ModifyLiquidityParams(-600, 600, 1_000 ether, bytes32(0)), \"\"\n            );\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return \"\";\n    }\n\n    function _settle(Currency currency, int128 delta) private {\n        if (delta < 0) {\n            uint256 owed = uint256(-int256(delta));\n            manager.sync(currency);\n            if (currency.isAddressZero()) {\n                manager.settle{value: owed}();\n            } else {\n                require(ExecutedFeeToken(Currency.unwrap(currency)).transfer(address(manager), owed));\n                manager.settle();\n            }\n        } else if (delta > 0) {\n            manager.take(currency, address(this), uint256(uint128(delta)));\n        }\n    }\n\n    receive() external payable {}\n}\n\ncontract LilyGuardExecutedFeeProofTest is Test {\n    address private constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address private constant OWNER = 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1;\n    IPoolManager private manager;\n    LilyGuard private hook;\n    ExecutedFeeRouter private router;\n    PoolKey private key;\n\n    function setUp() public {\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        vm.etch(IMD, address(new ExecutedFeeToken()).code);\n        hook = (new LilyGuardDeployer(manager, OWNER, OWNER)).hook();\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 3_000, 60, IHooks(address(hook)));\n        manager.initialize(key, uint160(1 << 96));\n        router = new ExecutedFeeRouter(manager);\n        vm.deal(address(router), 100_000 ether);\n        ExecutedFeeToken(IMD).mint(address(router), 100_000 ether);\n        router.seed(key);\n    }\n\n    // Expected fee: 0.0099 ETH on core's executed 0.99 ETH input.\n    // Actual fee: 0.01 ETH (1.010101...% of the executed ETH side).\n    function test_exactETHInputFeeIsOnePercentOfExecutedETH() public {\n        _assertExecutedFee(true, -1 ether);\n    }\n\n    // Expected fee: 0.0101 ETH on core's executed 1.01 ETH output.\n    // Actual fee: 0.01 ETH (0.990099...% of the executed ETH side).\n    function test_exactETHOutputFeeIsOnePercentOfExecutedETH() public {\n        _assertExecutedFee(false, 1 ether);\n    }\n\n    // The same property already holds when IMD, rather than ETH, specifies the amount.\n    function test_controlTokenSpecifiedFeeUsesExecutedETH() public {\n        _assertExecutedFee(true, 1 ether);\n    }\n\n    function _assertExecutedFee(bool buy, int256 amount) private {\n        uint256 beforePaid = OWNER.balance;\n        uint256 beforePending = hook.pendingFees(key.toId());\n        vm.recordLogs();\n        router.swap(key, buy, amount);\n        Vm.Log[] memory logs = vm.getRecordedLogs();\n        bytes32 swapTopic = keccak256(\"Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)\");\n        bool found;\n        int128 executedETH;\n        for (uint256 i; i < logs.length; ++i) {\n            if (logs[i].emitter == address(manager) && logs[i].topics[0] == swapTopic) {\n                (executedETH,,,,,) =\n                    abi.decode(logs[i].data, (int128, int128, uint160, uint128, int24, uint24));\n                found = true;\n                break;\n            }\n        }\n        assertTrue(found, \"missing core Swap log\");\n        uint256 actualFee = OWNER.balance - beforePaid + hook.pendingFees(key.toId()) - beforePending;\n        uint256 executed = executedETH < 0 ? uint256(-int256(executedETH)) : uint256(int256(executedETH));\n        assertGt(executed, 0, \"must exercise a real fill\");\n        // Permit one wei of rounding when solving a fee-inclusive exact budget. The discrepancy\n        // on the current implementation is 100 trillion wei, rather than rounding dust.\n        assertApproxEqAbs(actualFee, executed / 100, 1, \"fee must be 1% of the ETH actually traded\");\n    }\n}","reproduction":"State: fresh PoolManager, LilyGuard from LilyGuardDeployer(manager, 0x5Bff..., 0x5Bff...), test ERC-20 at IMD, pool (ETH, IMD, fee 3000, spacing 60, hook) initialized at sqrtPriceX96 = 2^96 with 1000e18 liquidity in [-600, 600]. (1) swap zeroForOne=true, amountSpecified=-1e18: core Swap event amount0 = -0.99e18, trader amount0 = -1e18, IMD out 986056732423396135, hook fee to recipient 10000000000000000. (2) From the same state (vm.snapshotState/revertToState), swap zeroForOne=true, amountSpecified=+986056732423396135 (exact IMD out): core Swap event amount0 = -0.99e18 (identical AMM execution), trader amount0 = -0.9999e18, hook fee 9900000000000000. (3) swap zeroForOne=false, amountSpecified=+1e18 (exact ETH out): core amount0 = +1.01e18, fee 10000000000000000; the same net via exact IMD input pays 10100000000000000. Expected per brief: one fee for one traded ETH amount (0.0099e18 in (1) and (2), 0.0101e18 in (3)). Actual: 0.01e18 vs 0.0099e18, and 0.01e18 vs 0.0101e18. Reproduced in test/scratch/JudgeProbe.t.sol::test_sameTradeDifferentFeeByMode and by the attached proof (2 of 3 tests fail on this code: 10000000000000000 != 9900000000000000 and 10000000000000000 != 10100000000000000; the token-specified control passes).","severity":"low","snippet":"            fee = _fee(_abs(params.amountSpecified));","title":"Fee base differs by swap mode: ETH-specified swaps are charged 1% of the specified amount, token-specified swaps 1% of the ETH the AMM traded, so the same AMM execution pays different hook fees"},{"citation":"resolved","description":"Merged from audit_flow, audit_economics and audit_math. _validateKey checks currency0, currency1 and hooks but not key.fee. Hooks.isValidHookAddress allows fee == 0x800000 (LPFeeLibrary.DYNAMIC_FEE_FLAG) on any hook with flags, and PoolManager.initialize stores lpFee = 0 for it (getInitialLPFee). LilyGuard never calls updateDynamicLPFee and beforeSwap returns lpFeeOverride 0 without the OVERRIDE_FEE_FLAG (line 146), so the stored 0 is used on every swap forever. Because initialization is permissionless and the key is public, anyone can register (ETH, IMD, 0x800000, spacing, hook) under this hook; it gets its own PoolId, pause state and fee claims, is indistinguishable through the hook's views from a static-fee pool, and LPs who provide to it earn nothing while the hook still takes 1% of the ETH side. A creator choosing a dynamic tier because the hook looks like a fee hook gets a pool the hook can never service. Bounded to LPs of a pool opened with that flag, and the creator can equally choose a static 0 fee, so low. Fix: revert in _validateKey when key.fee.isDynamicFee() (LPFeeLibrary), or implement the dynamic fee.","line":219,"path":"src/LilyGuard.sol","reproduction":"State: LilyGuard deployed, fresh PoolManager, test ERC-20 at IMD. Call manager.initialize(PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 0x800000, 60, IHooks(hook)), 2^96). Expected: beforeInitialize reverts UnsupportedPool (hook has no dynamic-fee logic). Actual: succeeds; hook.token(poolId) == IMD; manager.getSlot0(poolId).lpFee == 0. Add 1000e18 liquidity in [-600, 600] and swap zeroForOne=true amountSpecified=-1e18: trader receives 989020869339354039 IMD (986056732423396135 on the 3000 tier), the recipient still receives 10000000000000000 wei hook fee, and getFeeGrowthGlobals(poolId) stays (0, 0). Reproduced in test/scratch/JudgeProbe.t.sol::test_dynamicFeePoolInitializesWithZeroLpFee.","severity":"low","snippet":"            !key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != IMD","title":"beforeInitialize accepts v4's dynamic-fee flag although the hook never sets or overrides an LP fee, so such an ETH/IMD pool trades with a permanent 0% LP fee while still paying the 1% hook fee"},{"citation":"resolved","description":"From audit_flow, re-measured. The search is correct: the 85-byte CREATE2 preimage (0xff ++ address(this) ++ salt ++ keccak(initcode ++ args)) is laid out correctly, shl(248, 0xff) / shl(96, address()) have the operands in Yul order (the aderyn incorrect-shift-order lead is a false positive), and the salt is the first match from zero as the brief mandates. Fourteen address bits must match, so the expected number of candidates is 16384 and the tail is geometric: about 3% of deployer addresses need more than 55k candidates (over 7M gas) and about 0.2% need more than 100k (over 13M). The deployer's address, and therefore the salt and the gas, are fixed by the factory address and the launch number, so the deployer service's simulation against gasCeilingWei is exact; a launch that lands in the tail is refused or runs out of gas rather than losing funds. Not a defect against the brief, which requires the increment-from-zero search; recorded so the gas ceiling is set with this distribution in mind. Initcode is 8,320 bytes and LilyGuard's runtime 6,629 bytes, well inside the 49,152 / 24,576 limits.","line":32,"path":"src/LilyGuardDeployer.sol","reproduction":"Deploy bytes.concat(type(LilyGuardDeployer).creationCode, abi.encode(manager, 0x5Bff..., 0x5Bff...)) by create2 from a test contract: factory salt 0 mines hook salt 23658 and the creation consumes 4,424,444 gas; factory salt 1 mines hook salt 75009 and consumes 10,894,670 gas; factory salt 2 mines hook salt 2877 and consumes 1,806,038 gas. Measured in test/scratch/JudgeProbe.t.sol::test_deployerGas with forge 1.8.3, solc 0.8.26, optimizer 200 runs.","severity":"info","snippet":"            for {} 1 {} {","title":"On-chain salt search is unbounded by design: about 126 gas per candidate, 2^14 candidates on average, and the launch constructor needs 1.8M to 10.9M gas depending on the deployer address"}],"hash":"8b7e722ac888be7d9cfb144a943d9104d8ea17cb1db3f22add79b40f4730c4e3","nodeId":"21283b11-e0b9-4540-a86a-4c433e1fa0cf","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json` in the repository root. No source file was changed.\n\n**Outcome:** no critical, high or medium defects. The specialists' nine reports collapse into two low findings and one informational note, all reproduced against the code with a real local v4 PoolManager.\n\n- **Fee base differs by swap mode (low, merged from all five reports).** ETH-specified swaps are charged 1% of the specified amount, token-specified swaps 1% of the ETH the AMM traded. The identical AMM execution pays 0.01 ETH via exact ETH input but 0.0099 ETH via exact IMD output, and the same sell pays 0.01 versus 0.0101 ETH. The README documents the convention and the gap is bounded to 1% of the fee, so I kept it at low rather than the test writer's medium. The specialist proof fails on the current code for exactly this reason and is attached. Suggested fix: compute the exact-input fee as mulDiv(A, 100, 10100) and the exact-output fee as mulDiv(A, 100, 9900).\n- **Dynamic-fee flag accepted at initialization (low, merged from three reports).** A pool keyed with fee 0x800000 registers, stores an LP fee of zero forever, and still pays the 1% hook fee while LPs earn nothing. Fix is a one-line isDynamicFee rejection in the key validation.\n- **Unbounded salt search gas (info).** The search is correct and the aderyn shift-order lead is a false positive. I measured 1.8M to 10.9M gas across three deployer addresses, so the launch gas ceiling must be set from a simulation at the real address.\n\n**Dropped:** the rounding-down note, since it is documented and not farmable.\n\n**Coverage:** all seven entry points answered. claimFees, pause, unpause and unlockCallback hold. afterSwap and beforeSwap carry finding 1, beforeInitialize carries finding 2. Delta conservation, owner powers, the deployer's CREATE2 layout and the manifest were also checked and hold. The accepted suite of 60 tests passes with the assignment seed, and contract sizes are well within limits.","treeHash":null,"usage":{"cachedInputTokens":853467,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":27053,"runtime":"claude","turns":18,"wallClockMs":387582}},{"artifacts":[],"attempt":1,"bundleHash":"fa32b9ad4789926192271f4d9ebd65e13b9d4e0356e2714c3b75bc0eda79f877","device":"990f3cfeaa0e6c9a","findings":[{"description":"The brief requires a 1.0% fee computed on the amount actually traded. For ETH-specified swaps, beforeSwap and afterSwap compute floor(abs(params.amountSpecified)/100), even though the positive beforeSwap delta changes the ETH amount executed by the AMM. Exact ETH input therefore overcharges relative to the executed ETH side, and exact ETH output undercharges. The two token-specified modes already use abs(delta.amount0()) and satisfy the requested relationship. The full-fill check prevents partial-fill overpayment but does not correct this fee-base discrepancy on fully filled swaps. This also affects deferred claims. Existing accepted tests LilyGuardSwapsTest.test_buyExactETHInput, test_sellExactETHOutput, and the specified-ETH fee oracles encode the requested-amount calculation; their success must not be taken as validation of the required executed-amount percentage.","line":163,"path":"src/LilyGuard.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Vm} from \"forge-std/Vm.sol\";\nimport {LilyGuard} from \"src/LilyGuard.sol\";\nimport {LilyGuardDeployer} from \"src/LilyGuardDeployer.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\n\ncontract ExecutedFeeToken {\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n        totalSupply += amount;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ExecutedFeeRouter is IUnlockCallback {\n    IPoolManager private immutable manager;\n    address private immutable controller;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n        controller = msg.sender;\n    }\n\n    function seed(PoolKey memory key) external {\n        require(msg.sender == controller, \"controller\");\n        manager.unlock(abi.encode(key, false, SwapParams(false, 0, 0)));\n    }\n\n    function swap(PoolKey memory key, bool buy, int256 amount) external {\n        require(msg.sender == controller, \"controller\");\n        manager.unlock(\n            abi.encode(\n                key,\n                true,\n                SwapParams(buy, amount, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1)\n            )\n        );\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"manager\");\n        (PoolKey memory key, bool swapping, SwapParams memory params) =\n            abi.decode(data, (PoolKey, bool, SwapParams));\n        BalanceDelta delta;\n        if (swapping) {\n            delta = manager.swap(key, params, \"\");\n        } else {\n            (delta,) = manager.modifyLiquidity(\n                key, ModifyLiquidityParams(-600, 600, 1_000 ether, bytes32(0)), \"\"\n            );\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return \"\";\n    }\n\n    function _settle(Currency currency, int128 delta) private {\n        if (delta < 0) {\n            uint256 owed = uint256(-int256(delta));\n            manager.sync(currency);\n            if (currency.isAddressZero()) {\n                manager.settle{value: owed}();\n            } else {\n                require(ExecutedFeeToken(Currency.unwrap(currency)).transfer(address(manager), owed));\n                manager.settle();\n            }\n        } else if (delta > 0) {\n            manager.take(currency, address(this), uint256(uint128(delta)));\n        }\n    }\n\n    receive() external payable {}\n}\n\ncontract LilyGuardExecutedFeeProofTest is Test {\n    address private constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address private constant OWNER = 0x5Bff2a2FEcD1d6152676813b21346238733bEAB1;\n    IPoolManager private manager;\n    LilyGuard private hook;\n    ExecutedFeeRouter private router;\n    PoolKey private key;\n\n    function setUp() public {\n        manager = IPoolManager(address(new PoolManager(address(this))));\n        vm.etch(IMD, address(new ExecutedFeeToken()).code);\n        hook = (new LilyGuardDeployer(manager, OWNER, OWNER)).hook();\n        key = PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 3_000, 60, IHooks(address(hook)));\n        manager.initialize(key, uint160(1 << 96));\n        router = new ExecutedFeeRouter(manager);\n        vm.deal(address(router), 100_000 ether);\n        ExecutedFeeToken(IMD).mint(address(router), 100_000 ether);\n        router.seed(key);\n    }\n\n    // Expected fee: 0.0099 ETH on core's executed 0.99 ETH input.\n    // Actual fee: 0.01 ETH (1.010101...% of the executed ETH side).\n    function test_exactETHInputFeeIsOnePercentOfExecutedETH() public {\n        _assertExecutedFee(true, -1 ether);\n    }\n\n    // Expected fee: 0.0101 ETH on core's executed 1.01 ETH output.\n    // Actual fee: 0.01 ETH (0.990099...% of the executed ETH side).\n    function test_exactETHOutputFeeIsOnePercentOfExecutedETH() public {\n        _assertExecutedFee(false, 1 ether);\n    }\n\n    // The same property already holds when IMD, rather than ETH, specifies the amount.\n    function test_controlTokenSpecifiedFeeUsesExecutedETH() public {\n        _assertExecutedFee(true, 1 ether);\n    }\n\n    function _assertExecutedFee(bool buy, int256 amount) private {\n        uint256 beforePaid = OWNER.balance;\n        uint256 beforePending = hook.pendingFees(key.toId());\n        vm.recordLogs();\n        router.swap(key, buy, amount);\n        Vm.Log[] memory logs = vm.getRecordedLogs();\n        bytes32 swapTopic = keccak256(\"Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)\");\n        bool found;\n        int128 executedETH;\n        for (uint256 i; i < logs.length; ++i) {\n            if (logs[i].emitter == address(manager) && logs[i].topics[0] == swapTopic) {\n                (executedETH,,,,,) =\n                    abi.decode(logs[i].data, (int128, int128, uint160, uint128, int24, uint24));\n                found = true;\n                break;\n            }\n        }\n        assertTrue(found, \"missing core Swap log\");\n        uint256 actualFee = OWNER.balance - beforePaid + hook.pendingFees(key.toId()) - beforePending;\n        uint256 executed = executedETH < 0 ? uint256(-int256(executedETH)) : uint256(int256(executedETH));\n        assertGt(executed, 0, \"must exercise a real fill\");\n        // Permit one wei of rounding when solving a fee-inclusive exact budget. The discrepancy\n        // on the current implementation is 100 trillion wei, rather than rounding dust.\n        assertApproxEqAbs(actualFee, executed / 100, 1, \"fee must be 1% of the ETH actually traded\");\n    }\n}","reproduction":"Run forge test --match-path test/scratch/LilyGuardExecutedFeeProof.t.sol after materializing the proof below. It uses a local real v4 PoolManager, an ERC20 stand-in only at the specified IMD address, and a settled pool with 1000e18 liquidity at ticks [-600,600], initial sqrtPriceX96=2^96 and LP fee=3000. A buy specifying -1000000000000000000 wei ETH executes -990000000000000000 wei in core but pays 10000000000000000 wei to the recipient; expected floor(abs(coreETH)/100)=9900000000000000. A sell specifying +1000000000000000000 wei ETH executes +1010000000000000000 wei in core but pays 10000000000000000; expected 10100000000000000. The independent oracle reads the core Swap event, not the hook fee event or quoted amount. Both assertions fail with those exact values even allowing one wei of rounding for a fee-inclusive budget; the token-specified control passes. Compute the specified-currency adjustment consistently with a fee of floor(abs(executedETH)/100), while retaining the partial-ETH-fill rejection. Reproduction was run locally with the assignment fuzz seed; 2 tests failed and the control passed.","severity":"medium","title":"ETH-specified swaps charge on the requested amount instead of the executed ETH side"}],"hash":"9dbc42eaf86f4e916e426f2cf5a4a4696ab16f9c3c37a93797bbe3ee1254f59e","nodeId":"a903f8f4-3003-472f-b19c-fc3cc59dcc87","outcome":"completed","summary":"Added failure-path fuzz tests and invariants covering three traders, pool isolation, claims, and paused withdrawals.\n\n`forge build` and seeded `forge test` pass: **60 offline tests**.\n\nReported one medium fee-calculation defect with a reproduced failing proof in [.imd-findings.json](/home/imd6/.identitymd/work/50fae95a-207c-4ecc-93ba-f0d863d885d7/a903f8f4-3003-472f-b19c-fc3cc59dcc87/.imd-findings.json).","treeHash":"f10bd2b19cc8bf5f586f097a8a89164f0df76f57","usage":{"cachedInputTokens":2662045,"inputTokens":145363,"model":"gpt-6.1-sol","outputTokens":29302,"runtime":"codex","turns":7,"wallClockMs":641289}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6b37e4ab65246705","findings":[{"citation":"resolved","description":"Numerical-gap seam (boundary x invariant). The brief's invariant is one fee: 1.0% of the ETH side, computed on the amount actually traded. The hook uses two different bases depending on which currency the router specifies. When ETH is specified (buy with exact ETH input, sell with exact ETH output) the fee is mulDiv(|amountSpecified|, 100, 10000), i.e. 1% of the trader-side ETH figure (gross budget on a buy, net proceeds on a sell), while the AMM actually trades amountSpecified +/- fee. When IMD is specified the fee is 1% of delta.amount0(), the ETH the AMM actually traded. Consequences, measured on a 1000-ether-liquidity 0.3% pool at price 1:1 (test/scratch probe): a buy paying gross 1.014053170522578748 ETH receives 1.000000000000000000 IMD with fee 0.010040130401213650 ETH in exact-IMD-output mode, but only 0.999900099990010000 IMD with fee 0.010140531705225787 ETH in exact-ETH-input mode: same gross ETH, 100,401,304,012,137 wei more fee and 99,900,009,990,000 wei less IMD. On the sell side an exact-IMD-input of 1 IMD yields net 0.986046911229504184 ETH with fee 0.009960069810399032, whereas requesting that same net ETH as exact output charges fee 0.009860469112295041 and costs only 0.999899900309979939 IMD. So the exact-ETH-input fee is 1/99 = 1.0101% of the ETH the AMM traded, the exact-ETH-output fee is 1/101 = 0.990% of the ETH the AMM supplied, and only the two token-specified modes pay exactly 1.00% of traded ETH. The README documents the gross/net convention, so this is a documented design choice with a ~1%-of-fee magnitude, not a loss-of-funds bug; it is reported because it contradicts the literal requirement 'computed on the amount actually traded' and makes the effective rate depend on router mode. Fix preserving design: in beforeSwap for exact ETH input compute fee so that fee == 1% of (A - fee), i.e. fee = mulDiv(A, 100, 10100); for exact ETH output fee = mulDiv(A, 100, 9900) so fee == 1% of (A + fee); keep afterSwap's expected = amountSpecified + fee check unchanged.","line":163,"path":"src/LilyGuard.sol","reproduction":"Setup: fresh PoolManager, LilyGuard via LilyGuardDeployer, pool key (ETH, IMD, fee 3000, spacing 60, hook), initialize at sqrtPrice 2^96, add 1000e18 liquidity in ticks [-600, 600]. Swap 1 (snapshot, then revert): zeroForOne=true, amountSpecified=+1e18 (exact IMD out), limit MIN_SQRT_PRICE+1. Observed: trader ETH delta -1014053170522578748, fee 10040130401213650, IMD +1e18. Swap 2 from the same state: zeroForOne=true, amountSpecified=-1014053170522578748 (exact ETH in, same gross). Observed: fee 10140531705225787, IMD +999900099990010000. Expected per brief: equal fee (1% of ETH actually traded, 1003912638817352961 wei traded -> 10039126388173529 wei fee) and equal output for the same gross ETH. Actual: fee differs by 100401304012137 wei and output by 99900009990000 wei.","severity":"low","snippet":"            fee = _fee(_abs(params.amountSpecified));","title":"Fee basis differs by swap mode: exact-ETH swaps are charged 1% of the trader's ETH, token-specified swaps 1% of the AMM's ETH, so the same trade pays a different fee and the exact-ETH-input fee is not"},{"citation":"resolved","description":"Boundary input not covered by _validateKey: key.fee == LPFeeLibrary.DYNAMIC_FEE_FLAG (0x800000). v4's Hooks.isValidHookAddress allows a dynamic-fee pool on any hook with flags, and PoolManager.initialize stores lpFee = 0 for it. Only the hook may later call updateDynamicLPFee or return an override from beforeSwap; LilyGuard does neither (it returns lpFeeOverride 0 without the override flag), so the pool runs with a 0 LP fee forever while the hook's 1% fee still applies. Since initialization is permissionless and all state is per PoolId, this does not hurt the creator's own pool, but a creator or storefront that picks the dynamic flag by mistake gets a pool that pays LPs nothing. Verified: initializing (ETH, IMD, 0x800000, 60, hook) at 2^96 succeeds, getSlot0 reports lpFee 0, a 1 ETH exact-input buy returns 989020869339354039 IMD (no LP fee) with the hook fee charged. Optional hardening consistent with the design: revert in beforeInitialize when key.fee.isDynamicFee().","line":219,"path":"src/LilyGuard.sol","reproduction":"manager.initialize(PoolKey(Currency.wrap(address(0)), Currency.wrap(IMD), 0x800000, 60, IHooks(hook)), 2^96) -> succeeds (expected per 'any other initialization reverts' semantics applied to a fee tier the hook cannot administer: revert). manager.getSlot0(poolId).lpFee == 0; after adding liquidity, swap(zeroForOne=true, amountSpecified=-1e18) executes with lpFee 0 and hook fee 0.01 ETH.","severity":"info","snippet":"            !key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != IMD","title":"beforeInitialize accepts the dynamic-fee flag, creating an ETH/IMD pool whose LP fee is permanently zero because the hook never sets or overrides it"},{"citation":"resolved","description":"Math-precision zero-rounding check. mulDiv rounds toward zero; the Pashov checklist prefers fees to round up. Measured: exact ETH input of 99 wei -> fee 0; 100 wei -> fee 1; 199 wei -> fee 1 (0.50%). Splitting a trade into sub-100-wei swaps to avoid the fee costs about 10^5 gas per 99 wei (1e-16 ETH) of volume, so the leak cannot be farmed; the README states 'dust below one wei produces no fee'. Recorded for completeness and to confirm the rounding direction is intentional. If desired, FullMath.mulDivRoundingUp would make the fee never zero on a non-zero ETH leg; the afterSwap expected check and beforeSwap delta would stay consistent since both call the same _fee.","line":229,"path":"src/LilyGuard.sol","reproduction":"Pool as in finding 1. swap(zeroForOne=true, amountSpecified=-99) -> recipient balance unchanged, pendingFees 0 (fee 0). swap(zeroForOne=true, amountSpecified=-199) -> fee 1 wei. Expected if fees rounded up: 1 wei and 2 wei respectively.","severity":"info","snippet":"        return FullMath.mulDiv(amount, FEE_BPS, BPS_DENOMINATOR).toInt128();","title":"Hook fee rounds down, so trades under 100 wei of ETH pay no fee and 100-199 wei pay a flat 1 wei (0.5%-1%); not economically exploitable"}],"hash":"a2959e84a22c3b81a0250a58482acb9acc36634ba29209f2a714437b55b8ea83","nodeId":"ecb1ccd4-d7b3-4b69-8365-b3e84afa3e96","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result. The findings file is valid JSON, snippet lines match the tree, and the scratch tests are removed, leaving only the findings file as a change.\n\n**Outcome: no critical, high or medium defects in the math.** The hook's fee arithmetic, delta accounting and boundary handling hold up against the Math Precision, Boundary and Numerical Gap guides.\n\nWhat I verified, by tracing against the vendored v4-core and running the suite plus my own probes:\n\n- **Delta accounting** nets to zero on every path. The hook debits the fee with `take` or `mint` inside afterSwap and the PoolManager credits it back afterwards. Redemption burns then takes the same amount.\n- **Partial-fill guard** is exact. For a fully filled ETH-specified swap the pool's delta equals the specified amount plus fee to the wei, and any shortfall reverts before payment. Empty pools and price-limit stops both revert as the brief requires.\n- **Casts and extremes** are safe. The absolute-value helper handles the signed minimum, and oversized amounts revert through checked casts before the pool would have rejected them anyway.\n- **Salt mining** lays out the CREATE2 preimage correctly and masks the right 14 bits. Aderyn's shift-order warning is a false positive.\n\nThree findings were recorded, all with concrete inputs:\n\n1. **Low.** The fee basis differs by swap mode. Exact-ETH swaps are charged 1% of the trader's ETH figure, token-specified swaps 1% of the ETH the AMM traded. For the same gross ETH on a buy, the exact-input mode charges about 1% more fee and returns less IMD. The README documents this convention, but it departs from the brief's \"computed on the amount actually traded.\"\n2. **Info.** beforeInitialize accepts the dynamic-fee flag, which yields a pool with a permanently zero LP fee since the hook never sets or overrides it.\n3. **Info.** Fees round down, so sub-100-wei trades pay nothing. This is not farmable given gas costs.\n\nCoverage rows answer all seven listed entry points plus three invariants. The one area I did not independently exercise is a live-chain simulation of the deployer's unbounded salt search gas at the real deployment address, which the README itself assigns to the launch operator.","treeHash":null,"usage":{"cachedInputTokens":811674,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":33058,"runtime":"claude","turns":22,"wallClockMs":499347}}],"verification":[{"checks":[{"durationMs":2951,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.85s\nCompiler run successful!\n","passed":true},{"durationMs":696,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/LilyGuardClaims.t.sol:LilyGuardClaimsTest\n[PASS] test_claimsStayIsolatedAcrossFeeTiersAndSpacingAndCanBeClaimedPaused() (gas: 2168775)\n[PASS] test_directPayoutDoesNotAutomaticallyRedeemOldClaims() (gas: 1037874)\n[PASS] test_emptyPoolCannotPayAQuotedFeeForNoTrade() (gas: 593561)\n[PASS] test_firstBuyOnTokenOnlyFreshManagerMintsThenRedeemsClaim() (gas: 892544)\n[PASS] test_firstExactTokenOutputBuyAlsoDefersETHFee() (gas: 786966)\n[PASS] test_nestedManagerSwapDuringFeePaymentIsRejected() (gas: 1409972)\n[PASS] test_recipientRejectingETHDefersFeesWithoutFreezingBuysOrSells() (gas: 2398543)\n[PASS] test_reentryDuringClaimCannotRedeemTwiceOrPause() (gas: 1802804)\n[PASS] test_reentryIntoClaimFeesDuringDirectPayoutIsRejected() (gas: 1169803)\n[PASS] test_unfundedRedemptionRollsBackAndCanBeRetried() (gas: 932415)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 12.82ms (11.97ms CPU time)\n\nRan 14 tests for test/LilyGuardAccess.t.sol:LilyGuardAccessTest\n[PASS] testFuzz_specifiedFeeAndOnlySpecifiedETHDelta(uint128,bool) (runs: 256, μ: 66291, ~: 66318)\nLogs:\n  Bound result 3\n\n[PASS] test_disabledCallbacksAlwaysRefuse() (gas: 118140)\n[PASS] test_enabledCallbacksAndUnlockRefuseNonManagerCallers() (gas: 122125)\n[PASS] test_existingTokenAndLiveSupplyArePoolScoped() (gas: 247626)\n[PASS] test_extremeSpecifiedAmountsRevertWithSafeCastRatherThanArithmeticOverflow() (gas: 128161)\n[PASS] test_failedInitializationLeavesNoPoolRegistration() (gas: 199000)\n[PASS] test_initializationRejectsAnyOtherToken() (gas: 137740)\n[PASS] test_initializationRejectsNonNativeCurrency0() (gas: 135664)\n[PASS] test_initializationRejectsReversedCurrencies() (gas: 36515)\n[PASS] test_managerCallbacksRejectAKeyNamingAnotherHook() (gas: 162784)\n[PASS] test_onlyTheGivenOwnerMayPauseAndUnpause() (gas: 202764)\n[PASS] test_ownerHasNoOtherAdminEntryPoints() (gas: 230511)\n[PASS] test_unknownPoolsCannotUseTokenSupplyPauseClaimsOrSwapCallbacks() (gas: 285580)\n[PASS] test_unsolicitedManagerUnlockCallbackIsRejected() (gas: 44175)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 19.82ms (8.28ms CPU time)\n\nRan 8 tests for test/LilyGuardDeployer.t.sol:LilyGuardDeployerTest\n[PASS] test_deployerForwardsDistinctOwnerAndRecipientWithoutTakingPrivileges() (gas: 29254)\n[PASS] test_deployerHasNoPostLaunchDeployOrAdminFunctions() (gas: 96425)\n[PASS] test_directHookDeploymentAtWrongFlagsReverts() (gas: 12947)\n[PASS] test_launchParametersOnEmptyChainAndExactlyTwoCreationsWithNoExternalCalls() (gas: 78851)\n[PASS] test_permissionReportExactlyMatchesAddressBits() (gas: 18188)\n[PASS] test_runtimeSizesInitcodeSizesAndAbsenceOfEscapeOpcodes() (gas: 4618029)\n[PASS] test_saltIsTheFirstMatchingCandidateStartingAtZero() (gas: 3426493)\n[PASS] test_zeroConstructorArgumentsAreRejected() (gas: 4038)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 41.61ms (76.25ms CPU time)\n\nRan 14 tests for test/LilyGuardSwaps.t.sol:LilyGuardSwapsTest\n[PASS] testFuzz_allSwapModesConserveFunds(uint96,uint8) (runs: 256, μ: 345319, ~: 346055)\nLogs:\n  Bound result 100\n\n[PASS] testFuzz_pauseIsolation(uint24) (runs: 256, μ: 524075, ~: 523979)\nLogs:\n  Bound result 16834\n\n[PASS] test_addLiquidityAndDonateWhilePaused() (gas: 369743)\n[PASS] test_buyExactETHInput() (gas: 362764)\n[PASS] test_buyExactIMDOutput() (gas: 343704)\n[PASS] test_feesRoundDownToWei() (gas: 631938)\n[PASS] test_partialETHInputRevertsAndRollsBackEverything() (gas: 387012)\n[PASS] test_partialETHOutputRevertsAndRollsBackEverything() (gas: 379859)\n[PASS] test_partialTokenInputChargesOnlyExecutedETH() (gas: 340521)\n[PASS] test_partialTokenOutputChargesOnlyExecutedETH() (gas: 343646)\n[PASS] test_pauseOnlyStopsItsPoolSwapsAndUnpauseRestoresThem() (gas: 1262102)\n[PASS] test_removeLiquidityAndCollectWhilePaused() (gas: 678512)\n[PASS] test_sellExactETHOutput() (gas: 341962)\n[PASS] test_sellExactIMDInput() (gas: 340469)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 42.21ms (64.20ms CPU time)\n\nRan 1 test for test/LilyGuardInvariant.t.sol:LilyGuardInvariantTest\n[PASS]\nLilyGuardInvariantTest invariants:\n[PASS] invariant_ETHAndIMDAreConservedAndNeverHeldByHook\n[PASS] invariant_everyFeeIsEitherPaidOrAccountedAsAPoolClaim\n[PASS] invariant_pauseStateIsPoolSpecificAndManagerAlwaysRelocks\n LilyGuardInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭------------------+-----------------------+-------+---------+----------╮\n| Contract         | Selector              | Calls | Reverts | Discards |\n+=======================================================================+\n| LilyGuardHandler | claim                 | 594   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | collect               | 633   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setPaused             | 635   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setRecipientRejection | 597   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | swap                  | 613   | 0       | 0        |\n╰------------------+-----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 9999999999014065\n  Bound result 9999999999010654\n  Bound result 9999999999009604\n  Bound result 9999999999003367\n  Bound result 9999999999001358\n  Bound result 9999999999000378\n  Bound result 6021764184347516\n  Bound result 9999999999008437\n  Bound result 9999999999002066\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 591.66ms (560.61ms CPU time)\n\nRan 5 test suites in 593.26ms (708.13ms CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":49,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LilyGuard.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LilyGuard.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"LilyGuard.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LilyGuard.claimFees(bytes32)\",\"LilyGuard.pause(bytes32)\",\"LilyGuard.unlockCallback(bytes)\",\"LilyGuard.unpause(bytes32)\"],\"files\":{\".gitignore\":3,\"DEPENDENCIES.json\":20,\"LICENSE\":23,\"README.md\":175,\"docs/SECURITY.md\":97,\"docs/hook-configuration.json\":28,\"foundry.toml\":21,\"launch.json\":14,\"remappings.txt\":3,\"src/LilyGuard.sol\":295,\"src/LilyGuardDeployer.sol\":44,\"test/LilyGuardAccess.t.sol\":199,\"test/LilyGuardClaims.t.sol\":191,\"test/LilyGuardDeployer.t.sol\":167,\"test/LilyGuardInvariant.t.sol\":161,\"test/LilyGuardSwaps.t.sol\":170,\"test/LilyGuardTestBase.sol\":152,\"test/mocks/MockIMD.sol\":32,\"test/mocks/PoolActor.sol\":79,\"test/mocks/RecipientProbe.sol\":27},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"38710a950a484f01afe4de839516ed2af6dd0d8d2a2a3604615cfc9a4aa2e864","verifiedTreeHash":"9f63a56a81e633e9cca3c58bfe0f94f1e0ba4648","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":2609,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.52s\nCompiler run successful!\n","passed":true},{"durationMs":600,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/LilyGuardClaims.t.sol:LilyGuardClaimsTest\n[PASS] test_claimsStayIsolatedAcrossFeeTiersAndSpacingAndCanBeClaimedPaused() (gas: 2168775)\n[PASS] test_directPayoutDoesNotAutomaticallyRedeemOldClaims() (gas: 1037874)\n[PASS] test_emptyPoolCannotPayAQuotedFeeForNoTrade() (gas: 593561)\n[PASS] test_firstBuyOnTokenOnlyFreshManagerMintsThenRedeemsClaim() (gas: 892544)\n[PASS] test_firstExactTokenOutputBuyAlsoDefersETHFee() (gas: 786966)\n[PASS] test_nestedManagerSwapDuringFeePaymentIsRejected() (gas: 1409972)\n[PASS] test_recipientRejectingETHDefersFeesWithoutFreezingBuysOrSells() (gas: 2398543)\n[PASS] test_reentryDuringClaimCannotRedeemTwiceOrPause() (gas: 1802804)\n[PASS] test_reentryIntoClaimFeesDuringDirectPayoutIsRejected() (gas: 1169803)\n[PASS] test_unfundedRedemptionRollsBackAndCanBeRetried() (gas: 932415)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 34.61ms (11.63ms CPU time)\n\nRan 8 tests for test/LilyGuardDeployer.t.sol:LilyGuardDeployerTest\n[PASS] test_deployerForwardsDistinctOwnerAndRecipientWithoutTakingPrivileges() (gas: 29254)\n[PASS] test_deployerHasNoPostLaunchDeployOrAdminFunctions() (gas: 96425)\n[PASS] test_directHookDeploymentAtWrongFlagsReverts() (gas: 12947)\n[PASS] test_launchParametersOnEmptyChainAndExactlyTwoCreationsWithNoExternalCalls() (gas: 78851)\n[PASS] test_permissionReportExactlyMatchesAddressBits() (gas: 18188)\n[PASS] test_runtimeSizesInitcodeSizesAndAbsenceOfEscapeOpcodes() (gas: 4618029)\n[PASS] test_saltIsTheFirstMatchingCandidateStartingAtZero() (gas: 3426493)\n[PASS] test_zeroConstructorArgumentsAreRejected() (gas: 4038)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 43.65ms (57.16ms CPU time)\n\nRan 14 tests for test/LilyGuardAccess.t.sol:LilyGuardAccessTest\n[PASS] testFuzz_specifiedFeeAndOnlySpecifiedETHDelta(uint128,bool) (runs: 256, μ: 66291, ~: 66318)\nLogs:\n  Bound result 1269008577203171243379386588\n\n[PASS] test_disabledCallbacksAlwaysRefuse() (gas: 118140)\n[PASS] test_enabledCallbacksAndUnlockRefuseNonManagerCallers() (gas: 122125)\n[PASS] test_existingTokenAndLiveSupplyArePoolScoped() (gas: 247626)\n[PASS] test_extremeSpecifiedAmountsRevertWithSafeCastRatherThanArithmeticOverflow() (gas: 128161)\n[PASS] test_failedInitializationLeavesNoPoolRegistration() (gas: 199000)\n[PASS] test_initializationRejectsAnyOtherToken() (gas: 137740)\n[PASS] test_initializationRejectsNonNativeCurrency0() (gas: 135664)\n[PASS] test_initializationRejectsReversedCurrencies() (gas: 36515)\n[PASS] test_managerCallbacksRejectAKeyNamingAnotherHook() (gas: 162784)\n[PASS] test_onlyTheGivenOwnerMayPauseAndUnpause() (gas: 202764)\n[PASS] test_ownerHasNoOtherAdminEntryPoints() (gas: 230511)\n[PASS] test_unknownPoolsCannotUseTokenSupplyPauseClaimsOrSwapCallbacks() (gas: 285580)\n[PASS] test_unsolicitedManagerUnlockCallbackIsRejected() (gas: 44175)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 43.74ms (7.84ms CPU time)\n\nRan 14 tests for test/LilyGuardSwaps.t.sol:LilyGuardSwapsTest\n[PASS] testFuzz_allSwapModesConserveFunds(uint96,uint8) (runs: 256, μ: 345319, ~: 346055)\nLogs:\n  Bound result 100\n\n[PASS] testFuzz_pauseIsolation(uint24) (runs: 256, μ: 524075, ~: 523979)\nLogs:\n  Bound result 98896\n\n[PASS] test_addLiquidityAndDonateWhilePaused() (gas: 369743)\n[PASS] test_buyExactETHInput() (gas: 362764)\n[PASS] test_buyExactIMDOutput() (gas: 343704)\n[PASS] test_feesRoundDownToWei() (gas: 631938)\n[PASS] test_partialETHInputRevertsAndRollsBackEverything() (gas: 387012)\n[PASS] test_partialETHOutputRevertsAndRollsBackEverything() (gas: 379859)\n[PASS] test_partialTokenInputChargesOnlyExecutedETH() (gas: 340521)\n[PASS] test_partialTokenOutputChargesOnlyExecutedETH() (gas: 343646)\n[PASS] test_pauseOnlyStopsItsPoolSwapsAndUnpauseRestoresThem() (gas: 1262102)\n[PASS] test_removeLiquidityAndCollectWhilePaused() (gas: 678512)\n[PASS] test_sellExactETHOutput() (gas: 341962)\n[PASS] test_sellExactIMDInput() (gas: 340469)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 43.73ms (65.05ms CPU time)\n\nRan 1 test for test/LilyGuardInvariant.t.sol:LilyGuardInvariantTest\n[PASS]\nLilyGuardInvariantTest invariants:\n[PASS] invariant_ETHAndIMDAreConservedAndNeverHeldByHook\n[PASS] invariant_everyFeeIsEitherPaidOrAccountedAsAPoolClaim\n[PASS] invariant_pauseStateIsPoolSpecificAndManagerAlwaysRelocks\n LilyGuardInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭------------------+-----------------------+-------+---------+----------╮\n| Contract         | Selector              | Calls | Reverts | Discards |\n+=======================================================================+\n| LilyGuardHandler | claim                 | 594   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | collect               | 633   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setPaused             | 635   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setRecipientRejection | 597   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | swap                  | 613   | 0       | 0        |\n╰------------------+-----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 9999999999014065\n  Bound result 9999999999010654\n  Bound result 9999999999009604\n  Bound result 9999999999003367\n  Bound result 9999999999001358\n  Bound result 9999999999000378\n  Bound result 6021764184347515\n  Bound result 9999999999008437\n  Bound result 9999999999002066\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 505.52ms (491.56ms CPU time)\n\nRan 5 test suites in 507.23ms (671.25ms CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LilyGuard.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LilyGuard.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"LilyGuard.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LilyGuard.claimFees(bytes32)\",\"LilyGuard.pause(bytes32)\",\"LilyGuard.unlockCallback(bytes)\",\"LilyGuard.unpause(bytes32)\"],\"files\":{\".gitignore\":3,\"DEPENDENCIES.json\":20,\"LICENSE\":23,\"README.md\":175,\"docs/SECURITY.md\":97,\"docs/hook-configuration.json\":28,\"foundry.toml\":21,\"remappings.txt\":3,\"src/LilyGuard.sol\":295,\"src/LilyGuardDeployer.sol\":44,\"test/LilyGuardAccess.t.sol\":199,\"test/LilyGuardClaims.t.sol\":191,\"test/LilyGuardDeployer.t.sol\":167,\"test/LilyGuardInvariant.t.sol\":161,\"test/LilyGuardSwaps.t.sol\":170,\"test/LilyGuardTestBase.sol\":152,\"test/mocks/MockIMD.sol\":32,\"test/mocks/PoolActor.sol\":79,\"test/mocks/RecipientProbe.sol\":27},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":810,"exitCode":0,"name":"slither","output":"[low/medium] reentrancy-benign at src/LilyGuard.sol:203: Reentrancy in LilyGuard._payFee(PoolId,uint256) (src/LilyGuard.sol#203-215):\n[low/medium] reentrancy-events at src/LilyGuard.sol:190: Reentrancy in LilyGuard.unlockCallback(bytes) (src/LilyGuard.sol#190-201):","passed":true},{"durationMs":326,"exitCode":0,"name":"aderyn","output":"[high] incorrect-shift-order at src/LilyGuardDeployer.sol:29: Incorrect Assembly Shift Parameter Order\n[low] centralization-risk at src/LilyGuard.sol:106: Centralization Risk (2 places)\n[low] large-numeric-literal at src/LilyGuard.sol:29: Large Numeric Literal\n[low] non-reentrant-not-first at src/LilyGuard.sol:106: `nonReentrant` is Not the First Modifier (5 places)\n[low] state-change-without-event at src/LilyGuard.sol:131: State Change Without Event (2 places)\n[low] unchecked-return at src/LilyGuard.sol:107: Unchecked Return (5 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"47b4c613a1305f87c058b27fa851354d58e371d6e6a7ea8b7caabeabb9421831","verifiedTreeHash":"744a2e3e24e27eb44cd08018bf80f731df5fb36a","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":8053,"exitCode":0,"name":"build","output":"Compiling 79 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.89s\nCompiler run successful!\n","passed":true},{"durationMs":8507,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/LilyGuardClaims.t.sol:LilyGuardClaimsTest\n[PASS] test_claimsStayIsolatedAcrossFeeTiersAndSpacingAndCanBeClaimedPaused() (gas: 2168775)\n[PASS] test_directPayoutDoesNotAutomaticallyRedeemOldClaims() (gas: 1037874)\n[PASS] test_emptyPoolCannotPayAQuotedFeeForNoTrade() (gas: 593561)\n[PASS] test_firstBuyOnTokenOnlyFreshManagerMintsThenRedeemsClaim() (gas: 892544)\n[PASS] test_firstExactTokenOutputBuyAlsoDefersETHFee() (gas: 786966)\n[PASS] test_nestedManagerSwapDuringFeePaymentIsRejected() (gas: 1409972)\n[PASS] test_recipientRejectingETHDefersFeesWithoutFreezingBuysOrSells() (gas: 2398543)\n[PASS] test_reentryDuringClaimCannotRedeemTwiceOrPause() (gas: 1802804)\n[PASS] test_reentryIntoClaimFeesDuringDirectPayoutIsRejected() (gas: 1169803)\n[PASS] test_unfundedRedemptionRollsBackAndCanBeRetried() (gas: 932415)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 20.72ms (16.90ms CPU time)\n\nRan 8 tests for test/LilyGuardDeployer.t.sol:LilyGuardDeployerTest\n[PASS] test_deployerForwardsDistinctOwnerAndRecipientWithoutTakingPrivileges() (gas: 29254)\n[PASS] test_deployerHasNoPostLaunchDeployOrAdminFunctions() (gas: 96425)\n[PASS] test_directHookDeploymentAtWrongFlagsReverts() (gas: 12947)\n[PASS] test_launchParametersOnEmptyChainAndExactlyTwoCreationsWithNoExternalCalls() (gas: 78851)\n[PASS] test_permissionReportExactlyMatchesAddressBits() (gas: 18188)\n[PASS] test_runtimeSizesInitcodeSizesAndAbsenceOfEscapeOpcodes() (gas: 4618029)\n[PASS] test_saltIsTheFirstMatchingCandidateStartingAtZero() (gas: 3426493)\n[PASS] test_zeroConstructorArgumentsAreRejected() (gas: 4038)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 206.11ms (95.03ms CPU time)\n\nRan 14 tests for test/LilyGuardAccess.t.sol:LilyGuardAccessTest\n[PASS] testFuzz_specifiedFeeAndOnlySpecifiedETHDelta(uint128,bool) (runs: 256, μ: 66289, ~: 66318)\nLogs:\n  Bound result 1269008577203171243379386588\n\n[PASS] test_disabledCallbacksAlwaysRefuse() (gas: 118140)\n[PASS] test_enabledCallbacksAndUnlockRefuseNonManagerCallers() (gas: 122125)\n[PASS] test_existingTokenAndLiveSupplyArePoolScoped() (gas: 247626)\n[PASS] test_extremeSpecifiedAmountsRevertWithSafeCastRatherThanArithmeticOverflow() (gas: 128161)\n[PASS] test_failedInitializationLeavesNoPoolRegistration() (gas: 199000)\n[PASS] test_initializationRejectsAnyOtherToken() (gas: 137740)\n[PASS] test_initializationRejectsNonNativeCurrency0() (gas: 135664)\n[PASS] test_initializationRejectsReversedCurrencies() (gas: 36515)\n[PASS] test_managerCallbacksRejectAKeyNamingAnotherHook() (gas: 162784)\n[PASS] test_onlyTheGivenOwnerMayPauseAndUnpause() (gas: 202764)\n[PASS] test_ownerHasNoOtherAdminEntryPoints() (gas: 230511)\n[PASS] test_unknownPoolsCannotUseTokenSupplyPauseClaimsOrSwapCallbacks() (gas: 285580)\n[PASS] test_unsolicitedManagerUnlockCallbackIsRejected() (gas: 44175)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 206.27ms (178.16ms CPU time)\n\nRan 14 tests for test/LilyGuardSwaps.t.sol:LilyGuardSwapsTest\n[PASS] testFuzz_allSwapModesConserveFunds(uint96,uint8) (runs: 256, μ: 345353, ~: 346060)\nLogs:\n  Bound result 538069437642419981\n\n[PASS] testFuzz_pauseIsolation(uint24) (runs: 256, μ: 524075, ~: 523979)\nLogs:\n  Bound result 86234\n\n[PASS] test_addLiquidityAndDonateWhilePaused() (gas: 369743)\n[PASS] test_buyExactETHInput() (gas: 362764)\n[PASS] test_buyExactIMDOutput() (gas: 343704)\n[PASS] test_feesRoundDownToWei() (gas: 631938)\n[PASS] test_partialETHInputRevertsAndRollsBackEverything() (gas: 387012)\n[PASS] test_partialETHOutputRevertsAndRollsBackEverything() (gas: 379859)\n[PASS] test_partialTokenInputChargesOnlyExecutedETH() (gas: 340521)\n[PASS] test_partialTokenOutputChargesOnlyExecutedETH() (gas: 343646)\n[PASS] test_pauseOnlyStopsItsPoolSwapsAndUnpauseRestoresThem() (gas: 1262102)\n[PASS] test_removeLiquidityAndCollectWhilePaused() (gas: 678512)\n[PASS] test_sellExactETHOutput() (gas: 341962)\n[PASS] test_sellExactIMDInput() (gas: 340469)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 206.18ms (291.77ms CPU time)\n\nRan 11 tests for test/LilyGuardFailureEdges.t.sol:LilyGuardFailureEdgesTest\n[PASS] testFuzz_nativePoolWithAnyOtherTokenCannotRegister(uint160) (runs: 1000, μ: 407075, ~: 407084)\nLogs:\n  Bound result 7923\n\n[PASS] testFuzz_nonNativeCurrency0CannotRegisterEvenWhenCurrenciesAreSorted(uint160) (runs: 1000, μ: 407381, ~: 407221)\nLogs:\n  Bound result 28165730240872246255460810298544889081128519194\n\n[PASS] testFuzz_unknownPoolCannotRedeemPauseOrEraseAnotherPoolsClaims(bytes32) (runs: 1000, μ: 1516695, ~: 1517016)\n[PASS] test_duplicateInitializationCannotClearPauseOrExistingClaims() (gas: 1503748)\n[PASS] test_emptyTokenSpecifiedSwapsCannotManufactureFeesOrOutput() (gas: 1277773)\n[PASS] test_exactFeeBalancePaysDirectly() (gas: 1110912)\n[PASS] test_liquidityExhaustionRollsBackBothETHSpecifiedDirectionsAndOldClaims() (gas: 2717030)\n[PASS] test_oneWeiShortOfFeeDefersWholeFeeAndCanRedeemAfterSettlement() (gas: 1189906)\n[PASS] test_oneWeiTokenSpecifiedTradesRoundFeeToZero() (gas: 861142)\n[PASS] test_unrelatedCreatorCanInitializeSameFeeDifferentSpacingWhileFirstPoolPaused() (gas: 1396851)\n[PASS] test_zeroAmountSwapCannotPayOrAccrueAClaim() (gas: 579742)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 206.75ms (552.22ms CPU time)\n\nRan 1 test for test/LilyGuardInvariant.t.sol:LilyGuardInvariantTest\n[PASS]\nLilyGuardInvariantTest invariants:\n[PASS] invariant_ETHAndIMDAreConservedAndNeverHeldByHook\n[PASS] invariant_everyFeeIsEitherPaidOrAccountedAsAPoolClaim\n[PASS] invariant_pauseStateIsPoolSpecificAndManagerAlwaysRelocks\n LilyGuardInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭------------------+-----------------------+-------+---------+----------╮\n| Contract         | Selector              | Calls | Reverts | Discards |\n+=======================================================================+\n| LilyGuardHandler | claim                 | 594   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | collect               | 633   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setPaused             | 635   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | setRecipientRejection | 597   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| LilyGuardHandler | swap                  | 613   | 0       | 0        |\n╰------------------+-----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 9999999999014065\n  Bound result 499999500\n  Bound result 10000000000000000\n  Bound result 9999999999003366\n  Bound result 10000000000000000\n  Bound result 9999999999000378\n  Bound result 499999500\n  Bound result 499999500\n  Bound result 10000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.05s (1.02s CPU time)\n\nRan 2 tests for test/LilyGuardLifecycleInvariant.t.sol:LilyGuardLifecycleInvariantTest\n[PASS]\nLilyGuardLifecycleInvariantTest invariants:\n[PASS] invariant_claimsAndPauseAreIsolatedByTheEntirePoolId\n[PASS] invariant_multipleTradersConserveETHAndTheExistingTokenSupply\n LilyGuardLifecycleInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭---------------------------+-----------------------+-------+---------+----------╮\n| Contract                  | Selector              | Calls | Reverts | Discards |\n+================================================================================+\n| LilyGuardLifecycleHandler | changeLiquidity       | 2873  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | claim                 | 2963  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | collect               | 2991  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | setPaused             | 2899  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | setRecipientRejection | 2857  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | swap                  | 2903  | 0       | 0        |\n|---------------------------+-----------------------+-------+---------+----------|\n| LilyGuardLifecycleHandler | unauthorizedPause     | 2994  | 0       | 0        |\n╰---------------------------+-----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 111255953\n  Bound result 1334\n  Bound result 5363\n  Bound result 50000000000000000\n  Bound result 105320580\n  Bound result 50000000000000000\n  Bound result 1697316610299424597\n  Bound result 38466722820447066\n  Bound result 10000000000000000\n  Bound result 2512\n  Bound result 1978\n  Bound result 1983\n  Bound result 10000000000000000\n  Bound result 5000000000000000000\n  Bound result 1913\n  Bound result 7153\n  Bound result 10000000000000000\n  Bound result 3364511341\n  Bound result 456\n  Bound result 10000000000000000\n  Bound result 46472265655691808\n  Bound result 2121\n  Bound result 10000000000000000\n  Bound result 2871\n  Bound result 15437\n\n[PASS] test_lifecycleExitsAfterRejectedPayoutsPausesAndCompleteWithdrawals() (gas: 2826960)\nLogs:\n  Bound result 10000000000000000\n  Bound result 10000000000000000\n  Bound result 5000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.32s (8.30s CPU time)\n\nRan 7 test suites in 8.32s (10.22s CPU time): 60 tests passed, 0 failed, 0 skipped (60 total tests)\n","passed":true},{"durationMs":102,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LilyGuard.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"LilyGuard.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"LilyGuard.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"LilyGuard.claimFees(bytes32)\",\"LilyGuard.pause(bytes32)\",\"LilyGuard.unlockCallback(bytes)\",\"LilyGuard.unpause(bytes32)\"],\"files\":{\".gitignore\":3,\"DEPENDENCIES.json\":20,\"LICENSE\":23,\"README.md\":175,\"docs/SECURITY.md\":97,\"docs/hook-configuration.json\":28,\"foundry.toml\":21,\"remappings.txt\":3,\"src/LilyGuard.sol\":295,\"src/LilyGuardDeployer.sol\":44,\"test/LilyGuardAccess.t.sol\":199,\"test/LilyGuardClaims.t.sol\":191,\"test/LilyGuardDeployer.t.sol\":167,\"test/LilyGuardFailureEdges.t.sol\":212,\"test/LilyGuardInvariant.t.sol\":161,\"test/LilyGuardLifecycleInvariant.t.sol\":320,\"test/LilyGuardSwaps.t.sol\":170,\"test/LilyGuardTestBase.sol\":152,\"test/README.md\":38,\"test/mocks/MockIMD.sol\":32,\"test/mocks/PoolActor.sol\":79,\"test/mocks/RecipientProbe.sol\":27},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9dbc42eaf86f4e916e426f2cf5a4a4696ab16f9c3c37a93797bbe3ee1254f59e","verifiedTreeHash":"f10bd2b19cc8bf5f586f097a8a89164f0df76f57","verifierVersion":"0.1.0+560ab6a2"}]}