{"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":"d478eaff-9c39-44b0-aacf-db6d5c80eb88","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"1c97a6e2fbd7084ec1c8079ccd68fc253289ae6c421225862fe138abd0d453f5","dependsOn":["write_foundry_tests"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","state":"accepted"},{"acceptedSubmissionHash":"ef8fd73ce6b33721933a925522ad3ae0d3a2fbb46f8c7c93e2118d9f812a2e88","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"ff1fad07fad3b8e08a71e9c5201fd632e161ed8163eb407676ea8b0bedbe179e","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"ff1fad07fad3b8e08a71e9c5201fd632e161ed8163eb407676ea8b0bedbe179e","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Add a handler-driven stateful invariant suite for JackpotHook from launch 186 (src/JackpotHook.sol at this commit; the same hook source is live on Sepolia at 0xd08e759d3d89eed2de3f03006a6e21ae341d4088 as a test toy with no real value), then review it. Tests only: add files under test/, do not change src/, foundry.toml, remappings.txt or lib/.\n\nWhat the hook does (follow this, nothing else): beforeSwap keeps F = floor(|amountSpecified| / 100) of the specified currency as ERC-6909 claims in a per-pool pot (pots(poolId) returns (eth, ice)); a swap whose F is at least 0.00001 ETH or 1 ICE issues one ticket to the address in 32-byte hookData, else to tx.origin. fillTank(key, pees) adds exactly 10 ICE per pee (1-1000) to the ICE pot and issues no ticket. draw(poolId, ticketId) may be called by anyone, once per ticket, from block B+2 to B+256 (B = the ticket's block), and pays only the recorded player: roll 77 pays floor(90%) of both pots; rolls 20, 40, 60, 80, 100 pay min(20 × ticket fee, floor(pot / 10)) in the ticket's currency; other rolls pay nothing; after B+256 a draw expires with roll 0 and no payout. There are no rounds, owners or admin paths.\n\nBuild on test/fixtures/JackpotFixture.sol (real PoolManager, PoolSwapTest router, _hashForRoll with vm.setBlockhash to force rolls). Handler actions: swaps in all four modes (exact-in/exact-out × both directions) with sizes on both sides of the ticket thresholds, from three players via hookData and via tx.origin; fillTank; draw of a random ticket at a random age in {B+1 (must revert TooEarly; catch it with try/catch inside the handler), B+2, B+256, B+257} with forced rolls {77, a multiple of 20, a loser}; vm.roll forward; and the same actions on a second pool that uses this hook with a different mock ERC-20 as currency1. The handler keeps ghost ledgers per pool and currency.\n\nInvariants: (1) for each pool and currency, pot == fees kept + tank fills − payouts in the ghost ledger; (2) for each currency, the sum of every pool's pot == PoolManager.balanceOf(hook, currency id), and the hook holds no native ETH and no ERC-20 balance of its own; (3) actions on one pool never change the other pool's pots or ticket counter; (4) every ticket is drawn at most once and its payout reaches only its recorded player; (5) no draw pays more than floor(90%) of the pre-draw pot in either currency, and a small prize never more than min(20 × fee, floor(pot / 10)); (6) a draw after B+256 always emits roll 0 and pays nothing; (7) nextTicketId only grows, by exactly one per qualifying swap. foundry.toml has no invariant section, so set runs 128 and depth 64 with forge's inline `/// forge-config: default.invariant...` comments, and never let a revert escape a handler action (bound inputs, skip impossible draws, catch expected reverts). If an invariant fails on the unmodified hook, keep it, write the shrunk call sequence in a comment and do not weaken it: that failure is the finding.","parentJobId":null,"planHash":"2fe9092a270ef1224599b4e498f0ee4a95d7507488ad4c8a6ecc32cba28c0393","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"d478eaff-9c39-44b0-aacf-db6d5c80eb88","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-300-add-handler-driven-stateful-invariant"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51031","feedbackHash":"12164b9d216a8142b6a282be7f5db2736a6d2c77b34684b0f91ecce5cc0bc802","nodeKey":"adversarial_review","submissionHash":"1c97a6e2fbd7084ec1c8079ccd68fc253289ae6c421225862fe138abd0d453f5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51194","feedbackHash":"79e1d10fbe80d78ba5bf9ea26f26bdd962e64b838455a6cdbad78fa5d0bd4644","nodeKey":"write_foundry_tests","submissionHash":"ef8fd73ce6b33721933a925522ad3ae0d3a2fbb46f8c7c93e2118d9f812a2e88","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"175f6d8a319df60b7552ee9511e3bb65597be2d819f237eda7905b1d6050ce5d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5fb4f77b3d119b1d","findings":[{"description":"All draw accounting observations occur after hook.draw returns, and the three generated ticket recipients are EOAs. Consequently, paying a recipient before debiting the pots leaves the same final balances and passes every invariant, despite exposing an undebited pot during the recipient callback. The existing PayoutReceiver tests only attempt guarded mutations and do not check accounting during payment. This misses the payout-before-debit regression explicitly requested for this review. Add a recipient callback that observes the paid currency's aggregate pots and corresponding claim balance during payment. This is a suite coverage defect; the unmodified hook debits before payment, and this reproduction does not establish a fund-stealing reentrancy vulnerability in that hook.","line":324,"path":"test/handlers/JackpotHandler.sol","reproduction":"In a disposable copy, move only `pot.eth -= payoutEth; pot.ice -= payoutIce;` from src/JackpotHook.sol:195-196 to immediately after the conditional poolManager.unlock call in draw, retaining every guard and other operation. Run `forge test --offline --match-contract JackpotInvariantTest --fuzz-seed 186`: all eight invariants pass for 128 runs / 8192 calls, with zero escaped reverts, and all six deterministic handler tests pass. The five existing JackpotAdversarialTest tests also pass. To expose the missed state, use JackpotFixture's initially empty pot at block 100, deploy a recipient whose receive() records hook.pots(poolId)'s ETH value and manager.balanceOf(address(hook), 0), and call `_swap(true, -int256(0.001 ether), abi.encode(address(recipient)))`. Ticket 0 has fee 10000000000000 wei. Set block 102 and blockhash(101) to `_hashForRoll(poolId, 0, 77)`, then draw ticket 0. The recipient receives 9000000000000 wei. Expected at its callback: pot and claims both equal 1000000000000 wei. Actual with the mutation: pot is 10000000000000 wei while claims are 1000000000000 wei; after draw returns both equal 1000000000000 wei, concealing the violation from the suite. The callback assertion passes against the unmodified hook and fails against this mutation. All mutations and probes were run outside the repository.","severity":"medium","title":"The invariant suite misses payout-before-debit regressions"}],"hash":"1c97a6e2fbd7084ec1c8079ccd68fc253289ae6c421225862fe138abd0d453f5","nodeId":"1862289f-9c85-4468-8173-9139df3c350c","outcome":"completed","summary":"Wrote [`.imd-findings.json`](.imd-findings.json) with one **medium** finding: all eight invariants pass a payout-before-debit mutation that exposes incorrect accounting during payment. A callback probe reproduces the missed state.\n\nNo implementation defect was substantiated in the unmodified hook.","treeHash":null,"usage":{"cachedInputTokens":723840,"inputTokens":67241,"model":"gpt-6-astra","outputTokens":7429,"runtime":"codex","turns":5,"wallClockMs":286821}},{"artifacts":[],"attempt":1,"bundleHash":"f6c01c4057d8cf4e2a4ca6acc96706d72f0322e386fd4934e65d8aae4d4ed0cf","device":"2f1d693f218c1c18","findings":[],"hash":"ef8fd73ce6b33721933a925522ad3ae0d3a2fbb46f8c7c93e2118d9f812a2e88","nodeId":"f8092fe0-2249-4ed9-87a2-270d80aed276","outcome":"completed","summary":"Added and reviewed the two-pool handler suite under `test/`, covering all requested invariants plus deterministic boundary, rounding, and rejection tests.\n\nValidation passed: `forge build` and offline `forge test`—41 tests passed. Invariants completed 128 runs × 64 calls with zero escaped reverts.\n\nNo reproducible hook defect found. Review notes are in `test/JackpotInvariant.review.md`.","treeHash":"533ee8561a31345ffd4b9f01c3f4c3175621ee7a","usage":{"cachedInputTokens":685440,"inputTokens":75534,"model":"gpt-6-astra","outputTokens":18233,"runtime":"codex","turns":7,"wallClockMs":617128}}],"verification":[{"checks":[{"durationMs":3006,"exitCode":0,"name":"build","output":"Compiling 93 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.92s\nCompiler run successful!\n","passed":true},{"durationMs":4343,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/JackpotAdversarial.t.sol:JackpotAdversarialTest\n[PASS] test_failedPaymentCanExpireWithoutCallingTheRejectingReceiver() (gas: 1369295)\n[PASS] test_failedPaymentRollsBackAndRetryRejectsReentrancy() (gas: 1107407)\n[PASS] test_initialOneSidedLiquidityFirstBuyFirstSellAndUnwind() (gas: 801936)\n[PASS] test_poolPotsAndTicketSequencesAreIsolatedWithSharedCurrencies() (gas: 1316930)\n[PASS] test_tankRejectsFeeOnTransferWithoutCreatingUnbackedPot() (gas: 678121)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 4.66ms (6.13ms CPU time)\n\nRan 5 tests for test/PepeIce.t.sol:PepeIceTest\n[PASS] testFuzz_transferConservesSupply(uint256) (runs: 256, μ: 40791, ~: 40698)\n[PASS] test_factoryGetsFixedSupplyAndMetadata() (gas: 23526)\n[PASS] test_invalidTransfersRevert() (gas: 20043)\n[PASS] test_noAdminOrSecondMintPath() (gas: 36616)\n[PASS] test_transferAndAllowanceAreExact() (gas: 74710)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 5.63ms (5.77ms CPU time)\n\nRan 20 tests for test/JackpotHook.t.sol:JackpotHookTest\n[PASS] testFuzz_swapExactAmountsAndFeeRounding(bool,bool,uint96) (runs: 256, μ: 305770, ~: 337557)\n[PASS] test_allFourSwapModesHaveExactAccounting() (gas: 960526)\n[PASS] test_allGoldenRollsPayCappedEth() (gas: 1887226)\n[PASS] test_callbacksRejectUntrustedCallers() (gas: 29327)\n[PASS] test_constructorRejectsWrongAddressFlags() (gas: 71836)\n[PASS] test_drawAtAge256StillUsesFutureHash() (gas: 706506)\n[PASS] test_drawTooEarlyAndMissingTickets() (gas: 594439)\n[PASS] test_emptyPoolCannotCollectFeeOrIssueTicket() (gas: 591919)\n[PASS] test_expiryIsBasedOnTicketBlock() (gas: 641241)\n[PASS] test_extremeAmountsRevertWithoutAccountingChanges() (gas: 223276)\n[PASS] test_factoryInitializationAndPermissions() (gas: 24901)\n[PASS] test_fillTankRejectsInvalidInputsAndMissingApproval() (gas: 158856)\n[PASS] test_fillTankSettlesExactClaimsAndEmitsPurchase() (gas: 181784)\n[PASS] test_goldenFlushCapsAtTenPercent() (gas: 955122)\n[PASS] test_goldenFlushUncappedTwentyTimesIceFee() (gas: 462247)\n[PASS] test_jackpotPaysBothPotsToRecordedPlayerOnce() (gas: 924743)\n[PASS] test_losingRollPaysNothingAndCannotBeDrawnAgain() (gas: 643526)\n[PASS] test_partialFillsRevertInEveryModeAndRollBackFees() (gas: 417956)\n[PASS] test_smallAndZeroFeeSwapsDoNotIssueTickets() (gas: 348220)\n[PASS] test_ticketAttributionAndThresholds() (gas: 947149)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 65.58ms (84.08ms CPU time)\n\nRan 3 tests for test/JackpotBoundaries.t.sol:JackpotBoundariesTest\n[PASS] test_everyRollInBothFeeCurrenciesMatchesThePayoutTable() (gas: 74571507)\n[PASS] test_unfundedTankPurchaseRollsBackExistingPot() (gas: 173115)\n[PASS] test_unpaidSwapRollsBackAmmFeeAndTicket() (gas: 759839)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 90.31ms (88.70ms CPU time)\n\nRan 1 test for test/JackpotConservation.t.sol:JackpotConservationTest\n[PASS] testFuzz_mixedSequencesNeverPayMoreThanReceived(uint256) (runs: 256, μ: 4263148, ~: 4315350)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 452.24ms (448.75ms CPU time)\n\nRan 14 tests for test/JackpotInvariant.t.sol:JackpotInvariantTest\n[PASS] invariant_actionsCannotChangeTheOtherPool() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_allPoolsShareExactlyTheirCurrencyClaims() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_drawPayoutsRespectBothCapsAndTheOutcomeTable() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_expectedFailuresAreAtomicAndNoUnexpectedRevertsOccur() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_expiredTicketsEmitZeroRollAndPayNothing() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_potsEqualFeesPlusFillsMinusPayouts() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_ticketCounterGrowsExactlyForQualifyingSwaps() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] invariant_ticketsResolveOnceAndOnlyPayTheirRecordedPlayer() (runs: 128, calls: 8192, reverts: 0)\n\n╭----------------+-------------+-------+---------+----------╮\n| Contract       | Selector    | Calls | Reverts | Discards |\n+===========================================================+\n| JackpotHandler | advance     | 1286  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | draw        | 1334  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | drawUnknown | 1413  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | fillTank    | 1441  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | rejectFill  | 1329  | 0       | 0        |\n|----------------+-------------+-------+---------+----------|\n| JackpotHandler | swap        | 1389  | 0       | 0        |\n╰----------------+-------------+-------+---------+----------╯\n\n[PASS] test_handlerCoversEverySwapModeThresholdAndPlayerAttribution() (gas: 78086959)\n[PASS] test_handlerDrawsAtBothValidBoundariesWithEveryOutcome() (gas: 62108846)\n[PASS] test_handlerExpiryIgnoresWinningHashAndNeverResurrectsTickets() (gas: 3263217)\n[PASS] test_handlerJackpotRoundsDownNonDivisiblePotsInBothPools() (gas: 2465299)\n[PASS] test_handlerRejectsUnknownTicketsAndInvalidOrUnfundedFills() (gas: 1696048)\n[PASS] test_handlerSmallPrizesExerciseBothSidesOfTheMinimumInBothCurrencies() (gas: 13172059)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 4.25s (25.43s CPU time)\n\nRan 6 test suites in 4.25s (4.86s CPU time): 48 tests passed, 0 failed, 0 skipped (48 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"ef8fd73ce6b33721933a925522ad3ae0d3a2fbb46f8c7c93e2118d9f812a2e88","verifiedTreeHash":"533ee8561a31345ffd4b9f01c3f4c3175621ee7a","verifierVersion":"0.1.0+ff982c0f"}]}