{"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":"75a9e0a5-b2dd-48c0-9aa1-2bb05916cf92","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"6d07b2b2971406786f8cb736e0b256bf8f1de9664e2b9149422535eca86d316a","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":"022ff0d2a6dacc5a830943bbc218f57c344f67a91b5e675a49846231f0ee2dbc","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":"fcb7210513f9da9915e9f8fdca4b85f3eafef53586de0ef2d383a50564c99aea","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":"182a0a864131fc5a13268b5d8103bd6e181849e057617150b77d9b8791a965d9","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":"d1cfd84226696a039942435b2a83d6e515b216b7055672a8496f17a96849e472","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":"cd3fd5aced422933df7ad830928e1df8b8be8660d334f4e05f492c449b4318e0","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":"494475f9b72995a369befa68850eff0ff9c2ed28193cca866b938ecadc1577e7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"17854e1b8a6600e18e3f30c95c1149340f78c159c395a5d35b433566e5641342","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":"MAD: Ethereum custom token + Uniswap v4 hook, paired with FWA. Holders pay to freeze others' MAD; launch tax burns FWA; fees buy NFTs for each round's top freezer via IMD's oracle.\n\nLAUNCH CONTEXT\nuniv4_hook, chainId 1, pairWith fwa, customToken true. Preserve pool/remainder settings. Launching wallet and explicit owner ($owner): 0xFc3C962FAD2C1cC77f1a0d46e7B8a2De79A21774, NOT payer/receiver/factory. Existing receiver: 0x4e2b3f2d201666d29bf1ce7aa665c8fdf768b481, resolved by app before submission. Immutable, nonzero; no setter/new receiver. No website.\n\nToken Name: Mutually Assured Destruction\nToken Symbol: MAD \n\n18 decimals, initial supply 1,000,000,000 MAD. No-argument constructor mints all to msg.sender (launch factory).\n\nTOKEN\n- freeze(target): caller pays floor(target MAD balance / 100); revert at zero. Burn floor(cost / 2), send remainder to target. Freeze target until now + 24 hours; another freeze resets, never adds time.\n- Target is nonzero, not caller, with no code or exactly the 23-byte EIP-7702 designation (0xef0100 + address). Never freeze other contracts.\n- Frozen wallets cannot send via transfer, transferFrom, sale or liquidity add on any pool. They can receive/pay freeze costs. Only freeze's internal debit/burn bypasses this, not general transfers/allowances.\n- No transfer tax, later minting, privileged freeze or confiscation.\n- Track full freeze costs per caller per round; highest cumulative spend leads, ties first to reach the total.\n- Only bound hook calls token.closeRound() at current block. freeze reverts for that block's remainder; next round starts next block. No overlapping ranges.\n- bindHook() binds once during the intended launch hook's beforeInitialize in the atomic launch. Authenticate binding; no arbitrary binding/rebinding.\n- Event Frozen(address indexed by, address indexed target, uint256 cost, uint64 until). Views: frozenUntil, freezeCost, currentRound, roundBlocks(round), spent(round, account), leader(round).\n\nHOOK AND FEES\n- MadHook: beforeInitialize binds token; beforeSwap/afterSwap return fee deltas. Authenticate PoolManager and intended MAD/FWA pool. Support both directions, exact input/output; never tax internal conversions again.\n- Swaps start closed, owner included. Only owner enables trading, once irreversibly via enableTrading(). Gate swaps only, not initialization/liquidity.\n- Fee on the FWA side: 99% when trading opens, linearly falling to 1% in 1,800 seconds: max(1%, 99% - 98% * elapsed / 1800). Precise integer math, explicit rounding, no duplicate callback charge.\n- Resting 1% applies during decay too: 0.50% Playground in FWA, 0.50% NFT budget converted to ETH. No separate MAD protocol share or protocol recipient. Percentages use the FWA-side fee base, NOT collected fees. Burn everything above 1% in the same swap: take that FWA from PoolManager into hook and call FWA.burn(exactAmount).\n- Accrue Playground's share as FWA ERC-6909 claims. Public payout() redeems claims directly PoolManager -> immutable receiver; never restricted hook -> receiver ERC20 transfers. Keep Playground and NFT budgets separate; no double payout or cross-subsidizing.\n- Playground's 0.50% is ERC20/FWA revenue; plain-call ETH instructions apply only to native ETH. No inline Playground conversion, hub/wallet payout or receiver.swapToETH call. Trading/payout work before setup. Existing adapter handles later authorized conversion; do not deploy one or change whitelists. Existing hub handles distribution; no duplicate split or IMD requester fees.\n- ONLY NFT share sells for ETH in the same swap on official ETH/FWA pool, per fwa-v2, within the current PoolManager unlock and owner-set sqrtPriceLimitX96. Unsold NFT claims wait for next swap; only actual ETH proceeds fund NFTs. Separate from Playground's adapter.\n\nNFTS AND ROUNDS\n- Per fwa-v2, public buy() outside blackout when NFT budget (including ETH claims) covers quoteAcquisitionPrice().total: redeem needed claims, acquire(address(this), 1, fee + margin, 0, slippage). Public claim(requestId) calls keepNFT within settlement window. Overpayment and withdrawAcquisitionRefund return to budget.\n- Public hook.closeRound(): if an unassigned kept NFT and current round leader exist, close round and assign oldest such NFT. No duplicate assignment.\n- Public askWinner(round): >=8 blocks after round ends, no winner/award/live ask. Intake v2 requestWithFailure per oracle-consumer, funded by donated IMD; pay live Intake.priceOf. Retry failed/unanswered-after-6-hours asks; reject superseded responses.\n- OracleQuestion constructs body and hash: chainId 1, absolute round block window, answerType address[], head 1, evidence chain, panel 30, quorum 20, validForSeconds 3600. Question with lowercase token address:\n\"Who spent most MAD freezing wallets in round {N} of token {mad} on Ethereum, blocks {first} to {last} inclusive? Sum cost in MAD base units from that token's Frozen(address indexed by, address indexed target, uint256 cost, uint64 until) events, grouped by by, ranked highest sum first. Ties: first to reach that total in block/transaction/log order.\"\n- Callback verifies sender, request, question hash, signer, consumer, chain, validity and replay protection per reference. Store first valid nonzero answer address; move nothing.\n- Public award(round) sends NFT to winner, or token.leader(round) if no answer arrived within 72 hours of closure. Failed transfer retains NFT for retry; success is final, with no duplicate award or late beneficiary change.\n\nADMINISTRATION AND SIZE\nOwner may only enable trading once, set FWA V2 address, ETH/FWA pool key, price/slippage bounds and oracle settings permitted by reference. No protocol-recipient setter or changing Playground recipient, fee rates, freeze rules or results. No arbitrary withdrawal of FWA/ETH/IMD/NFTs; only specified flows. Guard reentrancy.\nRuntime <=24,576 bytes each. If optimized hook is too large, constructor-deploy MadRewards for NFT/oracle operations. Hook retains FWA claims; forward only NFT-budget ETH to helper. Helper holds IMD/NFTs and closes token rounds only via hook. Preserve every rule.\n\nETHEREUM ADDRESSES\nFWA: 0xa0Df17B5aC76ABaBA36E1450E2cbCd18A620C845\nPoolManager: 0x000000000004444c5dc75cB358380D2e3dE08A90\nETH/FWA pool: currency0 zero (native ETH), currency1 FWA, fee 0, tickSpacing 60, hooks 0x2C67ebA8A50AF0dB5Fba55F725247a75CbDA6444. Hook fees still apply.\nFWA V2: 0x958C41181182e76F221331b2755b77D9e1426A98\nIntake v2: 0xa43e6f75ee006411f79ac1c84120606c2330de82\nIMD: 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7\nOracle signer: 0x5598aa9146215bc13eb26f2c692ad1461fd32982\n\nTESTS\nBoth swap directions/exact input-output; closed trading, owner-only one-time enable, 0/15/30-minute decay, exact burn/rounding, price-bound partial NFT sale. Equal 0.50% Playground/NFT shares, no MAD protocol cut/setter, direct PoolManager FWA payout to immutable receiver, separate budgets/no double payout, no adapter dependency. All freeze/send paths, frozen caller freeze payment, EIP-7702 vm.etch, contracts, binding, boundaries/ties. FWA V2/Intake mocks and reference conformance: refunds, wrong hash, stale/replayed callbacks, failed ask retry, 72-hour fallback, NFT retry/no double award. Fork tests when MAINNET_RPC_URL set: real FWA buys/sells during decay/at rest, burn, official-pool NFT sale, receiver payout, freeze. Mocks do not prove live integration.\n\nPlayground revenue (Ethereum, hub 0xdd4a83daf532778c55f8806c9adf9a0f0ebb62de): the IMD payer is project 1's receiver 0x4e2b3f2d201666d29bf1ce7aa665c8fdf768b481.\nPay Playground's share to it.\nPay it ETH with a plain call; anyone then calls depositETH() on it.\nTokens sent to it wait for Playground's swap adapters.\nPlayground splits what it receives: do not split it again.\nThe pool's requester fees already go to the receiver: do not add or redirect them.","parentJobId":null,"planHash":"15b9df6e829a1599f639a8f692299aedfec95fda9db1efb9eea1587b4d42d614","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"75a9e0a5-b2dd-48c0-9aa1-2bb05916cf92","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2422-mad-ethereum-custom-token"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52419","feedbackHash":"638f1e3c502cbd355c68a82d9f29ed1406c9dd6421fd0a92ee48ce43d5288b12","nodeKey":"audit_economics","submissionHash":"6d07b2b2971406786f8cb736e0b256bf8f1de9664e2b9149422535eca86d316a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51032","feedbackHash":"bc6988013b8a992214eda608e3a9fe9b76adba37fb4a07c09ca25ac2f95320f1","nodeKey":"audit_flow","submissionHash":"022ff0d2a6dacc5a830943bbc218f57c344f67a91b5e675a49846231f0ee2dbc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52667","feedbackHash":"236f89f0b9725aef0daec560a4cfb5b5beeefd25bbae2a15081a29fe29907441","nodeKey":"audit_judge","submissionHash":"fcb7210513f9da9915e9f8fdca4b85f3eafef53586de0ef2d383a50564c99aea","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52207","feedbackHash":"a6e825c7c1c5edb7efdc0ef7390f6ceddff1935859cad6d3329b488de29cb619","nodeKey":"audit_judge","submissionHash":"22227c87e9c589208355ebf91b1b943c2dc4ad525cfe1a64163e9aa5165e8120","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52180","feedbackHash":"65fe62a56c2818a3c81d3c8c5337b82ad2ea645e14611408b75460491a50f0ad","nodeKey":"audit_math","submissionHash":"182a0a864131fc5a13268b5d8103bd6e181849e057617150b77d9b8791a965d9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51512","feedbackHash":"b1d70307be6406016b1ea597aba3ee53f2443231d31d6ecb043cc13c9bf29652","nodeKey":"audit_permissions","submissionHash":"d1cfd84226696a039942435b2a83d6e515b216b7055672a8496f17a96849e472","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51339","feedbackHash":"88bf9a7d45ba67dbf8a53ce22f9448a111e4e9c67321caf0b407daea07b04b80","nodeKey":"build_contract_project","submissionHash":"cd3fd5aced422933df7ad830928e1df8b8be8660d334f4e05f492c449b4318e0","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51087","feedbackHash":"4570755e4ade9bf631c70d840203c703a4a1e187de87f44d45a30918e711976b","nodeKey":"build_contract_project","submissionHash":"dd7374e3e376658f0258033efb7931d741e5f2a7a119a1b3bf93fc733594ed93","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"53917","feedbackHash":"00a0e2876681753285fa542bd24d6828b10f29b7a2e1ef0b2b01c772b1f842c1","nodeKey":"manifest","submissionHash":"6b9739fe02a16b32f2c230b610cb4b85e958a7be9f5d1839f3b463a27c761d61","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52397","feedbackHash":"8b6b7dc09986f0cd9867f1d6ede6243aa3cbe2a36619e8a57e772fe675b1e1a2","nodeKey":"manifest","submissionHash":"494475f9b72995a369befa68850eff0ff9c2ed28193cca866b938ecadc1577e7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51413","feedbackHash":"a34c805aa6dd07a86e1d8f401e1ba0fd0557326088a2aa2188f12c74c93d7940","nodeKey":"write_foundry_tests","submissionHash":"646d59e5d5e91ccf9650ff047fd131d0c91ec1cb1747163d7cfc477f48172773","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52734","feedbackHash":"54eaab8c55748f34627d3c1c5abcd58178a4b82a655c52bca4292d7794357d4d","nodeKey":"write_foundry_tests","submissionHash":"17854e1b8a6600e18e3f30c95c1149340f78c159c395a5d35b433566e5641342","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"7d5a100b02c2e06508209c95bfc3863c4ab60357c1b556809f1f732cbb5b7434","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a9662a76cb5f51d","findings":[{"citation":"resolved","description":"_convertNFT() runs inside beforeSwap/afterSwap of every fee-bearing MAD swap. When the official ETH/FWA market's sqrt price is below sqrtPriceLimitX96 the hook sells the NFT FWA claims immediately, then requires the net ETH proceeds to meet minEthPerFwaX128 and otherwise reverts Slippage(), which reverts the trader's whole MAD swap. The official market hook keeps 1% of the ETH side (fwa-v2 reference), so with a consistent pair of bounds (limit at the price that equals the minimum) every market price in the ~1% band just above the floor makes net proceeds fall below the minimum. In that state no MAD swap in any direction can execute until the owner changes the bounds, and the band is re-entered every time the market drifts there. The price-limit branch (`current >= limit` at line 271) retains the claims gracefully; only the minimum-output branch turns an unfillable NFT sale into a trading halt. The brief says unsold NFT claims wait for the next swap and that conversion must never block trading; the fix is to skip the sale (or sell nothing) and keep the claims when proceeds would be below the minimum, instead of reverting the enclosing swap. The same branch also fires for dust trades: a swap whose FWA base is between 200 wei and a few hundred wei yields a 1-wei NFT share whose sale returns 0 ETH after rounding, and `proceeds == 0` with `sold > 0` reverts that swap too.","line":277,"path":"src/MadHook.sol","reproduction":"State: trading enabled and at rest (>=1800 s after enableTrading). Owner calls setConversionBounds(2*Q96, 1<<126): sell up to sqrt price 2*Q96 (4 FWA per ETH, 0.25 ETH/FWA floor) and require at least 0.25 ETH per FWA. Official ETH/FWA market initialized at Q96 with 1000 ETH range liquidity; any third party sells FWA on that market with sqrtPriceLimitX96 = 1.998*Q96 so the market rests at ~0.2505 ETH/FWA (still below the hook's limit, so the sale branch runs). Then a user buys MAD with 1,000 FWA exact input. Expected: the swap executes, the 5 FWA NFT share stays as claims for a later swap. Actual: _convertNFT sells 5 FWA, the market hook keeps 1% of the ETH, proceeds < ceil(5e18 * 2^126 / 2^128) and the hook reverts Slippage(); the PoolManager wraps it and the user's MAD swap fails. Run `forge test --match-path test/scratch/ConvertSlippageDoS.t.sol`: FAIL with WrappedError(..., Slippage()).","severity":"medium","snippet":"        if (\n            sold > offered\n                || (sold > 0\n                    && (proceeds == 0 || proceeds < Math.mulDiv(sold, minEthPerFwaX128, 1 << 128, Math.Rounding.Ceil)))\n        ) revert Slippage();","title":"NFT conversion's minimum-output check reverts the user's MAD swap whenever the ETH/FWA market sits inside the fee band above the owner's floor"},{"citation":"resolved","description":"_update() only enforces frozenUntil when isWallet(from) is true. freeze() already refuses contract targets, so the extra isWallet(from) condition can only matter for an address that was a wallet when frozen and has code afterwards. That is exactly a counterfactual CREATE2 address (a not-yet-deployed Safe, account factory wallet, or any contract whose address the holder controls): it holds MAD, gets frozen by a payer, then its owner deploys the contract there and the account can transfer, sell and add liquidity for the remaining freeze time. The freezer paid 1% of the target's balance (half burned) for a freeze the target voids at will. The brief says frozen wallets cannot send via any path; the check should enforce frozenUntil[from] regardless of the sender's current code (freeze() already guarantees contracts never get a nonzero frozenUntil).","line":110,"path":"src/MadToken.sol","reproduction":"ALICE holds 1,000,000 MAD at an address with no code (in production: a CREATE2 address whose deployment ALICE controls). BOB calls freeze(ALICE): BOB pays 10,000 MAD, frozenUntil[ALICE] = now + 24h. ALICE's transfer(CAROL, 1e18) reverts WalletFrozen. Deploy any contract at ALICE's address (test: vm.etch(ALICE, code); production: the CREATE2 factory call). ALICE's transfer(CAROL, 1e18) now succeeds while frozenUntil[ALICE] > block.timestamp. Expected: still WalletFrozen until the 24 hours pass.","severity":"low","snippet":"        if (from != address(0) && frozenUntil[from] > block.timestamp && isWallet(from)) {","title":"A frozen wallet escapes its freeze by acquiring code: the frozen check is skipped once the sender has non-7702 bytecode"},{"citation":"resolved","description":"For an FWA-specified swap _collect() runs in beforeSwap, before the router has settled the swapper's FWA input. poolManager.take(FWA, hook, burned) does an immediate ERC-20 transfer out of the manager's balance, so it is funded only by FWA the manager already holds (other pools' liquidity and claims). During the decay the burn is up to 98% of the input, so any buy whose input exceeds roughly manager_FWA_balance / 0.98 reverts with the FWA token's ERC20InsufficientBalance although the trade itself was fine. The ETH leg already guards this case (line 287 checks address(poolManager).balance and mints a claim otherwise); the FWA burn leg does not. A fix that keeps the brief's same-swap burn where possible: when FWA.balanceOf(poolManager) < burned, mint the burn amount as a hook claim into a separate burnClaims counter and expose a public function that unlocks, burns the claim, takes the FWA and calls FWA.burn, so the trade never reverts and nothing above 1% is ever kept.","line":257,"path":"src/MadHook.sol","reproduction":"Using the project's test harness (test/Base.t.sol): _open(0) so the fee is 99%; manager holds ~1,000,000 FWA from the launch pool's liquidity; mint ALICE 10,000,000 FWA. ALICE buys MAD with exact input = 3 * fwa.balanceOf(manager) through TestRouter (which settles after the swap). Expected: the buy executes (the swapper's input covers the burn once settled). Actual: beforeSwap -> _collect -> poolManager.take(FWA, hook, ~2.94M) reverts with ERC20InsufficientBalance(poolManager, ~1.0M, ~2.94M) wrapped in WrappedError; the swap fails. See test/scratch/Leads.t.sol test_bigBuyAtOpenRevertsWhenManagerLacksFwa (log shows the revert data).","severity":"low","snippet":"            poolManager.take(fwa, address(this), burned);","title":"Same-swap FWA burn is paid from the PoolManager's existing FWA reserves, so an exact-input buy larger than those reserves reverts under a post-paying router"},{"citation":"resolved","description":"beforeSwap charges the fee on the full specified FWA amount and afterSwap refuses any fill that differs from expectedSpecifiedFwa. A v4 swap that reaches the trader's price limit consumes less input (or yields less output) and is otherwise valid, but here it reverts the transaction. Routers that protect users with a price limit (rather than amountOutMinimum) therefore get reverts instead of partial fills on every FWA-specified route. The README documents this; it is recorded here so the judge can decide whether it meets the brief's 'support both directions, exact input/output'. No value is at risk.","line":236,"path":"src/MadHook.sol","reproduction":"Trading at rest. ALICE swaps FWA exact input 100,000 FWA with sqrtPriceLimitX96 0.1% away from the current price. Expected (plain v4 semantics): a partial fill up to the limit. Actual: afterSwap reverts PartialSpecifiedFill() (selector 0x03d3ff71) and the whole swap fails. See test/scratch/Leads.t.sol test_tightLimitFwaExactInputReverts.","severity":"info","snippet":"            if (base != expectedSpecifiedFwa) revert PartialSpecifiedFill();","title":"FWA-specified swaps that stop at the trader's sqrtPriceLimitX96 revert with PartialSpecifiedFill instead of filling partially"}],"hash":"022ff0d2a6dacc5a830943bbc218f57c344f67a91b5e675a49846231f0ee2dbc","nodeId":"52b925b6-9795-4d25-a140-09ba68069767","outcome":"completed","summary":"Review complete. The findings file is written and validated, and nothing in the tracked tree was changed (findings file and scratch tests are gitignored).\n\n**Outcome**\n\nFour findings, one with a self-contained failing proof, in `.imd-findings.json` with coverage rows for all 33 entry points.\n\n1. **Medium, with proof** at `src/MadHook.sol:277`. The NFT conversion's minimum-output check reverts the user's MAD swap. The official market hook keeps 1% of the ETH, so with a consistent limit/minimum pair, any market price inside the ~1% band above the owner's floor makes every MAD swap in both directions revert with `Slippage()` until the owner changes the bounds. The price-limit branch retains claims gracefully; only this branch halts trading. Dust-sized trades hit the same branch via `proceeds == 0`. Proof: `test/scratch/ConvertSlippageDoS.t.sol`, which fails on the current code with a wrapped `Slippage()` from inside `beforeSwap`.\n2. **Low** at `src/MadToken.sol:110`. The frozen-sender check is skipped once the sender has non-7702 code, so a frozen counterfactual CREATE2 address escapes its freeze by deploying the contract. Reproduced with `vm.etch` after a real freeze.\n3. **Low** at `src/MadHook.sol:257`. The same-swap burn is funded by the PoolManager's existing FWA reserves before a post-paying router settles. A 99%-fee buy with input above roughly those reserves reverts with `ERC20InsufficientBalance` from the manager. The ETH leg guards this case; the FWA burn leg does not. Reproduced against the project's harness.\n4. **Info** at `src/MadHook.sol:236`. FWA-specified swaps that stop at the trader's price limit revert with `PartialSpecifiedFill` instead of filling partially. The README documents this; recorded for the judge.\n\n**Coverage of my area (Execution Trace, Periphery, First Principles)**\n\n- Traced both swap directions and both exact modes through v4's delta accounting; fee, shares and burn sum to zero and no callback charges twice.\n- Traced the atomic launch binding, round closing, NFT buy/claim/assign/award, and the oracle ask/result/failure/supersede/fallback state machine. All hold as specified.\n- Periphery: `IFWAV2` struct layouts and status codes match the verified FWA V2 ABI; `IIntake.requestWithFailure` and the callback signatures match the oracle reference; the question text matches the brief; `OracleQuestion` and `OracleAttestation` match the reference copies.\n- Existing suite passes (58 tests), including oracle delivery under the 200k stipend.\n\n**Not reached**\n\n- Fork tests need `MAINNET_RPC_URL`, so live FWATokenHook behavior toward a hook as swap sender and the real PoolManager's FWA/ETH reserves are unverified.\n- Whether IMD's production factory deploys the token, the hook and the pool in one transaction could not be checked offline. The binding design reverts the launch if it does not, as the README states.","treeHash":null,"usage":{"cachedInputTokens":2452002,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":53527,"runtime":"claude","turns":41,"wallClockMs":938609}},{"artifacts":[],"attempt":1,"bundleHash":"773abea9bfdb9aebea11f121046380ec6ce6351083bcc0cda8dd191787e69962","device":"d32559488518f947","findings":[{"description":"The brief defines public hook.closeRound() as: 'if an unassigned kept NFT and current round leader exist, close round and assign oldest such NFT.' The revision adds two more conditions, MIN_ROUND_BLOCKS = 7200 (line 58: block.number + 1 >= first + 7200) and MIN_LEADER_SPEND = 1000 ether (line 60: spent(round, leader) >= 1000 MAD). Neither is in the brief, the owner cannot change them, and together they change who wins and when NFTs are given out. Every round now lasts at least about a day, and a round whose leader spent less than 1,000 MAD can never close until someone spends more. The change guards against a real race (a dust freeze capturing an NFT as soon as one is kept), but it alters the requested reward rule and should be confirmed by the requester rather than accepted silently. My delivered tests do not assert these minimums are correct; they only roll forward far enough to satisfy them so the rest of the lifecycle can be tested.","line":396,"path":"src/MadHook.sol","reproduction":"Using the repository's MadRewardsFixture: keep one NFT (hook.buy(); pool.fulfill(id); hook.claim(id)), then from ALICE call token.freeze(BOB) (cost 10,000 MAD, so ALICE is the round leader), roll 10 blocks and call hook.closeRound(). Expected per the brief: round 1 closes and the NFT is assigned. Actual: reverts NoRoundReward() until block first + 7199. Separately, a leader whose total round spend is 999 MAD gets NoRoundReward() at every block, although a leader and an unassigned kept NFT both exist.","severity":"medium","title":"closeRound adds a 7,200-block minimum round length and a 1,000 MAD minimum leader spend that the brief does not specify"},{"description":"The revision removed the post-swap Slippage check in _convertNFT. minEthPerFwaX128 is now compared only with the price implied by the sqrt price limit (setConversionBounds line 177 and _convertNFT line 310), never with what the sale actually returned. The sqrt limit bounds the pool's execution price, but whatever the market hook's afterSwap takes from the ETH output is not checked at all. The fwa-v2 reference says to 'set a minimum output'. As written, the setting called a minimum never stops a sale: if the ETH/FWA market's hook takes more than expected (today's official hook takes 1%, which by itself puts every sale near the bound about 1% below a minimum set equal to the bound price), the NFT budget is sold below the owner's configured minimum and the swap does not revert. Restoring the check would revert traders' swaps. A better fix is to skip the conversion and keep the claims when a quote is below the minimum, or to describe the parameter as a bound consistency check only.","line":310,"path":"src/MadHook.sol","reproduction":"Fixture MadTestBase. limit = Q96 + Q96/1000; _market(limit); OWNER calls setConversionBounds(limit, hook.ethPerFwaX128(limit)), i.e. a minimum of 0.998 ETH per FWA. vm.etch the market hook (0x2C67...6444) with a hook whose afterSwap mints itself 50% of amount0 and returns that as its delta. _open(1800); ALICE exact-input buys MAD with 10,000 FWA. Expected: proceeds >= sold * minEthPerFwaX128 / 2^128 = 0.998002996004994006 ETH for 1 FWA sold, or no sale. Actual: NFTConverted(sold = 1e18, ethReceived = 499500499500499500), so 0.4995 ETH is accepted into the NFT budget and nothing reverts.","severity":"low","title":"Owner-set minEthPerFwaX128 is no longer enforced on actual NFT-share sale proceeds"},{"description":"The brief asks to 'burn everything above 1% in the same swap'. When the PoolManager's FWA ERC-20 balance is below the burn amount (an exact-input buy whose router settles after the swap, during the 99% launch decay), _collect mints a burnClaims ERC-6909 claim instead, and the burn happens later through the permissionless flushBurn(). Accounting is sound: test/MadBurnDeferral.t.sol shows over random sequences that deferred claims are separate from Playground and NFT claims, are never paid out or sold, and that flushBurn destroys exactly the deferred amount once. On mainnet the PoolManager holds the ETH/FWA pool's FWA, so deferral should be rare, but strictly the burn is no longer always in the same swap. This needs requester acknowledgement only.","line":357,"path":"src/MadHook.sol","reproduction":null,"severity":"info","title":"Above-1% burn can be deferred out of the swap that charged it"}],"hash":"17854e1b8a6600e18e3f30c95c1149340f78c159c395a5d35b433566e5641342","nodeId":"2cac00d4-efee-430e-b10b-b68a42d135dd","outcome":"completed","summary":"The implementation was revised after my previous round, and five of my tests broke against it. I updated them, added a new suite for the changed code, and reported three issues instead of writing tests that treat them as correct. The full `forge test` run with the supplied seed passes: 109 passed, 0 failed, and the optional mainnet fork suite skipped because no RPC URL is set.\n\n**Repaired tests** (they failed because the rules changed, not because of bugs):\n- **Round closing:** `closeRound` now also needs a minimum round length and a minimum spend by the leader. The question-text test and the repurchase test now read the real block window, and the rewards invariant setup rolls forward far enough to close a round.\n- **Conversion bounds:** the setter now rejects a minimum price higher than the price limit allows, so my conversion handler sets the strictest minimum it will accept.\n\n**New suite, `test/MadBurnDeferral.t.sol`:**\n- **Deferred burns:** a random-sequence (invariant) test that makes buys too large for the PoolManager's FWA balance during the launch decay, mixed with sells, payouts and `flushBurn`. Over 256 sequences (15,360 calls, no reverts) it checks that the deferred-burn claim:\n  - stays separate from the Playground and NFT budgets;\n  - is never paid out or sold;\n  - is destroyed exactly once.\n\n  It also checks that every claim is backed by FWA held in the PoolManager and that FWA supply is conserved.\n- **Exact-output fee:** a 1,000-run fuzz test of exact-output buys and sells. It checks the fee is exactly `ceil(rate × gross FWA paid)`, the smallest fee that covers the schedule, checked against what the trader actually paid or received. There is also one fixed 1% rounding case.\n\n**Reported in `.imd-findings.json`:**\n- **Medium:** `closeRound` now requires rounds of at least 7,200 blocks (`MIN_ROUND_BLOCKS`) and a leader spend of at least 1,000 MAD (`MIN_LEADER_SPEND`). The brief only requires an unassigned NFT and a round leader. These limits change when and to whom NFTs go, so the requester should confirm them. My tests do not treat them as correct; they only move far enough ahead to meet them.\n- **Low:** the owner's minimum ETH-per-FWA price (`minEthPerFwaX128`) is no longer checked against what the NFT-share sale actually returns. I confirmed this with a market hook that takes 50%: 1 FWA sold for 0.4995 ETH against a configured minimum of 0.998, and nothing reverted.\n- **Info:** when the PoolManager holds too little FWA, the burn above 1% is not done in the same swap as the brief asks. It is kept as a claim and burned later by anyone calling `flushBurn()`. The new suite shows the accounting stays correct.\n\nThe low finding has a written reproduction but no standalone proof file, since proofs are only required for critical and high findings.\n\nThe fork tests have not been run against the revised hook, so live checks on real FWA are still owed. `test/TESTING.md` now records this, along with the new suite and the findings. All changes are under `test/`, apart from `.imd-findings.json` in the repository root.","treeHash":"dee89e2c4a3932d98786b10036d634278e375f61","usage":{"cachedInputTokens":3384448,"inputTokens":76,"model":"claude-opus-5-5","outputTokens":36536,"runtime":"claude","turns":39,"wallClockMs":1119231}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c722c2e9ac9aa084","findings":[{"citation":"resolved","description":"For a buy where MAD is the specified (exact-output) side, FWA is the unspecified input and afterSwap charges fee = floor(base * R / D) where base is the FWA the AMM consumed, then adds it as a surcharge, so the swapper pays base + fee. For every other mode the fee is R of the swapper-facing gross FWA (exact-input buys deduct R of gross input; exact-output sells gross up with ceil(N*R/(D-R)) so fee/gross = R). Only this mode uses fee/gross = R/(1+R). At open (R = 99%) that is 0.99/1.99 = 49.7%, and the same quantity of MAD that costs 100 FWA by exact-input costs 1.99 FWA by exact-output: a sniper buys the launch at 2% of the intended price. At 15 minutes (R = 50%) the effective rate is 33.3% instead of 50%; at rest (1%) the error is 0.01 points, which is why the fuzz suite, which asserts the surcharge formula instead of the brief, does not notice. The brief fixes the fee at max(1%, 99% - 98%*elapsed/1800) of the FWA side for both directions and both exact modes. README line 46 states that exact-output buys deliberately promise only a pool-side surcharge, which documents the gap rather than closing it. Fix: for the FWA-unspecified-input mode gross up the same way the exact-output sell does: fee = ceil(base * R / (D - R)) so fee / (base + fee) = R; keep floor for FWA-unspecified-output (sell exact-input MAD), which already charges R of the gross output.","line":238,"path":"src/MadHook.sol","reproduction":"Fresh launch, pool 1:1 with 1e24 full-range liquidity, owner calls enableTrading() at t0 (feeNumerator = 17,820,000 / 18,000,000). Swap 1: ALICE buys MAD with exact-input 100e18 FWA: FeeCharged fee = 99e18, ALICE receives M MAD (~0.98e18). Swap 2: ALICE buys exact-output M MAD. Expected: fee about 99% of the FWA she pays, i.e. gross about 100e18 again. Actual: gross FWA paid = 1,990,003,930,253,881,124 (1.99 FWA), fee = 990,001,955,251,930,811 (99,000,195,525,193,081,100 as fee*100 vs 197,010,389,095,134,231,276 as gross*99), 4,974 bps of gross. The scratch test prints both routes and fails on the fee/gross assertion.","severity":"high","snippet":"            fee = feeFor(base); // floor of actual FWA exchanged by the pool","title":"Exact-output MAD buys pay the launch tax on the pool-side base only: 99% opening fee collapses to 49.7% of the FWA paid"},{"citation":"resolved","description":"_convertNFT runs inside beforeSwap/afterSwap of every MAD swap. It defers (returns, keeps the claims) only when current >= limit. When the official pool's sqrt price sits one or a few units below the limit, the exact-input sale to the limit fills a handful of base units: v4 rounds amountIn up and amountOut down, and the official hook's 1% fee floors to zero or one, so sold = 1..3 and proceeds = 0, or proceeds is below ceil(sold * minEthPerFwaX128 / 2^128). The check then reverts Slippage, PoolManager wraps it in HookCallFailed, and the MAD swapper's whole trade reverts. The state that triggers it is produced by the hook's own design: a bounded sale stops exactly at the limit, and the next buy of any size on the ETH/FWA pool (buys are open there per fwa-v2) moves the price under it. One wei of ETH is enough. Nothing in the reverted transaction changes state, so every later MAD swap that carries nftFwaClaims > 0 reverts too, until the official pool moves far enough from the limit or the owner changes the bounds. A griefer can re-arm it after every market move for dust, or sandwich each MAD swap with a 1 wei buy. Boundary x precision: the slippage guard is correct for a real fill and wrong for a rounding-sized one, and that case is exempted from the 'stale bound retains claims' path. Fix: decide before swapping. Compute the FWA sellable to the limit (SqrtPriceMath.getAmount1Delta(current, limit, liquidity)) and skip the sale, keeping the claims, when it is below a dust threshold; or tolerate rounding in the minimum (compare proceeds + 1 against the floor of sold * min / 2^128) and treat proceeds == 0 on a dust fill as a deferral rather than a revert.","line":277,"path":"src/MadHook.sol","reproduction":"Launch at rest (1% fee). Official ETH/FWA pool at 1 ETH/FWA with 1000 ETH liquidity; owner setConversionBounds(limit = Q96 + Q96/10000, min = 0.98 * 2^128). Swap 1: ALICE buys MAD with 1000e18 FWA: NFT claims sell up to the limit, slot0.sqrtPriceX96 == limit (partial fill, claims kept, as intended). Swap 2: anyone buys FWA on the ETH/FWA pool with 1 wei of ETH (zeroForOne exact-input); sqrtPrice is now below the limit by a few units. Swap 3: ALICE buys MAD with 1000e18 FWA. Expected: the trade executes and the conversion is deferred with claims kept. Actual: revert HookCallFailed(hook, 0x7dd37f70 = Slippage()) from MadHook.beforeSwap; every later MAD swap reverts the same way while the price sits there. The scratch test asserts the third swap succeeds and fails with that wrapped error.","severity":"medium","snippet":"        if (\n            sold > offered\n                || (sold > 0\n                    && (proceeds == 0 || proceeds < Math.mulDiv(sold, minEthPerFwaX128, 1 << 128, Math.Rounding.Ceil)))\n        ) revert Slippage();","title":"In-swap NFT conversion reverts the swapper's trade on a dust fill: one wei bought on the ETH/FWA pool just under the owner's limit blocks every MAD swap"},{"citation":"resolved","description":"freeze() accepts any target with no code, which includes a counterfactual contract address (a CREATE2 smart wallet or Safe that has received MAD before deployment). The send-side check in _update re-evaluates isWallet(from) at transfer time, so once code longer than 23 bytes appears at the frozen address the freeze no longer applies, inside the 24-hour window. The holder deploys the wallet and moves the full balance, keeping the 0.5% the freezer paid them; the freezer's payment bought nothing. The brief freezes the target until now + 24 hours; a 7702 designation is the only code the spec allows to change the wallet's shape, and it stays 23 bytes. Fix: record the wallet decision at freeze time and apply the stored frozenUntil regardless of later code (keep isWallet only in freeze), or at least exclude addresses that gain non-7702 code from receiving the 0.5% remainder.","line":110,"path":"src/MadToken.sol","reproduction":"Deploy MadToken; transfer 1000e18 MAD to address A = 0xC0FFEE (no code). Call freeze(A): frozenUntil[A] = now + 24h, A receives 5e18. prank(A) transfer(address(1), 1) reverts WalletFrozen as intended. vm.etch(A, <any contract code>) (on mainnet: the CREATE2 deployment of the wallet at A). From A, transfer the whole balance: succeeds; balanceOf(address(1)) == 1005e18 within the freeze window. The scratch test test/scratch/CounterfactualEscape.t.sol passes on this code, showing the escape.","severity":"low","snippet":"        if (from != address(0) && frozenUntil[from] > block.timestamp && isWallet(from)) {","title":"A frozen address with no code escapes the freeze by deploying code at that address (counterfactual CREATE2 wallet)"}],"hash":"182a0a864131fc5a13268b5d8103bd6e181849e057617150b77d9b8791a965d9","nodeId":"208f466a-7de7-49b8-85da-a7e1532bb904","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds three substantiated findings and a coverage row for every listed entry point.\n\n**Findings**\n\n1. **High. Exact-output MAD buys dodge the launch tax.** `MadHook.afterSwap` charges the fee on the pool-side FWA base and adds it as a surcharge, so the fee is R/(1+R) of what the swapper pays instead of R. At open the same MAD that costs 100 FWA by exact-input costs 1.99 FWA by exact-output, with the fee at 4,974 bps of gross instead of 9,900. The README line 46 acknowledges this as a deliberate surcharge, but it defeats the 99% opening tax the brief requires. Proof test under test/scratch/ExactOutputBuyFee.t.sol fails on the current code.\n\n2. **Medium. Dust fill in the in-swap NFT conversion reverts every MAD swap.** After a bounded sale stops exactly at the owner's limit, a 1 wei ETH buy on the ETH/FWA pool leaves the price a few units under it. The next conversion sells a handful of base units, the ETH output rounds to zero or under the minimum, and `_convertNFT` reverts `Slippage`, which reverts the swapper's trade. Nothing changes state, so trading stays blocked until the official pool moves or the owner changes bounds. Proof test under test/scratch/DustFillConversionDoS.t.sol reproduces the `HookCallFailed(Slippage)` revert.\n\n3. **Low. Frozen empty address escapes by deploying code.** `MadToken._update` re-evaluates `isWallet(from)` at send time, so a counterfactual wallet frozen while empty can deploy and move its full balance inside the 24 hours, keeping the freezer's 0.5% remainder. Reproduced in a scratch test.\n\n**Coverage**\n\nAll 32 entry points have rows, plus two arithmetic invariants that hold (fee = 2·share + burned never underflows; the exact-output sell gross-up divisor is at least 180,000). The one `unreached` row is the mainnet fork rehearsal, since no RPC URL is set and the official FWA hook was only exercised through mocks.\n\nChecked and found sound in my area: the linear decay at 0, 900, 1799 and 1800 seconds, the int128 bounds on specified amounts, the 0.50%/0.50% split and burn-of-dust accounting, the freeze cost and burn floors, strict-greater tie handling, same-block round closure, the 8-block, 6-hour and 72-hour oracle boundaries, and the settlement-window comparison against the fwa-v2 reference.","treeHash":null,"usage":{"cachedInputTokens":2476502,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":47873,"runtime":"claude","turns":34,"wallClockMs":1041534}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f009de0ce52c7b3a","findings":[{"citation":"resolved","description":"The brief fixes one schedule for the FWA side of every swap, max(1%, 99% - 98%*elapsed/1800). Three of the four modes charge r of the gross FWA the trader faces: exact-input FWA buys deduct floor(B*r) from the input (beforeSwap line 205), exact-input MAD sells deduct floor(B*r) from the gross output (afterSwap line 238, FWA output), and exact-output FWA sells gross up with ceil(N*r/(D-r)) (line 210) so fee/gross == r. The fourth mode, exact-output MAD (FWA is the unspecified input), reuses the floor branch at line 238: fee = floor(base*r) where base is only the FWA the AMM consumed, and the fee is then added on top, so the trader pays base + fee and the effective rate is r/(1+r). At open (r = 99%) that is 49.7% instead of 99%; at 15 minutes 33% instead of 50%; at rest 0.99% instead of 1%. Any router can submit exact-output swaps, so during the decay the same MAD costs about 50x less FWA through this route; the FWA burn, the Playground share and the NFT share (both base/200) all shrink by the same ratio. The existing fuzz test encodes the surcharge convention and README line 46 documents it, but it contradicts the single schedule the brief specifies. Merged from audit_economics, audit_permissions and audit_math (same root cause). Fix: in the afterSwap unspecified branch, when fwaDelta < 0 (FWA is the input) gross up as the exact-output sell does: rate = feeNumerator(); fee = Math.mulDiv(base, rate, RATE_DENOMINATOR - rate, Math.Rounding.Ceil); base += fee; then _collect(base, fee). Keep the floor branch for the FWA-output case.","line":238,"path":"src/MadHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {MadHook} from \"src/MadHook.sol\";\nimport {MadToken} from \"src/MadToken.sol\";\n\n/// @dev FWA stand-in: transfers only to or from the PoolManager, public burn.\ncontract ScratchFWA is ERC20 {\n    address public immutable manager;\n\n    constructor(address m) ERC20(\"Fake World Assets\", \"FWA\") {\n        manager = m;\n    }\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function burn(uint256 amount) external {\n        _burn(msg.sender, amount);\n    }\n\n    function _update(address from, address to, uint256 amount) internal override {\n        require(from == address(0) || to == address(0) || from == manager || to == manager, \"InvalidTransfer\");\n        super._update(from, to, amount);\n    }\n}\n\n/// @dev Router that settles after the swap, like production routers.\ncontract ScratchRouter is IUnlockCallback {\n    IPoolManager public immutable manager;\n    address private payer;\n\n    constructor(IPoolManager m) {\n        manager = m;\n    }\n\n    function swap(PoolKey memory key, SwapParams memory p) external returns (BalanceDelta d) {\n        payer = msg.sender;\n        d = abi.decode(manager.unlock(abi.encode(uint8(0), key, abi.encode(p))), (BalanceDelta));\n        delete payer;\n    }\n\n    function modify(PoolKey memory key, ModifyLiquidityParams memory p) external returns (BalanceDelta d) {\n        payer = msg.sender;\n        d = abi.decode(manager.unlock(abi.encode(uint8(1), key, abi.encode(p))), (BalanceDelta));\n        delete payer;\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager));\n        (uint8 action, PoolKey memory key, bytes memory args) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (action == 0) d = manager.swap(key, abi.decode(args, (SwapParams)), \"\");\n        else (d,) = manager.modifyLiquidity(key, abi.decode(args, (ModifyLiquidityParams)), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 d) private {\n        if (d < 0) {\n            manager.sync(c);\n            IERC20(Currency.unwrap(c)).transferFrom(payer, address(manager), uint256(-int256(d)));\n            manager.settle();\n        } else if (d > 0) {\n            manager.take(c, payer, uint256(int256(d)));\n        }\n    }\n}\n\n/// @dev Deploys token, mines the hook salt, initializes the pool in one transaction (as the factory does).\ncontract ScratchLaunch {\n    function launch(bytes memory tokenCode, bytes memory hookCode, IPoolManager manager, address owner, address fwa)\n        external\n        returns (address token, address hook, PoolKey memory key)\n    {\n        assembly (\"memory-safe\") { token := create(0, add(tokenCode, 32), mload(tokenCode)) }\n        require(token != address(0));\n        bytes memory initCode = bytes.concat(hookCode, abi.encode(manager, token, owner, address(this)));\n        bytes32 hash = keccak256(initCode);\n        for (uint256 i; i < 200_000; ++i) {\n            bytes32 salt = bytes32(i);\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, hash)))));\n            if (uint160(predicted) & ((1 << 14) - 1) != 0x20cc) continue;\n            assembly (\"memory-safe\") { hook := create2(0, add(initCode, 32), mload(initCode), salt) }\n            break;\n        }\n        require(hook != address(0));\n        (address c0, address c1) = token < fwa ? (token, fwa) : (fwa, token);\n        key = PoolKey(Currency.wrap(c0), Currency.wrap(c1), 12_500, 60, IHooks(hook));\n        manager.initialize(key, 79228162514264337593543950336);\n        require(IERC20(token).transfer(msg.sender, IERC20(token).totalSupply()));\n    }\n}\n\ncontract ExactOutputBuyTaxTest is Test {\n    address internal constant FWA = 0xa0Df17B5aC76ABaBA36E1450E2cbCd18A620C845;\n    address internal constant OWNER = 0xFc3C962FAD2C1cC77f1a0d46e7B8a2De79A21774;\n    address internal constant ALICE = address(0xA11CE);\n    PoolManager internal manager;\n    ScratchRouter internal router;\n    MadToken internal token;\n    MadHook internal hook;\n    ScratchFWA internal fwa;\n    PoolKey internal key;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.roll(100);\n        vm.warp(1_800_000_000);\n        manager = new PoolManager(address(this));\n        router = new ScratchRouter(manager);\n        vm.etch(FWA, address(new ScratchFWA(address(manager))).code);\n        fwa = ScratchFWA(FWA);\n        ScratchLaunch factory = new ScratchLaunch();\n        (address tokenAddress, address hookAddress, PoolKey memory launchKey) =\n            factory.launch(vm.getCode(\"MadToken.sol:MadToken\"), vm.getCode(\"MadHook.sol:MadHook\"), manager, OWNER, FWA);\n        token = MadToken(tokenAddress);\n        hook = MadHook(hookAddress);\n        key = launchKey;\n        token.approve(address(router), type(uint256).max);\n        fwa.approve(address(router), type(uint256).max);\n        fwa.mint(address(this), 1_000_000_000 ether);\n        router.modify(key, ModifyLiquidityParams(-887220, 887220, 1_000_000 ether, bytes32(0)));\n        fwa.mint(ALICE, 10_000_000 ether);\n        vm.startPrank(ALICE);\n        token.approve(address(router), type(uint256).max);\n        fwa.approve(address(router), type(uint256).max);\n        vm.stopPrank();\n        vm.prank(OWNER);\n        hook.enableTrading(); // elapsed 0: fee rate 99% of the FWA side\n    }\n\n    function _buy(int256 amount) internal returns (uint256 fwaPaid, uint256 madReceived) {\n        bool z = Currency.unwrap(key.currency0) == FWA;\n        uint256 f0 = fwa.balanceOf(ALICE);\n        uint256 m0 = token.balanceOf(ALICE);\n        vm.prank(ALICE);\n        router.swap(key, SwapParams(z, amount, z ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1));\n        fwaPaid = f0 - fwa.balanceOf(ALICE);\n        madReceived = token.balanceOf(ALICE) - m0;\n    }\n\n    /// Brief: the fee is max(1%, 99% - 98% * elapsed / 1800) of the FWA side for every swap mode.\n    /// At open an exact-input buy keeps 99% of the FWA the buyer hands over; the exact-output buy\n    /// of the very same MAD must cost about the same gross FWA. It costs about 2% of it instead.\n    function test_exactOutputBuyPaysSameTaxAsExactInputBuy() public {\n        (uint256 paidIn, uint256 madIn) = _buy(-100 ether);\n        emit log_named_uint(\"exact-input: FWA paid\", paidIn);\n        emit log_named_uint(\"exact-input: MAD received\", madIn);\n        assertGt(madIn, 0);\n\n        (uint256 paidOut, uint256 madOut) = _buy(int256(madIn));\n        emit log_named_uint(\"exact-output: FWA paid for the same MAD\", paidOut);\n        assertEq(madOut, madIn, \"same MAD bought\");\n\n        // Same MAD, same instant, same schedule: the gross FWA must be within 1% of the other route.\n        assertApproxEqRel(paidOut, paidIn, 0.01e18, \"exact-output buy must pay the 99% launch tax too\");\n    }\n}","reproduction":"Fresh launch, pool 1:1 with 1,000,000e18 full-range liquidity, owner calls enableTrading() in the same block (elapsed 0, feeNumerator 9900*1800). Swap 1: ALICE buys MAD with exact input 100e18 FWA: fee 99e18, she receives 987,499,024,844,712,965 wei MAD. Swap 2: ALICE buys exact output of that same MAD amount. Expected: about 100e18 FWA paid (99% of the FWA side). Actual: 1,990,003,930,253,881,124 wei FWA paid (1.99 FWA), i.e. fee/gross = 49.7%. Run: forge test --match-path test/scratch/ExactOutputBuyTax.t.sol -vv; it fails with '1990003930253881124 !~= 100000000000000000000'.","severity":"high","snippet":"            fee = feeFor(base); // floor of actual FWA exchanged by the pool","title":"Exact-output MAD buys are taxed at r/(1+r) of the FWA paid: the 99% launch tax collapses to 49.7% on that route"},{"citation":"resolved","description":"_convertNFT runs inside beforeSwap or afterSwap of every fee-bearing MAD swap once nftFwaClaims > 0 and a limit is set. It defers (keeps the claims) only when current >= limit (line 271). When the official market's sqrt price is just below the limit, the exact-input sale bounded by the limit fills a rounding-sized amount: v4 rounds amountIn up (ceil(L*dP/2^96) = 1 wei of FWA for a 1-unit gap) and amountOut down (0 wei of ETH), so sold = 1 and proceeds = 0 and the check above reverts Slippage(). The PoolManager wraps it as HookCallFailed and the trader's whole MAD swap reverts, in both directions and both modes. The same branch fires with no attacker for any swap whose FWA base is a few hundred wei (share = 1 wei, sale returns 0 ETH), whenever the market drifts into the band just under the limit, and whenever the market hook's 1% ETH fee pushes net proceeds under minEthPerFwaX128 at a price the limit still allows. An unprivileged FWA holder creates the state at will: one exact-input FWA sell on the official market with sqrtPriceLimitX96 = limit - 1 (sells are always open per fwa-v2; the price stops exactly at the limit), costing only the market hook's 1% on the amount needed to reach that price, and re-parks after every market move. Until the owner changes bounds, no MAD swap executes; the only mitigation that the griefer cannot track is a limit below spot, which disables NFT conversion entirely. The brief and README require unsold NFT claims to wait for the next swap, so a conversion that cannot fill must never fail the trade. Merged from audit_economics, audit_flow, audit_permissions and audit_math (same root cause: the post-sale revert). Fix: decide before swapping and never revert the user's swap on the conversion outcome: compute the FWA sellable up to the limit (SqrtPriceMath.getAmount1Delta(current, limit, liquidity)) and skip the sale when it is below a dust threshold or when the price implied at the limit net of the market's 1% fee is under minEthPerFwaX128; keep proceeds == 0 on a dust fill as a deferral, not a revert.","line":277,"path":"src/MadHook.sol","reproduction":"Trading open and at rest (1%). Official ETH/FWA market initialised at Q96 with 1000 ETH full-range liquidity; owner setConversionBounds(limit = 2*Q96, min = 1<<126). 1) ALICE buys MAD with 1000e18 FWA: succeeds, NFT share sold. 2) GRIEFER (any FWA holder) swaps on the market SwapParams(zeroForOne=false, amountSpecified=-1_000_000e18, sqrtPriceLimitX96 = limit - 1); getSlot0 shows sqrtPriceX96 == limit - 1. 3) ALICE repeats the 1000e18 FWA buy. Expected: the trade executes and the 5e18 FWA NFT share stays in nftFwaClaims. Actual: revert WrappedError(hook, 0x575e24b4 beforeSwap, 0x7dd37f70 Slippage(), HookCallFailed); the same happens for a MAD sell. Dust variant on the repo harness: at rest with the market at Q96 and no griefer, a buy with exact input 300 wei FWA (share 1 wei) reverts the same way. Run: forge test --match-path test/scratch/ConversionGrief.t.sol -vv.","severity":"high","snippet":"        if (\n            sold > offered\n                || (sold > 0\n                    && (proceeds == 0 || proceeds < Math.mulDiv(sold, minEthPerFwaX128, 1 << 128, Math.Rounding.Ceil)))\n        ) revert Slippage();","title":"Anyone can halt all MAD trading: an ETH/FWA price parked one unit under the owner's sell limit (or any dust NFT share) makes _convertNFT revert the trader's swap"},{"citation":"resolved","description":"closeRound is permissionless and its only conditions are a nonzero leader and an unassigned kept NFT. A round opens at last + 1 with an empty leaderboard, and freeze(target) costs floor(balanceOf(target)/100), so a target holding 100 wei MAD costs 1 wei and makes the first freezer of the round its leader. Whoever bundles token.freeze(dustWallet) and hook.closeRound() in one transaction in the first block of a round ends a one-block round with themselves as leader and the oldest kept NFT assigned to it; after 8 blocks askWinner/award, or the 72-hour fallback to token.leader, delivers it. A queue of N kept NFTs is drained one per block for 1 wei each, and because claim() is public the sniper can chain claim -> freeze -> closeRound. NFTs are bought with 0.50% of all trading volume, so their value goes to a bot rather than to the holders who actually spend MAD freezing wallets. The code follows the brief's wording for closeRound, so this needs a scope decision; merged from audit_economics and audit_permissions. Fix: require a minimum round length in blocks since roundBlocks[round].first and/or a minimum leader spend before closeRound, while preserving the one-close-per-block and oldest-NFT rules.","line":342,"path":"src/MadHook.sol","reproduction":"Repo harness MadRewardsFixture: _keep() twice so keptNFTs.length == 2. ALICE freezes BOB and anyone calls hook.closeRound() (round 1, NFT index 0 assigned correctly). vm.roll(+1). Victim V holds 100 wei MAD, sniper S holds 1 wei. S in one transaction: token.freeze(V) (cost 1 wei, 0 burned, 1 wei to V) then hook.closeRound(). Observed: token.leader(2) == S, spent(2, S) == 1, roundBlocks(2).first == last (one-block round), rewards.rounds(2).nftIndexPlusOne == 2. After 72 hours and 8 blocks hook.award(2) returns true and nft.ownerOf(2) == S. Expected: an NFT funded by 0.50% of trading volume goes to a round's meaningful top freezer. Scratch test SnipeTest.test_oneWeiFreezeCapturesNft in test/scratch/Leads.t.sol passes on this code, showing the capture.","severity":"medium","snippet":"        if (mad.leader(round) == address(0) || !rewards.hasUnassignedNFT()) revert NoRoundReward();","title":"A 1-wei freeze plus an immediate closeRound captures each kept NFT: rounds have no minimum length or spend"},{"citation":"resolved","description":"freeze() accepts any target with no code, which includes a not-yet-deployed CREATE2 address (a counterfactual Safe or account-factory wallet) that already holds MAD. _update re-evaluates isWallet(from) at send time, so once code longer than 23 bytes appears at the frozen address the freeze no longer applies for the rest of the 24 hours. The holder gets frozen (the freezer burns cost/2 and pays cost/2 to the target), deploys the wallet in the same block, and transfers, sells or adds liquidity immediately; the freezer's spend bought nothing and the target netted 0.5% of its balance. The comment on line 109 shows this is deliberate, but the brief says a frozen wallet cannot send for 24 hours. Merged from audit_economics, audit_flow, audit_permissions and audit_math. Fix: enforce frozenUntil[from] > block.timestamp regardless of the sender's current code (freeze() already guarantees contracts never receive a nonzero frozenUntil).","line":110,"path":"src/MadToken.sol","reproduction":"Repo harness: ALICE transfers 1000e18 MAD to A = 0xC0FFEE (no code). BOB calls freeze(A): BOB pays 10e18, 5e18 burned, 5e18 to A, frozenUntil[A] = now + 24h. prank(A) transfer(CAROL, 1) reverts WalletFrozen. vm.etch(A, <Mover bytecode>) (on mainnet: the CREATE2 deployment at A). prank(A) transfer(CAROL, 1005e18) succeeds inside the freeze window; CAROL's balance rises by 1005e18. Expected: WalletFrozen until the 24 hours pass. Scratch test LeadsTest.test_counterfactualEscape in test/scratch/Leads.t.sol passes on this code, showing the escape.","severity":"low","snippet":"        if (from != address(0) && frozenUntil[from] > block.timestamp && isWallet(from)) {","title":"A frozen counterfactual address escapes the freeze by deploying code: _update skips the frozen check once the sender has non-7702 code"},{"citation":"resolved","description":"For an FWA-specified swap _collect runs in beforeSwap, before the router has settled the swapper's FWA input (Universal Router and the test router settle after the swap inside the same unlock). poolManager.take does an immediate ERC-20 transfer from the manager's balance, so the burn is funded only by FWA the manager already holds. During the decay the burn is up to 98% of the input, so any buy whose input exceeds about manager_FWA_balance / 0.98 reverts with the FWA token's insufficient-balance error although the trade itself was fine. The ETH leg already guards this (line 287 mints a claim when the manager lacks ETH); the FWA burn leg does not. README line 60 documents the limitation. On mainnet the manager holds the ETH/FWA pool's FWA, so this bites only large early buys, hence low. From audit_flow. Fix: when IERC20(FWA).balanceOf(poolManager) < burned, mint the burn amount as a hook claim into a separate burn counter and expose a public function that unlocks, burns the claim, takes the FWA and calls FWA.burn, so nothing above 1% is ever kept and the trade never reverts.","line":257,"path":"src/MadHook.sol","reproduction":"Repo harness: _open(0) so the fee is 99%; the manager holds 999,999,999,999,999,999,945,647 wei FWA from the launch pool's liquidity; mint ALICE 10x that. ALICE buys MAD with exact input 3 * manager FWA balance through TestRouter. Expected: the buy executes, since the swapper's input covers the burn once settled. Actual: beforeSwap -> _collect -> poolManager.take(FWA, hook, ~2.97e24) reverts (ERC20InsufficientBalance wrapped in WrappedError). A buy of half the reserve succeeds. Scratch test LeadsTest.test_bigBuyAtOpenReverts in test/scratch/Leads.t.sol.","severity":"low","snippet":"            poolManager.take(fwa, address(this), burned);","title":"Same-swap FWA burn is paid from the PoolManager's existing FWA reserves, so an exact-input buy larger than those reserves reverts under a post-settling router"},{"citation":"resolved","description":"Documented privileged powers, reported separately as a trust assumption (the brief permits these setters but also says no arbitrary withdrawal of ETH and no changing results). buy() trusts whatever fwaV2 points at: it reads quoteAcquisitionPrice() from it, redeems hook ETH claims up to that total, and sends `total` via acquire{value: total}. An owner-set contract whose quote returns total = nftBudget() and whose acquire keeps msg.value and returns one id receives the entire NFT budget on the next public buy(). Likewise setOracleSettings(ownerIntake, action, asset, 0, ownerKey) followed by askWinner lets the owner deliver a self-signed attestation naming any address (onOracleResult accepts it since p.signer == oracleSigner), so award() sends the NFT to the owner's choice; and setEthFwaPool(ownerPool) plus setConversionBounds(limit, 1) sells NFT FWA claims into an owner pool. From audit_permissions. If this trust is not intended, pin fwaV2 and the ETH/FWA pool to the official addresses (or a verified successor registry) and make the signer immutable or registry-backed.","line":124,"path":"src/MadRewards.sol","reproduction":"Owner calls rewards.setFwaV2(Drain) where Drain.isPurchaseBlackout() returns false, Drain.quoteAcquisitionPrice() returns (0, 0, rewards.nftBudget()) and Drain.acquire() keeps msg.value and returns [1]. Anyone calls hook.buy(): line 161 redeems the hook's ETH claims and line 163 sends the whole budget to Drain; rewards' balance and nftEthClaims are 0 afterwards. Expected per the brief: only specified flows move ETH.","severity":"low","snippet":"        fwaV2 = pool;","title":"Owner setters are a full trust boundary over rewards: setFwaV2 can drain the whole NFT ETH budget and setOracleSettings can pick winners"},{"citation":"resolved","description":"beforeSwap charges the fee on the full specified FWA amount and afterSwap refuses any fill that differs from expectedSpecifiedFwa. A v4 swap that reaches the trader's price limit (or exhausts liquidity) consumes less input or yields less output and is otherwise valid, but here the transaction reverts. Routers that protect users with a price limit rather than amountOutMinimum get reverts instead of partial fills on every FWA-specified route. README line 46 documents it; recorded so the requester can decide whether it meets 'support both directions, exact input/output'. No value at risk. From audit_flow.","line":236,"path":"src/MadHook.sol","reproduction":"Repo harness, trading at rest: ALICE swaps exact input 100,000e18 FWA with sqrtPriceLimitX96 0.1% away from the current price. Expected (plain v4 semantics): a partial fill up to the limit. Actual: afterSwap reverts PartialSpecifiedFill() and the whole swap fails. Scratch test LeadsTest.test_tightLimitReverts in test/scratch/Leads.t.sol.","severity":"info","snippet":"            if (base != expectedSpecifiedFwa) revert PartialSpecifiedFill();","title":"FWA-specified swaps that stop at the trader's sqrtPriceLimitX96 revert with PartialSpecifiedFill instead of filling partially"},{"citation":"resolved","description":"bindHook() succeeds only while the transient slot set in MadToken's constructor is still 1, i.e. within the same transaction as the token's deployment. MadHook.beforeInitialize calls mad.bindHook() unconditionally, so if the launch factory creates the token in one transaction and deploys the hook and initializes the pool in another, beforeInitialize reverts InvalidBinding and the pool can never be initialized with this hook. The brief states the launch is atomic and the harness models it that way, so this is recorded as a deployment precondition, not a defect; it needs the factory's transaction structure to confirm either way.","line":51,"path":"src/MadToken.sol","reproduction":"Deploy MadToken in transaction 1 (vm.startBroadcast or a separate call). In transaction 2 deploy MadHook(manager, token, owner, factory) and call manager.initialize(key, price) from the factory. Expected under an atomic factory: pool initialized and token.hook() == hook. Actual with a split launch: PoolManager reverts with WrappedError(hook, beforeInitialize, InvalidBinding(), HookCallFailed).","severity":"info","snippet":"        if (active != 1 || hook != address(0) || msg.sender.code.length == 0) revert InvalidBinding();","title":"Hook binding depends on the token constructor and pool initialization sharing one transaction (transient flag); a factory that deploys the token in an earlier transaction cannot launch"}],"hash":"22227c87e9c589208355ebf91b1b943c2dc4ad525cfe1a64163e9aa5165e8120","nodeId":"6a3c5acd-ecfa-495d-b976-646975938c5e","outcome":"completed","summary":"The review is complete. The findings file `.imd-findings.json` holds 8 findings and a 35-row coverage record, and the project's own 95 tests pass as a baseline. No repository file outside `test/scratch/` was changed.\n\n**What I kept, after reproducing each lead against the code**\n\n- **High, exact-output MAD buys pay half the launch tax.** The afterSwap branch at `src/MadHook.sol:238` charges floor(base × r) on the pool-side FWA and adds it on top, so the trader-facing rate is r/(1+r). At open the same MAD that costs 100 FWA by exact input costs 1.99 FWA by exact output. Three specialists reported this; merged into one. Proof attached, fails on current code.\n- **High, anyone can halt MAD trading.** The post-sale check at `src/MadHook.sol:277` reverts the trader's whole swap when the NFT-share sale fills a rounding-sized amount. One FWA sell parking the ETH/FWA market one unit under the owner's limit makes every later MAD swap revert, and a 300-wei buy reverts on its own. Four specialists reported variants; merged. Proof attached, fails with the wrapped Slippage error.\n- **Medium, 1-wei freeze snipes each kept NFT.** With no minimum round length, freeze on a 100-wei wallet plus closeRound in one transaction wins the oldest NFT. Reproduced end to end through award on the repo harness. Needs a scope decision since the code follows the brief's wording.\n- **Low, three items.** A frozen counterfactual address escapes by deploying code. The same-swap burn is funded from the manager's existing FWA reserves, so large early buys revert under post-settling routers. The owner setters are a full trust boundary, with setFwaV2 able to send the whole NFT budget to an owner contract on the next public buy.\n- **Info, two items.** FWA-specified swaps revert instead of partially filling at a price limit, as documented. Hook binding requires the token constructor and pool initialization in one transaction, which the brief asserts but the factory structure would have to confirm.\n\n**Dropped or declined**\n\nNothing from the specialists failed to reproduce. The slither and aderyn lines were all either the above root causes, intentional patterns, or benign (the divide-before-multiply is the exact 0.50% share split; the \"locked ether\" is the NFT budget that buy() spends).\n\n**Coverage**\n\nAll 33 entry points are answered, 25 hold and 8 point at findings, plus two invariant rows for the equal-share accounting and the manifest and contract sizes. The fork tests were not run, since no RPC is configured, so live FWA and Intake behavior rests on the mocks and the references.","treeHash":null,"usage":{"cachedInputTokens":2282182,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":50059,"runtime":"claude","turns":32,"wallClockMs":1250897}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d192c3efd847050a","findings":[],"hash":"3568c0b89626951b39567d6c23984a43593e53c3356403d3ca5a8096f00ce6ea","nodeId":"208f466a-7de7-49b8-85da-a7e1532bb904","outcome":"failed","summary":"Error: claude native binary not installed.\n\nEither postinstall did not run (--ignore-scripts, some pnpm configs)\nor the platform-native optional dependency was not downloaded\n(--omit=optional).\n\nRun the postinstall manually (adjust path for local vs global install):\n  node node_modules/@anthropic-ai/claude-code/install.cjs\n\nOr reinstall without --ignore-scripts / --omit=optional.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":22}},{"artifacts":[],"attempt":1,"bundleHash":"f86925851d614d30b10a36e7ad2641a2ee3b87213e637322cf41b8f696afb5f7","device":"827152cce766ca0d","findings":[],"hash":"494475f9b72995a369befa68850eff0ff9c2ed28193cca866b938ecadc1577e7","nodeId":"c21db80d-f5fa-4042-a935-32cc535545c7","outcome":"completed","summary":"I updated `launch.json`, and it still matches the code: the hook's constructor arguments, its permissions, and the token's constructor and supply are unchanged. `forge build` succeeds and `forge test` with the verifier's fuzz seed passes 68 tests, skips 1, and fails none. No other file changed. The manifest was checked by reading it against the code; nothing here runs the schema itself.\n\nWhat the manifest says, checked against the source:\n- **Hook:** `MadHook(IPoolManager, IMadToken, address owner, address factory)` is written as `[\"$poolManager\", \"$token\", \"$owner\", \"$factory\"]`. Its permissions are `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta` and `afterSwapReturnDelta`, which is exactly what `getHookPermissions` returns.\n- **Token:** `MadToken` takes no constructor arguments and mints `1000000000000000000000000000` (1 billion MAD at 18 decimals) to `msg.sender`, the launch factory.\n- **Pool:** FWA `0xa0df…c845`, fee 12500, tick spacing 60 and the one-to-one starting price, all as given. The hook's `beforeInitialize` only accepts fee 12500 and tick spacing 60, so these agree.\n\nThe only edit is to `notes`, which now describe the hook changes accepted since the last version:\n- **Round limits:** `closeRound()` only closes a round that is at least 7,200 blocks long and whose leader has spent at least 1,000 MAD.\n- **Deferred burn:** when the PoolManager doesn't hold enough FWA to burn the fee during the swap, the amount is kept as a burn-only claim and anyone can burn it later with `flushBurn()`.\n- **Exact-output buys:** the fee is grossed up so these pay the same share as every other swap type.\n- **NFT-budget sale:** the sale of the NFT share for ETH is checked before it runs and can never make a MAD swap fail.\n- **Rewards contract:** `MadRewards` is deployed by the hook.\n\n`notes` is now 3,541 characters, under the 4,000 cap.","treeHash":"a52646fa3892f6710071eb1fce086a3b96243db9","usage":{"cachedInputTokens":256757,"inputTokens":12,"model":"claude-opus-5-5","outputTokens":2418,"runtime":"claude","turns":6,"wallClockMs":404335}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dc34db8e17664ebd","findings":[],"hash":"4bc27c106cf5e4a6edb6201051d5aa7034192d9cd8fd8782be00ce9b4a41c3f1","nodeId":"5f91c837-8195-4b3b-bc1d-9b368aadff7a","outcome":"failed","summary":"Error: claude native binary not installed.\n\nEither postinstall did not run (--ignore-scripts, some pnpm configs)\nor the platform-native optional dependency was not downloaded\n(--omit=optional).\n\nRun the postinstall manually (adjust path for local vs global install):\n  node node_modules/@anthropic-ai/claude-code/install.cjs\n\nOr reinstall without --ignore-scripts / --omit=optional.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":34}},{"artifacts":[],"attempt":1,"bundleHash":"f41e8334c06ec4ea942297581a967c3bfa89c79cf7e0cda5626531a0336ec75e","device":"cb213fe03251a512","findings":[],"hash":"646d59e5d5e91ccf9650ff047fd131d0c91ec1cb1747163d7cfc477f48172773","nodeId":"2cac00d4-efee-430e-b10b-b68a42d135dd","outcome":"completed","summary":"Added adversarial, fuzz, and invariant tests under `test/`.\n\n- `forge build` passed.\n- Seeded suite: **95 passed, 0 failed**, optional fork suite skipped.\n- Separate mainnet fork run: **2 passed**.\n- No reproducible implementation defects found.\n\nNFT acquisition and oracle delivery remain mock-tested; live swap integration passed.","treeHash":"5b06f1981f0d74460eb9bec45d272407250f240a","usage":{"cachedInputTokens":7477760,"inputTokens":196708,"model":"gpt-6-astra","outputTokens":39189,"runtime":"codex","turns":13,"wallClockMs":1194078}},{"artifacts":[],"attempt":1,"bundleHash":"72bf4a1dabd2620ee80f0ee7f77ef3df7ee246224ccdba4d43a6650d33a1184d","device":"1eba9b0f789fcc77","findings":[],"hash":"6b9739fe02a16b32f2c230b610cb4b85e958a7be9f5d1839f3b463a27c761d61","nodeId":"c21db80d-f5fa-4042-a935-32cc535545c7","outcome":"completed","summary":"The manifest is written and verified. Only launch.json was added to the tree.\n\n**What launch.json says**\n\n- `\"kind\": \"univ4_hook\"` is the first field.\n- Hook is MadHook with constructor arguments `$poolManager`, `$token`, `$owner`, `$factory`, matching the declaration order in src/MadHook.sol:79.\n- Permissions are beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta and afterSwapReturnDelta, exactly what getHookPermissions returns at src/MadHook.sol:104.\n- Token is MadToken, name \"Mutually Assured Destruction\", symbol MAD, 18 decimals, empty constructorArgs (no-argument constructor), totalSupply 1e27 base units, which is the 1,000,000,000 MAD the constructor mints once to msg.sender.\n- Pool is the launch's own: pairedCurrency 0xa0df17b5ac76ababa36e1450e2cbcd18a620c845, fee 12500, tickSpacing 60, initialPrice 79228162514264337593543950336. The hook's beforeInitialize accepts exactly fee 12500 and tick spacing 60.\n- Notes run 3,179 characters and describe freeze mechanics, the atomic one-time binding, the decaying FWA-side fee split (0.50% Playground claims, 0.50% NFT share sold for ETH in-swap, rest burned), direct PoolManager-to-receiver payout to the immutable receiver, the owner's limited settings, and the NFT, oracle and award flow.\n\n**Checks**\n\n- The JSON parses, the kind is first, the supply string equals 10^27, and the price is below 2^256.\n- `forge build` compiles cleanly with only lint warnings.\n- `forge test` with the verifier's fuzz seed: 58 tests passed, 0 failed, including the fuzz and invariant suites. I skipped only the mainnet fork suite, which needs live RPC and does not touch the manifest.\n\nNo placeholder gaps remain. The receiver is immutable in the hook's code, so no address had to be written by hand in the manifest.","treeHash":"38ee5b5ad97c7fe1e589152e7e65170284c24d5a","usage":{"cachedInputTokens":403661,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":4672,"runtime":"claude","turns":8,"wallClockMs":111536}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7e50667f872c7f5e","findings":[{"citation":"resolved","description":"The brief fixes one schedule for the FWA side of every swap: max(1%, 99% - 98%*elapsed/1800). For an exact-input FWA buy (beforeSwap, line 205) the fee is r of the gross FWA the buyer pays, and for an exact-output FWA sell (line 210) the fee is grossed up with ceil(N*r/(D-r)) so that it is r of the gross FWA leaving the pool. The remaining buy path, exact-output MAD (FWA is the unspecified input), is handled in afterSwap: base is the FWA the AMM consumed and fee = floor(base*r) is added as a surcharge, so the buyer pays base + fee and the effective rate is r/(1+r). At open (r = 99%) that is 49.7% instead of 99%; at 15 minutes (r = 50%) it is 33% instead of 50%. Path divergence: the same MAD costs ~50x less FWA through the exact-output route during the first seconds, and roughly half the intended tax across the whole 30-minute decay. Every router can submit exact-output swaps, so the launch tax that is supposed to burn FWA is halved by anyone who picks that mode; Playground and NFT shares (base/200) shrink by the same ratio. README line 46 documents the surcharge convention, but it contradicts the single 99%-to-1% schedule the brief specifies for the FWA side and creates a cheaper, economically dominant path. Fix: in the afterSwap unspecified branch, when FWA is the input (buy), gross up like the exact-output sell: fee = mulDiv(base, rate, RATE_DENOMINATOR - rate, Ceil), and use base + fee as the fee base for the 0.50% shares so all four paths charge r of the gross FWA side.","line":238,"path":"src/MadHook.sol","reproduction":"State: pool initialised at price 1, 1,000,000e18 full-range liquidity, trading just enabled (elapsed 0). 1) ALICE swaps exact-output +1e18 MAD (buy). Observed: FWA paid 2.015191888609610132e18, fee charged 1.002532648102268357e18 (claims+burn), effective rate 4974 bps. 2) ALICE swaps exact-input -2.015191888609610132e18 FWA (same FWA). Observed: fee 1.995039969723514030e18, 9899 bps, and she receives only 0.0199 MAD versus 1 MAD in step 1. Expected: both buys charge 99% of the FWA handed over. Run: forge test --match-path test/scratch/ExactOutputBuyTax.t.sol -vv","severity":"high","snippet":"            fee = feeFor(base); // floor of actual FWA exchanged by the pool","title":"Exact-output MAD buys pay ~half the launch tax: fee is r of the pool-side base, not of the FWA the buyer hands over"},{"citation":"resolved","description":"_convertNFT runs inside every taxed swap (from _collect, called by beforeSwap or afterSwap) and sells the accumulated NFT share on the ETH/FWA market with sqrtPriceLimitX96 = the owner's limit. If the market's current sqrt price is below the limit by one unit, the bounded exact-input sale moves the price to the limit by consuming ceil(L*1/2^96) = 1 wei of FWA and returns floor(L*2^96/(p*p')) = 0 wei of ETH. sold = 1, proceeds = 0, so the check above reverts the whole user swap. The revert is not confined to the hook: it bubbles through the PoolManager as HookCallFailed and the trader's swap fails. An unprivileged party can create and keep this state at will: sell FWA on the market with sqrtPriceLimitX96 = limit - 1 (exact input; the pool stops exactly at that price), costing only the market hook's 1% fee on the amount needed to reach that price (which can be tiny if the limit is near spot). Every MAD/FWA swap then reverts until the owner changes bounds, and the griefer simply re-parks the price under the new limit. The same revert also fires without an attacker whenever the market drifts into that band, or when fees make proceeds fall below minEthPerFwaX128 at the limit. The brief and README require unsold NFT claims to wait for the next swap; a conversion failure must never fail the trade. Fix: decide before swapping (compare the current price and the minimum-output bound; skip the sale, retaining the claim, when the available price band cannot satisfy it) and never revert the user's swap on the conversion outcome. Validated minimal change for the proof: drop the proceeds == 0 revert and round the minimum with Floor so a dust fill cannot fail the trade; pair it with a pre-swap check that the ETH-per-FWA price at the limit, net of the market's 1% fee, is at least minEthPerFwaX128, otherwise retain the claims.","line":277,"path":"src/MadHook.sol","reproduction":"State: ETH/FWA market initialised at price 1 with 1000 ETH full-range liquidity, owner set setConversionBounds(limit = 2*Q96, minEthPerFwaX128 = 2^126), trading at rest (1%). 1) ALICE buys MAD with 1000e18 FWA: succeeds. 2) GRIEFER (any FWA holder) calls router.swap(market, SwapParams(false, -1_000_000e18, limit - 1)): the market's sqrtPriceX96 becomes exactly limit - 1. 3) ALICE repeats the same buy. Observed: revert WrappedError(hook, beforeSwap.selector, 0x7dd37f70 = MadHook.Slippage(), HookCallFailed). Expected: the swap succeeds and the 0.50% NFT share stays as nftFwaClaims. Run: forge test --match-path test/scratch/ConversionGrief.t.sol -vv","severity":"high","snippet":"        if (\n            sold > offered\n                || (sold > 0\n                    && (proceeds == 0 || proceeds < Math.mulDiv(sold, minEthPerFwaX128, 1 << 128, Math.Rounding.Ceil)))\n        ) revert Slippage();","title":"Anyone can halt all MAD trading: parking the ETH/FWA price one unit below the sell limit makes every taxed swap revert with Slippage"},{"citation":"resolved","description":"closeRound is permissionless and its only conditions are a non-zero leader and a kept NFT. The leader is whoever has the highest cumulative freeze spend in the round, with no minimum, and freeze(target) costs floor(balanceOf(target)/100), so a target holding 100 wei MAD costs 1 wei. A round opens at the block after the previous close with zero spend. Whoever first freezes in a fresh round becomes leader and can close it in the same transaction if an unassigned kept NFT exists, so a queue of N kept NFTs (or each NFT the moment rewards.claim() keeps it, which is also public and can be bundled) is drained one per block for 1 wei MAD each. The NFTs are bought with 0.50% of all trading volume, so the value flows to a bot instead of to the holders who actually spend MAD freezing wallets. The MadRewards.t.sol test suite only checks the leader/NFT preconditions, not the economics. Fix (needs a scope decision since the brief says only 'if an unassigned kept NFT and current round leader exist'): require a minimum round length in blocks since roundBlocks[round].first (e.g. several thousand blocks) and/or a minimum leader spend before closeRound, so a round cannot be opened and closed by the same actor within a block.","line":342,"path":"src/MadHook.sol","reproduction":"State: rewards holds two kept NFTs (keptNFTs.length = 2, nextUnassigned = 0); round R just closed at block B so round R+1 starts at B+1 with no spend; victim wallet V holds 100 wei MAD; attacker A holds 2 wei MAD. Block B+1, one tx from A: token.freeze(V) (cost 1 wei, burned 0, 1 wei to V; spent[R+1][A] = 1, leader(R+1) = A), then hook.closeRound(): leader != 0 and hasUnassignedNFT() so token round R+1 closes at B+1 and rounds[R+1].nftIndexPlusOne = 1. Block B+2, repeat: freeze(V) for 1 wei, closeRound() assigns NFT index 1 to round R+2 with A as leader. After 8 blocks askWinner/award (or the 72-hour fallback to token.leader) deliver both NFTs to A. Expected: an NFT funded by 0.50% of trading volume goes to a round's meaningful top freezer; observed: it goes to whoever spends 1 wei first after each closure.","severity":"medium","snippet":"        if (mad.leader(round) == address(0) || !rewards.hasUnassignedNFT()) revert NoRoundReward();","title":"A 1-wei freeze plus an immediate closeRound captures each kept NFT; rounds have no minimum length or spend"},{"citation":"resolved","description":"freeze() accepts any target with no code (isWallet), including a CREATE2 address whose deployer has not yet run. _update re-checks isWallet(from) at transfer time, so an address that acquires code after being frozen is no longer blocked. A holder who keeps MAD at a pre-computed CREATE2 address can therefore be frozen (the freezer burns cost/2 and sends cost/2 to the target), then deploy the contract in the same block and transfer, sell or add liquidity immediately. The freezer's spend still counts toward spent/leader, but the freeze itself has no effect and the target nets +0.5% of its balance per freeze. The README notes the behaviour as intended for code deployed later, but the economic outcome inverts the mechanism for anyone who prepares the escape. Fix: keep the frozenUntil check independent of isWallet(from) in _update (a contract that was an empty wallet when frozen stays frozen until the timestamp). A counterfactual CREATE2 address is indistinguishable from an EOA before deployment, so filtering targets cannot close this gap.","line":110,"path":"src/MadToken.sol","reproduction":"State: deployer D computes A = CREATE2(D, salt, initcode of a contract that forwards MAD on demand); D sends 1,000e18 MAD to A (no code). Bob calls token.freeze(A): cost 10e18, 5e18 burned, 5e18 to A, frozenUntil[A] = now + 24h, Frozen event. Same block, D deploys the contract at A and calls its forward(): token.transfer(D, 1,005e18) from A. Observed: succeeds because isWallet(A) is now false. Expected per the brief: a frozen wallet cannot send for 24 hours. Bob is out 10e18 MAD for nothing and A gained 5e18.","severity":"low","snippet":"        if (from != address(0) && frozenUntil[from] > block.timestamp && isWallet(from)) {","title":"A frozen counterfactual address escapes the freeze by deploying code, so the freezer's payment is wasted and the target pockets half of it"}],"hash":"6d07b2b2971406786f8cb736e0b256bf8f1de9664e2b9149422535eca86d316a","nodeId":"901c377e-01f8-47fc-9ee2-cdd94e608a42","outcome":"completed","summary":"The review is complete. The findings file is written, the tree is untouched apart from the ignored findings file and the scratch proofs, and both high-severity proofs fail on the current code and pass under a candidate fix.\n\n**Findings written to `.imd-findings.json`** (4 findings, 35 coverage rows, none unreached):\n\n1. **High: exact-output MAD buys pay about half the launch tax.** The unspecified-FWA buy branch in `afterSwap` charges r of the pool-side base and adds it as a surcharge, so the effective rate is r/(1+r). At open the buyer pays 49.7% instead of 99%. The proof shows the same 2.015 FWA yields 1 MAD via exact-output but 0.0199 MAD via exact-input. The README documents this convention, but it contradicts the brief's single schedule and gives sophisticated buyers a dominant path that also shrinks the Playground and NFT shares.\n\n2. **High: anyone can halt all MAD trading.** The in-swap NFT conversion sells up to the owner's price limit. If the ETH/FWA market sits one sqrt-price unit below the limit, the bounded sale fills 1 wei of FWA for 0 ETH and the `Slippage` check reverts the user's swap, bubbled through the PoolManager. A griefer parks the price there with one exact-input sell using a price limit of `limit - 1`, paying only the market's 1% fee on a tiny amount, and can re-park after every owner adjustment. The proof reproduces the revert and the candidate fix (tolerant rounding plus a pre-swap price check, never reverting the trade) makes it pass.\n\n3. **Medium: a 1-wei freeze plus an immediate `closeRound` captures each kept NFT.** Rounds have no minimum length or spend, so the first freezer after each close can lead and close in the same block, draining a queue of kept NFTs one per block. This follows the brief's literal closure rule, so the fix is flagged as a scope decision.\n\n4. **Low: a frozen counterfactual CREATE2 address escapes by deploying code**, because the frozen check in `_update` is skipped once the sender has code. The freezer's cost is wasted and the target nets half of it.\n\n**Coverage notes.** All 31 listed entry points have rows. The oracle callbacks, award and fallback paths, claim budgets, payout separation, binding and round-boundary logic traced clean against the Economic Security, Invariant and Flow Gap guides. Not reproduced in this budget: a proof test for finding 3 (it needs the FWA V2 mock flow), and any live-integration behavior of the real FWA token and market hook, which the mocks only model.","treeHash":null,"usage":{"cachedInputTokens":3068222,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":54151,"runtime":"claude","turns":41,"wallClockMs":1068657}},{"artifacts":[],"attempt":1,"bundleHash":"875388b6a65d2e7f4a400d417856608e73041baef4cb089571b4f967302859bf","device":"75052237a39b6e12","findings":[],"hash":"cd3fd5aced422933df7ad830928e1df8b8be8660d334f4e05f492c449b4318e0","nodeId":"5f91c837-8195-4b3b-bc1d-9b368aadff7a","outcome":"completed","summary":"The protected tests do not touch any of the changed interfaces, so nothing there is affected. Everything is verified: build passes, all 68 project tests plus the reviewer's proof pass, and `forge fmt --check` is clean.\n\n**Summary of this revision**\n\nFive findings reproduced and were fixed; three were answered as disputed with reasons in `.imd-responses.json`.\n\n- **Exact-output buy tax (high, proof)** was real. The unspecified-side branch in `afterSwap` now grosses up the net FWA the pool consumed, so the fee is the schedule's share of what the trader pays on all four routes. The proof passes, and a route-parity test at 99% and 50% was added.\n- **Conversion griefing (high)** was real. The NFT sale is now decided before it runs and never reverts the trader's swap. It is skipped when claims are dust, the market sits at or above the bound, or the in-range liquidity has less than `MIN_CONVERSION` room under the bound. The minimum ETH/FWA is validated against the price at the bound in the setter instead of a post-sale revert.\n- **Dust-freeze NFT capture (medium)** was real. Closing a round now needs at least 7,200 blocks and a leader spend of at least 1,000 MAD, both named constants documented in the README.\n- **Counterfactual freeze escape (low)** was real. The frozen check no longer consults the sender's current code.\n- **Burn funded from manager reserves (low)** was real. A burn the manager cannot fund yet becomes a separate burn claim that public `flushBurn()` burns later, tracked by the invariant suite.\n- **Owner setters, partial-fill revert, atomic binding** are disputed: the brief grants the setters and requires atomic binding, and the partial-fill revert is the safe v4 behaviour. The README now states the owner trust boundary plainly.\n\nHook runtime grew to 12,769 bytes, still well under the limit. The README and verification record were updated for every behaviour change.","treeHash":"45b6ea52f555893bec5f80943dca9e569dfcef13","usage":{"cachedInputTokens":2552684,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":47644,"runtime":"claude","turns":54,"wallClockMs":1416840}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8c9c4986ca881107","findings":[{"citation":"resolved","description":"Asymmetry between the two FWA-paying buy modes. In beforeSwap the exact-output-FWA path grosses the fee up (fee = ceil(amount*rate/(den-rate)), base = amount+fee) so that fee/gross == rate. In afterSwap, when FWA is the unspecified INPUT (swapper requests an exact MAD output and pays FWA), the fee is computed on the pool-side FWA amount `base` and then charged ON TOP of it (the swapper pays base + fee), so fee/(FWA actually paid) = rate/(1+rate). At trading open (99%) the effective tax is 99/199 = 49.7% instead of 99%; at 50% it is 33%; at the resting 1% it is 0.99%. Any buyer during the decay simply quotes an exact MAD output instead of an exact FWA input and receives ~50x more MAD for the same FWA, and the burn/Playground/NFT shares collect about half of what the identical FWA spend would have produced via the other mode. The existing fuzz test (_assertSwap, buyMad && !exactInput) encodes this behavior as expected, so it is not caught. Fix: in the afterSwap unspecified branch, when fwaDelta < 0 (FWA is the input) gross the fee up exactly as beforeSwap does for exact-output FWA: rate = feeNumerator(); fee = mulDiv(base, rate, RATE_DENOMINATOR - rate, Ceil); base += fee; then _collect(base, fee).","line":238,"path":"src/MadHook.sol","reproduction":"Fresh launch, owner calls enableTrading() (elapsed 0, rate 99%). Pool 1:1 with 1e6 ether liquidity. (a) Alice swaps exact input 100 FWA for MAD: fee = 99 FWA, she receives ~0.99 MAD. (b) Alice swaps exact output 50 MAD: pool side needs ~50.6 FWA, fee = 50.13 FWA, she pays 100.76 FWA total and receives 50 MAD. Expected per spec (99% of the FWA side): fee ~99.76 FWA on 100.76 paid. Actual 50.13 FWA. For the same ~100 FWA, exact output yields 49.6 MAD vs 0.99 MAD. See proof test test/scratch/FeeAsymmetryProof.t.sol (fails on current code: '50129187740392042156 !~= 99757083603380163890'; passes once afterSwap grosses up the FWA-input fee).","severity":"high","snippet":"            fee = feeFor(base); // floor of actual FWA exchanged by the pool\n            _collect(base, fee);","title":"Exact-output MAD buys are taxed on the pool-side FWA only, halving the launch tax versus exact-input buys"},{"citation":"resolved","description":"Trust-gap seam (permissionless access x leader-by-cumulative-spend economics). closeRound has no minimum round length and no delay after the previous closure; the only conditions are 'some leader exists' and 'an unassigned kept NFT exists'. A new round opens with an empty leaderboard at last+1, so whenever a second kept NFT is waiting (or whenever buy()/claim() lands while the current round has no or a tiny leader), the first actor to atomically call token.freeze(dustWallet) (cost 1 wei when the target holds 100 wei MAD) and hook.closeRound() in one transaction becomes leader of a one-block round and has the NFT assigned to themselves. The intended 'highest cumulative freezer of the round' competition never happens for that NFT; honest freezers who intended to compete in the round are excluded. Since claim() is public, the sniper can also chain claim(requestId) -> freeze -> closeRound in one transaction. Fix needs a design decision: enforce a minimum round duration in blocks/time before closeRound (e.g. last+N blocks or closedAt+T), and/or only allow closure when the round has been open at least as long as a configured epoch. Preserve the one-close-per-block and oldest-NFT rules.","line":340,"path":"src/MadHook.sol","reproduction":"Verified with a scratch test on the repo harness: buy()+fulfill+claim twice so keptNFTs=2. Alice freezes Bob (10,000 MAD) and anyone closes round 1 (NFT #1 assigned to Alice, correct). vm.roll(+1). Attacker contract S holds 1 wei MAD; dust wallet D holds 100 wei MAD. In ONE transaction S calls token.freeze(D) (cost 1 wei, 0 burned, 1 wei to D) then hook.closeRound(). Result: token.leader(2) == S, spent(2,S) == 1, rounds(2).nftIndexPlusOne == 2 (NFT #2 assigned to S), roundBlocks(2).first == last (one-block round). After askWinner/award (or 72h fallback) S receives the NFT for 1 wei of MAD.","severity":"medium","snippet":"    function closeRound() external nonReentrant returns (uint256 round) {\n        round = mad.currentRound();\n        if (mad.leader(round) == address(0) || !rewards.hasUnassignedNFT()) revert NoRoundReward();\n        mad.closeRound();\n        rewards.assignRound(round);\n    }","title":"Permissionless closeRound lets anyone end a round at the instant of their choice and win a stockpiled NFT with a 1-wei freeze"},{"citation":"resolved","description":"Branch asymmetry inside _convertNFT: when the market is already at/above the owner's sqrtPriceLimitX96 the function silently returns and retains the claims (line 271), but when the market is just below the limit the hook executes a sale that is cut off by the limit after 1 wei of FWA, receives 0 ETH, and then REVERTS with Slippage, which reverts the user's entire MAD swap (wrapped by the PoolManager as WrappedError(hook, beforeSwap, Slippage())). _convertNFT runs on every fee-bearing swap once nftFwaClaims > 0 and a limit is set, so an attacker who moves the official ETH/FWA pool sqrt price to limit-1 (a single exact-input FWA sell with sqrtPriceLimit = limit-1, which they can re-pin whenever arbitrage moves it) makes every buy and sell of MAD revert until the owner changes the bounds (which the attacker can track). The same revert fires without any attacker whenever the owner's minEthPerFwaX128 is not strictly below the net price implied by the limit minus the official hook's 1% fee. No funds are lost; trading is denied during the period when the launch tax and NFT budget are supposed to accrue. Fix: never let the conversion revert the user's swap. Decide before swapping whether the sale is worth attempting (e.g. skip when limit - current is below a margin, or quote the expected output first), and treat a sale that cannot meet the bound as 'retain claims' by not swapping at all; if a post-swap check is kept, require the sale to be large enough that a dust fill is impossible, or skip conversion entirely when `current` is within a configured band of `limit`.","line":277,"path":"src/MadHook.sol","reproduction":"Verified with a scratch test on the repo harness: _open(0); market initialized at Q96 with 1000 ETH liquidity; owner setConversionBounds(limit = 1.1*Q96, min = 1<<126). A normal MAD buy succeeds and converts. Bob then swaps on the ETH/FWA pool: SwapParams(zeroForOne=false, -500 ether, sqrtPriceLimitX96 = limit-1); getSlot0 shows sqrtPrice == limit-1. Now _swap(true,-1 ether) and _swap(false,-1 ether) on the MAD pool both revert (inner error Slippage() 0x7dd37f70 from beforeSwap 0x575e24b4). Expected: the swap executes and unsold NFT claims wait for a later swap, as they do when current >= limit.","severity":"medium","snippet":"        if (\n            sold > offered\n                || (sold > 0\n                    && (proceeds == 0 || proceeds < Math.mulDiv(sold, minEthPerFwaX128, 1 << 128, Math.Rounding.Ceil)))\n        ) revert Slippage();","title":"NFT conversion reverts the whole MAD swap on a dust sale; pinning the ETH/FWA price one unit below the owner's limit blocks all MAD trading"},{"citation":"resolved","description":"Documented privileged powers (trust assumption, reported separately per the brief's 'No ... changing ... results' and 'No arbitrary withdrawal of FWA/ETH/IMD/NFTs' requirements). Three concrete sequences, each permitted by an onlyOwner guard that is correct in isolation: (1) setOracleSettings(intake=ownerContract, action, asset, 0, signer=ownerKey) then askWinner(round) pays the IMD to the owner's intake and the owner signs an attestation naming any address for the round; onOracleResult accepts it (p.signer == oracleSigner holds because both were set before the ask), so award(round) sends the NFT to the owner's choice instead of the real top freezer. (2) setFwaV2(ownerContract) whose quoteAcquisitionPrice() returns total = nftBudget(); buy() redeems all hook ETH claims and sends `total` to it via acquire{value: total}, draining the NFT budget. (3) setEthFwaPool(ownerPool with fee 999000 and a trivial 1-flag hook) then setConversionBounds(anyLimit, 1): the next MAD swap sells all nftFwaClaims into the owner's pool for >=1 wei ETH, the owner as sole LP collecting ~99.9% as LP fees. The spec permits these setters, so this is not a bypass, but the owner is fully trusted with the NFT budget and the results; consider pinning the FWA V2 and ETH/FWA pool to the official addresses (or making the signer immutable / a registry) if that trust is not intended.","line":148,"path":"src/MadRewards.sol","reproduction":"(1) Owner: setOracleSettings(IntakeMock, bytes32('x'), IMD, 0, vm.addr(ownerKey)); anyone: askWinner(r) after 8 blocks; owner signs Attestation{questionHash = rewards.questionBody(r).hash, chainId 1, fromBlock/toBlock = roundBlocks(r), answerType 4, answer = abi.encode([owner]), panel 30, quorum 20, agreed 20, issuedAt now, expiresAt now+3600} with ownerKey and delivers it from IntakeMock -> rounds(r).winner == owner -> award(r) transfers the NFT to owner. (2) Owner: setFwaV2(Drain) where Drain.quoteAcquisitionPrice() returns (0,0,rewards.nftBudget()) and acquire() keeps msg.value and returns [1]; anyone calls buy() -> rewards and hook ETH budget is now in Drain. (3) as described.","severity":"low","snippet":"        _setOracleSigner(signer_);","title":"Owner setters can redirect every reward flow: pick winners via signer rotation, drain the ETH budget via a fake FWA V2, and sell NFT FWA claims into an owner pool"},{"citation":"resolved","description":"Asymmetry between the freeze-time check and the send-time check. freeze() accepts any target with no code (a not-yet-deployed CREATE2 address passes isWallet), but _update only enforces the freeze when `from` still passes isWallet. A holder who keeps MAD at a counterfactual CREATE2 address can therefore be frozen (the freezer pays 1% of the balance, half burned, half credited to the target), then deploy the contract in the next transaction and transfer everything out: the 24-hour freeze is ineffective and the freezer's spend is wasted while the target pocketed 0.5%. The comment on line 109 shows this is deliberate, but it inverts the economics for the freezer (they pay, the target profits and escapes). Fix options: enforce the frozen check regardless of code (frozenUntil[from] > block.timestamp alone), or refuse to freeze addresses whose nonce-free counterfactual status cannot be established (not possible on chain) and document the limitation to freezers.","line":110,"path":"src/MadToken.sol","reproduction":"Verified with a scratch test on the repo harness: predicted = CREATE2 address of Mover(token, CAROL) (salt 1) from the test contract; token.transfer(predicted, 1000 MAD) (code.length == 0, isWallet true). Alice calls freeze(predicted): she pays 10 MAD (5 burned, 5 to predicted), frozenUntil[predicted] = now+24h. In the same block the deployer runs create2 (succeeds; the transfer must be a call after construction because code is empty during the constructor) and calls Mover.move(): token.transfer(CAROL, 1005 MAD) succeeds because isWallet(predicted) is now false and the WalletFrozen check at line 110 is skipped. CAROL's balance rises by 1005 MAD while the freeze is still active. Expected: predicted cannot send until frozenUntil.","severity":"low","snippet":"        if (from != address(0) && frozenUntil[from] > block.timestamp && isWallet(from)) {","title":"Freeze is void once code is deployed at the frozen address: counterfactual (CREATE2) wallets can be frozen but move funds immediately after deployment"}],"hash":"d1cfd84226696a039942435b2a83d6e515b216b7055672a8496f17a96849e472","nodeId":"dbb76da8-9850-459f-8bc2-37ae38e8ea10","outcome":"completed","summary":"Review complete. The findings file is written and verified against the tree, and no tracked source file was changed.\n\n**Outcome**: 5 findings in `.imd-findings.json`, with coverage rows for all 33 listed entry points (27 hold, 6 carry findings, none unreached).\n\n**Findings**\n\n1. **High. Exact-output MAD buys pay about half the launch tax.** In afterSwap, when FWA is the unspecified input, the fee is computed on the pool-side amount and charged on top, so the effective rate is rate/(1+rate). At open that is 49.7% instead of 99%. Buying 50 MAD by exact output cost 100.76 FWA with a 50.13 FWA fee, while 100 FWA by exact input bought 0.99 MAD. The exact-output-FWA path in beforeSwap grosses up correctly, so this is a one-sided asymmetry. The attached proof test fails on current code and passes once afterSwap grosses up the same way.\n\n2. **Medium. Permissionless closeRound with no minimum round length.** With two kept NFTs, a contract that freezes a 100-wei wallet (cost 1 wei) and calls closeRound in one transaction becomes leader of a one-block round and gets the second NFT assigned. Verified with a scratch test.\n\n3. **Medium. Conversion revert path blocks all MAD trading.** When the ETH/FWA price sits one unit below the owner's sqrt limit, the sale sells dust for 0 ETH and reverts with Slippage, reverting the user's swap. An attacker pinned the price with one swap and every MAD buy and sell then reverted. The at-or-above-limit branch silently retains claims, so the two edges are treated asymmetrically.\n\n4. **Low, trust assumption. Owner setters redirect every reward flow.** Signer rotation lets the owner author winners, a fake FWA V2 drains the ETH budget through buy(), and an owner pool plus a 1-wei minimum sells all NFT FWA claims to the owner. The spec permits these setters and the oracle-consumer reference shows an owner-set signer, so this is documented, not reported as a bypass.\n\n5. **Low. Counterfactual wallets escape freeze.** A CREATE2 address can be frozen while empty, then deployed and emptied in the same block because the send-time check exempts addresses with code. Verified with a scratch test; the freezer's 1% payment is wasted and the target keeps the 0.5% remainder.\n\n**Not reached in depth**: ERC-6909 claim balances held inside the PoolManager let a pre-funded frozen wallet still sell without any ERC20 transfer. I did not substantiate it with a test and left it out. Fork behavior, FWA transfer rules for the PoolManager-to-receiver payout, and the oracle body's explicit evidence field were outside my area and only read, not tested.","treeHash":null,"usage":{"cachedInputTokens":2803726,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":52073,"runtime":"claude","turns":38,"wallClockMs":949493}},{"artifacts":[],"attempt":1,"bundleHash":"184967187b0ec2e1e05a3a5bb7ed74c241df6b17d254066be68c7d378ba68ae1","device":"87645bda72145989","findings":[],"hash":"dd7374e3e376658f0258033efb7931d741e5f2a7a119a1b3bf93fc733594ed93","nodeId":"5f91c837-8195-4b3b-bc1d-9b368aadff7a","outcome":"completed","summary":"Implemented the contracts, vendored dependencies, tests, and [deployment documentation](README.md).\n\nValidation passed: build, offline rebuild, formatting, and 58 tests. Mainnet fork tests skipped because `MAINNET_RPC_URL` was unset.\n\nOne verifier issue remains: its generic initialization fixture conflicts with the required atomic hook binding. Details and validation limits are recorded in [verification.md](docs/verification.md).","treeHash":"3b3d29602b7bfcafb0b6e6533dd876ec6b8cbf22","usage":{"cachedInputTokens":6232192,"inputTokens":256601,"model":"gpt-6-astra","outputTokens":89593,"runtime":"codex","turns":10,"wallClockMs":2471333}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b38bea3c04869138","findings":[{"citation":"resolved","description":"The revision correctly removed the post-sale Slippage revert that let anyone halt trading (settled finding e18cb5be). What replaced it compares the owner's minimum only with ethPerFwaX128(limit), in setConversionBounds (line 182) and here. The market hook's fee on the ETH output (1% on the official ETH/FWA pool per fwa-v2) and v4 rounding are not accounted for, so with the bounds the setter accepts (minimum == price at the bound) every sale returns less ETH per FWA than the parameter named 'minimum' and nothing skips or reverts. The shortfall against the owner's stated floor is about 1% plus rounding on the official pool; on a different owner-set market key it is whatever that hook takes. This is a labelling and parameter-validation gap, not a loss path a third party can trigger: the sqrt limit still bounds the pool price. Fix options that keep the no-revert design: account for the market fee when validating the pair, e.g. require minimumEthPerFwaX128 <= ethPerFwaX128(limit) * 99 / 100 for the official pool (or an owner-set fee bps per market key) and apply the same net-of-fee comparison here; or rename/document the parameter as a bound-consistency check only, so the owner does not rely on it as an execution floor. Raised by write_foundry_tests; reproduced with the repository's MockMarketHook (1% ETH fee), no custom hook needed.","line":310,"path":"src/MadHook.sol","reproduction":"Repo harness MadTestBase. _open(1800) (rest, 1%). Etch MockMarketHook at 0x2C67...6444, initialize the ETH/FWA market at Q96 with 1000 ETH full-range liquidity. limit = Q96 + Q96/1000; OWNER calls setConversionBounds(limit, hook.ethPerFwaX128(limit)) and it is accepted (min == price at bound, 0.998002996004994006 ETH per FWA). ALICE exact-input buys MAD with 10,000 FWA (NFT share 50 FWA). Expected per the parameter's name: proceeds >= sold * min / 2^128 for the FWA sold, or no sale and claims retained. Actual (scratch test test_minimumNotEnforcedOnProceeds, passes on this code): sold = 1e18 FWA, proceeds = 0.989010989010989010 ETH, required at min = 0.998002996004994006 ETH; the sale is accepted into the NFT budget below the configured minimum and no event or revert signals it.","severity":"low","snippet":"        if (current == 0 || current >= limit || ethPerFwaX128(limit) < minEthPerFwaX128) return;","title":"minEthPerFwaX128 is only checked against the price at the bound, never against the ETH the NFT-share sale actually returns"},{"citation":"resolved","description":"This is the author's answer to my earlier medium finding (39a7da0b: a 1-wei freeze bundled with closeRound captured each kept NFT in a one-block round). I reproduced that the capture is now blocked and that the constants work as documented: a dust leader can never close, a 999 MAD leader cannot close, a leader with >= 1,000 MAD closes once block.number >= first + 7199, and the one-close-per-block and oldest-NFT rules are unchanged. The brief defines closeRound only as 'if an unassigned kept NFT and current round leader exist, close round and assign oldest such NFT', so both floors are additions the owner cannot change; the README and launch.json notes state them. Not a defect; recorded (as write_foundry_tests also asked) so the requester confirms the floors or picks other values. No code change is needed unless the requester wants different constants.","line":401,"path":"src/MadHook.sol","reproduction":"Repo harness MadRewardsFixture: _keep() twice; ALICE freezes BOB; roll to first + 7199; closeRound() succeeds (round 1). Next block: S holds 1 wei, V holds 100 wei; S calls token.freeze(V) (cost 1 wei) then hook.closeRound(): reverts NoRoundReward(); still reverts at first + 7210 because spent(round, S) = 1 < 1000e18. CAROL freezes T holding 99,900 MAD (cost 999 MAD): closeRound still reverts NoRoundReward(); after a second freeze bringing CAROL's round spend above 1,000 MAD, closeRound() succeeds. Scratch test SnipeSettleTest.test_oneWeiSnipeBlocked passes on this code.","severity":"info","snippet":"        if (\n            lead == address(0) || mad.spent(round, lead) < MIN_LEADER_SPEND\n                || block.number + 1 < uint256(first) + MIN_ROUND_BLOCKS || !rewards.hasUnassignedNFT()\n        ) revert NoRoundReward();","title":"closeRound now requires a 7,200-block round and a 1,000 MAD leader spend, which the brief's wording for closeRound does not state (scope decision to confirm)"},{"citation":"resolved","description":"This is the author's answer to my earlier low finding (38d98a02: at 99% an exact-input buy larger than the manager's FWA reserves reverted in beforeSwap's take). I reproduced the fix: a buy of three times the manager's FWA at open now executes, the 2.94e24 wei burn is minted as a separate hook claim (BurnDeferred), and flushBurn() burns exactly that amount once and zeroes the counter. The brief says 'burn everything above 1% in the same swap'; the deferred path burns it later, in a permissionless call, and the claim can never be paid out or sold (burnClaims is never read by payout, _convertNFT or redeemNftETH). On mainnet the manager holds the ETH/FWA pool's FWA so deferral should be rare. Needs requester acknowledgement only; no change proposed.","line":291,"path":"src/MadHook.sol","reproduction":"Repo harness: _open(0); mb = fwa.balanceOf(manager) (about 1e24); mint ALICE 10*mb; ALICE exact-input buys MAD with 3*mb through TestRouter (post-settling). Expected under the strict brief wording: fwa.burned() rises by 0.98*3*mb inside the swap. Actual: the swap succeeds, burned-in-swap = 0, hook.burnClaims() = 2,939,999,999,999,999,999,840,203; hook.flushBurn() then burns exactly that and burnClaims() == 0. Scratch test SettleTest.test_bigBuyAtOpenDefers.","severity":"info","snippet":"                burnClaims += burned;","title":"The above-1% FWA burn is deferred to flushBurn() when the PoolManager holds less FWA than the burn, so it is not always burned in the same swap"},{"citation":"resolved","description":"Settlement of my earlier low finding 78df7598. The author disputes it as a brief-granted capability and the README now states the risk and recommends a reviewed multisig. I accept that: the brief grants 'set FWA V2 address, ETH/FWA pool key, price/slippage bounds and oracle settings', so the powers are intentional, and no non-owner path to them exists. Recorded as a trust assumption per the Pashov adapter (privileged powers documented separately), not a defect: an owner who sets fwaV2 to a contract whose quoteAcquisitionPrice returns total = nftBudget() and whose acquire keeps msg.value receives the whole NFT budget on the next public buy(); an owner who sets intake and signer to their own can answer askWinner with any address. No change requested.","line":122,"path":"src/MadRewards.sol","reproduction":"Owner calls rewards.setFwaV2(Drain) where Drain.isPurchaseBlackout() returns false, quoteAcquisitionPrice() returns (0, 0, rewards.nftBudget()) and acquire() keeps msg.value and returns [1]. Anyone calls hook.buy(): line 161 redeems the hook's ETH claims and line 163 sends the whole budget to Drain. Only the owner can create that state.","severity":"info","snippet":"    function setFwaV2(IFWAV2 pool) external onlyOwner nonReentrant {","title":"Owner setters remain a full trust boundary over rewards: a hostile setFwaV2 target drains the NFT ETH budget and a hostile Intake/signer names the winner (documented trust assumption)"}],"hash":"fcb7210513f9da9915e9f8fdca4b85f3eafef53586de0ef2d383a50564c99aea","nodeId":"6a3c5acd-ecfa-495d-b976-646975938c5e","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json` with 4 findings and a 37-row coverage record.\n\n**Settlement of my earlier findings**\n\n- **Exact-output buy tax (high): fixed.** My attached proof now passes on the revised code. Buying the same MAD by exact output costs 100.0002 FWA against 100 FWA by exact input at the 99% opening rate.\n- **Conversion halts trading (high): fixed.** I re-ran the griefing sequence. With the ETH/FWA market parked one unit under the owner's limit, buys and sells in both modes execute and the NFT claims wait. A 300-wei buy also executes.\n- **1-wei round capture (medium): fixed.** A dust freeze plus closeRound now reverts, a 999 MAD leader cannot close, and a leader above 1,000 MAD closes once the round spans 7,200 blocks.\n- **Counterfactual freeze escape (low): fixed.** Etching code at a frozen address no longer lets it transfer until the deadline passes.\n- **Large buy reverts at open (low): fixed.** A buy of three times the manager's FWA now executes, defers the burn as a separate claim, and flushBurn burns exactly that amount once.\n- **Owner setters, partial fills, transient binding (disputed):** I accept the author's reasoning on all three. The brief grants the setters, v4 cannot refund a specified-side delta in afterSwap, and the brief itself requires the atomic binding. The owner powers stay in the file as an info trust note only.\n\n**What remains in the file**\n\n- **Low:** the conversion minimum is now compared only with the price at the sqrt limit. With the official market's 1% fee, a sale at a minimum equal to the bound price returns 0.989 ETH per FWA against a stated 0.998 floor, and nothing skips or reverts. Fix by validating the pair net of the market fee or by documenting the parameter as a consistency check.\n- **Info:** the new round floors deviate from the brief's wording and need the requester's confirmation. The deferred burn path needs acknowledgement. The owner trust boundary is documented.\n\nThe full suite passes with 110 tests, and all three contracts are within the runtime and initcode size limits. Scratch files were removed and no repository files were changed.","treeHash":null,"usage":{"cachedInputTokens":1519698,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":26555,"runtime":"claude","turns":20,"wallClockMs":637571}}],"verification":[{"checks":[{"durationMs":98756,"exitCode":0,"name":"build","output":"Compiling 127 files with Solc 0.8.26\nSolc 0.8.26 finished in 98.63s\nCompiler run successful!\n","passed":true},{"durationMs":20345,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 319.96µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.29ms (1.10ms CPU time)\n\nRan 12 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234712, ~: 234857)\nLogs:\n  Bound result 11280180419\n\n[PASS] test_7702ExactDesignationOnly() (gas: 441919)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265544)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242335)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1260504)\n[PASS] test_frozenCounterfactualAddressStaysFrozenAfterDeployingCode() (gas: 331056)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 816763)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281557)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210392)\n[PASS] test_tiesAndRoundBoundary() (gas: 843153)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 54.06ms (15.24ms CPU time)\n\nRan 16 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1536860)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 589659)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1588550)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2691233)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2828671)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2228104)\n[PASS] test_dustFreezeAndShortRoundCannotCaptureAnNFT() (gas: 2282622)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4110701)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2787146)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1388591)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1636239)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 3034334)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2375977)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3273195)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4482366)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5499058)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 54.15ms (31.59ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 26080693)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 68.52ms (38.21ms CPU time)\n\nRan 16 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 529734, ~: 536561)\nLogs:\n  Bound result 9\n  Bound result 975412939109079873\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4923701)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226942)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1616126)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1277778)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1194929)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 670698)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915682)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1339634)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1369965)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2754749)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 324423)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 686559)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293784)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1546207)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 83.98ms (41.39ms CPU time)\n\nRan 2 tests for test/MadBurnDeferral.t.sol:MadExactOutputFeeTest\n[PASS] testFuzz_exactOutputRoutesChargeCeilingOfScheduleOnGross(uint96,uint16,bool) (runs: 1000, μ: 440933, ~: 444869)\n[PASS] test_exactOutputAtRestChargesOnePercentRoundedUp() (gas: 397684)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 102.99ms (34.02ms CPU time)\n\nRan 17 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 528967, ~: 537213)\nLogs:\n  Bound result 1697\n  Bound result 17339\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4924222)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 227062)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1621023)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1280670)\n[PASS] test_fwaSortsFirst() (gas: 5998)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1199321)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 675091)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 920121)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1344027)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1374358)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2759447)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329807)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 691170)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293640)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1551776)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 136.98ms (61.87ms CPU time)\n\nRan 6 tests for test/MadTokenAdversarial.t.sol:MadTokenBoundaryTest\n[PASS] testFuzz_codeLengthAndPrefixAreBothRequired(uint8,bytes3) (runs: 1000, μ: 191175, ~: 179027)\nLogs:\n  Bound result 4\n\n[PASS] test_delegationChangeAndRevocationDoNotRemoveFreeze() (gas: 422940)\n[PASS] test_freezeCannotSpendApprovedTokensFromSomeoneElse() (gas: 242448)\n[PASS] test_frozenTransferFromRollsBackFiniteAllowanceAndCanRetryAtExpiry() (gas: 448442)\n[PASS] test_insufficientSecondFreezeDebitRollsBackBurnAndRoundAccounting() (gas: 230387)\n[PASS] test_minimumFreezeCostsOneWeiAndBurnsZero() (gas: 352192)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 136.87ms (137.61ms CPU time)\n\nRan 7 tests for test/MadHookAdversarial.t.sol:MadHookAdversarialTest\n[PASS] testFuzz_feeRoundingBoundAndMonotonicDecay(uint128,uint16,uint16) (runs: 1000, μ: 69808, ~: 69811)\nLogs:\n  Bound result 2326\n  Bound result 2630\n\n[PASS] test_conversionAndMarketSettersRejectUnauthorizedAndUnsafeBounds() (gas: 240700)\n[PASS] test_failedNativeRedemptionRetainsOnlyNFTClaims() (gas: 1524117)\n[PASS] test_failedPayoutCannotDestroyClaimsAndLaterRetryPaysOnce() (gas: 681229)\n[PASS] test_poolChangeInvalidatesConversionPermissionButPreservesBudgets() (gas: 1840316)\n[PASS] test_restingDustAndLaunchFeeBoundariesThroughRealManager() (gas: 4057110)\n[PASS] test_zeroAndOutOfInt128SwapsCannotPoisonNextSwap() (gas: 554583)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 149.02ms (126.31ms CPU time)\n\nRan 18 tests for test/MadRewardsAdversarial.t.sol:MadRewardsAdversarialTest\n[PASS] testFuzz_malformedAcquisitionOrListingCannotEnterQueue(uint8) (runs: 256, μ: 994497, ~: 977918)\n[PASS] testFuzz_malformedSignedAnswersLeavePendingRequestUsable(uint8) (runs: 256, μ: 2817888, ~: 2822348)\n[PASS] test_callbackFitsStipendWithColdConsumerTokenAndSigner() (gas: 2428491)\n[PASS] test_differentRequestCannotEnqueueAnAlreadyHeldNFT() (gas: 1620162)\n[PASS] test_exactFallbackBoundaryInvalidatesAskBeforeAnyAwardAttempt() (gas: 2533209)\n[PASS] test_failedRetryAtPriceCeilingLeavesOriginalRequestActive() (gas: 2775792)\n[PASS] test_intakeRotationDoesNotAuthorizeNewIntakeForOldRequest() (gas: 3205442)\n[PASS] test_invalidAcquireArrayRollsBackPaymentAndCanRetry() (gas: 451051)\n[PASS] test_keepReturningWithoutDeliveryCannotCreatePhantomNFT() (gas: 1018437)\n[PASS] test_liveSettlementWindowAndExactBoundary() (gas: 931618)\n[PASS] test_nftReceiverCannotReenterAcquisitionDuringAward() (gas: 3305085)\n[PASS] test_questionBodyParsesAndHashMatchesIndependentCanonicalDocument() (gas: 1381582)\n[PASS] test_rotatedSignerCannotRewritePendingRequestAndRetryUsesNewSigner() (gas: 3369790)\n[PASS] test_sameNFTCanBePurchasedAgainWithoutReopeningOldRound() (gas: 4507491)\n[PASS] test_signatureForAnotherConsumerCannotSetWinner() (gas: 2508502)\n[PASS] test_unsolicitedNFTCannotBecomeRoundReward() (gas: 177680)\n[PASS] test_validWinnerBeforeFallbackRemainsWinnerAfterDeadline() (gas: 2515801)\n[PASS] test_zeroLiveQuoteCannotSpendBudget() (gas: 114207)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 164.29ms (216.06ms CPU time)\n\nRan 2 tests for test/MadBurnDeferral.t.sol:MadBurnDeferralInvariantTest\n[PASS] invariant_claimsMatchSeparateObligationsAndBurnsAreConserved() (runs: 256, calls: 15360, reverts: 0)\n\n╭------------------------+-----------+-------+---------+----------╮\n| Contract               | Selector  | Calls | Reverts | Discards |\n+=================================================================+\n| MadBurnDeferralHandler | bigBuy    | 2483  | 0       | 0        |\n|------------------------+-----------+-------+---------+----------|\n| MadBurnDeferralHandler | buy       | 2619  | 0       | 0        |\n|------------------------+-----------+-------+---------+----------|\n| MadBurnDeferralHandler | flushBurn | 2583  | 0       | 0        |\n|------------------------+-----------+-------+---------+----------|\n| MadBurnDeferralHandler | payout    | 2621  | 0       | 0        |\n|------------------------+-----------+-------+---------+----------|\n| MadBurnDeferralHandler | sell      | 2590  | 0       | 0        |\n|------------------------+-----------+-------+---------+----------|\n| MadBurnDeferralHandler | wait      | 2464  | 0       | 0        |\n╰------------------------+-----------+-------+---------+----------╯\n\n[PASS] test_burnDeferralHandlerReachesDeferFlushAndSell() (gas: 2367211)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.91s (2.87s CPU time)\n\nRan 2 tests for test/MadHookAdversarial.t.sol:MadConversionInvariantTest\n[PASS] invariant_feeRevenueConservationWithPartialConversionsAndPayouts() (runs: 256, calls: 20480, reverts: 0)\n\n╭----------------------+----------+-------+---------+----------╮\n| Contract             | Selector | Calls | Reverts | Discards |\n+==============================================================+\n| MadConversionHandler | flush    | 5166  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | payout   | 5010  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | setLimit | 5135  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | swap     | 5169  | 0       | 0        |\n╰----------------------+----------+-------+---------+----------╯\n\n[PASS] test_conversionHandlerExercisesPartialDeferredAndCompleteSales() (gas: 1620596)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.27s (3.22s CPU time)\n\nRan 2 tests for test/MadRewardsInvariant.t.sol:MadRewardsInvariantTest\n[PASS]\nMadRewardsInvariantTest invariants:\n[PASS] invariant_ethAndImdConservationNoOutstandingOracleAllowance\n[PASS] invariant_everyKeptNFTIsHeldOrAwardedExactlyOnce\n[PASS] invariant_roundsFollowIndependentOracleAndAwardState\n MadRewardsInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭----------------------------+----------------+-------+---------+----------╮\n| Contract                   | Selector       | Calls | Reverts | Discards |\n+==========================================================================+\n| MadRewardsLifecycleHandler | advance        | 1807  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | answer         | 1876  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | ask            | 1874  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | award          | 1824  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | buy            | 1884  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | claim          | 1798  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | donate         | 1922  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | failure        | 1924  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | freezeAndClose | 1809  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | resolveDraw    | 1838  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | withdrawRefund | 1924  | 0       | 0        |\n╰----------------------------+----------------+-------+---------+----------╯\n\n[PASS] test_lifecycleHandlerExercisesRefundRetryRejectedAwardAndFallback() (gas: 3161833)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.26s (6.23s CPU time)\n\nRan 2 tests for test/MadTokenAdversarial.t.sol:MadTokenLedgerInvariantTest\n[PASS]\nMadTokenLedgerInvariantTest invariants:\n[PASS] invariant_balancesAllowancesAndSupplyMatchIndependentLedger\n[PASS] invariant_roundWindowsAndLeadersMatchPaidCostsAndOrder\n MadTokenLedgerInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭-----------------------+---------------+-------+---------+----------╮\n| Contract              | Selector      | Calls | Reverts | Discards |\n+====================================================================+\n| MadTokenLedgerHandler | advance       | 3341  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | approve       | 3389  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | close         | 3490  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | delegatedMove | 3457  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | freeze        | 3396  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | move          | 3407  | 0       | 0        |\n╰-----------------------+---------------+-------+---------+----------╯\n\n[PASS] test_ledgerHandlerReachesSuccessFailureAndRoundTransition() (gas: 2088278)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 11.72s (11.68s CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.23s (20.15s CPU time)\n\nRan 16 test suites in 20.23s (45.35s CPU time): 109 tests passed, 0 failed, 1 skipped (110 total tests)\n","passed":true},{"durationMs":77,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushBurn()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":425,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":117,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":18,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadBurnDeferral.t.sol\":268,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":350,\"test/MadHookAdversarial.t.sol\":293,\"test/MadInvariant.t.sol\":91,\"test/MadRewards.t.sol\":447,\"test/MadRewardsAdversarial.t.sol\":371,\"test/MadRewardsInvariant.t.sol\":395,\"test/MadToken.t.sol\":264,\"test/MadTokenAdversarial.t.sol\":335,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/TESTING.md\":90,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":124,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"17854e1b8a6600e18e3f30c95c1149340f78c159c395a5d35b433566e5641342","verifiedTreeHash":"dee89e2c4a3932d98786b10036d634278e375f61","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":54965,"exitCode":0,"name":"build","output":"Compiling 122 files with Solc 0.8.26\nSolc 0.8.26 finished in 54.86s\nCompiler run successful!\n","passed":true},{"durationMs":21132,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 326.00µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.38ms (1.01ms CPU time)\n\nRan 16 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1536743)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 589483)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1588506)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2691116)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2686808)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2227879)\n[PASS] test_dustFreezeAndShortRoundCannotCaptureAnNFT() (gas: 2282397)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4110584)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2787029)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1388474)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1635961)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 3034217)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2234187)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3273078)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4482141)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5498941)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 34.04ms (27.49ms CPU time)\n\nRan 12 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234712, ~: 234857)\nLogs:\n  Bound result 1647\n\n[PASS] test_7702ExactDesignationOnly() (gas: 441919)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265544)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242335)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1260504)\n[PASS] test_frozenCounterfactualAddressStaysFrozenAfterDeployingCode() (gas: 331056)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 816763)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281557)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210392)\n[PASS] test_tiesAndRoundBoundary() (gas: 843153)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 36.08ms (12.02ms CPU time)\n\nRan 17 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 529372, ~: 537218)\nLogs:\n  Bound result 2736\n  Bound result 14887\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4924222)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 227062)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1621023)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1280670)\n[PASS] test_fwaSortsFirst() (gas: 5998)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1199321)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 675091)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 920121)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1344027)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1374358)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2759447)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329807)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 691170)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293640)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1551776)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 36.45ms (40.10ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 26080693)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 69.19ms (37.43ms CPU time)\n\nRan 16 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 529735, ~: 536561)\nLogs:\n  Bound result 2660\n  Bound result 13657\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4923701)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226942)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1616126)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1277778)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1194929)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 670698)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915682)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1339634)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1369965)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2754749)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 324423)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 686559)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293784)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1546207)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 94.57ms (60.43ms CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.00s (20.95s CPU time)\n\nRan 8 test suites in 21.00s (21.27s CPU time): 68 tests passed, 0 failed, 1 skipped (69 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushBurn()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"launch.json\":34,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":425,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":117,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":18,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":350,\"test/MadInvariant.t.sol\":91,\"test/MadRewards.t.sol\":445,\"test/MadToken.t.sol\":264,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":114,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"494475f9b72995a369befa68850eff0ff9c2ed28193cca866b938ecadc1577e7","verifiedTreeHash":"a52646fa3892f6710071eb1fce086a3b96243db9","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":88101,"exitCode":0,"name":"build","output":"Compiling 126 files with Solc 0.8.26\nSolc 0.8.26 finished in 87.97s\nCompiler run successful!\n","passed":true},{"durationMs":19576,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 367.56µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 37.45ms (1.24ms CPU time)\n\nRan 15 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1507304)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 589119)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1586225)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2660344)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2828329)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2194607)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4076479)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2755566)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1359453)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1635698)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 2995856)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2375767)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3242418)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4387691)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5460397)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 37.16ms (32.42ms CPU time)\n\nRan 6 tests for test/MadTokenAdversarial.t.sol:MadTokenBoundaryTest\n[PASS] testFuzz_codeLengthAndPrefixAreBothRequired(uint8,bytes3) (runs: 1000, μ: 191116, ~: 179061)\nLogs:\n  Bound result 34\n\n[PASS] test_delegationChangeAndRevocationDoNotRemoveFreeze() (gas: 423529)\n[PASS] test_freezeCannotSpendApprovedTokensFromSomeoneElse() (gas: 242482)\n[PASS] test_frozenTransferFromRollsBackFiniteAllowanceAndCanRetryAtExpiry() (gas: 451184)\n[PASS] test_insufficientSecondFreezeDebitRollsBackBurnAndRoundAccounting() (gas: 230421)\n[PASS] test_minimumFreezeCostsOneWeiAndBurnsZero() (gas: 352243)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 38.06ms (38.66ms CPU time)\n\nRan 7 tests for test/MadHookAdversarial.t.sol:MadHookAdversarialTest\n[PASS] testFuzz_feeRoundingBoundAndMonotonicDecay(uint128,uint16,uint16) (runs: 1000, μ: 69459, ~: 69459)\nLogs:\n  Bound result 1516\n  Bound result 0\n\n[PASS] test_conversionAndMarketSettersRejectUnauthorizedAndUnsafeBounds() (gas: 239860)\n[PASS] test_failedNativeRedemptionRetainsOnlyNFTClaims() (gas: 1521541)\n[PASS] test_failedPayoutCannotDestroyClaimsAndLaterRetryPaysOnce() (gas: 678452)\n[PASS] test_poolChangeInvalidatesConversionPermissionButPreservesBudgets() (gas: 1836459)\n[PASS] test_restingDustAndLaunchFeeBoundariesThroughRealManager() (gas: 4040504)\n[PASS] test_zeroAndOutOfInt128SwapsCannotPoisonNextSwap() (gas: 552862)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 37.78ms (25.98ms CPU time)\n\nRan 11 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234757, ~: 234874)\nLogs:\n  Bound result 11280180419\n\n[PASS] test_7702ExactDesignationOnly() (gas: 442317)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265522)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242352)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1261894)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 817162)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281748)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210443)\n[PASS] test_tiesAndRoundBoundary() (gas: 843165)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 49.00ms (34.27ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 24445083)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 52.56ms (37.27ms CPU time)\n\nRan 12 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 525640, ~: 533022)\nLogs:\n  Bound result 9\n  Bound result 975412939109079873\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4891802)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226210)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1532519)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915383)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1337053)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1366346)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 323820)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 683728)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293067)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1541538)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 68.20ms (59.58ms CPU time)\n\nRan 13 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 524558, ~: 533765)\nLogs:\n  Bound result 7\n  Bound result 24823403085384979642\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4893361)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226270)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1536819)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_fwaSortsFirst() (gas: 5906)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 919803)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1341500)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1370825)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329224)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 688394)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 292931)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1547181)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 68.22ms (31.96ms CPU time)\n\nRan 18 tests for test/MadRewardsAdversarial.t.sol:MadRewardsAdversarialTest\n[PASS] testFuzz_malformedAcquisitionOrListingCannotEnterQueue(uint8) (runs: 256, μ: 993628, ~: 977632)\n[PASS] testFuzz_malformedSignedAnswersLeavePendingRequestUsable(uint8) (runs: 256, μ: 2786620, ~: 2790787)\n[PASS] test_callbackFitsStipendWithColdConsumerTokenAndSigner() (gas: 2397755)\n[PASS] test_differentRequestCannotEnqueueAnAlreadyHeldNFT() (gas: 1619643)\n[PASS] test_exactFallbackBoundaryInvalidatesAskBeforeAnyAwardAttempt() (gas: 2502339)\n[PASS] test_failedRetryAtPriceCeilingLeavesOriginalRequestActive() (gas: 2744184)\n[PASS] test_intakeRotationDoesNotAuthorizeNewIntakeForOldRequest() (gas: 3174661)\n[PASS] test_invalidAcquireArrayRollsBackPaymentAndCanRetry() (gas: 450457)\n[PASS] test_keepReturningWithoutDeliveryCannotCreatePhantomNFT() (gas: 1018151)\n[PASS] test_liveSettlementWindowAndExactBoundary() (gas: 931332)\n[PASS] test_nftReceiverCannotReenterAcquisitionDuringAward() (gas: 3273890)\n[PASS] test_questionBodyParsesAndHashMatchesIndependentCanonicalDocument() (gas: 1337544)\n[PASS] test_rotatedSignerCannotRewritePendingRequestAndRetryUsesNewSigner() (gas: 3337378)\n[PASS] test_sameNFTCanBePurchasedAgainWithoutReopeningOldRound() (gas: 4414978)\n[PASS] test_signatureForAnotherConsumerCannotSetWinner() (gas: 2477761)\n[PASS] test_unsolicitedNFTCannotBecomeRoundReward() (gas: 177680)\n[PASS] test_validWinnerBeforeFallbackRemainsWinnerAfterDeadline() (gas: 2485052)\n[PASS] test_zeroLiveQuoteCannotSpendBudget() (gas: 113943)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 140.40ms (176.06ms CPU time)\n\nRan 2 tests for test/MadHookAdversarial.t.sol:MadConversionInvariantTest\n[PASS] invariant_feeRevenueConservationWithPartialConversionsAndPayouts() (runs: 256, calls: 20480, reverts: 0)\n\n╭----------------------+----------+-------+---------+----------╮\n| Contract             | Selector | Calls | Reverts | Discards |\n+==============================================================+\n| MadConversionHandler | flush    | 5166  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | payout   | 5010  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | setLimit | 5135  | 0       | 0        |\n|----------------------+----------+-------+---------+----------|\n| MadConversionHandler | swap     | 5169  | 0       | 0        |\n╰----------------------+----------+-------+---------+----------╯\n\n[PASS] test_conversionHandlerExercisesPartialDeferredAndCompleteSales() (gas: 1591254)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.31s (3.27s CPU time)\n\nRan 2 tests for test/MadRewardsInvariant.t.sol:MadRewardsInvariantTest\n[PASS]\nMadRewardsInvariantTest invariants:\n[PASS] invariant_ethAndImdConservationNoOutstandingOracleAllowance\n[PASS] invariant_everyKeptNFTIsHeldOrAwardedExactlyOnce\n[PASS] invariant_roundsFollowIndependentOracleAndAwardState\n MadRewardsInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭----------------------------+----------------+-------+---------+----------╮\n| Contract                   | Selector       | Calls | Reverts | Discards |\n+==========================================================================+\n| MadRewardsLifecycleHandler | advance        | 1807  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | answer         | 1876  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | ask            | 1874  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | award          | 1824  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | buy            | 1884  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | claim          | 1798  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | donate         | 1922  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | failure        | 1924  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | freezeAndClose | 1809  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | resolveDraw    | 1838  | 0       | 0        |\n|----------------------------+----------------+-------+---------+----------|\n| MadRewardsLifecycleHandler | withdrawRefund | 1924  | 0       | 0        |\n╰----------------------------+----------------+-------+---------+----------╯\n\n[PASS] test_lifecycleHandlerExercisesRefundRetryRejectedAwardAndFallback() (gas: 3158579)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.72s (6.71s CPU time)\n\nRan 2 tests for test/MadTokenAdversarial.t.sol:MadTokenLedgerInvariantTest\n[PASS]\nMadTokenLedgerInvariantTest invariants:\n[PASS] invariant_balancesAllowancesAndSupplyMatchIndependentLedger\n[PASS] invariant_roundWindowsAndLeadersMatchPaidCostsAndOrder\n MadTokenLedgerInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭-----------------------+---------------+-------+---------+----------╮\n| Contract              | Selector      | Calls | Reverts | Discards |\n+====================================================================+\n| MadTokenLedgerHandler | advance       | 3341  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | approve       | 3389  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | close         | 3490  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | delegatedMove | 3457  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | freeze        | 3396  | 0       | 0        |\n|-----------------------+---------------+-------+---------+----------|\n| MadTokenLedgerHandler | move          | 3407  | 0       | 0        |\n╰-----------------------+---------------+-------+---------+----------╯\n\n[PASS] test_ledgerHandlerReachesSuccessFailureAndRoundTransition() (gas: 2091177)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.69s (12.67s CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 19.46s (19.44s CPU time)\n\nRan 14 test suites in 19.46s (42.71s CPU time): 95 tests passed, 0 failed, 1 skipped (96 total tests)\n","passed":true},{"durationMs":95,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":363,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":115,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":17,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":258,\"test/MadHookAdversarial.t.sol\":285,\"test/MadInvariant.t.sol\":87,\"test/MadRewards.t.sol\":410,\"test/MadRewardsAdversarial.t.sol\":362,\"test/MadRewardsInvariant.t.sol\":391,\"test/MadToken.t.sol\":242,\"test/MadTokenAdversarial.t.sol\":335,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/TESTING.md\":83,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":124,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"646d59e5d5e91ccf9650ff047fd131d0c91ec1cb1747163d7cfc477f48172773","verifiedTreeHash":"5b06f1981f0d74460eb9bec45d272407250f240a","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":48178,"exitCode":0,"name":"build","output":"Compiling 122 files with Solc 0.8.26\nSolc 0.8.26 finished in 48.08s\nCompiler run successful!\n","passed":true},{"durationMs":19111,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 307.47µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.40ms (1.12ms CPU time)\n\nRan 15 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1507187)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 588943)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1586181)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2660227)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2686466)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2194382)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4076362)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2755449)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1359336)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1635420)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 2995739)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2233977)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3242301)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4387466)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5460280)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 23.64ms (28.73ms CPU time)\n\nRan 11 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234757, ~: 234874)\nLogs:\n  Bound result 1791\n\n[PASS] test_7702ExactDesignationOnly() (gas: 442317)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265522)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242352)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1261894)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 817162)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281748)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210443)\n[PASS] test_tiesAndRoundBoundary() (gas: 843165)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 33.30ms (16.36ms CPU time)\n\nRan 12 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 525834, ~: 533022)\nLogs:\n  Bound result 982\n  Bound result 13865\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4891802)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226210)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1532519)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915383)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1337053)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1366346)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 323820)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 683728)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293067)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1541538)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 35.54ms (35.19ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 24445083)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 46.75ms (35.60ms CPU time)\n\nRan 13 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 524954, ~: 533766)\nLogs:\n  Bound result 9\n  Bound result 975412939109079873\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4893361)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226270)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1536819)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_fwaSortsFirst() (gas: 5906)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 919803)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1341500)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1370825)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329224)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 688394)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 292931)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1547181)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 58.03ms (35.24ms CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 19.00s (18.98s CPU time)\n\nRan 8 test suites in 19.00s (19.20s CPU time): 58 tests passed, 0 failed, 1 skipped (59 total tests)\n","passed":true},{"durationMs":55,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"launch.json\":34,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":363,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":115,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":17,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":258,\"test/MadInvariant.t.sol\":87,\"test/MadRewards.t.sol\":408,\"test/MadToken.t.sol\":242,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":114,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6b9739fe02a16b32f2c230b610cb4b85e958a7be9f5d1839f3b463a27c761d61","verifiedTreeHash":"38ee5b5ad97c7fe1e589152e7e65170284c24d5a","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":65064,"exitCode":0,"name":"build","output":"Compiling 122 files with Solc 0.8.26\nSolc 0.8.26 finished in 64.91s\nCompiler run successful!\n","passed":true},{"durationMs":22918,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 336.54µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.32ms (1.14ms CPU time)\n\nRan 16 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1536743)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 589483)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1588506)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2691116)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2686808)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2227879)\n[PASS] test_dustFreezeAndShortRoundCannotCaptureAnNFT() (gas: 2282397)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4110584)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2787029)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1388474)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1635961)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 3034217)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2234187)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3273078)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4482141)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5498941)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 43.17ms (35.40ms CPU time)\n\nRan 12 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234712, ~: 234857)\nLogs:\n  Bound result 2130368737\n\n[PASS] test_7702ExactDesignationOnly() (gas: 441919)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265544)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242335)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1260504)\n[PASS] test_frozenCounterfactualAddressStaysFrozenAfterDeployingCode() (gas: 331056)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 816763)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281557)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210392)\n[PASS] test_tiesAndRoundBoundary() (gas: 843153)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 44.43ms (19.02ms CPU time)\n\nRan 17 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 529372, ~: 537218)\nLogs:\n  Bound result 3600\n  Bound result 17339\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4924222)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 227062)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1621023)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1280670)\n[PASS] test_fwaSortsFirst() (gas: 5998)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1199321)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 675091)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 920121)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1344027)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1374358)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2759447)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329807)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 691170)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293640)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1551776)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 54.69ms (52.10ms CPU time)\n\nRan 16 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 529735, ~: 536561)\nLogs:\n  Bound result 3600\n  Bound result 999999999999999992156\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4923701)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226942)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 106502)\n[PASS] test_dustShareNeverFailsASwap() (gas: 1616126)\n[PASS] test_exactOutputBuyPaysTheSameScheduleAsExactInputBuy() (gas: 1277778)\n[PASS] test_inconsistentMinimumRejectedAndStaleLimitDefers() (gas: 1194929)\n[PASS] test_largeBuyAtOpenDefersBurnUntilFlushed() (gas: 670698)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915682)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1339634)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1369965)\n[PASS] test_parkedMarketPriceDefersConversionInsteadOfFailingTrades() (gas: 2754749)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 324423)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 686559)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293784)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1546207)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 65.94ms (41.41ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 26080693)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 73.42ms (41.40ms CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 22.78s (22.74s CPU time)\n\nRan 8 test suites in 22.78s (23.07s CPU time): 68 tests passed, 0 failed, 1 skipped (69 total tests)\n","passed":true},{"durationMs":88,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushBurn()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":425,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":117,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":18,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":350,\"test/MadInvariant.t.sol\":91,\"test/MadRewards.t.sol\":445,\"test/MadToken.t.sol\":264,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":114,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":11554,"exitCode":0,"name":"slither","output":"[medium/medium] divide-before-multiply at src/MadHook.sol:275: MadHook._collect(uint256,uint256) (src/MadHook.sol#275-298) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/MadHook.sol:275: MadHook._collect(uint256,uint256) (src/MadHook.sol#275-298) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/MadRewards.sol:224: MadRewards.question(uint256) (src/MadRewards.sol#224-243) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MadHook.sol:302: Reentrancy in MadHook._convertNFT() (src/MadHook.sol#302-335):\n[medium/medium] reentrancy-no-eth at src/MadRewards.sol:330: Reentrancy in MadRewards.award(uint256) (src/MadRewards.sol#330-355):\n[medium/medium] reentrancy-no-eth at src/MadHook.sol:275: Reentrancy in MadHook._collect(uint256,uint256) (src/MadHook.sol#275-298):\n[medium/medium] reentrancy-no-eth at src/MadRewards.sol:255: Reentrancy in MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288):\n[medium/medium] uninitialized-local at src/MadHook.sol:253: MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).fee (src/MadHook.sol#253) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MadHook.sol:219: MadHook.beforeSwap(address,PoolKey,SwapParams,bytes).fee (src/MadHook.sol#219) is a local variable never initialized\n[medium/medium] unused-return at src/MadHook.sol:367: MadHook._redeemNftETH(uint256) (src/MadHook.sol#367-374) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MadHook.sol#371)\n[medium/medium] unused-return at src/MadRewards.sol:212: MadRewards.assignRound(uint256) (src/MadRewards.sol#212-222) ignores return value by (None,last) = mad.roundBlocks(round) (src/MadRewards.sol#214)\n[medium/medium] unused-return at src/MadHook.sol:396: MadHook.closeRound() (src/MadHook.sol#396-407) ignores return value by (first,None) = mad.roundBlocks(round) (src/MadHook.sol#399)\n[medium/medium] unused-return at src/MadHook.sol:396: MadHook.closeRound() (src/MadHook.sol#396-407) ignores return value by mad.closeRound() (src/MadHook.sol#405)\n[medium/medium] unused-return at src/MadHook.sol:302: MadHook._convertNFT() (src/MadHook.sol#302-335) ignores return value by (current,None,None,None) = poolManager.getSlot0(marketId) (src/MadHook.sol#308)\n[medium/medium] unused-return at src/MadRewards.sol:156: MadRewards.buy() (src/MadRewards.sol#156-169) ignores return value by (fee,None,total) = pool.quoteAcquisitionPrice() (src/MadRewards.sol#159)\n[medium/medium] unused-return at src/MadHook.sol:337: MadHook.payout() (src/MadHook.sol#337-345) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MadHook.sol#342)\n[medium/medium] unused-return at src/MadRewards.sol:255: MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288) ignores return value by (None,last) = mad.roundBlocks(round) (src/MadRewards.sol#261)\n[medium/medium] unused-return at src/MadHook.sol:357: MadHook.flushBurn() (src/MadHook.sol#357-365) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MadHook.sol#362)\n[medium/medium] unused-return at src/MadRewards.sol:195: MadRewards.withdrawAcquisitionRefund(address) (src/MadRewards.sol#195-201) ignores return value by IFWAV2(pool).withdrawAcquisitionRefund() (src/MadRewards.sol#198)\n[low/medium] reentrancy-benign at src/MadRewards.sol:171: Reentrancy in MadRewards.claim(uint256) (src/MadRewards.sol#171-192):\n[low/medium] reentrancy-benign at src/MadRewards.sol:156: Reentrancy in MadRewards.buy() (src/MadRewards.sol#156-169):\n[low/medium] reentrancy-benign at src/MadHook.sol:367: Reentrancy in MadHook._redeemNftETH(uint256) (src/MadHook.sol#367-374):\n[low/medium] reentrancy-benign at src/MadHook.sol:242: Reentrancy in MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MadHook.sol#242-273):\n[low/medium] reentrancy-benign at src/MadHook.sol:122: Reentrancy in MadHook.beforeInitialize(address,PoolKey,uint160) (src/MadHook.sol#122-142):\n[low/medium] reentrancy-benign at src/MadHook.sol:275: Reentrancy in MadHook._collect(uint256,uint256) (src/MadHook.sol#275-298):\n[low/medium] reentrancy-benign at src/MadHook.sol:337: Reentrancy in MadHook.payout() (src/MadHook.sol#337-345):\n[low/medium] reentrancy-benign at src/MadRewards.sol:330: Reentrancy in MadRewards.award(uint256) (src/MadRewards.sol#330-355):\n[low/medium] reentrancy-benign at src/MadHook.sol:357: Reentrancy in MadHook.flushBurn() (src/MadHook.sol#357-365):\n[low/medium] reentrancy-benign at src/MadHook.sol:302: Reentrancy in MadHook._convertNFT() (src/MadHook.sol#302-335):\n[low/medium] timestamp at src/MadRewards.sol:255: MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:290: MadRewards.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/MadRewards.sol#290-316) uses timestamp for comparisons\n[low/medium] timestamp at src/MadToken.sol:108: MadToken._update(address,address,uint256) (src/MadToken.sol#108-116) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:242: MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MadHook.sol#242-273) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:330: MadRewards.award(uint256) (src/MadRewards.sol#330-355) uses timestamp for comparisons\n[low/medium] timestamp at src/oracle/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/oracle/OracleAttestation.sol#157-165) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:205: MadHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MadHook.sol#205-240) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:224: MadRewards.question(uint256) (src/MadRewards.sol#224-243) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:171: MadRewards.claim(uint256) (src/MadRewards.sol#171-192) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:275: MadHook._collect(uint256,uint256) (src/MadHook.sol#275-298) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:153: MadHook.feeNumerator() (src/MadHook.sol#153-157) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:302: MadHook._convertNFT() (src/MadHook.sol#302-335) uses timestamp for comparisons","passed":true},{"durationMs":946,"exitCode":0,"name":"aderyn","output":"[high] contract-locks-ether at src/MadRewards.sol:22: Contract locks Ether without a withdraw function\n[high] reentrancy-state-change at src/MadHook.sol:129: Reentrancy: State change after external call (23 places)\n[low] centralization-risk at src/MadHook.sol:144: Centralization Risk (6 places)\n[low] internal-function-used-once at src/HookFlags.sol:22: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/MadHook.sol:54: Large Numeric Literal (4 places)\n[low] literal-instead-of-constant at src/MadHook.sol:100: Literal Instead of Constant (26 places)\n[low] local-variable-shadowing at src/MadRewards.sol:89: Local Variable Shadows State Variable\n[low] modifier-used-only-once at src/MadRewards.sol:115: Modifier Invoked Only Once\n[low] non-reentrant-not-first at src/MadHook.sol:125: `nonReentrant` is Not the First Modifier (10 places)\n[low] require-revert-in-loop at src/oracle/OracleQuestion.sol:114: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/MadHook.sol:122: State Change Without Event\n[low] unchecked-return at src/MadHook.sol:342: Unchecked Return (5 places)\n[low] uninitialized-local-variable at src/oracle/OracleQuestion.sol:114: Uninitialized Local Variable (4 places)","passed":true},{"durationMs":17247,"exitCode":0,"name":"proof 19a8b04e924d","output":"Compiling 114 files with Solc 0.8.26\nSolc 0.8.26 finished in 16.42s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-bdded720/Proof_19a8b04e924d.t.sol:ExactOutputBuyTaxTest\n[PASS] test_exactOutputBuyPaysSameTaxAsExactInputBuy() (gas: 614116)\nLogs:\n  exact-input: FWA paid: 100000000000000000000\n  exact-input: MAD received: 987499024844712965\n  exact-output: FWA paid for the same MAD: 100000197500195031400\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 31.47ms (476.41µs CPU time)\n\nRan 1 test suite in 32.34ms (31.47ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cd3fd5aced422933df7ad830928e1df8b8be8660d334f4e05f492c449b4318e0","verifiedTreeHash":"45b6ea52f555893bec5f80943dca9e569dfcef13","verifierVersion":"0.1.0+560ab6a2"},{"checks":[{"durationMs":47851,"exitCode":0,"name":"build","output":"Compiling 122 files with Solc 0.8.26\nSolc 0.8.26 finished in 47.75s\nCompiler run successful!\n","passed":true},{"durationMs":18076,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetFork.t.sol:MainnetForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 275.78µs (0.00ns CPU time)\n\nRan 5 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsProtocolSignatureUnder200kGas() (gas: 20157)\n[PASS] test_callbackAndFailureSelectorsCanonical() (gas: 4115)\n[PASS] test_digestMatchesProtocolUnchangedVector() (gas: 13258)\n[PASS] test_freshSignatureAndWrongDomain() (gas: 51755)\n[PASS] test_questionCanonicalProtocolVector() (gas: 40115)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.24ms (1.04ms CPU time)\n\nRan 15 tests for test/MadRewards.t.sol:MadRewardsTest\n[PASS] test_budgetAndOwnerRestrictions() (gas: 1507187)\n[PASS] test_buyQuoteBoundsBlackoutAndRefundOverpayment() (gas: 588943)\n[PASS] test_buyRedeemsOnlyNeededEthClaims() (gas: 1586181)\n[PASS] test_callbackStoresOnlyAndAwardOnceUnderGasStipend() (gas: 2660227)\n[PASS] test_claimWithinWindowOnlyOnceAndUsingOriginalPool() (gas: 2686466)\n[PASS] test_closeRequiresLeaderAndNftAssignsOldestKeptOnly() (gas: 2194382)\n[PASS] test_failureAndUnansweredTimeoutRetryRejectsSupersededResponse() (gas: 4076362)\n[PASS] test_fallbackAt72HoursAndLateAnswerCannotChangeBeneficiary() (gas: 2755449)\n[PASS] test_fallbackFailedTransferPreservesWinnerForRetry() (gas: 1359336)\n[PASS] test_lateClaimRejectedAndFailedKeepRetriable() (gas: 1635420)\n[PASS] test_questionExactBodyLivePriceAndEightConfirmations() (gas: 2995739)\n[PASS] test_refundReturnsToBudgetAndOldPoolStillWorksAfterChange() (gas: 2233977)\n[PASS] test_rejectedWinnerDeliveryRetriesAndReentrancyCannotDoubleAward() (gas: 3242301)\n[PASS] test_signedUuidCannotReplayAcrossRounds() (gas: 4387466)\n[PASS] test_wrongSenderUnknownHashChainPanelAndValidityRejected() (gas: 5460280)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 20.95ms (27.76ms CPU time)\n\nRan 11 tests for test/MadToken.t.sol:MadTokenTest\n[PASS] testFuzz_freezeConservesAndTracks(uint96) (runs: 256, μ: 234757, ~: 234874)\nLogs:\n  Bound result 4053\n\n[PASS] test_7702ExactDesignationOnly() (gas: 442317)\n[PASS] test_arbitraryBindAndRebindRejectedIncludingSameBlockLaterTransaction() (gas: 265522)\n[PASS] test_freezeCostBurnRemainderAndEvent() (gas: 242352)\n[PASS] test_frozenCannotTransferTransferFromSellOrAddLiquidity() (gas: 1261894)\n[PASS] test_frozenDebitsAlsoFailOnAnUnrelatedPool() (gas: 817162)\n[PASS] test_frozenFreezerCanPayButNoGeneralExemption() (gas: 281748)\n[PASS] test_repeatedFreezeResetsNotAdds() (gas: 232139)\n[PASS] test_supplyMetadataAndLaunchTransfers() (gas: 210443)\n[PASS] test_tiesAndRoundBoundary() (gas: 843165)\n[PASS] test_zeroSelfContractsAndNoFundsRejected() (gas: 159092)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 21.64ms (11.86ms CPU time)\n\nRan 12 tests for test/MadHook.t.sol:MadHookTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 525834, ~: 533022)\nLogs:\n  Bound result 9\n  Bound result 975412939109079873\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4891802)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226210)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1532519)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 915383)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1337053)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1366346)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 323820)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 683728)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 293067)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1541538)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 33.42ms (31.80ms CPU time)\n\nRan 1 test for test/RuntimeSafety.t.sol:RuntimeSafetyTest\n[PASS] test_noRuntimeEscapeHatches() (gas: 24445083)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.84ms (34.34ms CPU time)\n\nRan 13 tests for test/MadCurrencyOrder.t.sol:MadCurrencyOrderTest\n[PASS] testFuzz_swapFeeAccounting(uint96,uint16,bool,bool) (runs: 256, μ: 524954, ~: 533766)\nLogs:\n  Bound result 20\n  Bound result 16706\n\n[PASS] test_allSwapModesDuringDecayAndAtRest() (gas: 4893361)\n[PASS] test_callbacksAuthenticateManagerPoolAndContext() (gas: 226270)\n[PASS] test_conversionMinOutputFailureAtomicAndStaleLimitDefers() (gas: 1536819)\n[PASS] test_decayAtZeroFifteenAndThirtyMinutes() (gas: 105725)\n[PASS] test_fwaSortsFirst() (gas: 5906)\n[PASS] test_liquidityAndFeeCollectionNotGated() (gas: 919803)\n[PASS] test_nativeClaimsWhenManagerHasNoETHAndLaterRedemption() (gas: 1341500)\n[PASS] test_onlyNFTShareConvertsInsideSwap() (gas: 1370825)\n[PASS] test_partialMainSpecifiedSwapRevertsInsteadOfChargingUnspentInput() (gas: 329224)\n[PASS] test_payoutDirectFromManagerSeparateAndIdempotentBeforeSetup() (gas: 688394)\n[PASS] test_permissionsOwnerSizeAndClosedTrading() (gas: 292931)\n[PASS] test_priceBoundPartialNFTSaleCarriesOnlyUnsoldNFTClaims() (gas: 1547181)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 51.30ms (30.80ms CPU time)\n\nRan 1 test for test/MadInvariant.t.sol:MadInvariantTest\n[PASS]\nMadInvariantTest invariants:\n[PASS] invariant_claimBudgetsNeverMixOrDoublePay\n[PASS] invariant_supplyAndBurnConservation\n MadInvariantTest invariants (runs: 256, calls: 128000, reverts: 0)\n\n╭------------+----------+-------+---------+----------╮\n| Contract   | Selector | Calls | Reverts | Discards |\n+====================================================+\n| MadHandler | freeze   | 42640 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | payout   | 42920 | 0       | 0        |\n|------------+----------+-------+---------+----------|\n| MadHandler | swap     | 42440 | 0       | 0        |\n╰------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 17.98s (17.96s CPU time)\n\nRan 8 test suites in 17.98s (18.15s CPU time): 58 tests passed, 0 failed, 1 skipped (59 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MadHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MadHook.askWinner(uint256)\",\"MadHook.award(uint256)\",\"MadHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MadHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MadHook.buy()\",\"MadHook.claim(uint256)\",\"MadHook.closeRound()\",\"MadHook.enableTrading()\",\"MadHook.flushNftETH()\",\"MadHook.payout()\",\"MadHook.redeemNftETH(uint256)\",\"MadHook.setConversionBounds(uint160,uint256)\",\"MadHook.setEthFwaPool((address,address,uint24,int24,address))\",\"MadHook.unlockCallback(bytes)\",\"MadRewards.askWinner(uint256)\",\"MadRewards.assignRound(uint256)\",\"MadRewards.award(uint256)\",\"MadRewards.buy()\",\"MadRewards.claim(uint256)\",\"MadRewards.onOracleFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"MadRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"MadRewards.receive()\",\"MadRewards.setAcquisitionBounds(uint256,uint256)\",\"MadRewards.setFwaV2(address)\",\"MadRewards.setOracleSettings(address,bytes32,address,uint256,address)\",\"MadRewards.withdrawAcquisitionRefund(address)\",\"MadToken.approve(address,uint256)\",\"MadToken.bindHook()\",\"MadToken.closeRound()\",\"MadToken.freeze(address)\",\"MadToken.transfer(address,uint256)\",\"MadToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"LICENSE\":23,\"README.md\":109,\"docs/dependencies.json\":26,\"docs/oracle-wording-preflight.json\":88,\"docs/verification.md\":68,\"foundry.toml\":18,\"remappings.txt\":4,\"src/HookFlags.sol\":29,\"src/MadHook.sol\":363,\"src/MadRewards.sol\":356,\"src/MadToken.sol\":115,\"src/interfaces/IFWAV2.sol\":41,\"src/interfaces/IIntake.sol\":18,\"src/interfaces/IMadToken.sol\":17,\"src/oracle/OracleAttestation.sol\":221,\"src/oracle/OracleQuestion.sol\":255,\"test/Base.t.sol\":98,\"test/MadCurrencyOrder.t.sol\":25,\"test/MadHook.t.sol\":258,\"test/MadInvariant.t.sol\":87,\"test/MadRewards.t.sol\":408,\"test/MadToken.t.sol\":242,\"test/MainnetFork.t.sol\":124,\"test/OracleConformance.t.sol\":107,\"test/RuntimeSafety.t.sol\":28,\"test/mocks/AtomicLaunch.sol\":34,\"test/mocks/MockFWA.sol\":33,\"test/mocks/MockFWAV2.sol\":114,\"test/mocks/MockIntake.sol\":65,\"test/mocks/MockMarketHook.sol\":46,\"test/mocks/MockNFT.sol\":44,\"test/mocks/TestRouter.sol\":72},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":8918,"exitCode":0,"name":"slither","output":"[medium/medium] divide-before-multiply at src/MadHook.sol:247: MadHook._collect(uint256,uint256) (src/MadHook.sol#247-262) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/MadHook.sol:247: MadHook._collect(uint256,uint256) (src/MadHook.sol#247-262) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/MadRewards.sol:224: MadRewards.question(uint256) (src/MadRewards.sol#224-243) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MadHook.sol:264: MadHook._convertNFT() (src/MadHook.sol#264-295) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/MadHook.sol:264: MadHook._convertNFT() (src/MadHook.sol#264-295) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MadRewards.sol:330: Reentrancy in MadRewards.award(uint256) (src/MadRewards.sol#330-355):\n[medium/medium] reentrancy-no-eth at src/MadHook.sol:264: Reentrancy in MadHook._convertNFT() (src/MadHook.sol#264-295):\n[medium/medium] reentrancy-no-eth at src/MadHook.sol:247: Reentrancy in MadHook._collect(uint256,uint256) (src/MadHook.sol#247-262):\n[medium/medium] reentrancy-no-eth at src/MadRewards.sol:255: Reentrancy in MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288):\n[medium/medium] uninitialized-local at src/MadHook.sol:233: MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).fee (src/MadHook.sol#233) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MadHook.sol:199: MadHook.beforeSwap(address,PoolKey,SwapParams,bytes).fee (src/MadHook.sol#199) is a local variable never initialized\n[medium/medium] unused-return at src/MadHook.sol:340: MadHook.closeRound() (src/MadHook.sol#340-345) ignores return value by mad.closeRound() (src/MadHook.sol#343)\n[medium/medium] unused-return at src/MadHook.sol:316: MadHook._redeemNftETH(uint256) (src/MadHook.sol#316-323) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MadHook.sol#320)\n[medium/medium] unused-return at src/MadRewards.sol:212: MadRewards.assignRound(uint256) (src/MadRewards.sol#212-222) ignores return value by (None,last) = mad.roundBlocks(round) (src/MadRewards.sol#214)\n[medium/medium] unused-return at src/MadRewards.sol:156: MadRewards.buy() (src/MadRewards.sol#156-169) ignores return value by (fee,None,total) = pool.quoteAcquisitionPrice() (src/MadRewards.sol#159)\n[medium/medium] unused-return at src/MadHook.sol:264: MadHook._convertNFT() (src/MadHook.sol#264-295) ignores return value by (current,None,None,None) = poolManager.getSlot0(market.toId()) (src/MadHook.sol#269)\n[medium/medium] unused-return at src/MadRewards.sol:255: MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288) ignores return value by (None,last) = mad.roundBlocks(round) (src/MadRewards.sol#261)\n[medium/medium] unused-return at src/MadHook.sol:297: MadHook.payout() (src/MadHook.sol#297-305) ignores return value by poolManager.unlock(abi.encode(amount)) (src/MadHook.sol#302)\n[medium/medium] unused-return at src/MadRewards.sol:195: MadRewards.withdrawAcquisitionRefund(address) (src/MadRewards.sol#195-201) ignores return value by IFWAV2(pool).withdrawAcquisitionRefund() (src/MadRewards.sol#198)\n[low/medium] reentrancy-benign at src/MadHook.sol:112: Reentrancy in MadHook.beforeInitialize(address,PoolKey,uint160) (src/MadHook.sol#112-132):\n[low/medium] reentrancy-benign at src/MadHook.sol:264: Reentrancy in MadHook._convertNFT() (src/MadHook.sol#264-295):\n[low/medium] reentrancy-benign at src/MadRewards.sol:171: Reentrancy in MadRewards.claim(uint256) (src/MadRewards.sol#171-192):\n[low/medium] reentrancy-benign at src/MadRewards.sol:156: Reentrancy in MadRewards.buy() (src/MadRewards.sol#156-169):\n[low/medium] reentrancy-benign at src/MadHook.sol:297: Reentrancy in MadHook.payout() (src/MadHook.sol#297-305):\n[low/medium] reentrancy-benign at src/MadHook.sol:222: Reentrancy in MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MadHook.sol#222-245):\n[low/medium] reentrancy-benign at src/MadRewards.sol:330: Reentrancy in MadRewards.award(uint256) (src/MadRewards.sol#330-355):\n[low/medium] reentrancy-benign at src/MadHook.sol:316: Reentrancy in MadHook._redeemNftETH(uint256) (src/MadHook.sol#316-323):\n[low/medium] timestamp at src/MadRewards.sol:255: MadRewards.askWinner(uint256) (src/MadRewards.sol#255-288) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:143: MadHook.feeNumerator() (src/MadHook.sol#143-147) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:290: MadRewards.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/MadRewards.sol#290-316) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:330: MadRewards.award(uint256) (src/MadRewards.sol#330-355) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:222: MadHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MadHook.sol#222-245) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:264: MadHook._convertNFT() (src/MadHook.sol#264-295) uses timestamp for comparisons\n[low/medium] timestamp at src/oracle/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/oracle/OracleAttestation.sol#157-165) uses timestamp for comparisons\n[low/medium] timestamp at src/MadToken.sol:108: MadToken._update(address,address,uint256) (src/MadToken.sol#108-114) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:224: MadRewards.question(uint256) (src/MadRewards.sol#224-243) uses timestamp for comparisons\n[low/medium] timestamp at src/MadRewards.sol:171: MadRewards.claim(uint256) (src/MadRewards.sol#171-192) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:185: MadHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/MadHook.sol#185-220) uses timestamp for comparisons\n[low/medium] timestamp at src/MadHook.sol:247: MadHook._collect(uint256,uint256) (src/MadHook.sol#247-262) uses timestamp for comparisons","passed":true},{"durationMs":804,"exitCode":0,"name":"aderyn","output":"[high] contract-locks-ether at src/MadRewards.sol:22: Contract locks Ether without a withdraw function\n[high] reentrancy-state-change at src/MadHook.sol:119: Reentrancy: State change after external call (22 places)\n[low] centralization-risk at src/MadHook.sol:134: Centralization Risk (6 places)\n[low] internal-function-used-once at src/HookFlags.sol:22: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/MadHook.sol:51: Large Numeric Literal (4 places)\n[low] literal-instead-of-constant at src/MadHook.sol:90: Literal Instead of Constant (24 places)\n[low] local-variable-shadowing at src/MadRewards.sol:89: Local Variable Shadows State Variable\n[low] modifier-used-only-once at src/MadRewards.sol:115: Modifier Invoked Only Once\n[low] non-reentrant-not-first at src/MadHook.sol:115: `nonReentrant` is Not the First Modifier (10 places)\n[low] require-revert-in-loop at src/oracle/OracleQuestion.sol:114: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/MadHook.sol:112: State Change Without Event\n[low] unchecked-return at src/MadHook.sol:302: Unchecked Return (4 places)\n[low] uninitialized-local-variable at src/oracle/OracleQuestion.sol:114: Uninitialized Local Variable (4 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"dd7374e3e376658f0258033efb7931d741e5f2a7a119a1b3bf93fc733594ed93","verifiedTreeHash":"3b3d29602b7bfcafb0b6e6533dd876ec6b8cbf22","verifierVersion":"0.1.0+560ab6a2"}]}