{"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":"cb7160d0-6e8f-4a79-bb5e-a95df159f6fe","kind":"impl_tests_review","nodes":[{"acceptedSubmissionHash":"e2ddc404951d00ab0169fe0408fd7ee0aebdd28a2c694994bb9072dfdeac0554","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d37537abb20af46832b24602788d7895021ecaac20c6934313dc4f8aba4ad047","skillId":"implement-contract","tools":[]},"key":"impl","kind":"code","role":"implement","skillHash":"d37537abb20af46832b24602788d7895021ecaac20c6934313dc4f8aba4ad047","skillId":"implement-contract","state":"accepted"},{"acceptedSubmissionHash":"68439b2a4130498622eb34d86414521aa7796ca45bf2901a958418ccc82c8e93","dependsOn":["impl","tests"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","tools":[]},"key":"review","kind":"code","role":"review","skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","state":"accepted"},{"acceptedSubmissionHash":"77e2514679c3d5fc4d2a330918e7d63f8ce837aec8ab5de5bae1ac1732706e43","dependsOn":["impl"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Continue the accepted IMD Money Back project (parent job delivered src/MoneyBackToken.sol, src/MoneyBackHook.sol, src/RoundPayout.sol and their tests, all accepted). Do NOT change the three existing contracts' behaviour. Add the pieces the launch factory needs, in the same style (Solidity ^0.8.26, inlined minimal interfaces, no vendored dependencies, builds offline):\n\n1. src/MoneyBackRouter.sol so buyers can pay ETH. Constructor (IPoolManager manager, address hook) as [\"$poolManager\",\"$contract:MoneyBackHook\"]; read our PoolKey from hook.poolKey() and the token from hook.launchToken(). Public ETH/IMD pool on Robinhood Chain, hardcoded: currency0 = native ETH (address 0), currency1 = IMD 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127, fee 10000, tickSpacing 100, hooks = address 0. buyWithEth(uint256 minTokensOut, uint256 deadline) payable: inside one PoolManager unlock, swap all msg.value ETH -> IMD on the public pool, then all received IMD -> MONEYBACK on our pool (exact input both legs, settle native ETH with settle{value}), send MONEYBACK to msg.sender, refund any IMD or ETH dust to msg.sender; revert on minTokensOut or deadline. sellForEth(uint256 tokens, uint256 minEthOut, uint256 deadline): transferFrom MONEYBACK from msg.sender, MONEYBACK -> IMD on our pool, IMD -> ETH on the public pool, ETH to msg.sender. quoteBuy(uint256 ethIn) and quoteSell(uint256 tokens) views via a revert-and-catch simulation. Events BoughtWithEth(address indexed buyer, uint256 ethIn, uint256 imdIn, uint256 tokensOut) and SoldForEth(address indexed seller, uint256 tokensIn, uint256 imdOut, uint256 ethOut). No owner, no retained approvals, holds nothing between calls, reentrancy-guarded, receive() only accepts ETH from the PoolManager. The hook sees ordinary swaps, so its fees apply. If $contract:MoneyBackHook cannot be resolved by the factory, accept (manager, token, hook) with [\"$poolManager\",\"$token\",\"$hook\"] and say so in launch.json notes.\n\n2. test/MoneyBackRouter.t.sol: buy and sell happy paths through a mocked PoolManager with two pools, minOut and deadline reverts, dust refund, reentrancy, zero-value, and an invariant that the router's ETH, IMD and MONEYBACK balances are zero after every call.\n\n3. launch.json at the root: kind \"univ4_hook\"; token {contract \"MoneyBackToken\", name \"IMD Money Back\", symbol \"MONEYBACK\", decimals 18}; hook {contract \"MoneyBackHook\", constructorArgs [\"$poolManager\",\"$token\",\"$owner\"], permissions [\"beforeInitialize\",\"beforeSwap\",\"afterSwap\",\"beforeSwapReturnDelta\",\"afterSwapReturnDelta\"]}; contracts [{contract \"RoundPayout\", constructorArgs [\"$owner\"]}, {contract \"MoneyBackRouter\", constructorArgs [\"$poolManager\",\"$contract:MoneyBackHook\"]}]; pool {pairedCurrency \"0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127\", fee 12500, tickSpacing 60, initialPrice \"79228162514264337593543950336\"}; notes: one paragraph with constructor orders, the hook flag bits 0x20CC, the 4.25% rule, the sell surcharge (2000 bps decaying to 0 over 1800 s), sweep to the immutable payout (the paying wallet), the router's two pools, and that only RoundPayout has an owner. Note that the hook payout address is the paying wallet ($owner), not RoundPayout.\n\n4. foundry.toml: solc 0.8.26, evm_version cancun, optimizer true, optimizer_runs 200, bytecode_hash = \"none\", src/test/out/libs as standard. No remappings needed.\n\n5. script/MineHookSalt.s.sol: a pure helper that searches CREATE2 salts for the MoneyBackHook creation code + constructor args so the address ends in flag bits 0x20CC; never broadcasts. Document how the factory uses it.\n\n6. README.md: what each contract does, the fee maths in words with the four swap cases, the surcharge schedule, sweep, router usage (buy with ETH / sell for ETH), RoundPayout usage by the engine, deployment order and post-deploy verification checks. docs/abi/*.json exported for all four contracts. SECURITY_REVIEW.md: threat model, trust assumptions (only RoundPayout's owner is privileged), what the tests cover, known limitations (e.g. hook fee rounding, surcharge timing by block.timestamp).\n\nACCEPTANCE: forge build and forge test pass offline with the new foundry.toml; existing tests still pass unchanged; launch.json is valid for a univ4_hook launch with the two application contracts; the router ends every call with zero balance; buying through the router pays the hook's 4.25% exactly as a direct IMD buy would.","parentJobId":"57052175-0591-4ba2-9aa8-2bef020522a1","planHash":"fcf0e7e1b9fd7b1288242b56bb78ecfc2ec7a0961760943ad7f5d744730f7953","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"57052175-0591-4ba2-9aa8-2bef020522a1","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1178-src-moneybacktoken-sol-src-moneybackhook"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51027","feedbackHash":"686d2f1bcd26b9e6102c87a1ab1dd16b22b2a7758b42e563c8c3c146f2a23540","nodeKey":"impl","submissionHash":"e2ddc404951d00ab0169fe0408fd7ee0aebdd28a2c694994bb9072dfdeac0554","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52480","feedbackHash":"8e61c6487740db98ea84b14a60f0a9e0527c72ed13fa7e30719f803939437c8e","nodeKey":"review","submissionHash":"68439b2a4130498622eb34d86414521aa7796ca45bf2901a958418ccc82c8e93","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51081","feedbackHash":"42b44be826a0f104ad2f348d522a44cfab41276b3a6c36e1f9bec5bea5aa1553","nodeKey":"tests","submissionHash":"77e2514679c3d5fc4d2a330918e7d63f8ce837aec8ab5de5bae1ac1732706e43","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"ee42009794908e9894c94fae46edf89f5b92d9e9ea7a500fa45c8c50235d6176","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b0b4e7bbc84f9d80","findings":[{"citation":"resolved","description":"payRound increments failedCount once per failed leg but accumulates repeated recipients into a single failed[roundId][to] entry. retryFailed and writeOffFailed each clear that single entry and decrement failedCount by exactly 1. With a duplicated recipient, failedCount ends at a positive value while failed(roundId, to) is 0 for every address, and nothing can bring it back to 0: writeOffFailed reverts NothingFailed. This contradicts the contract's documented invariant that rounds(roundId).failedCount is the number of legs still failed. No funds are at risk and the owner is trusted; the counter is informational for the engine. The existing test test_duplicateFailedRecipientAccumulates stops after payRound and never retries, so it does not catch this. Note: RoundPayout is an accepted contract this job must not change; fixing it (count distinct recipients, or decrement by the number of accumulated legs) is a scope decision for the requester.","line":260,"path":"src/RoundPayout.sol","reproduction":"fund 100 tokens; token.setRevertsFor(A,true); owner calls payRound(1, token, [A, A, B], [3, 4, 1], 0,0,0) -> failed(1,A)=7, rounds(1).failedCount=2. token.setRevertsFor(A,false); owner calls retryFailed(1, [A]) -> returns 7, failed(1,A)=0. Expected rounds(1).failedCount == 0; actual == 1. owner calls writeOffFailed(1, A) -> reverts NothingFailed(1, A), so the counter can never be cleared. Verified with a scratch Foundry test against the current code.","severity":"low","snippet":"                failed[roundId][recipient] += amount;\n                ++failedCount;","title":"RoundPayout.failedCount is left permanently non-zero when a batch repeats a failed recipient"},{"citation":"resolved","description":"The router always offers the whole IMD output of leg 1 as exact input to the launch pool. MoneyBackHook charges the exact-input buy fee in beforeSwap on |amountSpecified| (the amount offered), not on the IMD the pool actually consumed. Real Uniswap v4 behaves the same way: the beforeSwap specified delta is subtracted from the swapper's delta regardless of how much of the remaining amount the pool fills. So when the launch pool's MONEYBACK liquidity is exhausted (price reaches the widest limit), a buyer is charged 4.25% of all the IMD they offered, receives tokens for only the filled part, and gets the rest back as IMD dust. The effective hook fee on the filled part can be far above 4.25%, contradicting the 'every trade pays 5.5%' product statement, and minTokensOut does not bound the fee paid. The test suite encodes this as expected (test_buyRefundsImdDustWhenOurPoolFillsPartially asserts 'hook kept its fee on the full leg'). Root cause is the accepted hook's case-1 rule, so this is a documentation / scope decision rather than a router bug: the router cannot know the fill in advance. At minimum it belongs in SECURITY_REVIEW.md known limitations and the frontend should compare quoteBuy output to the IMD offered.","line":311,"path":"src/MoneyBackRouter.sol","reproduction":"Fixture of test/MoneyBackRouter.t.sol with the launch pool capped at 100 IMD of input (pm.setCap(launchId, 100e18), modelling exhausted liquidity): buyWithEth{value: 1 ether}(0, now) -> leg 1 gives 990 IMD; hook fee = floor(990e18 * 425 / 10000) = 42.075 IMD; pool swaps 100 IMD; buyer gets 100 IMD worth of MONEYBACK and 847.925 IMD refunded. Expected (product statement): fee 4.25 IMD on the 100 IMD traded. Actual: 42.075 IMD, an effective 42% hook fee on the amount traded. hook.pending() == 42_075e15 after the call.","severity":"low","snippet":"        (a0, a1) = _swap(key, imdIs0, imdOut);","title":"Buy pays the 4.25% hook fee on IMD the launch pool never swapped when the pool fills partially"},{"citation":"resolved","description":"The event doc at lines 134-135 says imdOut is 'the IMD it [our pool] paid after the hook fee', and the task defines the event as (tokensIn, imdOut, ethOut). The code emits r.imdMid, which _sell sets to the IMD consumed by leg 2 on the public pool. The two agree only on a full fill. When the public pool fills partially the unconsumed IMD is refunded to the seller but the event under-reports imdOut by exactly that refund, so an indexer summing imdOut will not match the IMD that left the launch pool or the hook's FeeAccrued imdLeg. The buy-side event is consistent (imdIn == IMD our pool consumed), so the asymmetry looks unintended. The test test_sellRefundsImdDustWhenPublicPoolFillsPartially asserts the current value (100e18), locking in the behaviour instead of the documented one.","line":230,"path":"src/MoneyBackRouter.sol","reproduction":"pm.setCap(publicId, 100e18) in the router test fixture at T0; ALICE approves and calls sellForEth(1_000e18, 0, now). Our pool pays 987.5 IMD minus hook fee (4.25% + 20%) = 748.03125e18 IMD; the public pool consumes 100e18 and 648.03125e18 IMD is refunded to ALICE. Expected per the doc comment: SoldForEth(ALICE, 1000e18, 748_03125e13, ethOut). Actual: SoldForEth(ALICE, 1000e18, 100e18, ethOut).","severity":"low","snippet":"        emit SoldForEth(msg.sender, r.amountIn, r.imdMid, ethOut);","title":"SoldForEth.imdOut reports the IMD the public pool consumed, not the IMD our pool paid as documented"},{"citation":"resolved","description":"_sendAllEth reads address(this).balance before the refund call. The refund hands control to msg.sender while the PoolManager is already locked again and _expectedCallback is cleared; a contract buyer can run its own PoolManager.unlock there and take(native, router, x), which receive() accepts because msg.sender is the PoolManager. buyWithEth then returns with the router holding x wei, breaking the stated 'ETH balance is zero after every call' invariant; the wei goes to whoever trades next. Separately, IMD or MONEYBACK transferred straight to the router (not via PoolManager deltas) is stuck forever: the router only takes from its PoolManager deltas and has no sweep. Only the sender of the value loses anything, so this is a griefing-free invariant violation, but the acceptance criterion and SECURITY_REVIEW.md should state it precisely (zero PoolManager deltas and zero retained value from its own flows, not zero balance against third-party pushes).","line":420,"path":"src/MoneyBackRouter.sol","reproduction":"Contract B with receive(){ if (msg.sender==router) pm.unlock(\"\"); } and unlockCallback(){ pm.sync(native); pm.settle{value:1}(); pm.take(native, router, 1); }. pm.setCap(publicId, 0.5 ether) so a dust refund happens. B calls router.buyWithEth{value: 1 ether}(0, now). Expected: address(router).balance == 0 after the call. Actual: 1. Next buyer's refund is 1 wei larger. Also: token.transfer(router, 5e18) and IMD.transfer(router, 7e18) followed by any number of buys/sells leave both balances unchanged. Verified with a scratch Foundry test.","severity":"low","snippet":"        (bool ok,) = to.call{value: amount}(\"\");","title":"Router can end a call holding ETH, and tokens sent to it directly are unrecoverable"},{"citation":"resolved","description":"The task calls for 'quoteBuy(uint256 ethIn) and quoteSell(uint256 tokens) views via a revert-and-catch simulation'. Both are declared non-view and appear as state-changing entry points in the ABI. The implementation is correct in that respect: the real PoolManager's unlock writes transient storage (TSTORE), which reverts under STATICCALL, so a view quoter cannot work against v4 (the periphery V4Quoter is non-view for the same reason) and the functions are documented to be called with eth_call. They are state-neutral: the unlock always reverts with QuoteResult and _quote clears _expectedCallback. Recorded so the requester knows the spec deviation is deliberate and the launch.json / README should say the quotes are eth_call-only.","line":243,"path":"src/MoneyBackRouter.sol","reproduction":"ABI of MoneyBackRouter: quoteBuy(uint256) and quoteSell(uint256) have stateMutability nonpayable. A Solidity caller declaring them view and invoking via STATICCALL against the real PoolManager reverts; via eth_call they return the simulated amount (router test test_quoteBuyMatchesBuyAndChangesNothing shows quote == subsequent buy output with no state change).","severity":"info","snippet":"    function quoteBuy(uint256 ethIn) external returns (uint256 tokensOut) {","title":"quoteBuy / quoteSell are not view functions as the task text specifies"},{"citation":"resolved","description":"The workspace contains only src/ and test/. There is no launch.json, foundry.toml, script/MineHookSalt.s.sol, README.md, docs/abi/*.json or SECURITY_REVIEW.md, although the task text lists them and the acceptance clause says 'forge build and forge test pass offline with the new foundry.toml' and 'launch.json is valid for a univ4_hook launch'. The build does pass with Foundry defaults (solc 0.8.26 auto-detected, no remappings needed) and all 183 tests pass offline. The implementer's allowed paths were likely restricted to the two router files, in which case this is a scope gap for the requester to assign rather than a defect in the router. Recorded as an observation only; the files cannot be audited because they do not exist.","line":59,"path":"src/MoneyBackRouter.sol","reproduction":"ls at the repository root shows .git, .imd, src, test only. `test -f launch.json` and `test -f foundry.toml` both fail.","severity":"info","snippet":"contract MoneyBackRouter is IUnlockCallback {","title":"Deliverables 3-6 of the task are absent from the tree (launch.json, foundry.toml, MineHookSalt script, README, docs/abi, SECURITY_REVIEW.md)"},{"citation":"resolved","description":"MoneyBackRouter.sol and RoundPayout.sol each declare a top-level interface named IERC20 with different members. Solidity accepts this (different source units) and forge build succeeds, but artifact lookups by bare name (forge inspect IERC20 ..., some ABI export tooling for docs/abi) become ambiguous. Harmless at runtime; renaming one (e.g. IRouterERC20) avoids tooling friction when docs/abi is produced. aderyn's 'reused-contract-name' lead reduced to this.","line":16,"path":"src/MoneyBackRouter.sol","reproduction":"`forge inspect IERC20 abi` reports multiple artifacts for the name. Build and tests are unaffected.","severity":"info","snippet":"interface IERC20 {","title":"Interface name IERC20 is declared in two source files"}],"hash":"68439b2a4130498622eb34d86414521aa7796ca45bf2901a958418ccc82c8e93","nodeId":"fda58411-9b62-4756-9813-ab07134560ca","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the scratch tests are removed, and no project file was changed.\n\n**Outcome:** no critical, high or medium defects. The router is correct against real Uniswap v4 semantics. I traced both legs of buy and sell, native settle after `sync(address(0))`, partial-fill dust accounting, the callback hash gate, the reentrancy guard, the quote revert-and-catch path, and the hook's delta flow for exact-input buys and sells. All 183 tests pass offline, including the 49 router tests, the fuzz runs and the invariant suites.\n\n**Findings written to `.imd-findings.json`, ranked:**\n\n1. **Low, RoundPayout.** A batch that repeats a failed recipient increments the failed counter twice but retry or write-off clears it once. The counter is left at 1 with nothing owed and no way to clear it. Reproduced with a scratch test. This is in an accepted contract the job must not change, so I flagged it as a scope decision.\n2. **Low, router buy under partial fill.** The router offers all IMD as exact input, and the hook charges 4.25% on the amount offered, not the amount filled. When the launch pool's liquidity is exhausted the effective fee on the filled part is far higher. The existing test asserts this behaviour as intended. Root cause is the accepted hook design, so this belongs in the security review's known limitations.\n3. **Low, event mismatch.** The sell event's `imdOut` reports what the public pool consumed, while the doc comment says it is what our pool paid. They diverge only on partial fills.\n4. **Low, invariant wording.** A contract buyer can push ETH into the router through the PoolManager during its own refund, so the router ends that call holding a few wei that go to the next trader. Tokens sent directly to the router are stuck forever. Only the pusher loses anything.\n5. **Info.** The quote functions are not `view` as the task text says. That is correct and unavoidable against v4's transient storage, but the deviation should be documented.\n6. **Info.** Deliverables 3 to 6 (launch.json, foundry.toml, salt-mining script, README, ABI exports, SECURITY_REVIEW.md) are absent from the tree. Likely a path restriction on the implementer, recorded as a scope gap.\n7. **Info.** The `IERC20` interface name is declared in two source files. Harmless at runtime, awkward for artifact tooling.\n\n**Static analysis leads dismissed after checking:** the assembly shift order in the hook is correct Yul, the unchecked ETH send goes to `msg.sender`, and the state write after the external call in `sweep` is guarded by the reentrancy lock.\n\nThe coverage record answers all 24 listed entry points plus two invariants. No finding is high enough to carry a proof file.","treeHash":null,"usage":{"cachedInputTokens":1332976,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":45353,"runtime":"claude","turns":26,"wallClockMs":685664}},{"artifacts":[],"attempt":1,"bundleHash":"86c2dfca1748ffd0a749b6eb9ede86239dac274fe17502b8d589912797599c57","device":"2c968e88904ec22b","findings":[{"description":"For case 1 (exact-input buy, IMD specified) and case 4 (exact-output sell, IMD specified) the hook takes its fee in beforeSwap via beforeSwapReturnDelta, computed on |amountSpecified| before the pool runs. If the pool then consumes less than amountSpecified - fee (sqrtPriceLimitX96 reached, or liquidity exhausted), v4's Hooks.afterSwap still subtracts the full hook delta from the swapper's delta: the swapper pays poolConsumed + fee where fee = 4.25% of the full offered amount. The NatSpec invariant 'The swapper pays exactly |amountSpecified| IMD' and the 4.25% rule both fail on partial fills. The MoneyBackRouter is only exposed when our pool's liquidity runs out (it uses the widest price limits; its IMD-dust refund handles the leftover correctly), but any direct v4 trader using a tight sqrtPriceLimitX96 for slippage control (the normal way to do it) is overcharged on every partial fill. No funds are lost to third parties: the excess goes to the hook's pending pouch and is swept to payout. Possible fixes: (a) in afterSwap for cases 1/4, revert when |pool IMD delta| < amountSpecified - fee (no partial fills on this pool), or (b) move the fee for these cases to a design that charges on the IMD actually moved. The existing hook tests never model a partial fill, so they could not see this. Mirrored in test/MoneyBackRouter.t.sol::test_buyRefundsImdDustWhenOurPoolFillsPartially, which documents the current behaviour (fee on the full leg, dust refunded) without blessing it.","line":392,"path":"src/MoneyBackHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Proof for: MoneyBackHook charges the exact-input buy fee on the full amountSpecified even when the\n// pool only partially fills the swap (price limit reached / liquidity exhausted), so the swapper's\n// effective hook fee is not 4.25% of the IMD leg and the documented \"swapper pays exactly\n// |amountSpecified| IMD\" no longer holds.\n//\n// Self-contained: no forge-std is vendored in this project, so cheatcodes go through a minimal\n// interface. Runs with `forge test --match-path test/scratch/HookPartialFillFee.t.sol`.\n// FAILS on the current hook. PASSES once the hook either (a) reverts a case-1/case-4 swap whose pool\n// delta is smaller than amountSpecified minus the fee, or (b) charges the fee on the IMD actually\n// moved.\n\nimport {\n    MoneyBackHook,\n    Hooks,\n    IHooks,\n    IPoolManager,\n    PoolKey,\n    SwapParams,\n    Currency,\n    BalanceDelta,\n    BeforeSwapDelta\n} from \"src/MoneyBackHook.sol\";\nimport {MoneyBackToken} from \"src/MoneyBackToken.sol\";\n\ninterface VmP {\n    function etch(address, bytes calldata) external;\n    function warp(uint256) external;\n}\n\naddress constant IMD_ADDR = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n\ncontract ImdStub {\n    mapping(address => uint256) public balanceOf;\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Minimal v4-like manager: applies the hook's beforeSwap specified delta exactly as\n///      v4 Hooks.beforeSwap does, then runs a pool that consumes at most `cap` of the input\n///      (what happens on v4 when sqrtPriceLimitX96 is reached or liquidity runs out), then subtracts\n///      the hook delta from the swapper's delta exactly as v4 Hooks.afterSwap does.\ncontract PartialFillManager is IPoolManager {\n    mapping(address => mapping(uint256 => uint256)) public balanceOf;\n    uint256 public cap;\n\n    function unlock(bytes calldata) external pure returns (bytes memory) {\n        return \"\";\n    }\n\n    function mint(address to, uint256 id, uint256 amount) external {\n        balanceOf[to][id] += amount;\n    }\n\n    function burn(address, uint256, uint256) external {}\n    function take(Currency, address, uint256) external {}\n\n    function setCap(uint256 c) external {\n        cap = c;\n    }\n\n    function initialize(PoolKey memory key) external {\n        key.hooks.beforeInitialize(msg.sender, key, 1 << 96);\n    }\n\n    /// @return swapperImd the IMD the swapper ends up paying (negative delta, as magnitude)\n    /// @return poolImd    the IMD the pool actually consumed\n    /// @return hookFee    the IMD the hook kept\n    function exactInputBuy(PoolKey memory key, uint256 imdIn)\n        external\n        returns (uint256 swapperImd, uint256 poolImd, uint256 hookFee)\n    {\n        // IMD is currency0 in this fixture: a buy is zeroForOne, exact input.\n        SwapParams memory p = SwapParams({zeroForOne: true, amountSpecified: -int256(imdIn), sqrtPriceLimitX96: 0});\n        (, BeforeSwapDelta bsd,) = key.hooks.beforeSwap(msg.sender, key, p, \"\");\n        int256 hookSpecified;\n        assembly {\n            hookSpecified := sar(128, bsd)\n        }\n        require(p.amountSpecified + hookSpecified <= 0, \"HookDeltaExceedsSwapAmount\");\n        poolImd = uint256(-(p.amountSpecified + hookSpecified));\n        if (cap != 0 && poolImd > cap) poolImd = cap; // partial fill\n        int128 afterUnspecified = _after(key, p, poolImd);\n        hookFee = uint256(hookSpecified) + uint256(int256(afterUnspecified));\n        swapperImd = poolImd + uint256(hookSpecified);\n    }\n\n    function _after(PoolKey memory key, SwapParams memory p, uint256 poolImd) internal returns (int128 u) {\n        int256 a0 = -int256(poolImd);\n        int256 a1 = int256(poolImd * 9875 / 10_000);\n        BalanceDelta d;\n        assembly {\n            d := or(shl(128, a0), and(sub(shl(128, 1), 1), a1))\n        }\n        (, u) = key.hooks.afterSwap(msg.sender, key, p, d, \"\");\n    }\n}\n\ncontract HookPartialFillFeeTest {\n    VmP constant vm = VmP(address(uint160(uint256(keccak256(\"hevm cheat code\")))));\n\n    PartialFillManager pm;\n    MoneyBackToken token;\n    MoneyBackHook hook;\n    PoolKey key;\n\n    function setUp() public {\n        vm.etch(IMD_ADDR, address(new ImdStub()).code);\n        pm = new PartialFillManager();\n        token = new MoneyBackToken();\n        bytes32 initHash = keccak256(\n            abi.encodePacked(type(MoneyBackHook).creationCode, abi.encode(address(pm), address(token), address(0x9A70)))\n        );\n        for (uint256 salt = 0; salt < 1_000_000; ++salt) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash))))\n            );\n            if ((uint160(a) & Hooks.ALL_HOOK_MASK) == 0x20CC) {\n                hook = new MoneyBackHook{salt: bytes32(salt)}(IPoolManager(address(pm)), address(token), address(0x9A70));\n                break;\n            }\n        }\n        key = PoolKey(Currency.wrap(IMD_ADDR), Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));\n        vm.warp(1_750_000_000);\n        pm.initialize(key);\n    }\n\n    /// @notice Exact-input buy of 1000 IMD where the pool can only take 100 IMD (price limit reached).\n    ///         Expected: either the swap reverts, or the hook keeps 4.25% of what the swapper paid.\n    ///         Actual: the swapper pays 142.5 IMD, of which the hook keeps 42.5 (= 4.25% of 1000, i.e.\n    ///         29.8% of what was paid; 42.5% of what the pool received).\n    function test_partialFillBuyIsChargedOnlyOnWhatTheSwapperPays() public {\n        pm.setCap(100e18);\n        (bool ok, bytes memory ret) = address(pm).call(abi.encodeCall(pm.exactInputBuy, (key, 1_000e18)));\n        if (!ok) return; // fix (a): partial fills are rejected\n        (uint256 swapperImd, uint256 poolImd, uint256 hookFee) = abi.decode(ret, (uint256, uint256, uint256));\n        require(poolImd == 100e18, \"fixture: pool took 100 IMD\");\n        // fix (b): the fee is 4.25% of the IMD leg the swapper actually paid.\n        require(hookFee == swapperImd * 425 / 10_000, \"hook fee is not 4.25% of what the swapper paid\");\n    }\n}","reproduction":"Pool bound at t0 with IMD as currency0. Swapper submits an exact-input buy of 1000e18 IMD; the pool can only take 100e18 IMD before hitting the price limit. Expected: the swap reverts, or the hook keeps 4.25% of what the swapper actually pays. Actual: beforeSwap returns deltaSpecified = 42.5e18 (4.25% of 1000), the pool takes 100e18, the swapper's IMD delta is -142.5e18, the hook keeps 42.5e18 = 29.8% of what was paid and 42.5% of what the pool received; the swapper did not pay |amountSpecified| (1000e18). Run: forge test --match-path test/scratch/HookPartialFillFee.t.sol (fails on the current code; the proof uses the cheatcode address directly because no forge-std is vendored in this project).","severity":"low","title":"Hook charges the exact-input buy fee on the full amountSpecified even when the pool only partially fills, so the swapper's effective fee is not 4.25% and 'swapper pays exactly |amountSpecified|' break"},{"description":"The brief asks for 'quoteBuy and quoteSell views via a revert-and-catch simulation'. The implementation is deliberately non-view and documents why: the real v4 PoolManager's unlock/swap write transient storage and reject STATICCALL, so a view quote cannot run the real swaps. The functions are state-neutral (the inner unlock always reverts) and are meant for eth_call. The tests verify that quotes change nothing (hook pending, swap logs, balances, lock state) and equal the subsequent trade's output exactly. Front ends should use eth_call rather than expecting a view selector; no code change recommended.","line":243,"path":"src/MoneyBackRouter.sol","reproduction":"ABI: quoteBuy(uint256) and quoteSell(uint256) have stateMutability nonpayable. Calling them in a transaction succeeds and changes no state; test_quoteBuyMatchesBuyAndChangesNothing and test_quoteSellMatchesSellAndChangesNothing.","severity":"info","title":"quoteBuy / quoteSell are not view functions (the brief called them views)"},{"description":"sellForEth and any buyWithEth that leaves ETH dust forward ETH to msg.sender with a plain call and revert with EthTransferFailed if it fails. An EOA is always fine; a smart-contract wallet without receive()/fallback cannot receive sell proceeds or a dust refund, so the whole trade reverts (nothing is lost). This is the expected trade-off of 'holds nothing between calls'; documenting it in the README for integrators is enough.","line":420,"path":"src/MoneyBackRouter.sol","reproduction":"test_contractWithoutReceiveCanBuyFullFillOnly: a contract with no receive() buys fine on a full fill, reverts with EthTransferFailed on a buy with a partial first leg (cap 0.5 ETH on the public pool, 1 ETH sent) and on any sell.","severity":"info","title":"Contract traders without a payable receive() cannot sell, and cannot buy when the public pool fills partially"},{"description":"The event is documented as 'imdOut the IMD it (our pool) paid after the hook fee' but emits r.imdMid, the IMD consumed by the second leg. The two differ only when the ETH/IMD pool fills partially (the difference is refunded to the seller as IMD dust). Off-chain accounting that wants the hook-side figure should read the hook's FeeAccrued event or the pool delta instead. Same for BoughtWithEth.imdIn (IMD consumed by our pool = pool input + hook fee), which matches its NatSpec.","line":230,"path":"src/MoneyBackRouter.sol","reproduction":"test_sellRefundsImdDustWhenPublicPoolFillsPartially: cap the public pool at 100e18 IMD, sell 1000e18 MONEYBACK at t0; our pool pays 748.03125e18 IMD, the event carries imdOut = 100e18 and the seller receives 648.03125e18 IMD as dust.","severity":"info","title":"SoldForEth.imdOut is the IMD the public pool consumed, not the IMD our pool paid, when the public pool fills partially"}],"hash":"77e2514679c3d5fc4d2a330918e7d63f8ce837aec8ab5de5bae1ac1732706e43","nodeId":"b5b943ab-191a-4e74-ad5b-4bfa08def10a","outcome":"completed","summary":"Everything is in place. The findings file is intentionally excluded from git by the harness's own exclude list, so the only tracked change is the test file.\n\n**Delivered:** `test/MoneyBackRouter.t.sol`, 49 tests, all passing alongside the three existing suites (183 total, verified with `forge build` and `forge test` offline, no foundry.toml or forge-std needed, same cheatcode-interface style as the earlier suites).\n\n**What the suite covers**\n- **Two-pool PoolManager stand-in** that reproduces v4's unlock/callback with the non-zero delta count check, sync/settle semantics for native and ERC-20, take/mint/burn, hook delta flow for beforeSwap/afterSwap, configurable rate and LP fee, and a per-pool input cap that models partial fills.\n- **Happy paths:** buy and sell with exact amounts and events, both currency orders for our pool, round trip, sweep of router-generated fees to payout.\n- **Acceptance check:** a router buy accrues exactly the same hook fee and yields the same MONEYBACK as a direct IMD buy of the same amount through a plain v4-style integrator.\n- **Failure paths:** minOut, deadline, zero value, 1-wei inputs that yield nothing, int128 overflow, missing allowance or balance, pool not yet bound, tampered or skipped manager callback, callback from non-manager or with no call in flight, ETH from anyone but the manager, trader contracts that cannot receive ETH.\n- **Dust refunds:** ETH, IMD and MONEYBACK dust in every partial-fill combination on both legs.\n- **Reentrancy:** an attacker reentering from the ETH it receives tries buy, sell, both quotes, unlockCallback and a plain transfer. Every nested call fails with the right error and the outer trade still completes.\n- **Fuzz and invariants:** fuzzed buys and sells against the fee formula with random caps and timestamps. A handler drives random buys, sells, quotes, sweeps and warps including deliberately reverting calls. Invariants assert the router's ETH, IMD and MONEYBACK balances, claims and deltas are zero after every call, that hook pending equals fees minus swept, and that ETH, IMD and MONEYBACK are conserved.\n\n**Reported, not tested around,** in `.imd-findings.json` (one low, three info):\n- **Low, in the hook:** on an exact-input buy that only partially fills, the fee is taken on the full offered amount, so the swapper pays far more than 4.25% of what the pool used. The router is only exposed when liquidity runs out, but direct traders with tight price limits hit it on every partial fill. A self-contained proof test that fails on the current hook is embedded.\n- **Info:** quotes are not `view` for a sound reason, contract traders without `receive()` cannot sell, and the `SoldForEth` event's IMD field reflects the second leg rather than the first on a partial fill.\n\n**Still owed:** a fork run against Robinhood Chain to confirm the hardcoded public pool key is initialized with those exact parameters. No network is available here or to the verifier.","treeHash":"3e859fc7b64e3019d6812ed880f68605c50329c0","usage":{"cachedInputTokens":1984461,"inputTokens":452,"model":"claude-fable-5-1","outputTokens":145083,"runtime":"claude","turns":36,"wallClockMs":2001960}},{"artifacts":[],"attempt":1,"bundleHash":"a1605731c0064b95a8e7c7b29684d02a06251de3ba63a639c7b98dd5b6117878","device":"61b4050710026370","findings":[],"hash":"e2ddc404951d00ab0169fe0408fd7ee0aebdd28a2c694994bb9072dfdeac0554","nodeId":"90383eed-a7a3-471c-914a-235f1e9139b4","outcome":"completed","summary":"Delivered `src/MoneyBackRouter.sol`, the only path in my write budget. It builds and the full suite passes (134 existing tests unchanged plus 19 scratch tests). I left the commit to the harness since the tree is on a detached HEAD and the brief did not ask me to commit.\n\n**What the router does**\n\n- Constructor takes `(IPoolManager manager, address hook)`, reads the MONEYBACK token from the hook once and the bound pool key from the hook on every call, so it can be deployed before the pool is initialized.\n- The public ETH/IMD pool is hardcoded: native ETH as currency0, IMD as currency1, fee 10000, tickSpacing 100, no hook.\n- `buyWithEth` and `sellForEth` run both exact-input legs inside one PoolManager unlock, settle ETH with a value-carrying settle, take MONEYBACK and any IMD or MONEYBACK left over straight to the trader, and forward the router's whole ETH balance to the trader at the end. Both revert on deadline, zero input, output below the minimum, or zero output.\n- `quoteBuy` and `quoteSell` run the real swaps inside an unlock that always reverts with the result, then decode it. Any other revert reason is bubbled up unchanged.\n- The hook sees an ordinary swap from the router, so the scratch test confirms a router buy pays exactly the same 4.25% IMD fee and yields the same MONEYBACK as a direct IMD buy of the same amount.\n- No owner, no approvals, no retained balances. A reentrancy lock covers buy, sell and quote. `receive()` only accepts ETH from the PoolManager.\n\n**Two things to flag**\n\n- **The quotes are not typed `view`.** The PoolManager writes transient storage during a swap, so a quote run under STATICCALL would always revert on the real chain. The functions are state-neutral and documented for `eth_call`, which is the same shape as Uniswap's own V4Quoter. A tester can call them normally in Foundry.\n- **Callback binding.** The callback only runs for the exact data this contract just passed to `unlock`, once per call, via a stored hash. This is defense in depth on top of v4's own rule that the manager calls back only the unlocker.\n\n**What the scratch tests covered** (in `test/scratch/`, not delivered): buy and sell happy paths in both currency orders, surcharge at t0 and after decay, quotes matching execution, minOut and deadline and zero-value reverts, ETH, IMD and MONEYBACK dust refunds on partial fills, reentrancy from the trader's receive, direct and foreign callback rejection, receive() gating, no admin surface, an unbound-pool revert, a fuzzed round trip, and an invariant that the router's ETH, IMD, MONEYBACK and claim balances are zero after every call. I also verified build and tests under the brief's target settings (solc 0.8.26, cancun, optimizer 200) using CLI flags.\n\n**Not delivered, outside my allowed path:** the real `test/MoneyBackRouter.t.sol`, `launch.json`, `foundry.toml`, `script/MineHookSalt.s.sol`, README, ABI exports and SECURITY_REVIEW.md. Those items from the brief need their own paths granted in a follow-up task. For `launch.json`, note that the router's constructor is the two-argument form `[\"$poolManager\",\"$contract:MoneyBackHook\"]`.","treeHash":"5e7fdc12a3abd63040d1560256a9860782ddf8e9","usage":{"cachedInputTokens":3014554,"inputTokens":930,"model":"claude-fable-5-1","outputTokens":56914,"runtime":"claude","turns":42,"wallClockMs":715377}}],"verification":[{"checks":[{"durationMs":746,"exitCode":0,"name":"build","output":"Compiling 8 files with Solc 0.8.30\nSolc 0.8.30 finished in 637.85ms\nCompiler run successful!\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackHook.sol:339:13\n    │\n339 │         if (elapsed >= SURCHARGE_DURATION) return 0;\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/RoundPayout.sol:61:32\n   │\n61 │     function transferOwnership(address newOwner) external onlyOwner {\n   │                                ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:387:24\n    │\n387 │         r = abi.decode(poolManager.unlock(data), (Result));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:420:22\n    │\n420 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MoneyBackHook.sol:371:27\n    │\n371 │         _imdIsCurrency0 = imdIsCurrency0;\n    │                           ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackRouter.sol:202:13\n    │\n202 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackRouter.sol:210:9\n    │\n210 │         emit BoughtWithEth(msg.sender, r.amountIn, r.imdMid, tokensOut);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackRouter.sol:223:13\n    │\n223 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackRouter.sol:230:9\n    │\n230 │         emit SoldForEth(msg.sender, r.amountIn, r.imdMid, ethOut);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:392:39\n    │\n392 │         uint256 imdLeg = exactInput ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:398:13\n    │\n398 │         try poolManager.unlock(data) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:392:74\n    │\n392 │         uint256 imdLeg = exactInput ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackHook.sol:469:9\n    │\n469 │         emit FeeAccrued(isSell, baseFee, surcharge, imdLeg);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:412:41\n    │\n412 │         uint256 imdLeg = imdDelta < 0 ? uint256(-imdDelta) : uint256(imdDelta);\n    │                                         ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:412:62\n    │\n412 │         uint256 imdLeg = imdDelta < 0 ? uint256(-imdDelta) : uint256(imdDelta);\n    │                                                              ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackHook.sol:426:9\n    │\n426 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MoneyBackHook.sol:426:9\n    │\n426 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackHook.sol:440:9\n    │\n440 │         emit Swept(amount, payout);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:473:17\n    │\n473 │         if (x > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:360:40\n    │\n360 │         (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:473:25\n    │\n473 │         if (x > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:474:16\n    │\n474 │         return int128(uint128(x));\n    │                ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:474:23\n    │\n474 │         return int128(uint128(x));\n    │                       ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:193:9\n    │\n193 │         emit Funded(token, msg.sender, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:354:40\n    │\n354 │         (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:197:82\n    │\n197 │     function sweep(address token, address to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                  ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:200:9\n    │\n200 │         emit Swept(token, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:223:40\n    │\n223 │     ) external onlyOwner whenNotPaused nonReentrant returns (uint256 totalPaid) {\n    │                                        ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:244:9\n    │\n244 │         emit RoundPaid(roundId, token, ledgerHash, twapCloseX96, totalEligibleLoss, totalPaid, n);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:299:22\n    │\n299 │         r.amountIn = uint256(-a0);\n    │                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:300:26\n    │\n300 │         uint256 imdOut = uint256(a1);\n    │                          ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MoneyBackRouter.sol:305:13\n    │\n305 │             poolManager.settle{value: r.amountIn}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:258:17\n    │\n258 │                 emit Paid(roundId, recipient, amount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:262:17\n    │\n262 │                 emit PayFailed(roundId, recipient, amount, reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:313:20\n    │\n313 │         r.imdMid = uint256(-imdDelta);\n    │                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:314:23\n    │\n314 │         r.amountOut = uint256(tokenDelta);\n    │                       ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[arbitrary-send-erc20]: `transferFrom` uses an arbitrary `from`; require it to equal `msg.sender` or `address(this)`\n    ╭▸ src/MoneyBackRouter.sol:331:18\n    │\n331 │             if (!IERC20(token).transferFrom(d.user, address(poolManager), tokensIn)) revert TransferFromFailed();\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-erc20\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:340:22\n    │\n340 │         r.amountIn = uint256(-tokenDelta);\n    │                      ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:341:26\n    │\n341 │         uint256 imdOut = uint256(imdDelta);\n    │                          ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:346:20\n    │\n346 │         r.imdMid = uint256(-a1);\n    │                    ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:347:23\n    │\n347 │         r.amountOut = uint256(a0);\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:360:24\n    │\n360 │         if (amountIn > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:360:32\n    │\n360 │         if (amountIn > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:365:35\n    │\n365 │                 amountSpecified: -int256(amountIn),\n    │                                   ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `failed` is updated\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:403:40\n    │\n403 │             if (reason.length == 36 && bytes4(reason) == QuoteResult.selector) {\n    │                                        ━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'bytes4' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/MoneyBackRouter.sol:420:22\n    │\n420 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/RoundPayout.sol:302:13\n    │\n302 │             needed += owed;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:288:9\n    │\n288 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:316:17\n    │\n316 │                 emit Paid(roundId, recipient, amount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:318:17\n    │\n318 │                 emit PayFailed(roundId, recipient, amount, reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/RoundPayout.sol:301:28\n    │\n301 │             if (owed == 0) revert NothingFailed(roundId, to[i]);\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/RoundPayout.sol:309:30\n    │\n309 │             if (amount == 0) revert NothingFailed(roundId, recipient);\n    │                              ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:324:77\n    │\n324 │     function writeOffFailed(uint256 roundId, address to) external onlyOwner nonReentrant {\n    │                                                                             ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n     ╭▸ test/MoneyBackRouter.t.sol:1118:24\n     │\n1118 │         uint256 now_ = block.timestamp;\n     │                        ━━━━━━━━━━━━━━━\n1119 │         vm.warp(ts);\n     │         ─────────── `vm.warp` changes this environment here\n     │\n     ├ help: capture it with `vm.getBlockTimestamp()` instead\n     ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n     ╭▸ test/MoneyBackRouter.t.sol:1145:53\n     │\n1145 │         uint256 bought = _buyAs(ALICE, 10 ether, 0, block.timestamp);\n     │                                                     ━━━━━━━━━━━━━━━\n1146 │         vm.warp(T0 + 3600);\n     │         ────────────────── `vm.warp` changes this environment here\n     │\n     ├ help: capture it with `vm.getBlockTimestamp()` instead\n     ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n     ╭▸ test/MoneyBackRouter.t.sol:1458:49\n     │\n1458 │         assertEq(_sellAs(ALICE, tokens, quoted, block.timestamp), quoted, \"sell delivers exactly the quote\");\n     │                                                 ━━━━━━━━━━━━━━━\n1459 │         vm.warp(T0 + 1800);\n     │         ────────────────── `vm.warp` changes this environment here\n     │\n     ├ help: capture it with `vm.getBlockTimestamp()` instead\n     ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":1290,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 27 tests for test/MoneyBackToken.t.sol:MoneyBackTokenTest\n[PASS]\nMoneyBackTokenTest invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_totalSupplyNeverChanges\n[PASS] invariant_zeroAddressHoldsNothing\n MoneyBackTokenTest invariants (runs: 128, calls: 4096, reverts: 0)\n\n╭--------------+--------------+-------+---------+----------╮\n| Contract     | Selector     | Calls | Reverts | Discards |\n+==========================================================+\n| TokenHandler | approve      | 1400  | 0       | 0        |\n|--------------+--------------+-------+---------+----------|\n| TokenHandler | transfer     | 1370  | 0       | 0        |\n|--------------+--------------+-------+---------+----------|\n| TokenHandler | transferFrom | 1326  | 0       | 0        |\n╰--------------+--------------+-------+---------+----------╯\n\n[PASS] testFuzz_allowanceAccounting(uint256,uint256) (runs: 512, μ: 122170, ~: 139208)\n[PASS] testFuzz_transferAboveBalanceAlwaysReverts(uint256) (runs: 512, μ: 43541, ~: 43540)\n[PASS] testFuzz_transferConservesSupplyAndBalances(address,uint256) (runs: 512, μ: 97501, ~: 98122)\n[PASS] test_approveSetsAllowanceAndEmits() (gas: 101451)\n[PASS] test_approveZeroSpenderReverts() (gas: 31629)\n[PASS] test_constructorMintsWholeSupplyToDeployer() (gas: 18982)\n[PASS] test_deployerIsMsgSenderNotTxOrigin() (gas: 268862)\n[PASS] test_metadata() (gas: 19167)\n[PASS] test_noPrivilegedSurface() (gas: 173320)\n[PASS] test_rejectsEth() (gas: 33017)\n[PASS] test_supplyIsExactlyOneBillionWithEighteenDecimals() (gas: 14189)\n[PASS] test_transferFromDoesNotEmitApprovalOnSpend() (gas: 119007)\n[PASS] test_transferFromEmptyAccountRevertsEvenForOneWei() (gas: 35227)\n[PASS] test_transferFromExactAllowanceLeavesZero() (gas: 149819)\n[PASS] test_transferFromInfiniteAllowanceIsNotDecremented() (gas: 116001)\n[PASS] test_transferFromRevertsWhenAllowanceBelowAmount() (gas: 83525)\n[PASS] test_transferFromRevertsWhenBalanceBelowAmountEvenWithAllowance() (gas: 89471)\n[PASS] test_transferFromRevertsWithoutAllowance() (gas: 35930)\n[PASS] test_transferFromSpendsAllowance() (gas: 130210)\n[PASS] test_transferFromToZeroAddressReverts() (gas: 86196)\n[PASS] test_transferMovesBalanceAndEmits() (gas: 91234)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 42940)\n[PASS] test_transferToSelfKeepsBalance() (gas: 45100)\n[PASS] test_transferToZeroAddressReverts() (gas: 31668)\n[PASS] test_transferWholeBalanceLeavesZero() (gas: 72990)\n[PASS] test_transferZeroAmountSucceeds() (gas: 38742)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 436.82ms (639.64ms CPU time)\n\nRan 51 tests for test/RoundPayout.t.sol:RoundPayoutTest\n[PASS]\nRoundPayoutTest invariants:\n[PASS] invariant_balanceEqualsFundedMinusPaidMinusSwept\n[PASS] invariant_ownerNeverChangesWithoutHandoff\n[PASS] invariant_roundRecordsMatchLedger\n[PASS] invariant_sumPaidNeverExceedsFunded\n RoundPayoutTest invariants (runs: 96, calls: 3840, reverts: 18)\n\n╭---------------+---------------+-------+---------+----------╮\n| Contract      | Selector      | Calls | Reverts | Discards |\n+============================================================+\n| PayoutHandler | fund          | 418   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | pay           | 414   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | replay        | 452   | 18      | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | retry         | 418   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | setBlocked    | 417   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | strangerCalls | 406   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | sweep         | 474   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | togglePause   | 410   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | writeOff      | 431   | 0       | 0        |\n╰---------------+---------------+-------+---------+----------╯\n\n[PASS] testFuzz_batchSizeBoundary(uint16) (runs: 256, μ: 2658860, ~: 1578922)\n[PASS] testFuzz_payRoundAccounting(uint256,uint256,uint256,uint256,uint8) (runs: 512, μ: 317844, ~: 241861)\n[PASS] test_allTransfersFailedReverts() (gas: 363690)\n[PASS] test_constructorRejectsZeroOwner() (gas: 4930)\n[PASS] test_constructorSetsOwner() (gas: 24728)\n[PASS] test_duplicateFailedRecipientAccumulates() (gas: 401733)\n[PASS] test_fundAcceptsNoReturnToken() (gas: 747989)\n[PASS] test_fundPullsTokensFromAnyone() (gas: 181064)\n[PASS] test_fundRejectsEoaToken() (gas: 40342)\n[PASS] test_fundRejectsFalseReturningToken() (gas: 281849)\n[PASS] test_fundWithoutApprovalReverts() (gas: 99719)\n[PASS] test_fundZeroAmountSucceeds() (gas: 50754)\n[PASS] test_ownable2StepHandoff() (gas: 291664)\n[PASS] test_partialFailureIsRecordedAndBatchContinues() (gas: 451571)\n[PASS] test_partialFailureSilentRevertAndFalseReturn() (gas: 471459)\n[PASS] test_pauseTwiceAndUnpauseWhenRunningRevert() (gas: 109704)\n[PASS] test_payRoundBatchTooLarge() (gas: 661810)\n[PASS] test_payRoundBlockedWhilePaused() (gas: 403279)\n[PASS] test_payRoundEmptyBatch() (gas: 53908)\n[PASS] test_payRoundExactBalanceSucceeds() (gas: 341939)\n[PASS] test_payRoundIdempotent() (gas: 950232)\n[PASS] test_payRoundInsufficientBalanceUpFront() (gas: 136424)\n[PASS] test_payRoundIsTokenAgnosticPerRound() (gas: 1105422)\n[PASS] test_payRoundLengthMismatch() (gas: 50585)\n[PASS] test_payRoundMaxBatchOf500() (gas: 18486570)\n[PASS] test_payRoundPaysEveryLegAndRecords() (gas: 425820)\n[PASS] test_payRoundRoundIdZeroIsOrdinary() (gas: 299249)\n[PASS] test_payRoundSumOverflowReverts() (gas: 51898)\n[PASS] test_payRoundWithEoaTokenReverts() (gas: 51001)\n[PASS] test_payRoundWithNoReturnToken() (gas: 813401)\n[PASS] test_payRoundZeroAmountLegIsPaid() (gas: 168767)\n[PASS] test_payRoundZeroToken() (gas: 48139)\n[PASS] test_privilegedFunctionsRejectNonOwner() (gas: 173301)\n[PASS] test_reentrantTokenCannotReenterFundOrSweepDuringPayRound() (gas: 867722)\n[PASS] test_reentrantTokenCannotReenterPayRound() (gas: 719996)\n[PASS] test_renounceOwnershipLocksEverything() (gas: 153668)\n[PASS] test_retryFailedInsufficientBalanceAfterSweep() (gas: 543501)\n[PASS] test_retryFailedPaysAndClears() (gas: 596611)\n[PASS] test_retryFailedRevertsForUnpaidRound() (gas: 44744)\n[PASS] test_retryFailedShapeChecks() (gas: 1020563)\n[PASS] test_retryFailedStillFailingKeepsRecord() (gas: 486395)\n[PASS] test_singleLegFailureIsAllTransfersFailed() (gas: 196058)\n[PASS] test_sweepAboveBalanceReverts() (gas: 120617)\n[PASS] test_sweepByOwnerMovesTokens() (gas: 161709)\n[PASS] test_sweepRejectsShortReturnData() (gas: 243656)\n[PASS] test_sweepRejectsZeroRecipient() (gas: 104368)\n[PASS] test_sweepWorksWhilePaused() (gas: 191206)\n[PASS] test_transferOwnershipOnlyOwnerAndCancelable() (gas: 145838)\n[PASS] test_writeOffFailedClearsWithoutPaying() (gas: 537437)\n[PASS] test_writeOffFailedNothingOwedReverts() (gas: 41819)\nSuite result: ok. 51 passed; 0 failed; 0 skipped; finished in 607.95ms (831.83ms CPU time)\n\nRan 56 tests for test/MoneyBackHook.t.sol:MoneyBackHookTest\n[PASS]\nMoneyBackHookTest invariants:\n[PASS] invariant_hookNeverHoldsMoneybackClaims\n[PASS] invariant_lpFeeNeverOverriddenAndManagerLocked\n[PASS] invariant_pendingEqualsAccruedMinusSwept\n[PASS] invariant_sweepPaysOnlyPayout\n MoneyBackHookTest invariants (runs: 64, calls: 2560, reverts: 0)\n\n╭-------------+----------+-------+---------+----------╮\n| Contract    | Selector | Calls | Reverts | Discards |\n+=====================================================+\n| HookHandler | swap     | 832   | 0       | 0        |\n|-------------+----------+-------+---------+----------|\n| HookHandler | sweep    | 874   | 0       | 0        |\n|-------------+----------+-------+---------+----------|\n| HookHandler | warp     | 854   | 0       | 0        |\n╰-------------+----------+-------+---------+----------╯\n\n[PASS] testFuzz_feeMathsAllCases(uint8,uint256,uint32) (runs: 1000, μ: 194620, ~: 196608)\n[PASS] testFuzz_sequenceAccumulatesPending(uint256,uint256,uint256,uint32) (runs: 256, μ: 376910, ~: 381082)\n[PASS] testFuzz_surchargeFormula(uint32) (runs: 1000, μ: 20496, ~: 19795)\n[PASS] testFuzz_surchargeIsMonotoneNonIncreasing(uint32,uint32) (runs: 512, μ: 23360, ~: 23521)\n[PASS] testFuzz_tinyAmountsRoundDownToZeroFee(uint8,uint256,uint32) (runs: 512, μ: 134199, ~: 127379)\n[PASS] test_acceptanceSellCostAtT0AndAfterDecay() (gas: 361626)\n[PASS] test_afterSwapWithZeroPoolDeltaAccruesNothing() (gas: 73405)\n[PASS] test_allCasesWithImdAsCurrency1() (gas: 75743267)\n[PASS] test_buysNeverSurchargedAtAnyTime() (gas: 584709)\n[PASS] test_callbacksRejectNonPoolManager() (gas: 185823)\n[PASS] test_case1_exactInputBuy_t0() (gas: 196169)\n[PASS] test_case1_exactInputBuy_t1800() (gas: 195009)\n[PASS] test_case1_exactInputBuy_t1day() (gas: 195050)\n[PASS] test_case1_exactInputBuy_t900() (gas: 196180)\n[PASS] test_case2_exactOutputBuy_t0() (gas: 195081)\n[PASS] test_case2_exactOutputBuy_t1800() (gas: 193878)\n[PASS] test_case2_exactOutputBuy_t1day() (gas: 193830)\n[PASS] test_case2_exactOutputBuy_t900() (gas: 195136)\n[PASS] test_case3_exactInputSell_t0() (gas: 196629)\n[PASS] test_case3_exactInputSell_t1800() (gas: 194946)\n[PASS] test_case3_exactInputSell_t1day() (gas: 194987)\n[PASS] test_case3_exactInputSell_t900() (gas: 196630)\n[PASS] test_case4_exactOutputSell_t0() (gas: 196402)\n[PASS] test_case4_exactOutputSell_t1800() (gas: 194686)\n[PASS] test_case4_exactOutputSell_t1day() (gas: 160353)\n[PASS] test_case4_exactOutputSell_t900() (gas: 196371)\n[PASS] test_constructorRejectsImdAsLaunchToken() (gas: 67430)\n[PASS] test_constructorRejectsWrongFlagBits() (gas: 44992126)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 191778)\n[PASS] test_constructorStoresImmutablesAndConstants() (gas: 61667)\n[PASS] test_deploymentAddressFlagBitsMatchPermissions() (gas: 20990)\n[PASS] test_emptySweepSucceedsAndEmitsZero() (gas: 80231)\n[PASS] test_feeAccruedEventsPerCase() (gas: 478454)\n[PASS] test_fixedCallbacksOnlyChargeTheirOwnCases() (gas: 184224)\n[PASS] test_hookNeverMintsMoneybackClaimsEvenIfGiftedImdClaims() (gas: 321566)\n[PASS] test_hookRejectsEthAndHasNoAdminSurface() (gas: 175524)\n[PASS] test_hugeSpecifiedAmountRevertsAmountOverflow() (gas: 156520)\n[PASS] test_initializeAcceptsEitherCurrencyOrder() (gas: 220424256)\n[PASS] test_initializeBindsPoolAndRecordsTimestamp() (gas: 65079)\n[PASS] test_initializeEmitsPoolBound() (gas: 220430056)\n[PASS] test_initializeRejectsSecondPool() (gas: 79487)\n[PASS] test_initializeRejectsWrongKeys() (gas: 222821868)\n[PASS] test_surchargeAt1799IsStillChargedOnSells() (gas: 152102)\n[PASS] test_surchargeAt1800IsZeroOnSells() (gas: 151320)\n[PASS] test_surchargeBoundaries() (gas: 89558)\n[PASS] test_swapBeforeInitializationReverts() (gas: 220451676)\n[PASS] test_swapRejectsWrongPoolKey() (gas: 294432)\n[PASS] test_sweepAccumulatesAcrossRounds() (gas: 498442)\n[PASS] test_sweepCannotBeNestedInsideAnotherUnlock() (gas: 1110368)\n[PASS] test_sweepReentrancyFromPayoutTransferIsRejected() (gas: 864947)\n[PASS] test_sweepRevertsCleanlyIfTakeFails() (gas: 482872)\n[PASS] test_sweepTakesAllImdClaimsToPayout() (gas: 500083)\n[PASS] test_unlockCallbackOutsideSweepIsRejected() (gas: 184842)\n[PASS] test_viewsBeforeInitialization() (gas: 220275245)\n[PASS] test_zeroAmountSwapAccruesNothing() (gas: 232641)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 705.05ms (2.07s CPU time)\n\nRan 49 tests for test/MoneyBackRouter.t.sol:MoneyBackRouterTest\n[PASS]\nMoneyBackRouterTest invariants:\n[PASS] invariant_bindingsNeverChange\n[PASS] invariant_hookPendingEqualsRouterFeesMinusSwept\n[PASS] invariant_routerHoldsNothing\n[PASS] invariant_valueIsConserved\n MoneyBackRouterTest invariants (runs: 64, calls: 2560, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RouterHandler | buy      | 541   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RouterHandler | quote    | 492   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RouterHandler | sell     | 485   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RouterHandler | sweep    | 513   | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RouterHandler | warp     | 529   | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\n[PASS] testFuzz_buyMatchesFormulaAndLeavesNothing(uint256,uint32,uint256,uint256) (runs: 600, μ: 1421059, ~: 1448235)\n[PASS] testFuzz_sellMatchesFormulaAndLeavesNothing(uint256,uint32,uint256,uint256) (runs: 600, μ: 1538097, ~: 1568838)\n[PASS] test_acceptanceRouterBuyPaysHookExactlyLikeDirectImdBuy() (gas: 2351537)\n[PASS] test_beforePoolBoundEverythingReverts() (gas: 20300734)\n[PASS] test_buyAboveInt128RevertsAmountOverflow() (gas: 1080885)\n[PASS] test_buyAndSellRevertOnDeadline() (gas: 1435505)\n[PASS] test_buyHappyPath() (gas: 801599)\n[PASS] test_buyRefundsBothDustsWhenBothPoolsFillPartially() (gas: 830656)\n[PASS] test_buyRefundsEthDustWhenPublicPoolFillsPartially() (gas: 780539)\n[PASS] test_buyRefundsImdDustWhenOurPoolFillsPartially() (gas: 815407)\n[PASS] test_buyRevertsOnMinOut() (gas: 2128114)\n[PASS] test_buySwapLegsAreExactInputWithWidestLimitsAndRouterAsSender() (gas: 670499)\n[PASS] test_buyThenSellRoundTrip() (gas: 1340258)\n[PASS] test_buyTwiceAndByTwoUsers() (gas: 1858248)\n[PASS] test_buyWorksWithImdAsCurrency1() (gas: 19619991)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 2111547)\n[PASS] test_constructorStoresImmutablesFromHook() (gas: 41306)\n[PASS] test_contractWithoutReceiveCanBuyFullFillOnly() (gas: 2790826)\n[PASS] test_deadlineIsCheckedBeforeAmount() (gas: 68669)\n[PASS] test_feesFromRouterTradesAreSweepableToPayout() (gas: 1514313)\n[PASS] test_guardResetsAfterReverts() (gas: 2179244)\n[PASS] test_launchPoolKeyComesFromHook() (gas: 28424)\n[PASS] test_managerThatNeverCallsBackIsRejected() (gas: 290808)\n[PASS] test_publicPoolKeyIsHardcoded() (gas: 9214)\n[PASS] test_quoteBubblesUpForeignReverts() (gas: 19908370)\n[PASS] test_quoteBuyMatchesBuyAndChangesNothing() (gas: 2631846)\n[PASS] test_quoteCannotBeCalledWhileLocked() (gas: 878548)\n[PASS] test_quoteSellMatchesSellAndChangesNothing() (gas: 3416442)\n[PASS] test_quoteZeroReturnsZeroWithoutTouchingTheManager() (gas: 63413)\n[PASS] test_receiveRejectsEthFromAnyoneButTheManager() (gas: 813290)\n[PASS] test_reentrancyFromBuyDustRefundIsBlocked() (gas: 6315217)\n[PASS] test_reentrancyFromSellProceedsIsBlocked() (gas: 6116044)\n[PASS] test_routerHasNoAdminSurface() (gas: 170887)\n[PASS] test_sellAfterSurchargeDecayPaysOnlyBaseFee() (gas: 871387)\n[PASS] test_sellHappyPathAtT0WithSurcharge() (gas: 871101)\n[PASS] test_sellPullsExactlyTokensAndKeepsNoAllowance() (gas: 695544)\n[PASS] test_sellRefundsBothDusts() (gas: 941355)\n[PASS] test_sellRefundsImdDustWhenPublicPoolFillsPartially() (gas: 889842)\n[PASS] test_sellRefundsTokenDustWhenOurPoolFillsPartially() (gas: 878446)\n[PASS] test_sellRevertsOnMinOut() (gas: 1574719)\n[PASS] test_sellSwapLegsAreExactInputWithWidestLimits() (gas: 751934)\n[PASS] test_sellWhereSecondLegYieldsNothingRefundsNothingAndReverts() (gas: 946871)\n[PASS] test_sellWithoutAllowanceOrBalanceReverts() (gas: 666443)\n[PASS] test_tamperedCallbackDataIsRejected() (gas: 1070304)\n[PASS] test_tinyBuyThatYieldsNothingReverts() (gas: 2208756)\n[PASS] test_unlockCallbackRejectsManagerWithNoCallInFlight() (gas: 125100)\n[PASS] test_unlockCallbackRejectsNonManager() (gas: 63560)\n[PASS] test_zeroValueBuyAndZeroTokenSellRevert() (gas: 249053)\nSuite result: ok. 49 passed; 0 failed; 0 skipped; finished in 1.22s (1.68s CPU time)\n\nRan 4 test suites in 1.22s (2.97s CPU time): 183 tests passed, 0 failed, 0 skipped (183 total tests)\n","passed":true},{"durationMs":32,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MoneyBackHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MoneyBackHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MoneyBackHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MoneyBackHook.sweep()\",\"MoneyBackHook.unlockCallback(bytes)\",\"MoneyBackRouter.buyWithEth(uint256,uint256)\",\"MoneyBackRouter.quoteBuy(uint256)\",\"MoneyBackRouter.quoteSell(uint256)\",\"MoneyBackRouter.receive()\",\"MoneyBackRouter.sellForEth(uint256,uint256,uint256)\",\"MoneyBackRouter.unlockCallback(bytes)\",\"MoneyBackToken.approve(address,uint256)\",\"MoneyBackToken.transfer(address,uint256)\",\"MoneyBackToken.transferFrom(address,address,uint256)\",\"RoundPayout.acceptOwnership()\",\"RoundPayout.fund(address,uint256)\",\"RoundPayout.pause()\",\"RoundPayout.payRound(uint256,address,address[],uint256[],bytes32,uint256,uint256)\",\"RoundPayout.renounceOwnership()\",\"RoundPayout.retryFailed(uint256,address[])\",\"RoundPayout.sweep(address,address,uint256)\",\"RoundPayout.transferOwnership(address)\",\"RoundPayout.unpause()\",\"RoundPayout.writeOffFailed(uint256,address)\"],\"files\":{\"src/MoneyBackHook.sol\":498,\"src/MoneyBackRouter.sol\":444,\"src/MoneyBackToken.sol\":153,\"src/RoundPayout.sol\":373,\"test/MoneyBackHook.t.sol\":1298,\"test/MoneyBackRouter.t.sol\":1768,\"test/MoneyBackToken.t.sol\":372,\"test/RoundPayout.t.sol\":1241},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"77e2514679c3d5fc4d2a330918e7d63f8ce837aec8ab5de5bae1ac1732706e43","verifiedTreeHash":"3e859fc7b64e3019d6812ed880f68605c50329c0","verifierVersion":"0.1.0+fdeb4d4a"},{"checks":[{"durationMs":496,"exitCode":0,"name":"build","output":"Compiling 7 files with Solc 0.8.30\nSolc 0.8.30 finished in 404.82ms\nCompiler run successful!\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackHook.sol:339:13\n    │\n339 │         if (elapsed >= SURCHARGE_DURATION) return 0;\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:387:24\n    │\n387 │         r = abi.decode(poolManager.unlock(data), (Result));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:420:22\n    │\n420 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackRouter.sol:202:13\n    │\n202 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackRouter.sol:210:9\n    │\n210 │         emit BoughtWithEth(msg.sender, r.amountIn, r.imdMid, tokensOut);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MoneyBackHook.sol:371:27\n    │\n371 │         _imdIsCurrency0 = imdIsCurrency0;\n    │                           ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/MoneyBackRouter.sol:223:13\n    │\n223 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackRouter.sol:230:9\n    │\n230 │         emit SoldForEth(msg.sender, r.amountIn, r.imdMid, ethOut);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackRouter.sol:398:13\n    │\n398 │         try poolManager.unlock(data) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/RoundPayout.sol:61:32\n   │\n61 │     function transferOwnership(address newOwner) external onlyOwner {\n   │                                ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:392:39\n    │\n392 │         uint256 imdLeg = exactInput ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:392:74\n    │\n392 │         uint256 imdLeg = exactInput ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackHook.sol:469:9\n    │\n469 │         emit FeeAccrued(isSell, baseFee, surcharge, imdLeg);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:412:41\n    │\n412 │         uint256 imdLeg = imdDelta < 0 ? uint256(-imdDelta) : uint256(imdDelta);\n    │                                         ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:412:62\n    │\n412 │         uint256 imdLeg = imdDelta < 0 ? uint256(-imdDelta) : uint256(imdDelta);\n    │                                                              ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/MoneyBackHook.sol:426:9\n    │\n426 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MoneyBackHook.sol:426:9\n    │\n426 │         poolManager.unlock(\"\");\n    │         ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MoneyBackHook.sol:440:9\n    │\n440 │         emit Swept(amount, payout);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:473:17\n    │\n473 │         if (x > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:473:25\n    │\n473 │         if (x > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:474:16\n    │\n474 │         return int128(uint128(x));\n    │                ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackHook.sol:474:23\n    │\n474 │         return int128(uint128(x));\n    │                       ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:360:40\n    │\n360 │         (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:193:9\n    │\n193 │         emit Funded(token, msg.sender, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:299:22\n    │\n299 │         r.amountIn = uint256(-a0);\n    │                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:300:26\n    │\n300 │         uint256 imdOut = uint256(a1);\n    │                          ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MoneyBackRouter.sol:305:13\n    │\n305 │             poolManager.settle{value: r.amountIn}();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:313:20\n    │\n313 │         r.imdMid = uint256(-imdDelta);\n    │                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:314:23\n    │\n314 │         r.amountOut = uint256(tokenDelta);\n    │                       ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[arbitrary-send-erc20]: `transferFrom` uses an arbitrary `from`; require it to equal `msg.sender` or `address(this)`\n    ╭▸ src/MoneyBackRouter.sol:331:18\n    │\n331 │             if (!IERC20(token).transferFrom(d.user, address(poolManager), tokensIn)) revert TransferFromFailed();\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-erc20\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:354:40\n    │\n354 │         (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:197:82\n    │\n197 │     function sweep(address token, address to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                  ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:340:22\n    │\n340 │         r.amountIn = uint256(-tokenDelta);\n    │                      ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:341:26\n    │\n341 │         uint256 imdOut = uint256(imdDelta);\n    │                          ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:346:20\n    │\n346 │         r.imdMid = uint256(-a1);\n    │                    ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:347:23\n    │\n347 │         r.amountOut = uint256(a0);\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:200:9\n    │\n200 │         emit Swept(token, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:360:24\n    │\n360 │         if (amountIn > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:360:32\n    │\n360 │         if (amountIn > uint256(uint128(type(int128).max))) revert AmountOverflow();\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:365:35\n    │\n365 │                 amountSpecified: -int256(amountIn),\n    │                                   ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MoneyBackRouter.sol:403:40\n    │\n403 │             if (reason.length == 36 && bytes4(reason) == QuoteResult.selector) {\n    │                                        ━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'bytes4' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/MoneyBackRouter.sol:420:22\n    │\n420 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:223:40\n    │\n223 │     ) external onlyOwner whenNotPaused nonReentrant returns (uint256 totalPaid) {\n    │                                        ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:244:9\n    │\n244 │         emit RoundPaid(roundId, token, ledgerHash, twapCloseX96, totalEligibleLoss, totalPaid, n);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:258:17\n    │\n258 │                 emit Paid(roundId, recipient, amount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:262:17\n    │\n262 │                 emit PayFailed(roundId, recipient, amount, reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `failed` is updated\n    ╭▸ src/RoundPayout.sol:348:22\n    │\n348 │         (ok, data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/RoundPayout.sol:302:13\n    │\n302 │             needed += owed;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:288:9\n    │\n288 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:316:17\n    │\n316 │                 emit Paid(roundId, recipient, amount);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RoundPayout.sol:318:17\n    │\n318 │                 emit PayFailed(roundId, recipient, amount, reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/RoundPayout.sol:301:28\n    │\n301 │             if (owed == 0) revert NothingFailed(roundId, to[i]);\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/RoundPayout.sol:309:30\n    │\n309 │             if (amount == 0) revert NothingFailed(roundId, recipient);\n    │                              ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/RoundPayout.sol:324:77\n    │\n324 │     function writeOffFailed(uint256 roundId, address to) external onlyOwner nonReentrant {\n    │                                                                             ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\n","passed":true},{"durationMs":689,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 27 tests for test/MoneyBackToken.t.sol:MoneyBackTokenTest\n[PASS]\nMoneyBackTokenTest invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_totalSupplyNeverChanges\n[PASS] invariant_zeroAddressHoldsNothing\n MoneyBackTokenTest invariants (runs: 128, calls: 4096, reverts: 0)\n\n╭--------------+--------------+-------+---------+----------╮\n| Contract     | Selector     | Calls | Reverts | Discards |\n+==========================================================+\n| TokenHandler | approve      | 1352  | 0       | 0        |\n|--------------+--------------+-------+---------+----------|\n| TokenHandler | transfer     | 1401  | 0       | 0        |\n|--------------+--------------+-------+---------+----------|\n| TokenHandler | transferFrom | 1343  | 0       | 0        |\n╰--------------+--------------+-------+---------+----------╯\n\n[PASS] testFuzz_allowanceAccounting(uint256,uint256) (runs: 512, μ: 122081, ~: 139184)\n[PASS] testFuzz_transferAboveBalanceAlwaysReverts(uint256) (runs: 512, μ: 43541, ~: 43540)\n[PASS] testFuzz_transferConservesSupplyAndBalances(address,uint256) (runs: 512, μ: 97421, ~: 98146)\n[PASS] test_approveSetsAllowanceAndEmits() (gas: 101451)\n[PASS] test_approveZeroSpenderReverts() (gas: 31629)\n[PASS] test_constructorMintsWholeSupplyToDeployer() (gas: 18982)\n[PASS] test_deployerIsMsgSenderNotTxOrigin() (gas: 268874)\n[PASS] test_metadata() (gas: 19167)\n[PASS] test_noPrivilegedSurface() (gas: 173320)\n[PASS] test_rejectsEth() (gas: 33017)\n[PASS] test_supplyIsExactlyOneBillionWithEighteenDecimals() (gas: 14189)\n[PASS] test_transferFromDoesNotEmitApprovalOnSpend() (gas: 119007)\n[PASS] test_transferFromEmptyAccountRevertsEvenForOneWei() (gas: 35227)\n[PASS] test_transferFromExactAllowanceLeavesZero() (gas: 149819)\n[PASS] test_transferFromInfiniteAllowanceIsNotDecremented() (gas: 116001)\n[PASS] test_transferFromRevertsWhenAllowanceBelowAmount() (gas: 83525)\n[PASS] test_transferFromRevertsWhenBalanceBelowAmountEvenWithAllowance() (gas: 89471)\n[PASS] test_transferFromRevertsWithoutAllowance() (gas: 35930)\n[PASS] test_transferFromSpendsAllowance() (gas: 130210)\n[PASS] test_transferFromToZeroAddressReverts() (gas: 86196)\n[PASS] test_transferMovesBalanceAndEmits() (gas: 91234)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 42940)\n[PASS] test_transferToSelfKeepsBalance() (gas: 45100)\n[PASS] test_transferToZeroAddressReverts() (gas: 31668)\n[PASS] test_transferWholeBalanceLeavesZero() (gas: 72990)\n[PASS] test_transferZeroAmountSucceeds() (gas: 38742)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 375.43ms (440.35ms CPU time)\n\nRan 51 tests for test/RoundPayout.t.sol:RoundPayoutTest\n[PASS]\nRoundPayoutTest invariants:\n[PASS] invariant_balanceEqualsFundedMinusPaidMinusSwept\n[PASS] invariant_ownerNeverChangesWithoutHandoff\n[PASS] invariant_roundRecordsMatchLedger\n[PASS] invariant_sumPaidNeverExceedsFunded\n RoundPayoutTest invariants (runs: 96, calls: 3840, reverts: 31)\n\n╭---------------+---------------+-------+---------+----------╮\n| Contract      | Selector      | Calls | Reverts | Discards |\n+============================================================+\n| PayoutHandler | fund          | 439   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | pay           | 459   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | replay        | 431   | 31      | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | retry         | 412   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | setBlocked    | 421   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | strangerCalls | 459   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | sweep         | 397   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | togglePause   | 417   | 0       | 0        |\n|---------------+---------------+-------+---------+----------|\n| PayoutHandler | writeOff      | 405   | 0       | 0        |\n╰---------------+---------------+-------+---------+----------╯\n\n[PASS] testFuzz_batchSizeBoundary(uint16) (runs: 256, μ: 2609359, ~: 1817902)\n[PASS] testFuzz_payRoundAccounting(uint256,uint256,uint256,uint256,uint8) (runs: 512, μ: 321735, ~: 241909)\n[PASS] test_allTransfersFailedReverts() (gas: 363690)\n[PASS] test_constructorRejectsZeroOwner() (gas: 4930)\n[PASS] test_constructorSetsOwner() (gas: 24728)\n[PASS] test_duplicateFailedRecipientAccumulates() (gas: 401733)\n[PASS] test_fundAcceptsNoReturnToken() (gas: 747989)\n[PASS] test_fundPullsTokensFromAnyone() (gas: 181064)\n[PASS] test_fundRejectsEoaToken() (gas: 40342)\n[PASS] test_fundRejectsFalseReturningToken() (gas: 281849)\n[PASS] test_fundWithoutApprovalReverts() (gas: 99719)\n[PASS] test_fundZeroAmountSucceeds() (gas: 50754)\n[PASS] test_ownable2StepHandoff() (gas: 291664)\n[PASS] test_partialFailureIsRecordedAndBatchContinues() (gas: 451571)\n[PASS] test_partialFailureSilentRevertAndFalseReturn() (gas: 471459)\n[PASS] test_pauseTwiceAndUnpauseWhenRunningRevert() (gas: 109704)\n[PASS] test_payRoundBatchTooLarge() (gas: 661810)\n[PASS] test_payRoundBlockedWhilePaused() (gas: 403279)\n[PASS] test_payRoundEmptyBatch() (gas: 53908)\n[PASS] test_payRoundExactBalanceSucceeds() (gas: 341939)\n[PASS] test_payRoundIdempotent() (gas: 950232)\n[PASS] test_payRoundInsufficientBalanceUpFront() (gas: 136424)\n[PASS] test_payRoundIsTokenAgnosticPerRound() (gas: 1105422)\n[PASS] test_payRoundLengthMismatch() (gas: 50585)\n[PASS] test_payRoundMaxBatchOf500() (gas: 18486570)\n[PASS] test_payRoundPaysEveryLegAndRecords() (gas: 425820)\n[PASS] test_payRoundRoundIdZeroIsOrdinary() (gas: 299249)\n[PASS] test_payRoundSumOverflowReverts() (gas: 51898)\n[PASS] test_payRoundWithEoaTokenReverts() (gas: 51001)\n[PASS] test_payRoundWithNoReturnToken() (gas: 813401)\n[PASS] test_payRoundZeroAmountLegIsPaid() (gas: 168767)\n[PASS] test_payRoundZeroToken() (gas: 48139)\n[PASS] test_privilegedFunctionsRejectNonOwner() (gas: 173301)\n[PASS] test_reentrantTokenCannotReenterFundOrSweepDuringPayRound() (gas: 867722)\n[PASS] test_reentrantTokenCannotReenterPayRound() (gas: 719996)\n[PASS] test_renounceOwnershipLocksEverything() (gas: 153668)\n[PASS] test_retryFailedInsufficientBalanceAfterSweep() (gas: 543501)\n[PASS] test_retryFailedPaysAndClears() (gas: 596611)\n[PASS] test_retryFailedRevertsForUnpaidRound() (gas: 44744)\n[PASS] test_retryFailedShapeChecks() (gas: 1020563)\n[PASS] test_retryFailedStillFailingKeepsRecord() (gas: 486395)\n[PASS] test_singleLegFailureIsAllTransfersFailed() (gas: 196058)\n[PASS] test_sweepAboveBalanceReverts() (gas: 120617)\n[PASS] test_sweepByOwnerMovesTokens() (gas: 161709)\n[PASS] test_sweepRejectsShortReturnData() (gas: 243656)\n[PASS] test_sweepRejectsZeroRecipient() (gas: 104368)\n[PASS] test_sweepWorksWhilePaused() (gas: 191206)\n[PASS] test_transferOwnershipOnlyOwnerAndCancelable() (gas: 145838)\n[PASS] test_writeOffFailedClearsWithoutPaying() (gas: 537437)\n[PASS] test_writeOffFailedNothingOwedReverts() (gas: 41819)\nSuite result: ok. 51 passed; 0 failed; 0 skipped; finished in 612.66ms (941.26ms CPU time)\n\nRan 56 tests for test/MoneyBackHook.t.sol:MoneyBackHookTest\n[PASS]\nMoneyBackHookTest invariants:\n[PASS] invariant_hookNeverHoldsMoneybackClaims\n[PASS] invariant_lpFeeNeverOverriddenAndManagerLocked\n[PASS] invariant_pendingEqualsAccruedMinusSwept\n[PASS] invariant_sweepPaysOnlyPayout\n MoneyBackHookTest invariants (runs: 64, calls: 2560, reverts: 0)\n\n╭-------------+----------+-------+---------+----------╮\n| Contract    | Selector | Calls | Reverts | Discards |\n+=====================================================+\n| HookHandler | swap     | 883   | 0       | 0        |\n|-------------+----------+-------+---------+----------|\n| HookHandler | sweep    | 807   | 0       | 0        |\n|-------------+----------+-------+---------+----------|\n| HookHandler | warp     | 870   | 0       | 0        |\n╰-------------+----------+-------+---------+----------╯\n\n[PASS] testFuzz_feeMathsAllCases(uint8,uint256,uint32) (runs: 1000, μ: 194637, ~: 196608)\n[PASS] testFuzz_sequenceAccumulatesPending(uint256,uint256,uint256,uint32) (runs: 256, μ: 376494, ~: 381102)\n[PASS] testFuzz_surchargeFormula(uint32) (runs: 1000, μ: 20475, ~: 19795)\n[PASS] testFuzz_surchargeIsMonotoneNonIncreasing(uint32,uint32) (runs: 512, μ: 23390, ~: 23521)\n[PASS] testFuzz_tinyAmountsRoundDownToZeroFee(uint8,uint256,uint32) (runs: 512, μ: 134992, ~: 127379)\n[PASS] test_acceptanceSellCostAtT0AndAfterDecay() (gas: 361626)\n[PASS] test_afterSwapWithZeroPoolDeltaAccruesNothing() (gas: 73405)\n[PASS] test_allCasesWithImdAsCurrency1() (gas: 75743267)\n[PASS] test_buysNeverSurchargedAtAnyTime() (gas: 584709)\n[PASS] test_callbacksRejectNonPoolManager() (gas: 185823)\n[PASS] test_case1_exactInputBuy_t0() (gas: 196169)\n[PASS] test_case1_exactInputBuy_t1800() (gas: 195009)\n[PASS] test_case1_exactInputBuy_t1day() (gas: 195050)\n[PASS] test_case1_exactInputBuy_t900() (gas: 196180)\n[PASS] test_case2_exactOutputBuy_t0() (gas: 195081)\n[PASS] test_case2_exactOutputBuy_t1800() (gas: 193878)\n[PASS] test_case2_exactOutputBuy_t1day() (gas: 193830)\n[PASS] test_case2_exactOutputBuy_t900() (gas: 195136)\n[PASS] test_case3_exactInputSell_t0() (gas: 196629)\n[PASS] test_case3_exactInputSell_t1800() (gas: 194946)\n[PASS] test_case3_exactInputSell_t1day() (gas: 194987)\n[PASS] test_case3_exactInputSell_t900() (gas: 196630)\n[PASS] test_case4_exactOutputSell_t0() (gas: 196402)\n[PASS] test_case4_exactOutputSell_t1800() (gas: 194686)\n[PASS] test_case4_exactOutputSell_t1day() (gas: 160353)\n[PASS] test_case4_exactOutputSell_t900() (gas: 196371)\n[PASS] test_constructorRejectsImdAsLaunchToken() (gas: 67430)\n[PASS] test_constructorRejectsWrongFlagBits() (gas: 44992126)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 191778)\n[PASS] test_constructorStoresImmutablesAndConstants() (gas: 61667)\n[PASS] test_deploymentAddressFlagBitsMatchPermissions() (gas: 20990)\n[PASS] test_emptySweepSucceedsAndEmitsZero() (gas: 80231)\n[PASS] test_feeAccruedEventsPerCase() (gas: 478454)\n[PASS] test_fixedCallbacksOnlyChargeTheirOwnCases() (gas: 184224)\n[PASS] test_hookNeverMintsMoneybackClaimsEvenIfGiftedImdClaims() (gas: 321566)\n[PASS] test_hookRejectsEthAndHasNoAdminSurface() (gas: 175524)\n[PASS] test_hugeSpecifiedAmountRevertsAmountOverflow() (gas: 156520)\n[PASS] test_initializeAcceptsEitherCurrencyOrder() (gas: 220424256)\n[PASS] test_initializeBindsPoolAndRecordsTimestamp() (gas: 65079)\n[PASS] test_initializeEmitsPoolBound() (gas: 220430056)\n[PASS] test_initializeRejectsSecondPool() (gas: 79487)\n[PASS] test_initializeRejectsWrongKeys() (gas: 222821868)\n[PASS] test_surchargeAt1799IsStillChargedOnSells() (gas: 152102)\n[PASS] test_surchargeAt1800IsZeroOnSells() (gas: 151320)\n[PASS] test_surchargeBoundaries() (gas: 89558)\n[PASS] test_swapBeforeInitializationReverts() (gas: 220451676)\n[PASS] test_swapRejectsWrongPoolKey() (gas: 294432)\n[PASS] test_sweepAccumulatesAcrossRounds() (gas: 498442)\n[PASS] test_sweepCannotBeNestedInsideAnotherUnlock() (gas: 1110368)\n[PASS] test_sweepReentrancyFromPayoutTransferIsRejected() (gas: 864947)\n[PASS] test_sweepRevertsCleanlyIfTakeFails() (gas: 482872)\n[PASS] test_sweepTakesAllImdClaimsToPayout() (gas: 500083)\n[PASS] test_unlockCallbackOutsideSweepIsRejected() (gas: 184842)\n[PASS] test_viewsBeforeInitialization() (gas: 220275245)\n[PASS] test_zeroAmountSwapAccruesNothing() (gas: 232641)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 629.99ms (1.98s CPU time)\n\nRan 3 test suites in 632.66ms (1.62s CPU time): 134 tests passed, 0 failed, 0 skipped (134 total tests)\n","passed":true},{"durationMs":31,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MoneyBackHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MoneyBackHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"MoneyBackHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MoneyBackHook.sweep()\",\"MoneyBackHook.unlockCallback(bytes)\",\"MoneyBackRouter.buyWithEth(uint256,uint256)\",\"MoneyBackRouter.quoteBuy(uint256)\",\"MoneyBackRouter.quoteSell(uint256)\",\"MoneyBackRouter.receive()\",\"MoneyBackRouter.sellForEth(uint256,uint256,uint256)\",\"MoneyBackRouter.unlockCallback(bytes)\",\"MoneyBackToken.approve(address,uint256)\",\"MoneyBackToken.transfer(address,uint256)\",\"MoneyBackToken.transferFrom(address,address,uint256)\",\"RoundPayout.acceptOwnership()\",\"RoundPayout.fund(address,uint256)\",\"RoundPayout.pause()\",\"RoundPayout.payRound(uint256,address,address[],uint256[],bytes32,uint256,uint256)\",\"RoundPayout.renounceOwnership()\",\"RoundPayout.retryFailed(uint256,address[])\",\"RoundPayout.sweep(address,address,uint256)\",\"RoundPayout.transferOwnership(address)\",\"RoundPayout.unpause()\",\"RoundPayout.writeOffFailed(uint256,address)\"],\"files\":{\"src/MoneyBackHook.sol\":498,\"src/MoneyBackRouter.sol\":444,\"src/MoneyBackToken.sol\":153,\"src/RoundPayout.sol\":373,\"test/MoneyBackHook.t.sol\":1298,\"test/MoneyBackToken.t.sol\":372,\"test/RoundPayout.t.sol\":1241},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":190,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":289,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/MoneyBackRouter.sol:200: ETH transferred without address checks (2 places)\n[high] incorrect-shift-order at src/MoneyBackHook.sol:483: Incorrect Assembly Shift Parameter Order\n[high] reentrancy-state-change at src/MoneyBackHook.sol:426: Reentrancy: State change after external call (2 places)\n[high] reused-contract-name at src/MoneyBackRouter.sol:16: Contract Name Reused in Different Files (2 places)\n[low] centralization-risk at src/RoundPayout.sol:61: Centralization Risk (8 places)\n[low] costly-loop at src/RoundPayout.sol:252: Costly operations inside loop (2 places)\n[low] internal-function-used-once at src/MoneyBackHook.sol:107: Internal Function Used Only Once\n[low] large-numeric-literal at src/MoneyBackHook.sol:197: Large Numeric Literal (2 places)\n[low] missing-inheritance at src/MoneyBackToken.sol:14: Missing Inheritance\n[low] non-reentrant-not-first at src/RoundPayout.sol:197: `nonReentrant` is Not the First Modifier (4 places)\n[low] push-zero-opcode at src/MoneyBackHook.sol:2: PUSH0 Opcode (4 places)\n[low] require-revert-in-loop at src/RoundPayout.sol:299: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/MoneyBackHook.sol:423: State Change Without Event (4 places)\n[low] state-no-address-check at src/RoundPayout.sol:62: Address State Variable Set Without Checks (2 places)\n[low] unchecked-return at src/MoneyBackHook.sol:426: Unchecked Return\n[low] unsafe-erc20-operation at src/MoneyBackRouter.sol:331: Unsafe ERC20 Operation (4 places)\n[low] unspecific-solidity-pragma at src/MoneyBackRouter.sol:2: Unspecific Solidity Pragma (3 places)\n[low] unused-public-function at src/MoneyBackHook.sol:323: Public Function Not Used Internally (13 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e2ddc404951d00ab0169fe0408fd7ee0aebdd28a2c694994bb9072dfdeac0554","verifiedTreeHash":"5e7fdc12a3abd63040d1560256a9860782ddf8e9","verifierVersion":"0.1.0+fdeb4d4a"}]}