{"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":"6d498c89-8691-4e35-95a3-48d6ab8fc624","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"0a624d68a224f05c52185a6312a2d0b1cc29e2e8773e5dd8c65eed2ae1077176","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"d38266d37f6fda2ea625c6f587bf605299cd93955d12fc454d27cea8cc66bfde","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"728a5583e111db579d0d9630fff08c37dbb23b0ae890b4f601962bfa66da9cd8","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"93d3191e10bddfd2d2d6804e383735a8e308a4b88f39ca7b04ac53390cad9231","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"a8f91a8819ce5a3161eb56fac25a2ccef6ac0a5682b9bf4fb8761c07efd5ca3e","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7268cb1a98069e9fa027364da6af0e5df99d35faca1f4fff58e814af355b2072","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"d87bef714571b7e14587ab2313f25089ec3da3639f60606de5c8378732b4a243","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"878a141502937229edf1c970a3d010bd7c9fb67bc4b6c36398331fd4fc6be510","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"A custom token with a \"last buyer wins\" game: WIN ($WIN).\n\nPair: WIN/IMD on the chain selected for this launch, using that chain's IMD token and the DEX the launch factory uses (Uniswap v4 hook). Supply 1,000,000,000, 18 decimals. No owner powers after launch, no minting, no upgradeability.\n\nTrading fee (in the hook):\n- Every buy and sell pays a fee, always taken in IMD (from the IMD input on buys, from the IMD output on sells).\n- Anti-snipe at launch: the fee starts at 50% and decays linearly to the base fee of 3% over 30 minutes. If you find a better curve, explain why and use it.\n- Fee split: 90% to the prize bank, 10% to the team wallet 0x611F08c7226591708B5F53F29BF53f3830D54511 (pull-based, the team claims it). Nobody can withdraw from the bank except through the game rules.\n\nGame:\n- The bank holds IMD and starts empty: it is filled only by trading fees, nobody seeds it.\n- A qualifying buy: a buy whose IMD amount is at least the current minimum. Each qualifying buy makes the buyer the leader and resets the round timer to 10 minutes. Sells never reset the timer and never change the leader.\n- Minimum buy = the larger of 8.5 IMD (about $100 at launch) and 20% of the upcoming prize, and it rises by 5% with every qualifying buy inside a round. This blocks spam with tiny buys and makes endless resets more and more expensive, so every round ends. It resets when a new round starts.\n- When the timer runs out, anyone can call settle(): the prize goes to the leader and the rest of the bank stays for the next round. The next round starts with the next qualifying buy.\n- First round: it cannot end earlier than 3 hours after launch, and its prize is 20% of the bank. After that the normal 10-minute rule applies.\n- Every later round: prize = 5% of the bank at settlement.\n- If you see a way for bots to reliably win rounds (block stuffing, sequencer ordering on the selected chain, sandwiching the leader), explain it and add a defence.\n- Identify the real buyer robustly: the hook sees the router, not the user. Choose a safe approach (hookData from the router, tx.origin, or another), document the trade-offs and test it.\n\nViews for the website: bank balance, next prize, current minimum buy, leader, time left, round number, past winners.\n\nRequirements:\n- Foundry tests on a fork of the selected chain: anti-snipe decay, fee always in IMD on both sides, the 90/10 fee split, the first round's 3-hour minimum and 20% prize, the 10-minute reset, the 8.5 IMD floor and the minimum rising and resetting, settle before and after expiry, 5% prizes in later rounds, sells not affecting the game, reentrancy, and end-of-round sniping.\n- An independent review of the game economics and the hook.\n- Launch: all of the requester's share goes into liquidity (pool 90%, 0% to the requester), single-sided with no IMD or ETH added. Starting market cap 2500 IMD.","parentJobId":null,"planHash":"c95d3730e776baf842954faae8cc9b3bdf7b30674612e22d34217664fab85e83","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6d498c89-8691-4e35-95a3-48d6ab8fc624","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-784-custom-token-last-buyer"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51382","feedbackHash":"a3d6b521f3a9f4a5d99df702b1e64bc7fcfa69d17d18427c28c5743843c134b4","nodeKey":"audit_economics","submissionHash":"0a624d68a224f05c52185a6312a2d0b1cc29e2e8773e5dd8c65eed2ae1077176","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51039","feedbackHash":"ba822dbc0630678522346402360187f93ae8f11ec10639eb78fc26b5baabd081","nodeKey":"audit_flow","submissionHash":"d38266d37f6fda2ea625c6f587bf605299cd93955d12fc454d27cea8cc66bfde","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52166","feedbackHash":"725fd51a3a215b3f9780a4ea21c354bd43362dc2b1d11596723bfea0ae8c3079","nodeKey":"audit_judge","submissionHash":"e34fdc2a6b272ecc6cffe45559fa3feb20943695206d35d71492fa8922a77e0a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52176","feedbackHash":"b241ac6c999a9bff1b0b73ae206d2bd7f5fd2514707a52ca1370037a441ea5a8","nodeKey":"audit_judge","submissionHash":"728a5583e111db579d0d9630fff08c37dbb23b0ae890b4f601962bfa66da9cd8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52223","feedbackHash":"7a1e076b5b6c4df82af3ec293a7bf8d9ee035b3b6bfdba9ddeed35e1d2ce0448","nodeKey":"audit_math","submissionHash":"93d3191e10bddfd2d2d6804e383735a8e308a4b88f39ca7b04ac53390cad9231","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52205","feedbackHash":"8e3b6b9c805a38c8d754b4e245b51b4e427a4d532a98f9fd423751c4915ab0b1","nodeKey":"audit_permissions","submissionHash":"a8f91a8819ce5a3161eb56fac25a2ccef6ac0a5682b9bf4fb8761c07efd5ca3e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52222","feedbackHash":"dec4b63ee253029a301be7827c4d0a29add2f1c03ff10e93ee3ac91a2c15e55f","nodeKey":"build_contract_project","submissionHash":"730aa0bf0c35096134327dd9c402417dbc13f2f28b518c45809e7df5f8b09186","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52158","feedbackHash":"676fe07eb96fd3cc1bd65b49531eb195ca63e58a9050f75e1c2a5b49d456dde0","nodeKey":"build_contract_project","submissionHash":"7268cb1a98069e9fa027364da6af0e5df99d35faca1f4fff58e814af355b2072","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51530","feedbackHash":"3d91e549f4bf3e30e67cd42cc41ab20fef732a28a927065a4594e152f7775d21","nodeKey":"manifest","submissionHash":"d87bef714571b7e14587ab2313f25089ec3da3639f60606de5c8378732b4a243","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52207","feedbackHash":"bd10b3cce863b26970441f086fe0d6a24a5949fe8f6ddc9385ab434fdd0a213a","nodeKey":"manifest","submissionHash":"bf1578c867035bedbffdfdc8ccce7da3a6afd49fe35475fca7ee112561d50616","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52210","feedbackHash":"61a7efd2c97db590495ab854be460019993a271ac7415aef56ea6584196bcddb","nodeKey":"write_foundry_tests","submissionHash":"ce604e47e26e3564cc99b7c0a9568825574d7c69bedbf5f800cb3bb0d85c8739","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52165","feedbackHash":"7f704ffeb41f08dce5ea8275a83022c8965c6b3b05d53b2e6d79908c11992a63","nodeKey":"write_foundry_tests","submissionHash":"878a141502937229edf1c970a3d010bd7c9fb67bc4b6c36398331fd4fc6be510","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"454bbf497af859d22c64667588e1f8f3845276e8a4e292015285dc7e2ce807de","state":"completed","submissions":[{"artifacts":[],"attempt":2,"bundleHash":null,"device":"85f29f4c09dba910","findings":[{"citation":"resolved","description":"afterSwap compares the buy's gross IMD against minimumBuy() and, when it is short, simply treats the trade as a plain buy: the fee (3% after decay, up to 50% at launch) is accrued to the bank and team, the buyer receives WIN, and no error or flag reaches the buyer. There is no way for a bidder to say 'qualify or revert'. Because every qualifying buy multiplies the minimum by 1.05 (line 538) a bot that sees a pending bid of exactly the displayed minimum M can front-run it with its own bid of M: the bot becomes leader, the escalator rises to 1.05 M, and the victim's transaction then lands as a non-qualifying buy that funds the bank. The victim loses the fee on M and the lead; the bot paid only its own fee, which it was going to pay anyway to bid. Repeating this on every incoming bid lets the bot hold the lead for as long as it is willing to spend 3% of M per overtake while each victim loses 3% of M for nothing. This is exactly the leader-sandwich / ordering attack the brief lists under 'ways for bots to reliably win rounds'; the hook documents the race but has no defence for the victim's side. On an FCFS sequencer the same thing happens whenever two bids for the same minimum land in one block: the second one is silently converted into a donation. Fix that preserves the design: let the buyer opt into strictness through hookData (for example abi.encode(buyer, true)) and revert with a dedicated error when gross < minimum for such a buy; the revert happens inside the PoolManager unlock so the whole swap is undone and the buyer loses only gas. The website should always set the flag on bids. Optionally also let hookData carry the minimum the buyer saw, and revert when the on-chain minimum exceeds it.","line":326,"path":"src/WinGameHook.sol","reproduction":"State: round 1 settled, round 2 opened by carol, minimumBuy() = 8.925e18 (8.5 IMD x 1.05), fee 3%. Alice signs an exact-input buy of 8.925 IMD with hookData abi.encode(alice). Bob front-runs with an exact-input buy of 8.925 IMD, hookData abi.encode(bob): leader = bob, escalator -> 1.1025, minimumBuy() -> 9.371 IMD. Alice's buy then executes in the same block: gross 8.925 IMD < 9.371 IMD, so _qualify is skipped; bank += 0.240975 IMD and teamOwed += 0.026775 IMD (her full 0.26775 IMD fee), leader stays bob, qualifyingBuysInRound stays 2, unclaimedPrize[alice] = 0. Expected by the bidder: either lead the round or have the transaction revert. Actual: fee charged, no lead, no error. Cost to bob: 0.26775 IMD (his own bid fee). With a bank of 100,000 IMD the minimum is >= 1,000 IMD and each voided bid costs the victim >= 30 IMD. Reproduced with the project fixture (WinGameFixture): _finishRoundOne(); buyExactIn(carol, hook.minimumBuy()); M = hook.minimumBuy(); buyExactIn(bob, M); buyExactIn(alice, M); assert hook.leader() == bob and hook.bank() grew by M*0.027.","severity":"medium","snippet":"            if (gross >= minimum) _qualify(trader, gross);","title":"A buy that misses the qualifying minimum is silently executed and still pays the full fee, so an ordering-advantaged bot can void every competitor's at-minimum bid and keep the lead (the 'sandwiching "},{"citation":"resolved","description":"minimumBuy() is computed live from nextPrize(), which is a share of the live bank. Every buy and every sell, including sells that the brief says never touch the game, increases the bank and therefore raises the threshold that a bid already in flight will be judged against. The brief defines the minimum as max(8.5 IMD, 20% of the upcoming prize) which 'rises by 5% with every qualifying buy inside a round' and 'resets when a new round starts'; nothing in it moves the threshold on sells or sub-minimum buys. Once the bank exceeds 850 IMD the prize-linked term dominates the 8.5 IMD floor and the threshold becomes continuously moving. Combined with finding 1 (a short bid is silently accepted and charged), the cheapest possible griefing is a one-wei-scale sell placed ahead of a competitor's exact-minimum bid: the competitor pays the full fee and does not qualify, and the attacker risks nothing but a dust fee. It also means the website can never display an exact number that is guaranteed to qualify; the README asks the front end to 'add a small margin', which pushes a contract-level guarantee onto an off-chain guess. Fix: snapshot the prize-linked base when a round starts (and, if the design wants it, re-snapshot on each qualifying buy, since that is when the brief says it rises) and apply the escalator to the snapshot, so that between qualifying buys the minimum is a constant the website can show exactly. The lazy-close path must snapshot after _finalize so the next round starts from the reduced bank.","line":392,"path":"src/WinGameHook.sol","reproduction":"State: round 1 settled; carol buys 40,000 IMD (fee 1,200 IMD, bank 1,080.18 IMD) and leads round 2; minimumBuy() = 11.3419278 IMD (= 1,080.18 x 5% x 20% x 1.05). Alice signs a buy of exactly 11.3419278 IMD. Bob first does sellExactOut for 0.1 IMD of WIN: his fee is 0.0030928 IMD, of which 0.0027835 IMD enters the bank, and minimumBuy() becomes 11.3419570 IMD. Alice's buy then executes: 11.3419278 < 11.3419570, she pays her 0.3402578 IMD fee and does not qualify; leader stays carol, qualifyingBuysInRound stays 1. Expected per brief: the minimum is unchanged by sells, so an exact-minimum bid qualifies. Actual: a 0.003 IMD sell voids an 11.34 IMD bid. Reproduced with WinGameFixture: _finishRoundOne(); giveWin(bob, 50_000_000e18); buyExactIn(carol, 40_000e18); M = hook.minimumBuy(); sellExactOut(bob, 0.1e18); assert hook.minimumBuy() > M; buyExactIn(alice, M); assert hook.leader() == carol.","severity":"medium","snippet":"        uint256 base = nextPrize() * MIN_BUY_PRIZE_BPS / BPS;","title":"The prize-linked part of the minimum drifts with every trade, so a dust sell (fee ~0.003 IMD) is enough to void an exact-minimum bid; the brief says the minimum moves only with qualifying buys inside "},{"citation":"resolved","description":"afterSwap runs _finalizeIfExpired() before reading minimumBuy() (lines 323-324), so a buy arriving at or after the deadline is judged against the next round's fresh minimum: escalator 1, prize share 5%, bank reduced by the prize just paid. The views do not project that close: nextPrize() returns bank x current round's share, minimumBuy() keeps the expired round's escalator, roundNumber() returns the expired round. The website is told to show these ('Views for the website'), and gameState() bundles them. In the expired-unsettled window the shown minimum is at least escalator x too high, and after round 1 the shown prize is 20% of the bank while the round that the next buy actually opens pays 5% of 80% of it, i.e. 5x less. A buyer who sizes a bid from the view overpays the fee on the excess and may decide to bid for a prize that will not exist; a buyer who reads the shown minimum as the price of entry may be deterred from a bid that would in fact have qualified at the 8.5 IMD floor. No funds are lost on chain, so low. Fix: in nextPrize(), minimumBuy(), roundNumber() and gameState(), when roundActive && block.timestamp >= deadline, compute from the projected post-close state (bank - prize, escalator WAD, next round's share and number), and expose a flag so the UI can show 'round N ended, waiting for settle'.","line":387,"path":"src/WinGameHook.sol","reproduction":"Case A (later round): round 1 settled; alice makes 4 qualifying buys at the floor in round 2 (escalator 1.2155); warp to deadline + 1. Views: minimumBuy() = 10.3318 IMD, nextPrize() = 0.058639 IMD, roundNumber() = 2, bank = 1.172774 IMD. bob buys 8.5 IMD (below the shown minimum): the round closes for alice with prize 0.058639 IMD, bob leads round 3, roundsStarted = 3. The next-round prize the view should have shown was (1.172774 - 0.058639) x 5% = 0.055707 IMD and the minimum 8.5 IMD. Case B (round 1): alice buys 1,000 IMD after decay (bank 27 IMD, round 1); warp to launch + 3h. Views: nextPrize() = 5.4 IMD (20%), minimumBuy() = 8.925 IMD. bob buys 8.5 IMD: round 1 closes, bob opens round 2 at the floor, and nextPrize() for round 2 is 1.091475 IMD. Reproduced with WinGameFixture as described.","severity":"low","snippet":"        return bank * _prizeBps() / BPS;","title":"While a round is expired but not yet settled, minimumBuy(), nextPrize() and roundNumber() describe the dead round, not what the next buy will be judged against; the website overstates the minimum by t"},{"citation":"resolved","description":"The brief asks that any way for bots to reliably win rounds, naming block stuffing explicitly, be explained and given a defence. The README (section 5) and docs/REVIEW.md (A3) explain it and quantify the break-even as 'bank > 20x the stuffing cost', but the contract contains nothing that changes that arithmetic: the timer is a flat 10 minutes from the last qualifying buy, the prize is a fixed 5% of an unbounded bank, and there is no absolute prize cap or relation between the prize and what the leader paid. On a low-gas L2 ten minutes of full blocks can cost well under a thousand dollars, so the attack becomes profitable at a bank in the low tens of thousands of IMD, which is where a successful launch would put it. This is a design residual the author accepted, recorded here so the judge sees it next to the chain choice (not pinned in this tree). Possible mechanical mitigations, each a design change the requester must approve: cap each round's prize at a multiple of the leader's qualifying buy (bounds the attacker's payoff by their own stake), or at an absolute IMD amount; lengthen the timer as the bank grows; or require a minimum number of blocks between the last qualifying buy and settlement in addition to the time. None of these is free of trade-offs, which is why this is reported as info rather than as a defect with a fix.","line":522,"path":"src/WinGameHook.sol","reproduction":"State, not a failing input: bank B IMD, prize P = 0.05 B. Attacker makes a qualifying buy of max(8.5, 0.01 B) IMD and then submits enough gas-burning transactions to fill every block for 600 s (about 300 blocks at a 2 s block time). No competing buy can land, settle() is called by the attacker at deadline, and the attacker receives P. Profitable whenever P exceeds the gas spent, i.e. B > 20 x (cost of 300 full blocks), independent of anything the hook does. Cost and feasibility depend entirely on the selected chain's block gas limit, base-fee dynamics and ordering policy, which this repository does not pin.","severity":"info","snippet":"        uint64 newDeadline = uint64(block.timestamp + ROUND_DURATION);","title":"Block stuffing has no mechanical defence; the prize is 5% of the bank with no absolute cap, so once the bank exceeds about 20x the cost of ten minutes of full blocks on the selected chain a stuffing b"}],"hash":"0a624d68a224f05c52185a6312a2d0b1cc29e2e8773e5dd8c65eed2ae1077176","nodeId":"c6e47841-d732-485c-b087-e9239ace7ceb","outcome":"completed","summary":"The findings file is in place at the repository root (git excludes it by design via `.git/info/exclude`), the scratch tests are removed, and the original 134-test suite still passes with the tree otherwise untouched.\n\n## Review complete\n\nI audited the WIN game economics, invariants and flow gaps in `src/WinGameHook.sol` and `src/WinToken.sol` against the Pashov Economic Security, Invariant and Flow Gap guides, plus the v4-security and x-ray checklists. The results are in `.imd-findings.json`: four findings and fifteen coverage rows, all nine verifier-listed entry points answered.\n\n**Findings**\n\n1. **Medium.** A buy that misses the qualifying minimum is silently executed as a plain trade and still pays the full fee. Because each qualifying buy raises the threshold by 5%, a bot that front-runs an at-minimum bid with its own at-minimum bid voids the victim's bid while keeping the lead. Reproduced: victim pays 0.26775 IMD on an 8.925 IMD bid, gets no lead, no error. This is the leader-sandwich case the brief asked to defend; suggested fix is an opt-in \"qualify or revert\" flag in hookData.\n2. **Medium.** The prize-linked part of the minimum drifts with every trade, including sells, which the brief says never touch the game. Reproduced: a 0.1 IMD sell paying 0.003 IMD of fee moved the minimum enough to void an exact 11.34 IMD bid. Suggested fix is snapshotting the base at round start.\n3. **Low.** While a round is expired but unsettled, the website views describe the dead round. Reproduced: shown minimum 10.33 IMD versus an actual 8.5 IMD floor, and shown prize 5.4 IMD versus an actual next-round prize of 1.09 IMD.\n4. **Info.** Block stuffing has no mechanical defence. The prize is 5% of an uncapped bank, so once the bank exceeds about 20 times the cost of ten minutes of full blocks on the chosen chain, stuffing wins every round. The author documented this residual; I recorded it beside the unpinned chain choice.\n\n**What held.** Fee math on all four swap shapes in both pool orientations, claim-based fee custody and delta composition, the 90/10 split, round-1 floor and 20% prize, lazy close versus `settle()` parity, reentrancy and unlock-nesting guards, conservation of hook claims, and the token's three entry points. No critical or high defects were found, so no proof tests were attached.\n\n**Not reached.** No fork run (no RPC or `network.json` pinned), no live IMD token behaviour on the selected chain, and no standalone invariant campaign beyond the existing fuzz test.","treeHash":null,"usage":{"cachedInputTokens":2116951,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":60240,"runtime":"claude","turns":36,"wallClockMs":904042}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d20c1a95c50699ea","findings":[],"hash":"150002576eb29a6ae266cb14bb70737be15cde78bc48b1980bfc5320518c375d","nodeId":"698439c6-8504-4262-be28-abe6cf604955","outcome":"failed","summary":"could not restore accepted work: dependency did not apply: fatal: transport 'file' not allowed\n","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":305}},{"artifacts":[],"attempt":3,"bundleHash":"11caf02eb533db16efc301a2e7249fd46ff99d3ed69b0c17e1d2992ba348d50a","device":"4dd67dae195771b6","findings":[],"hash":"7268cb1a98069e9fa027364da6af0e5df99d35faca1f4fff58e814af355b2072","nodeId":"698439c6-8504-4262-be28-abe6cf604955","outcome":"completed","summary":"The revision is complete. All ten findings were reproduced on the starting tree before anything was changed, and every one has an answer in `.imd-responses.json`.\n\n**Verification on the result**\n\n| Check | Result |\n| --- | --- |\n| `forge build` (solc 0.8.26) | passes |\n| `forge test` | 179 project tests pass, plus the reviewer's proof passes under `test/scratch/` |\n| `forge fmt --check` | passes |\n| Protected paths | none touched |\n\n**Fixed in `src/WinGameHook.sol`**\n\n- **Starting price (high, proof).** `beforeInitialize` now derives the expected tick from the ordering it observes (−129,000 when WIN is currency0, +129,000 otherwise) and reverts `WrongStartingPrice` outside ±300 ticks. The proof fails on the old tree and passes now. Both exact prices are exposed by `launchSqrtPriceX96(bool)` and listed in the README.\n- **Voided challenges (medium).** `hookData` accepts `abi.encode(buyer, mustLead)`. A flagged buy that falls short at execution reverts `NotQualifying`, so the challenger pays gas and no fee. All three reviewer cases are tested. Unflagged buys keep the brief's literal rule.\n- **Identity (medium).** With no `hookData` the hook now asks the router for `msgSender()` before falling back to `tx.origin`, so a smart account through the Universal Router is credited correctly. The remaining residual (router with neither) is tested by name and documented.\n- **Unpayable winner (low).** `settle()` always closes the round and pushes the prize best-effort; on failure it emits `PrizeDeferred` and leaves the prize claimable. A payout attempted inside an open manager lock is refused up front with `ManagerUnlocked`.\n- **Stale views (low).** `minimumBuy()`, `nextPrize()` and `roundNumber()` project the post-close state once the deadline has passed, matching what `afterSwap` enforces. `pendingPrize()` and new `gameState()` fields describe the closing round.\n- **Decimals (low).** The paired currency must report 18 decimals or initialization reverts.\n\n**Disputed, with reasoning recorded**\n\n- **Stranger initialization.** Both proposed hook-level checks can kill the real launch under a factory flow this tree cannot see (CREATE2 helper, position-manager initialization, no manifest placeholder for the pair). The reference script now initializes in the same call, and the README states the atomicity requirement.\n- **Sell-back lead, open team claim, block stuffing.** Each is the brief's rule as written. The README claim that a sell-back \"gains nothing\" was wrong and is corrected with the measured cost (6.47% of the minimum). The mechanical alternatives are listed for the requester in `docs/REVIEW.md` section E.\n\n**For the deployer:** the manifest's single `initialPrice` is only right when the deployed WIN address sorts below IMD. The hook now refuses the mismatch loudly, so the manifest node must write the price after the token address is known or pin the token's salt.","treeHash":"639aa277efd600d8c1847ea6744b3c3583a4ce85","usage":{"cachedInputTokens":7897963,"inputTokens":1410,"model":"claude-fable-5-1","outputTokens":107766,"runtime":"claude","turns":93,"wallClockMs":1424513}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee2cc12e7dfe571a","findings":[],"hash":"728a5583e111db579d0d9630fff08c37dbb23b0ae890b4f601962bfa66da9cd8","nodeId":"1ce4f137-e226-41c3-89ef-4a79151b56d1","outcome":"completed","summary":"Review settled. Summary of this round:\n\n**Previously reported, now confirmed fixed (each re-run with my own scratch reproductions, not just the author's tests):**\n1. **Orientation/price** — my attached proof `Proof_a30bd48ea51f` now passes: `beforeInitialize` converts the supplied price to a tick and reverts `WrongStartingPrice` unless it is within ±300 of ∓129,000 for the observed ordering (`src/WinGameHook.sol:283-287`).\n2. **Short buy silently executed** — `abi.encode(buyer, true)` hookData reverts `NotQualifying` inside `afterSwap`; verified fee-free reverts for the dust-buy, leader-requalify-and-sell-back, and exact-output-undercut triggers.\n3. **tx.origin fallback** — `_resolveTrader` now probes the router's `msgSender()` before `tx.origin`; a 4337-style trade (`vm.prank(bob, bundler)`) through a msgSender router credits bob.\n4. **settle() reverting for an unpayable leader** — `settle()` finalizes unconditionally and defers via `PrizeDeferred`; round closes, accounting holds, `claimPrize` works once unblocked.\n5. **Stale views after expiry** — `minimumBuy()/nextPrize()/roundNumber()` project the post-close state; a flagged buy of shown−1 wei reverts, shown value leads, round-1 pending shows 5% terms.\n6. **Decimals** — paired currency must report 18 decimals at initialization.\n7. **Fork suite** — rehearsal bodies now run offline on the vendored PoolManager.\n\n**Disputed by the author, dispute accepted (not reopened):** stranger binding under non-atomic deployment (factory is atomic; deploy script can now initialize in the same call), buy-and-sell-back cost, open `claimTeamFees()`, block stuffing — all are the brief's rules or requester decisions, now documented accurately.\n\n**Fresh pass over the changed lines** (try/catch payout, `ManagerUnlocked` guard, 64-byte hookData parsing, msgSender probe, view projection): nothing new reproducible. Protected floor suites pass with flags 0x20CC (3/3 hook, 7/7 token); full suite 240 passed, 0 failed, 2 network-gated skips.\n\n`.imd-findings.json` holds an empty findings list and 13 coverage rows (all 9 entry points `holds`, plus invariants and residuals). No tree files were modified; scratch tests live only under `test/scratch/`.","treeHash":null,"usage":{"cachedInputTokens":2873935,"inputTokens":48,"model":"claude-fable-5-1","outputTokens":27017,"runtime":"claude","turns":25,"wallClockMs":422625}},{"artifacts":[],"attempt":1,"bundleHash":"8d4bb4c21032dfddb75187d04978cc365b11f6123cbf313325f60154b9a693bd","device":"357c46e3781993d4","findings":[],"hash":"730aa0bf0c35096134327dd9c402417dbc13f2f28b518c45809e7df5f8b09186","nodeId":"698439c6-8504-4262-be28-abe6cf604955","outcome":"completed","summary":"Delivered and committed on `main` (commit `99b1299`). Clean rebuild, all 134 tests, and `forge fmt --check` pass with solc 0.8.26 pinned; the pinned IdentityMD floor suites also pass against this exact creation code.\n\n**What was built**\n\n- `src/WinToken.sol`: plain fixed-supply ERC-20, 10^27 units minted to the deployer, no owner or mint.\n- `src/WinGameHook.sol`: the whole game. Fee in IMD on all four swap shapes (exact-in/out buy and sell), 50% to 3% linear decay over 30 minutes, 90/10 bank/team split, rounds with a 10-minute timer, 3-hour floor and 20% prize for round 1, 5% after, minimum buy of max(8.5 IMD, 20% of next prize) rising 5% per qualifying buy and resetting per round, permissionless `settle()`, website views and a one-call `gameState()`.\n- `src/HookFlags.sol` and `test/mocks/MockERC20.sol` at the paths the protected floor tests import.\n- `script/DeployWin.s.sol`: reference deployment whose `deploy(Config)` the tests call directly.\n- `README.md` (design, assumptions, launch parameters, operations) and `docs/REVIEW.md` (adversarial review with findings and open items).\n- Vendored forge-std, v4-core sources and solmate `Owned.sol` as plain files, no submodules.\n\n**Key design decisions**\n\n- Fees are minted as ERC-6909 claims, so the first buy on a WIN-only pool with an empty PoolManager works; claims are redeemed via the hook's own unlock on payouts. This avoids the known `take`-in-afterSwap failure.\n- Buy after the deadline closes the expired round for its real leader before anything else, so post-expiry sniping cannot steal a round. Block stuffing and sequencer ordering are analysed and quantified in the README rather than claimed solved.\n- Buyer identity comes from `hookData`, with a `tx.origin` fallback for plain wallets, and the trade-offs for smart accounts and relayers are documented.\n- The linear fee curve was kept as specified; the README explains why the alternatives are worse against snipers.\n- Partial fills on the specified IMD side revert instead of over-charging.\n\n**Caveats to be aware of**\n\n- No `network.json` was pinned, so the chain, PoolManager and IMD address are deployment parameters. The tests deploy their own PoolManager and run unchanged under a fork URL, but no fork run happened here because the verifier is offline.\n- The review in `docs/REVIEW.md` is a self-review structured for an independent contributor; it is not the independent review the brief requires, and it lists what that reviewer must close before funds are at stake.\n- The launch manifest is written by the separate manifest node; the README section 9 holds everything it needs.","treeHash":"1d80e5fdc52addd62e41878a434652a148f00649","usage":{"cachedInputTokens":4333831,"inputTokens":962,"model":"claude-fable-5-1","outputTokens":133747,"runtime":"claude","turns":54,"wallClockMs":1679812}},{"artifacts":[],"attempt":1,"bundleHash":"d0906e8b57e0152bda5f1e03d679e85563926d2361efe2fb30fc8b8e4ec1e5a0","device":"e12f98dda6acc55f","findings":[],"hash":"878a141502937229edf1c970a3d010bd7c9fb67bc4b6c36398331fd4fc6be510","nodeId":"d0002acd-4a5c-4735-a79d-977abaea2d6e","outcome":"completed","summary":"Everything is in place. Summary of this revision:\n\n## What I found when reproducing\n\nThe advisory finding reproduces (fork suite skips without `WIN_FORK_RPC_URL`), but the tree was not in the state it described: `forge test` on the merged tree **failed 7 of my own tests**. The other contributor's accepted revision of `src/WinGameHook.sol` changed the view semantics while an expired round waits for `settle()` — `roundNumber()`, `nextPrize()`, `minimumBuy()` now describe the *next* round, and the closing round moved to a new `pendingPrize()` and `gameState().pending*` fields — and added a `mustLead` hookData flag. My assertions encoded the old semantics; the hook's new behaviour is documented and consistent, so these were stale tests, not a defect.\n\n## Changes (all under `test/`)\n\n- **`WinGameHook.Adversarial.t.sol`** — two tests now read the expiring round's prize from `pendingPrize()` and additionally pin `nextPrize() == (bank − pending) × 5%`.\n- **`invariant/WinGameHook.Invariant.t.sol`** — round-state, minimum-buy and website-view checks now cover both the running and the expired-pending case with exact formulas; `minimumBuy()` is cross-checked against the handler's independent model; the new `pending*` view fields are asserted.\n- **`invariant/WinGameHandler.sol`** — drives the new must-lead path: a refused must-lead buy must leave fee, bank, round and lazy-close untouched with the exact `NotQualifying` revert; a landed one must lead; a flagged sell must be refused with `MustLeadOnlyOnBuys`. Coverage counters added to the \"harness is not vacuous\" script.\n- **`WinGameHook.Fork.t.sol`** (the finding) — the four rehearsal tests now run unconditionally on the vendored PoolManager as a control (`WinGameHookRehearsalTest`/`Flipped`), with the fork runners reusing the same bodies and still skipping honestly. Since this machine had network, I ran the fork variant against the real **Base, Arbitrum and Ethereum** PoolManagers (stand-in IMD, both orientations, 8/8 each) and recorded blocks in the file header.\n\n## Results\n\n- `forge test`: **239 passed, 0 failed, 2 skipped** (the two network-gated fork runners); invariants 256 runs × 50 depth × 4 campaigns, zero violations.\n- `.imd-responses.json` written with verdict `fixed` and the full detail. No `.imd-findings.json`: nothing I tested reveals a defect in the revised hook.\n\nWhat remains open and cannot be closed offline — stated in the response rather than papered over: the selected chain's real IMD (decimals/transfer semantics) and WIN's ordering against its address; `test_rehearsal_imdHasEighteenDecimals` is the gate for that once a chain is pinned.","treeHash":"1298331586049940ca25dcdea26379fe1c8576ee","usage":{"cachedInputTokens":5906367,"inputTokens":99,"model":"claude-fable-5-1","outputTokens":45563,"runtime":"claude","turns":52,"wallClockMs":658446}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"e2a4a53638df3fc6","findings":[],"hash":"87c97a8c1b843015fd8830cc6449e052da92dd8eb7ba164f50813d35d36c0693","nodeId":"698439c6-8504-4262-be28-abe6cf604955","outcome":"failed","summary":"could not restore accepted work: dependency did not apply: fatal: transport 'file' not allowed\n","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":374}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f20ec62b0213db3f","findings":[{"citation":"resolved","description":"afterSwap compares `gross` with `minimumBuy()` read at execution time and, when the buy falls short by even 1 wei, lets the swap go through as an ordinary buy. The buyer has no way to say 'revert unless I become leader'. Two numeric seams make the miss easy to cause. (1) Exact-output buys: `gross = poolIn + fee` (lines 303-305) is the unspecified side, so it depends on the pool price. A sell placed just before the buy lowers the price, the same WIN amount costs less IMD, and `gross` drops under the minimum. README section 5 states the opposite ('Qualification is measured in gross IMD, not in WIN received, so price manipulation cannot disqualify a buy'); that holds only for exact-input buys. (2) Exact-input buys: whenever the minimum is prize-linked (bank > 212.5 IMD in round 1, > 850 IMD later) every fee from any trade raises `minimumBuy()`, so a buy for exactly the displayed minimum misses after any intervening swap; below those thresholds an intervening qualifying buy raises it 5%. The sell used in (1) is not a qualifying buy, so the incumbent leader keeps both the lead and the original deadline. The challenger still pays the hook fee on a purchase they only made to take the lead, and pays it again to exit. Preconditions: for the deliberate version the leader must be able to order a transaction ahead of the challenger's (public mempool or sequencer ordering, which the brief asks to be considered); the accidental version of (2) needs only one unrelated trade between the website's read and the buy. Minimal fix that keeps the rules: let hookData carry an opt-in flag (for example a 64-byte `abi.encode(buyer, mustLead)`) and revert with `NotQualifying(minimum, gross)` when a flagged buy does not take the lead; correct the README claim for exact-output buys.","line":326,"path":"src/WinGameHook.sol","reproduction":"Fixture state (test/utils/WinGameFixture.sol, WIN = currency0, LP fee 3000, 900M WIN single-sided). Case 1, exact-output sandwich: at launch alice `buyExactIn(500 ether)` (fee 50%, bank = 225 IMD, alice leads round 1); warp to launch+30min. `minimumBuy()` = 9.45 IMD. Carol prepares `buyExactOut(winOut)` where winOut is the WIN that 1.01 x minimum = 9.5445 IMD buys right now. Control: sent alone, it costs gross 9.5445 IMD and carol becomes leader. Attack: alice first `sellExactIn(5% of her WIN)` (receives 13.3066 IMD); carol's identical buy now costs gross 9.440369110833086836 IMD against a minimum of 9.465556413737749345 IMD, so `hook.leader()` is still alice and `deadline` is unchanged; alice buys the WIN back. Alice's total net cost: 1.025738823549819979 IMD. Expected: carol's buy either takes the lead or reverts. Actual: it executes, carol pays 9.44 IMD (0.283 IMD fee) and is not leader. Case 2, exact-input drift: at launch alice `buyExactIn(2000 ether)` (bank 900 IMD); warp to launch+30min; `minimumBuy()` = 37.800000000000000000 IMD. Bob sells 1000 WIN (a dust trade); `minimumBuy()` = 37.800005899742884836 IMD. Carol `buyExactIn(37.8 ether)` as displayed a moment earlier: she pays 37.8 IMD and `hook.leader() == carol` is false.","severity":"medium","snippet":"if (gross >= minimum) _qualify(trader, gross);","title":"A buy aimed at the lead silently stops qualifying when the minimum or the price moves; the leader can sandwich a challenger for ~1 IMD and keep the lead without resetting their own timer"},{"citation":"resolved","description":"Between a round's deadline and its settlement, `minimumBuy()` (lines 391-395) still uses the expired round's `escalator`, its prize share (`_prizeBps()` = 20% while round 1 is unsettled) and the bank before the prize leaves it. A buy in that window is not judged against that number: afterSwap first runs `_finalizeIfExpired()` (line 323), which pays the prize out of the bank, resets `escalator` to 1 and moves to the 5% prize share, and only then reads the minimum (line 324). The view's NatSpec says 'Gross IMD (fee included) a buy must spend right now to become the leader', and the brief lists 'current minimum buy' among the website views. The window lasts until someone calls `settle()` or buys, so it can be long. Effect: the website shows a minimum several times higher than the real one, honest users either overspend or stay away, and anyone who computes the post-settlement value opens the next round at the true (much lower) price. `gameState().minimumBuy` has the same error. Fix: when `roundActive && block.timestamp >= deadline`, compute the views as if the round were already finalized (bank minus the pending prize, PRIZE_BPS, escalator = WAD), the same order afterSwap uses.","line":391,"path":"src/WinGameHook.sol","reproduction":"Fixture state, WIN = currency0. At launch alice `buyExactIn(2000 ether)`; bob then makes 10 buys of exactly `minimumBuy()` each. Bank = 1138.290247383875546020 IMD, escalator = 1.05^11. Warp to launch + 3h + 1s (`settleable()` is true, nobody has settled). `hook.minimumBuy()` returns 77.874504442423895050 IMD. Expected: the amount a buy must spend now to lead, which is 20% x 5% x (1138.29 - 227.66) = 9.106321979071004368 IMD. Proof it is the real threshold: carol `buyExactIn(9.106321979071004368 ether)` in that state; round 1 is closed for bob, `hook.leader() == carol` and `roundNumber() == 2`.","severity":"low","snippet":"function minimumBuy() public view returns (uint256) {","title":"minimumBuy() and gameState() report the expired round's escalated minimum, not the minimum the next buy is actually held to (8.55x too high in the example)"},{"citation":"resolved","description":"`MIN_BUY_FLOOR = 8.5 ether` is compared with raw IMD amounts (`gross >= minimum`), so it means 8.5 IMD only if the paired currency has 18 decimals. `beforeInitialize` (lines 223-246) takes whatever currency is paired with WIN as `imd` and never reads `decimals()`. This tree pins no chain and no IMD address (README section 7 says they are deployment parameters) and the assumptions list covers fee-on-transfer, rebasing and blocklists but not decimals; the tests use an 18-decimal mock. If the launch chain's IMD has fewer decimals, the floor is unreachable: no buy ever qualifies, no round ever starts, and because the bank can only leave through a round prize and there is no owner, 90% of every trading fee is locked in the hook permanently. With more than 18 decimals the floor becomes dust and the anti-spam guarantee disappears. I could not verify the real IMD's decimals from the supplied inputs, hence low; the check is one line. Fix: in `beforeInitialize`, require `decimals() == 18` on the paired ERC-20 (revert otherwise), or store a floor of `85 * 10**decimals / 10` at initialization.","line":65,"path":"src/WinGameHook.sol","reproduction":"Initialize the pool WIN / X where X is an ERC-20 with `decimals() == 6` (the hook accepts it: `beforeInitialize` only checks the LP fee and that one side is WIN). `minimumBuy()` returns 8_500_000_000_000_000_000 raw units = 8,500,000,000,000 whole X. A buy of 100 X (100_000_000 raw) pays its fee into `bank` but `gross >= minimum` is false, so `roundActive` stays false forever; `settle()` reverts `NoActiveRound` and no function can move `bank`. Expected: floor of 8.5 X (8_500_000 raw) or a revert at initialization. Actual: initialization succeeds and the bank is unreachable.","severity":"low","snippet":"uint256 public constant MIN_BUY_FLOOR = 8.5 ether;","title":"The 8.5 IMD floor is hard-coded for an 18-decimal pair and beforeInitialize never checks the paired currency's decimals"},{"citation":"resolved","description":"The qualifying amount is the gross IMD of one swap (`gross = specified`, or `poolIn + fee` for exact-output buys). Nothing ties the lead to keeping the position, so a buyer can sell the WIN straight back, in the same transaction if they wish. What they actually pay is the hook fee twice plus the LP fee twice, about 6.5% of the minimum once the anti-snipe window is over, and 90% of the hook fees land in the bank they are competing for. The brief's floor of '8.5 IMD (about $100)' meant to block spam and make 'endless resets more and more expensive' is therefore about 0.55 IMD (~$6.5) of real cost per reset, and each 5% step raises that real cost by only 5% of 6.5%. Rounds still end (the cost stays exponential), but the escalation bites roughly 15x later than README/REVIEW A4 computes ('after ~34 overtakes the minimum exceeds the prize'): the break-even moves to roughly 89 overtakes. README section 5 ('Flash-loan buy ... gains nothing') misses that it gains the lead. This is the requested rule, so the fix is a scope decision rather than a patch: either accept and document the real cost (and size the floor for it), or make part of a qualifying buy non-recoverable (for example a surcharge on qualifying buys paid into the bank).","line":294,"path":"src/WinGameHook.sol","reproduction":"Fixture state, WIN = currency0. Alice `buyExactIn(300 ether)` at launch; warp to launch + 3h + 1s; `settle()` (round 1 closed, fee now 3%). `minimumBuy()` = 8.5 IMD. Sniper: `d = buyExactIn(sniper, 8.5 ether)` then `sellExactIn(sniper, WIN received)`. Result: `hook.leader() == sniper`, sniper holds 0 WIN, net IMD spent = 0.550182942845501953. Repeating buy-at-minimum-then-sell 20 more times moves the minimum to 23.680682018413949622 IMD at a total net cost of 19.099509511337750896 IMD, against nominal minimums summing to about 295 IMD. Expected per the brief: each reset costs the buyer the minimum. Actual: about 6.5% of it.","severity":"low","snippet":"gross = specified;","title":"The minimum buy is measured in swap volume, which is recoverable: taking the lead costs about 6.5% of the stated minimum (0.55 IMD instead of 8.5)"},{"citation":"resolved","description":"Three numbers for the later manifest step. (a) `sqrt(2.5e-6) * 2^96` is 125270724187523965593206900 (1.25271e26), not 1.2536e26; the quoted value corresponds to a price of 2.5036e-6. (b) The exact tick for 2.5e-6 is -128,998.65; the fixture and README round it to -129,000 (sqrtPriceX96 125262255113908064987203231, market cap 2,499.66 IMD). `pool.initialPrice` is a sqrtPriceX96 and need not sit on a tick, so the exact value in (a) can be used. (c) The WIN-only position starts one spacing above, at -128,940, so the first buy crosses an empty 60-tick gap and the first WIN is sold at 1.0001^-128940 = 2.5147e-6 IMD, a starting market cap of 2,514.70 IMD (0.59% over the brief's 2,500). Initializing at the lower tick's price (position lower bound = current price, still zero IMD required) removes the gap. For WIN = currency1 the mirror values are sqrt(400000) * 2^96 = 50108289675009586237282760313921 and the same gap on the other side. No contract change is needed.","line":300,"path":"README.md","reproduction":"python: Decimal('2.5e-6').sqrt() * 2**96 = 1.252707e26 (README says 1.2536e26). 1.0001**-129000 * 1e9 = 2499.66; 1.0001**-128940 * 1e9 = 2514.70. In the fixture, the pool tick after initialization is -129,000 and liquidity begins at -128,940, so the first executed price is the latter.","severity":"info","snippet":"spacing **−129,000**, `sqrtPriceX96 = sqrt(2.5e-6) · 2^96 ≈ 1.2536e26`, and the WIN-only position","title":"Launch price figures in the README are off: sqrtPriceX96 is misquoted and the first WIN trades at a 2,514.7 IMD market cap, not 2,500"}],"hash":"93d3191e10bddfd2d2d6804e383735a8e308a4b88f39ca7b04ac53390cad9231","nodeId":"31324a8c-6cb8-48eb-b63b-3bd8c92c7715","outcome":"completed","summary":"The audit is finished and `.imd-findings.json` is written: five findings (one medium, three low, one info) and a coverage row for all nine entry points. I found no critical or high defect in the math, so no finding carries a proof test; each has a reproduction with exact inputs and the numbers I measured in scratch tests, which I then deleted. No tracked file was changed.\n\n## Findings\n\n1. **Medium — a buy aimed at the lead can silently miss it** (`src/WinGameHook.sol:326`). The buyer cannot ask for \"revert unless I become leader\", so a buy that falls 1 wei short still executes as an ordinary buy.\n   - **Exact-output buys:** the qualifying amount depends on the pool price. The leader sells just before the challenger's buy, the challenger's cost drops to 9.4404 IMD against a 9.4656 minimum, and the leader keeps both the lead and the original deadline for a net 1.03 IMD. The README claims price manipulation cannot disqualify a buy; that only holds for exact-input.\n   - **Exact-input buys:** once the minimum is prize-linked, any intervening fee raises it. A dust sell moved it from 37.8 to 37.8000059 IMD and a 37.8 IMD buy no longer took the lead.\n   - The deliberate version needs the leader to order a transaction ahead of the challenger's; the accidental one needs only one unrelated trade.\n2. **Low — `minimumBuy()` and `gameState()` are wrong after a round expires and before it is settled** (`:391`). The view showed 77.87 IMD while the next buy was actually held to 9.106 IMD, because the swap path closes the round before reading the minimum and the view does not.\n3. **Low — the 8.5 IMD floor assumes an 18-decimal pair** (`:65`). `beforeInitialize` never checks the paired currency's decimals. With a 6-decimal IMD no buy could ever qualify and the bank would be locked for good. I could not verify the real IMD's decimals from the inputs, and this case is argued from the arithmetic, not run.\n4. **Low — taking the lead costs about 6.5% of the stated minimum** (`:294`). Buying 8.5 IMD and selling straight back left the buyer as leader for a net 0.55 IMD. Rounds still end, but the escalation bites much later than the README's \"~34 overtakes\" (roughly 89). This is the requested rule, so any fix is a scope decision.\n5. **Info — launch price figures in the README are off** (`README.md:300`). The quoted `sqrtPriceX96` is wrong (1.2536e26; the exact value is 1.25271e26), and the 60-tick gap below the position means the first WIN trades at a 2,514.7 IMD market cap, not 2,500.\n\n## Coverage\n\n- **Holds:** `settle`, `claimPrize`, `claimTeamFees`, `unlockCallback` and the three `WinToken` functions.\n- **Carries findings:** `afterSwap` (1 and 4) and `beforeInitialize` (3).\n- **Fee and accounting invariants:** a 256-run fuzz of mixed swaps with tight price limits and partial fills kept the hook's claims equal to bank + team share + unclaimed prizes, and the fee within 1 wei of the rate, for all four swap shapes.\n- **Checked by reading:** the decay bounds, the escalator cap and the round-1 deadline floor.\n- **Not reached:** nothing ran against a fork or a real IMD token; the tree pins no chain or IMD address. Access-control and wider game-economics questions outside the three math guides were only looked at in passing.","treeHash":null,"usage":{"cachedInputTokens":1809197,"inputTokens":33,"model":"claude-fable-5-1","outputTokens":66639,"runtime":"claude","turns":18,"wallClockMs":757922}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa5c50e7abe46571","findings":[{"citation":"resolved","description":"Trust gap (access x economics x asymmetry). Whether a buy takes the lead is decided inside afterSwap against minimumBuy() read at execution time. minimumBuy() = max(8.5 IMD, 20% of nextPrize()) * escalator, and nextPrize() follows the live `bank`, which every swap by anyone moves through _accrue. Once 20% of the prize exceeds the 8.5 IMD floor (bank > 850 IMD in later rounds, > 212.5 IMD in round 1) any trade that lands before the challenger raises the minimum above the amount the website showed. The challenger's swap does not revert: it executes, pays the full hook fee, and simply does not make them leader; the timer is not reset. There is no input (hookData flag or otherwise) by which a buyer can say 'revert unless I take the lead'. The actor who profits is the current leader: it front-runs (public mempool / priority ordering), or just lands in the same block ahead of, every challenger with a non-qualifying dust buy and keeps the lead until the timer runs out. In the floor regime the same is done with the leader's own qualifying buy sold straight back (+5% escalator for ~6.5% of the minimum), which beats any margin under 5%. README section 5 states the opposite ('Sandwiching the leader ... No benefit ... Front-running leaves the victim as leader'), and A5 in docs/REVIEW.md leaves the margin to the website. Guard gap: sells and non-qualifying buys are meant to 'change nothing except the bank', yet through the bank they move the qualification threshold of everybody else's pending buy. Fix that keeps the rules: (a) accept an optional flag in hookData that makes afterSwap revert when the buy does not qualify, so a voided challenge costs gas and not a fee-bearing position; and (b) take the prize-linked part of the minimum from a bank snapshot fixed at the last qualifying buy / round start, so only a qualifying buy (a visible 5% step that costs the leader a real minimum buy) can move it. (b) changes 'upcoming prize' from live to snapshot and needs the requester's decision.","line":324,"path":"src/WinGameHook.sol","reproduction":"With the repo fixture (test/utils/WinGameFixture.sol): 1) at launch carol does buyExactIn(carol, 20_000 ether) (50% fee), warp to launch+3h, hook.settle() -> bank = 7,200 IMD. 2) buyExactIn(alice, hook.minimumBuy()) -> alice leads round 2. 3) shown = hook.minimumBuy() = 75.620412 IMD (what the site displays to bob). 4) alice: buyExactIn(alice, 0.01 ether) (costs her 0.01 IMD gross, 0.0003 IMD fee) -> hook.minimumBuy() = 75.620414835 IMD. 5) buyExactIn(bob, shown). Expected (README s5): bob is the leader and the deadline is now+10min. Actual: swap succeeds, bob pays 75.620412 IMD including a 2.27 IMD fee (2.0418 IMD to the bank), hook.leader() is still alice and hook.deadline() is unchanged; alice collects the round at expiry. Floor-regime variant: bank < 850 IMD, alice leads round 2, shown = 8.925 IMD, bob sends shown*1.04 = 9.282 IMD; alice first does buyExactIn(alice, 8.925 ether) and sells the WIN back (net cost 0.5777 IMD), the minimum becomes 9.37125 IMD and bob's 9.282 IMD buy executes without taking the lead.","severity":"medium","snippet":"            uint256 minimum = minimumBuy();\n            _accrue(fee);\n            if (gross >= minimum) _qualify(trader, gross);","title":"The leader can void any challenger's buy for dust: the minimum is re-read from the live bank inside the swap and the buyer cannot require qualification"},{"citation":"resolved","description":"Buyer identity is the only 'permission' in the game: it decides who is paid. _resolveTrader takes it from hookData and otherwise from tx.origin. For any buy whose transaction is signed by someone other than the buyer (ERC-4337 smart accounts through a bundler, sponsored EIP-7702 accounts, relayed/meta transactions, Safe executions sent by another owner) and that does not come from the project website, hookData is empty and the lead - and later the whole prize, pushed by settle() or claimPrize() - goes to the bundler/relayer EOA. The buyer paid the minimum and the fee and gets nothing; the bundler can also knowingly keep the prize. docs/REVIEW.md C4 accepts this on the ground that the only alternative is an admin-kept router allowlist. That is not the only alternative: the `sender` argument of afterSwap (discarded at line 273) is the router, and Uniswap's Universal Router and the v4-periphery routers expose msgSender() precisely so hooks can read the account that opened the lock. A try/staticcall of msgSender() on `sender`, used when hookData is absent and before falling back to tx.origin, needs no owner and no list, and is no weaker than hookData (a router that lies can only gift the lead, the same as naming someone in hookData). tx.origin then remains only for routers without msgSender().","line":605,"path":"src/WinGameHook.sol","reproduction":"Fixture, after round 1 is settled: min = hook.minimumBuy(); vm.prank(bob, bundler) (bob's account pays, `bundler` is tx.origin, as with a 4337 UserOperation); swapRouter.swap(key, SwapParams(!winIsCurrency0, -int256(min), limit), TestSettings(false,false), \"\") with empty hookData. Expected: bob (the account that paid 8.5 IMD) is the leader. Actual: hook.leader() == bundler; after warp +10 min, hook.settle() transfers the prize (0.020655 IMD in this state, 5% of the bank in general) to `bundler`, and bob has no claim (unclaimedPrize(bob) == 0).","severity":"medium","snippet":"        if (trader == address(0)) trader = tx.origin;","title":"tx.origin fallback pays the prize to the bundler/relayer whenever hookData is absent; the router's own msgSender() is never consulted"},{"citation":"resolved","description":"Asymmetry between the two ways a round closes. The lazy path (_finalizeIfExpired in afterSwap) only credits unclaimedPrize[leader]; settle() credits it and then, in the same call, runs _payPrize -> poolManager.take(imd, leader, amount). If that transfer reverts the whole settle() reverts and the round stays open (roundActive, settleable() == true) until some later buy closes it lazily. The leader is whatever address the buyer wrote into hookData, so any buyer can choose one that the IMD token refuses to credit (a blocklisted address, the token contract itself on tokens that forbid it; with the native pair that beforeInitialize also accepts, any contract without receive()). README section 7 says the opposite: 'a blocklisted winner could not be paid (their own claimPrize would revert; nothing else would)'. Impact is liveness only: 'anyone can call settle()' is broken for that round, the website's settle button reverts, and the round can be closed only by a buy; the prize then sits in unclaimedPrize forever. Unreachable if the launch chain's IMD never reverts on transfer to a non-zero address, which the README lists as unverified. Fix: in settle() finalize unconditionally and make the transfer best-effort (try the payout, leave the amount in unclaimedPrize on failure), or drop the push and let the winner use claimPrize.","line":343,"path":"src/WinGameHook.sol","reproduction":"Fixture with an IMD mock whose _transfer has require(to != address(this)) (or a blocklist): finish round 1; buyExactIn(bob, hook.minimumBuy(), abi.encode(address(imd))) -> leader == address(imd); warp +10 min; hook.settle() from any account. Expected: round 2 closes, winner recorded, prize left claimable. Actual: settle() reverts (the take inside unlockCallback reverts), hook.settleable() stays true and hook.roundActive() stays true on every further settle() call.","severity":"low","snippet":"        address winner = leader;\n        _finalize();\n        _payPrize(winner);","title":"settle() closes the round and pushes the prize in one step, so a leader that cannot be paid makes settle() revert for everyone"},{"citation":"resolved","description":"Unprotected initialization. The single-use binding (poolInitialized, imd, _poolId, launchTime) is written by the first PoolManager.initialize call that names this hook, and PoolManager.initialize is open to everyone and needs no tokens. The only conditions are an LP fee of 500/3000/10000 and WIN on one side: the `sender` argument is ignored, the other currency is accepted whatever it is (it becomes `imd`), and tickSpacing and price are free. The launch factory deploys and initializes in one transaction, which closes the window, but nothing in the hook enforces that and the repository's own reference deployment (script/DeployWin.s.sol run()) deploys token and hook and stops. In any such non-atomic deployment an outsider can (a) bind the hook to WIN/<worthless token>: `imd` is then the attacker's token, the real WIN/IMD pool can never be created with this hook (PoolAlreadyInitialized) and the hook is permanently useless; or (b) initialize the correct WIN/IMD key early: launchTime starts then, so by the time liquidity is added more than 30 minutes later the 50% anti-snipe fee has already decayed to 3% and the first round's 3-hour floor is partly or wholly spent. Fix without adding an owner: record the deployer in the constructor (immutable) and require `sender == deployer` in beforeInitialize, and/or take the paired currency as a constructor argument and require the other currency to equal it.","line":229,"path":"src/WinGameHook.sol","reproduction":"Fixture: win2 = new WinToken(); h2 = deployHook(manager, address(win2)) (what DeployWin.run() leaves on chain). As an unrelated account: manager.initialize(PoolKey(sorted(win2, evilToken), fee 500, tickSpacing 1, hooks h2), sqrtPrice at tick 0). Expected: refused, only the launcher may bind the hook and only to IMD. Actual: succeeds, h2.imd() == evilToken, h2.poolInitialized() == true; the launcher's manager.initialize(PoolKey(sorted(win2, IMD), 3000, 60, h2), price) now reverts with PoolAlreadyInitialized wrapped by the manager. Variant (b): the outsider initializes the real key; warp 31 minutes; add liquidity; h2.currentFeePips() == 30_000 on the first buy instead of 500_000.","severity":"low","snippet":"        if (c0 == winToken) {\n            winIsCurrency0 = true;\n            imd = key.currency1;\n        } else if (c1 == winToken) {\n            winIsCurrency0 = false;\n            imd = key.currency0;\n        } else {\n            revert PoolMustPairWinToken();\n        }","title":"beforeInitialize binds the hook to whichever pool is initialized first: no check of the initializer or of the paired currency"},{"citation":"resolved","description":"View <-> modify asymmetry. afterSwap first runs _finalizeIfExpired() and only then reads minimumBuy(), i.e. on a state where the escalator is back to 1, the prize has left the bank and (after round 1) the prize share has dropped from 20% to 5%. The public view has no such step: between the deadline and the close it still multiplies by the expired round's escalator and uses the expired round's prize share. The comment at lines 320-322 ('so it matches what the website displayed') and the NatSpec ('Gross IMD a buy must spend right now to become the leader') are therefore wrong in exactly the window in which the next round is opened. Users reading the site are told to pay several times the real price or are put off; a bot that reads the real rule opens the next round at the true, lower minimum. Fix: in minimumBuy() (and nextPrize()/roundNumber() where they feed it) evaluate the post-close state when roundActive && block.timestamp >= deadline: escalator = WAD, bank less the pending prize, later-round prize share.","line":391,"path":"src/WinGameHook.sol","reproduction":"Fixture: finish round 1; make 20 qualifying buys in round 2 (alternating alice/bob, each buyExactIn(x, hook.minimumBuy())); warp +10 min without calling settle(). hook.minimumBuy() and gameState().minimumBuy return 22.553030493727571071 IMD (8.5 * 1.05^20). Then buyExactIn(carol, 11.2765 ether), half of the displayed minimum. Expected from the view: not a qualifying buy. Actual: the buy closes round 2 and carol is leader of round 3; the real threshold was 8.5 IMD.","severity":"low","snippet":"    function minimumBuy() public view returns (uint256) {\n        uint256 base = nextPrize() * MIN_BUY_PRIZE_BPS / BPS;\n        if (base < MIN_BUY_FLOOR) base = MIN_BUY_FLOOR;\n        return base * escalator / WAD;\n    }","title":"minimumBuy() and gameState() overstate the minimum while a round is expired but not yet closed"},{"citation":"resolved","description":"Economics x asymmetry seam, reported from the permission side: the buy branch credits the full gross amount towards the minimum, the sell branch has no effect on the game, and nothing ties the two together. A caller can therefore buy the minimum and sell the WIN back in the same transaction (inside a single PoolManager unlock the two deltas net out, so only the two fees have to be funded). The real price of a timer reset is 2 x 3% hook fee + 2 x 0.3% LP fee of the minimum, not the minimum. In later rounds the minimum is 1% of the bank and the prize 5% of the bank, so a reset costs about 0.065% of the bank against a prize 77 times larger, and the 'minimum buy = 20% of the prize' barrier in the brief is in effect a 1.3% barrier. README section 5 says a buy-and-sell-back 'gains nothing'; it gains the lead and a full 10-minute timer. This is what makes the front-run in finding 1 and unattended-hours farming (open a round for 0.065% of the bank, collect 5% if nobody answers in 10 minutes) cheap for bots. No rule-preserving fix removes it completely; options for the requester: void the lead if the leader's address sells during the round, or accept and document the true cost instead of the nominal one.","line":326,"path":"src/WinGameHook.sol","reproduction":"Fixture, after round 1 is settled (fee 3%): min = hook.minimumBuy() = 8.5 IMD. buyExactIn(bob, min); sellExactIn(bob, <all WIN just received>). Expected per README s5: nothing gained. Actual: hook.leader() == bob with deadline = now + 10 min, and bob's IMD balance is down by only 0.550177922494261455 IMD (6.47% of the minimum).","severity":"low","snippet":"            if (gross >= minimum) _qualify(trader, gross);\n        } else {\n            _accrue(fee);\n        }","title":"Taking the lead costs about 6.5% of the displayed minimum and no capital: a buy counts in full even when it is sold back in the same transaction"},{"citation":"resolved","description":"The brief: '10% to the team wallet ... (pull-based, the team claims it)'. The function has no caller check, so in practice the share is pushed to TEAM_WALLET whenever any third party chooses. Funds cannot be redirected; the consequences are that the team does not control the timing of receipt, and that if 0x611F...4511 is a contract wallet not yet deployed on the launch chain the IMD can be forced onto a code-less address before the team is ready, where a team-only claim would have left it safely in the hook. docs/REVIEW.md C7 accepts this; it is recorded here because it departs from the letter of the brief. Fix if the letter matters: require(msg.sender == TEAM_WALLET) - a fixed-address check is not an owner power.","line":356,"path":"src/WinGameHook.sol","reproduction":"After any fee-bearing swap (teamOwed > 0): vm.prank(address(0xBEEF)); hook.claimTeamFees(). Expected under 'the team claims it': revert for a caller that is not the team wallet. Actual: succeeds and transfers teamOwed IMD to 0x611F08c7226591708B5F53F29BF53f3830D54511.","severity":"info","snippet":"    function claimTeamFees() external nonReentrant {\n        uint256 amount = teamOwed;\n        if (amount == 0) revert NothingToClaim();\n        teamOwed = 0;\n        _payout(TEAM_WALLET, amount);\n        emit TeamFeesPaid(TEAM_WALLET, amount);","title":"claimTeamFees() is open to any caller although the brief makes the claim the team's own action"}],"hash":"a8f91a8819ce5a3161eb56fac25a2ccef6ac0a5682b9bf4fb8761c07efd5ca3e","nodeId":"b5dd71be-161e-440a-acc5-794448fb0bb0","outcome":"completed","summary":"The review is written to `.imd-findings.json`: seven findings (two medium, four low, one info) and a coverage row for all nine entry points. No critical or high defect turned up in the permission model, so no finding carries a proof test. No source file was changed, and the scratch tests I used were removed.\n\nSix of the seven findings were reproduced with scratch tests against the repo's own fixture (mock IMD, local PoolManager, not a fork); the info item follows directly from the code. The numbers quoted in the file come from those runs.\n\n| # | Severity | Where | Finding |\n|---|---|---|---|\n| 1 | medium | `afterSwap`, line 324 | The current leader can void a challenger's buy with a dust trade. A 0.01 IMD buy ahead of the challenger nudged the minimum above the displayed 75.62 IMD; the challenger's swap still executed, paid a 2.27 IMD fee, and did not take the lead. The README says front-running leaves the victim as leader. |\n| 2 | medium | `_resolveTrader`, line 605 | With empty `hookData` the lead and the prize go to `tx.origin`, i.e. the bundler or relayer for smart-account and sponsored trades. The review doc accepts this as unavoidable without an admin, but asking the router for `msgSender()` first needs no owner. |\n| 3 | low | `settle()`, line 343 | Closing the round and paying the leader happen in one step, so a leader address the token refuses to credit makes `settle()` revert for everyone until a later buy closes the round. Only reachable if the chain's IMD can revert on transfer, which is unverified. |\n| 4 | low | `beforeInitialize`, line 229 | The first caller to initialize a pool binds the hook; neither the initializer nor the paired currency is checked. The factory's single-transaction launch closes this, but the repo's own deploy script leaves a hook anyone can brick or start the anti-snipe clock on early. |\n| 5 | low | `minimumBuy()`, line 391 | Once a round has expired but not been closed, the view shows the old escalated minimum (22.55 IMD after 20 buys) while a buy of half that qualified; the real threshold is the reset 8.5 IMD. |\n| 6 | low | `afterSwap`, line 326 | Buying the minimum and selling straight back took the lead for 0.55 IMD on an 8.5 IMD minimum (about 6.5%). The README says this \"gains nothing\". This is game economics more than permissions, and I found no fix that keeps the rules intact. |\n| 7 | info | `claimTeamFees()`, line 356 | Anyone can push the team's share to the team wallet, where the brief says the team claims it. The destination cannot be changed. |\n\nCoverage: `claimPrize`, `unlockCallback` and the three `WinToken` functions hold; `afterSwap`, `beforeInitialize`, `settle` and `claimTeamFees` carry the findings above. I also recorded rows for `beforeSwap` and for two invariants (nothing leaves the bank except a prize; no owner, mint or upgrade), all of which hold.\n\nNot reached: no chain or IMD address was pinned and there is no `launch.json` yet, so nothing was checked on a fork, against the real IMD token, or against a manifest. The single-unlock, zero-capital version of finding 6 is argued from v4's flash accounting, not run; the two-swap version is what I measured.","treeHash":null,"usage":{"cachedInputTokens":1439648,"inputTokens":26,"model":"claude-fable-5-1","outputTokens":62512,"runtime":"claude","turns":15,"wallClockMs":739079}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"41ff1f83c6e68e78","findings":[],"hash":"a9ec5ebb5f795d40451600df4df5dc319897eac5d7d91dd02a3a695d133e05d7","nodeId":"c6e47841-d732-485c-b087-e9239ace7ceb","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":4,"wallClockMs":467400}},{"artifacts":[],"attempt":1,"bundleHash":"987addf94e7394d078ea98da1322b43504ac6b7828bda3f00bb24cdea3231d8e","device":"f009de0ce52c7b3a","findings":[],"hash":"bf1578c867035bedbffdfdc8ccce7da3a6afd49fe35475fca7ee112561d50616","nodeId":"9dfaa778-c065-4995-8b27-93b996778650","outcome":"completed","summary":"`launch.json` is written and validates against the launch manifest schema; it is the only file changed (left uncommitted in the working tree).\n\nOne thing needs the deployer's attention: `pool.initialPrice` is only correct if the deployed WIN address sorts below IMD. The token ordering isn't known until the token is deployed, and the schema has a single price field.\n\n- **WIN is currency0** (written in the manifest): `125262255113908064987203232`, tick −129000.\n- **WIN is currency1**: `50111677533496076234078224273595`, tick +129000.\n\nUsing the wrong one misprices WIN by about 1.6e11×. This caveat and both values are in the notes.\n\nWhat the manifest declares:\n\n- **Hook**: `WinGameHook`, constructor args `[\"$poolManager\", \"$token\"]`, permissions `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta`, `afterSwapReturnDelta` — exactly what `getHookPermissions` returns (flags `0x20CC`).\n- **Token**: `WinToken`, name `WIN`, symbol `WIN`, 18 decimals.\n- **Pool**: paired with `0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127`, fee `3000`, tick spacing `60`.\n- **Notes**: 3,607 characters, under the 4,000 cap.\n\nThe starting price is snapped to the spacing-60 tick the test fixture uses, so the market cap is 2,499.66 IMD rather than exactly 2,500 (0.014% under). I did this so the initial tick lands on a spacing boundary for the single-sided seeding; say so if you want the exact 2,500 price instead.\n\nChecks I ran:\n\n- **Build and tests**: `forge build` succeeds and `forge test` passes 145 tests. That count includes the protected hook and token suites, which I copied into `test/scratch/` and ran against the creation code with the declared flags, supply and decimals.\n- **Fork run**: the repository's 134 tests also pass with `--fork-url` on chain 4663, where the IMD address resolves to an 18-decimal token with symbol `IMD`. They deploy their own PoolManager and a mock IMD, so this does not exercise the chain's real PoolManager or IMD.\n\nTwo things the brief asks for are still owed, and the notes say so:\n\n- **Independent review**: `docs/REVIEW.md` is a self-review by the contributor who wrote the code, not an independent one.\n- **Fork rehearsal**: no test runs against the chain's real PoolManager and IMD token.","treeHash":"b81d8d411591008a4f8fe2a37dc508483e135ed3","usage":{"cachedInputTokens":693778,"inputTokens":22,"model":"claude-fable-5-1","outputTokens":13459,"runtime":"claude","turns":14,"wallClockMs":208963}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"575a41a8798743c2","findings":[],"hash":"bfc225cfa7572f990e9eaa39e1970ed07b1dc06b4864cda23e6065dca267f963","nodeId":"3cb5fec1-00fe-4d68-9449-f58c4412fb37","outcome":"failed","summary":"PREMIUM CAPACITY EXHAUSTED: account_auth_unavailable","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":598586}},{"artifacts":[],"attempt":1,"bundleHash":"7eced7e4c59ba9096355f6ea322df21a93560b1f04fefd55a4805a6c15617637","device":"dff6c0d3de4aa913","findings":[{"description":"The brief asks for a website view of the 'current minimum buy', and minimumBuy() is documented as 'Gross IMD (fee included) a buy must spend right now to become the leader'. afterSwap() first closes an expired round (_finalizeIfExpired, line 323) and only then reads minimumBuy(), so a buy that arrives after the deadline is judged against the NEXT round's minimum: escalator back at 1.0, bank already reduced by the prize, and 5% instead of 20% prize share when round 1 is the one that expired. The external view does not simulate that close. Between the deadline and settle()/the next buy it keeps returning the dead round's escalated minimum computed on the pre-prize bank. The displayed value is therefore higher than the enforced one, by 1.05^n for n qualifying buys of the dead round and by up to a further ~5x when round 1 has just expired with a prize-linked minimum (4% of the bank shown vs 0.8% enforced). Consequences: (1) the website tells honest users they must spend more than they need to in exactly the window where the next round's first, cheapest lead is available, while a bot that computes the real number off-chain takes it for less; (2) anything that treats minimumBuy()-1 as 'will not take the lead' (integrations, the UI's 'this buy will not qualify' hint) is wrong in that window. No funds are at risk and the displayed value is never lower than the enforced one. nextPrize(), leader and roundNumber() in the same window describe the round awaiting settlement, which is defensible; minimumBuy() is the one whose own documentation is contradicted. Suggested fix: in minimumBuy() (and gameState()), when roundActive && block.timestamp >= deadline, compute the value from the post-close state (bank - prize, next round's prize share, escalator = 1e18).","line":391,"path":"src/WinGameHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {WinToken} from \"src/WinToken.sol\";\nimport {WinGameHook} from \"src/WinGameHook.sol\";\n\n/// Minimal stand-in for IMD (plain 18-decimal ERC-20).\ncontract ProofImd {\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// Proof: while a round has expired but has not been settled, `minimumBuy()` (and\n/// `gameState().minimumBuy`) report a minimum that is higher than the one a buy is actually judged\n/// against. A buy one wei UNDER the displayed minimum takes the lead of the next round.\ncontract ProofStaleMinimumBuyView is Test {\n    uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);\n\n    PoolManager manager;\n    ProofImd imd;\n    WinToken win;\n    WinGameHook hook;\n    PoolSwapTest router;\n    PoolKey key;\n    bool winIs0;\n    address alice = address(0xA11CE);\n    address bob = address(0xB0B);\n    address carol = address(0xCA201);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        manager = new PoolManager(address(this));\n        imd = new ProofImd();\n        win = new WinToken();\n        bytes memory initCode = abi.encodePacked(type(WinGameHook).creationCode, abi.encode(manager, address(win)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i = 0;; i++) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & ((1 << 14) - 1) != FLAGS) continue;\n            hook = new WinGameHook{salt: bytes32(i)}(IPoolManager(address(manager)), address(win));\n            break;\n        }\n        winIs0 = address(win) < address(imd);\n        (address c0, address c1) = winIs0 ? (address(win), address(imd)) : (address(imd), address(win));\n        key = PoolKey(Currency.wrap(c0), Currency.wrap(c1), 3_000, 60, IHooks(address(hook)));\n        router = new PoolSwapTest(manager);\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(manager);\n        manager.initialize(key, TickMath.getSqrtPriceAtTick(winIs0 ? int24(-129_000) : int24(129_000)));\n        win.approve(address(lp), type(uint256).max);\n        // WIN-only liquidity on the side of the price that holds WIN.\n        (int24 lower, int24 upper) = winIs0\n            ? (int24(-128_940), TickMath.maxUsableTick(60))\n            : (TickMath.minUsableTick(60), int24(128_940));\n        lp.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, 1e24, 0), \"\");\n\n        address[3] memory who = [alice, bob, carol];\n        for (uint256 i = 0; i < 3; i++) {\n            imd.mint(who[i], 1_000_000 ether);\n            vm.prank(who[i]);\n            imd.approve(address(router), type(uint256).max);\n        }\n    }\n\n    function buy(address who, uint256 imdAmount) internal {\n        bool zeroForOne = !winIs0;\n        vm.prank(who, who);\n        router.swap(\n            key,\n            SwapParams(zeroForOne, -int256(imdAmount), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            abi.encode(who)\n        );\n    }\n\n    function test_displayedMinimumIsTheMinimumABuyIsJudgedAgainst() public {\n        vm.warp(block.timestamp + 30 minutes); // base fee\n        buy(alice, hook.minimumBuy()); // opens round 1\n        buy(bob, hook.minimumBuy()); // second qualifying buy: minimum is now 8.5 * 1.05^2\n        vm.warp(hook.deadline()); // round 1 is over; nobody has called settle() yet\n\n        uint256 displayed = hook.minimumBuy();\n        assertEq(hook.gameState().minimumBuy, displayed);\n\n        // \"Gross IMD (fee included) a buy must spend right now to become the leader\": one wei less\n        // must therefore NOT make carol the leader.\n        buy(carol, displayed - 1);\n        assertTrue(hook.leader() != carol, \"a buy below the displayed minimum became the leader\");\n    }\n}","reproduction":"Base fee (launch + 30 min). alice buys minimumBuy() = 8.5 IMD (opens round 1), bob buys minimumBuy() = 8.925 IMD. Warp to deadline (launch + 3h); nobody calls settle(). Now hook.minimumBuy() and hook.gameState().minimumBuy both return 8.5 * 1.05^2 = 9.3712... IMD. carol buys minimumBuy() - 1 wei. Expected (per the view's documentation): the buy does not qualify, leader stays unset/unchanged. Actual: round 1 is closed lazily for bob, the buy is compared with 8.5 IMD, qualifies, and carol is leader of round 2. The attached test fails today with 'a buy below the displayed minimum became the leader' and passes once the view accounts for the pending close.","severity":"low","title":"minimumBuy() and gameState().minimumBuy overstate the minimum while a round is expired but not yet settled"},{"description":"beforeInitialize() accepts the first PoolKey that contains winToken and one of the three LP fee tiers, from any caller, and then refuses every other pool forever (PoolAlreadyInitialized). It cannot tell IMD from any other token, it ignores who initializes, the starting price and the tick spacing, and there is no way to rebind. This is harmless when the launch factory deploys the hook and initializes the WIN/IMD pool in one transaction, as the launch flow does. It is not harmless on the path the repository itself ships for rehearsals: script/DeployWin.s.sol deploys token and hook and stops, leaving initialization to a later transaction. In that window anyone can bind the hook to a junk pool, after which the real WIN/IMD pool cannot be created with this hook and the hook (address mined, team wallet constant) is dead; `imd` then points at the attacker's token. Reported so that nobody launches through the script or any non-atomic flow; if a non-atomic flow must be supported, pin the counter-currency (constructor argument) or restrict the initializer.","line":223,"path":"src/WinGameHook.sol","reproduction":"Deploy WinToken and WinGameHook as DeployWin.deploy() does. From an unrelated account call manager.initialize(PoolKey(WIN, JUNK, fee 500, tickSpacing 1, hooks = hook), sqrtPrice at tick 0). Expected: refused, or at least the WIN/IMD pool can still be created. Actual: succeeds, hook.imd() == JUNK, hook.poolInitialized() == true, and manager.initialize(WIN/IMD key with this hook) now reverts with PoolAlreadyInitialized. (Checked locally in a scratch test; the existing test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency shows the same acceptance of an arbitrary counter-currency.)","severity":"info","title":"The hook binds itself to the first pool anyone initializes with it, with any counter-currency, price, fee tier and tick spacing"},{"description":"MIN_BUY_FLOOR = 8.5 ether is compared with raw token units of whatever currency the pool pairs WIN with. The selected chain's IMD token was not pinned to this task (README and docs/REVIEW.md list it as an open item), so nothing verifies that it has 18 decimals. With any other decimals the floor is not '8.5 IMD': with 6 decimals it is 8.5e12 whole tokens, no buy can ever qualify, no round ever starts, and because the bank can only leave through a round prize every fee collected (90% of all fees) is locked permanently. A one-line guard in beforeInitialize (decimals() == 18 for the non-WIN currency) or a floor scaled by decimals would remove the assumption. test/WinGameHook.Fork.t.sol::test_fork_imdHasEighteenDecimals checks this precondition against the real token when WIN_FORK_IMD is supplied.","line":65,"path":"src/WinGameHook.sol","reproduction":"Initialize the pool against a 6-decimal token. hook.minimumBuy() returns 8_500_000_000_000_000_000 raw units = 8.5e12 tokens. Expected: minimum of 8.5 tokens (8_500_000 raw units). Actual: unreachable minimum; roundActive never becomes true while bank keeps growing with no exit.","severity":"info","title":"The 8.5 IMD floor is the raw constant 8.5e18: the counter-currency's decimals are assumed, never checked"}],"hash":"ce604e47e26e3564cc99b7c0a9568825574d7c69bedbf5f800cb3bb0d85c8739","nodeId":"d0002acd-4a5c-4735-a79d-977abaea2d6e","outcome":"completed","summary":"I added the missing invariant campaign, an adversarial unit/fuzz file and a fork rehearsal, all under `test/`. `forge build` succeeds and `forge test` passes offline: 186 passed, 0 failed, 2 skipped (the two fork suites, which skip themselves without an RPC). No tracked file was modified.\n\n## What was added\n\n- **`test/invariant/WinGameHandler.sol`** — a handler with four actors and bounded random actions:\n  - **Trades:** all four swap shapes, buys aimed exactly at / one wei under / just over the minimum, and last-second, on-deadline and post-deadline snipes.\n  - **Game calls:** `settle`, `claimPrize`, `claimTeamFees`.\n  - **Environment:** time jumps, raw-IMD and ERC-6909 donations to the hook, third-party liquidity, and direct calls to the callbacks from a non-manager.\n  - **Buyer identity:** eight `hookData` / `tx.origin` variants.\n  \n  After every action the hook's full state is compared with a small model restated from the brief.\n- **`test/invariant/WinGameHook.Invariant.t.sol`** — two campaigns (`invariant_money`, `invariant_game`) at 256 runs × depth 50, in four variants: from launch, after round 1, and each with WIN on the other side of the pool. They check:\n  - the hook's claims equal bank + team share + unclaimed prizes;\n  - the bank is fed only by 90% of fees and drained only by prizes;\n  - IMD and WIN are conserved across the system;\n  - the round state machine and the minimum-buy formula;\n  - past winners are append-only, and round 1 never closes before launch + 3h;\n  - the fee curve and the website views agree.\n  \n  A scripted `test_handlerReachesEveryPath` guards against the handler going idle.\n- **`test/WinGameHook.Adversarial.t.sol`** — 23 tests, most run in both orientations:\n  - same-block races;\n  - \"any qualifying buy gives the field a full ten minutes\" (fuzz);\n  - the exact qualification boundary (fuzz);\n  - identity abuse;\n  - claims that must revert;\n  - settle one second early and on the deadline;\n  - a sell larger than the pool's IMD;\n  - order-splitting and dust;\n  - round-trip cost (fuzz);\n  - gas not growing with winner history;\n  - callbacks for the wrong or an uninitialised pool;\n  - a winner that swaps from inside its own prize payout.\n- **`test/WinGameHook.Fork.t.sol`** — launch rehearsal against the chain's real PoolManager, configured by `WIN_FORK_RPC_URL`, `WIN_FORK_POOL_MANAGER` and optional `WIN_FORK_IMD`.\n\n## How far it was checked\n\n- **Mutation check:** I temporarily broke the hook four ways (sells closing a round, lazy close after the leader is overwritten, payout not reducing the unclaimed total, wrong exact-output fee). The invariants caught all four; the source was restored each time.\n- **Reachability:** within 50 calls the campaigns reach settles plus lazy closes, five or more winners, four or more qualifying buys in a round, and prize claims.\n- **Fork:** the rehearsal passes on Base mainnet with a stand-in IMD and with DAI as an 18-decimal real token. It has not been run against the actual IMD token, because the selected chain and IMD address were not pinned to this task — that run is still owed.\n\n## Findings (`.imd-findings.json`)\n\nNo critical or high defect turned up. Three items are reported rather than asserted as correct:\n\n1. **Low — `minimumBuy()` overstates the minimum while a round is expired but unsettled.** A buy in that window is judged against the next round's reset minimum, so a buy one wei under the displayed value takes the lead. In the proof the view shows 9.37 IMD while 8.5 IMD is enforced. A self-contained failing proof test is attached.\n2. **Info — the hook binds to the first pool anyone initialises with it**, with any counter-currency, price and tick spacing. This is safe with the factory's atomic deploy-and-initialise, but not through `script/DeployWin.s.sol`, which leaves a gap.\n3. **Info — the 8.5 IMD floor is the raw constant `8.5e18`**, and IMD's decimals are never checked. With a non-18-decimal token no buy could qualify and the bank would be locked. The for","treeHash":"8a7e70d48b4d042310bdd0fcc831bf3d0274ec0b","usage":{"cachedInputTokens":3967272,"inputTokens":50,"model":"claude-fable-5-1","outputTokens":111453,"runtime":"claude","turns":32,"wallClockMs":2110349}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"9ab27edcfd62be02","findings":[{"citation":"resolved","description":"Once the prize-linked term dominates (20% of nextPrize > 8.5 IMD, i.e. bank > ~850 IMD in rounds >= 2 or > ~212 IMD in round 1), minimumBuy() is a function of the live bank: every fee-paying trade, including sells and sub-minimum buys, raises it. afterSwap reads `uint256 minimum = minimumBuy();` (line 324) at execution time, so a competitor who submits exactly the minimum the contract advertised can be made non-qualifying by any trade that lands first. The current leader has a direct incentive and a near-zero cost: a 1e6-wei buy (fee 30,000 wei, 27,000 wei to the bank) moves the minimum by ~300 wei, enough to disqualify an exact-minimum buy; against a buyer adding margin m the leader needs roughly m/(0.009*escalator) in fees, which is still far below the prize for small margins (e.g. ~1.1% of the bank in fees defends a 5%-of-bank prize against a 1% margin). The disqualified buyer still pays the full trading fee and the pool price impact, and the round timer is not reset for them. The brief asks for a defence against bots sandwiching the leader; this is the inverse (the leader sandwiching challengers), and the hook offers none: 'sells never affect the game' holds for the timer and the leader but not for the minimum. README A5 accepts the moving minimum and tells the website to add 'a small margin'; the attack shows the margin must be large to be safe, and that the leader can simply keep the minimum moving every block (dust trades) on chains without a public mempool. Fix: snapshot the prize-linked base when a round starts and on each qualifying buy (store e.g. `roundMinBase = max(MIN_BUY_FLOOR, nextPrize()*MIN_BUY_PRIZE_BPS/BPS)` in _qualify and _finalize, and have minimumBuy() return roundMinBase*escalator/WAD), so the minimum only changes through qualifying buys, which are the game's visible moves. Optionally let hookData carry a 'must qualify' flag and revert the swap in afterSwap when gross < minimum, so a lead-taking buy that can no longer qualify costs nothing.","line":391,"path":"src/WinGameHook.sol","reproduction":"Foundry, WinGameFixture setup (fresh PoolManager, WIN-only pool). 1) warp past decay; alice buys minimumBuy(); warp 3h; settle() (round 1 closed). 2) carol buyExactIn 50,000 IMD (fee 1,500; bank = 1350.1836 IMD; carol leads round 2). 3) alice buyExactIn hook.minimumBuy() -> alice leads. 4) hook.minimumBuy() == 14.889994306982865000 IMD (website shows this to bob). 5) alice buyExactIn 1,000,000 wei (1e-12 IMD) with no other effect -> minimumBuy() == 14.889994306982865297. 6) bob buyExactIn exactly 14.889994306982865000 IMD. Expected (per the displayed minimum): bob becomes leader and the timer resets. Actual: hook.leader() == alice, bob's IMD balance fell by the full 14.889994306982865000 (3% fee paid, 90% of it into alice's prize), no RoundStarted/QualifyingBuy for bob. Measured cost to defeat a 1% margin (bob sends 15.038894250052693650): alice needs a non-qualifying front-run of ~14.89 IMD, i.e. any buy at the minimum simply re-qualifies her for 0.45 IMD in fees; for sub-1% margins the dust trade suffices. Verified with test/scratch/Leads.t.sol::test_lead_frontRunDisqualifiesExactMinimumBuy (passes on current code, i.e. the attack succeeds).","severity":"medium","snippet":"    function minimumBuy() public view returns (uint256) {\n        uint256 base = nextPrize() * MIN_BUY_PRIZE_BPS / BPS;\n        if (base < MIN_BUY_FLOOR) base = MIN_BUY_FLOOR;\n        return base * escalator / WAD;\n    }","title":"Minimum buy is read from the live bank, so the leader can disqualify a competitor's lead-taking buy with a dust trade (buyer pays the fee, gets no lead)"},{"citation":"resolved","description":"afterSwap closes an expired round lazily (_finalizeIfExpired) before it reads minimumBuy(), so the threshold actually applied to the next buy is the fresh one: escalator 1x, prize-linked base computed on bank minus the dead round's prize, round number +1. The views do not apply the same rule: with roundActive still true after the deadline, roundNumber() returns the dead round, leader() the dead leader, nextPrize() the dead round's prize, and minimumBuy() the dead round's escalated minimum. A website that shows 'minimum buy' between expiry and the next buy/settle tells users a number that is higher than what qualifies (users overpay to take a lead that a far smaller buy would take) and a round number/leader that the next buy will immediately replace. settleable() is correct, so a front end can special-case it, but the brief's named views are wrong in that window. Fix: make the views compute the post-finalization state when `roundActive && block.timestamp >= deadline` (bank - prize, escalator WAD, roundsStarted + 1, leader address(0)), or expose an explicit `pendingMinimumBuy()`.","line":404,"path":"src/WinGameHook.sol","reproduction":"Foundry, WinGameFixture setup. 1) finish round 1 (qualifying buy, warp 3h, settle). 2) alice makes 5 consecutive qualifying buys of hook.minimumBuy() -> escalator 1.2762815625e18. 3) vm.warp(hook.deadline()). 4) hook.minimumBuy() returns 10.848393281250000000 IMD; hook.roundNumber() returns 2; hook.leader() returns alice. 5) bob buyExactIn 8.5 IMD (below the shown minimum). Expected per the views: bob does not qualify. Actual: round 2 is closed for alice, bob qualifies and leads round 3 (roundsStarted == 3, leader == bob). Verified with test/scratch/Leads.t.sol::test_lead_viewsStaleAfterExpiry.","severity":"low","snippet":"    function roundNumber() public view returns (uint64) {\n        return roundActive ? roundsStarted : roundsStarted + 1;\n    }","title":"Website views report the expired round's data until settle() or the next buy: minimumBuy(), roundNumber(), leader(), nextPrize() are stale after the deadline"},{"citation":"resolved","description":"The brief asks for robust identification of the real buyer. The chosen approach is user-supplied hookData, falling back to tx.origin. Two consequences, both documented in README section 6 / REVIEW C4 as accepted trade-offs, are recorded here because they move a prize to a party other than the one that paid for the qualifying buy: (1) any contract (vault, DAO treasury, smart-account via a bundler, relayer-submitted trade) that buys through a router without setting hookData makes the EOA that signed the transaction the leader; when the round settles, that EOA personally receives a prize funded by the contract's IMD; (2) hookData is not bound to the payer, so a third-party router or aggregator that fills in hookData with its own address takes the lead that its users pay for. Neither is a theft from the bank, but both pay the prize to the wrong party relative to who paid the qualifying buy. A no-owner alternative that avoids both: treat a buy as qualifying only when hookData explicitly names the buyer (no tx.origin fallback), and have the website always pass it; buys without hookData still pay the fee into the bank but cannot take the lead, so nobody is mis-credited.","line":605,"path":"src/WinGameHook.sol","reproduction":"Foundry, WinGameFixture setup, warp past decay. Deploy a VaultBuyer contract that holds 1,000 IMD and swaps directly through poolManager.unlock/swap with empty hookData. Prank tx.origin = keeper (an EOA with no IMD). keeper calls vault.buy(100 IMD). Expected: the vault (payer) is the leader or the buy does not qualify. Actual: hook.leader() == keeper; after warping 3h and settle(), keeper's IMD balance is > 0 (the round-1 prize) while the vault's balance is 900 IMD. Verified with test/scratch/Leads.t.sol::test_lead_contractBuyerWithoutHookDataGivesLeadToKeeper.","severity":"low","snippet":"        if (trader == address(0)) trader = tx.origin;","title":"Buyer identity: a contract that buys without hookData credits the lead and the prize to tx.origin (its keeper), and hookData is unauthenticated so any router can name itself"},{"citation":"resolved","description":"Every test deploys its own PoolManager and a MockERC20 IMD; no test calls vm.createSelectFork, reads a chain id, or pins the real IMD token or PoolManager addresses (there is no .imd/reads/network.json in this task either). The suite is chain-agnostic, which is good, but the brief's requirement that the tests run on a fork of the selected chain is not met, and the assumptions the hook relies on (IMD is a plain ERC-20 with no fee, hooks or blocklist; the deployed PoolManager matches the vendored v4-core, which I verified is byte-identical to upstream main) remain unverified against the launch chain. Not a code defect; recorded so the judge can require the fork rehearsal (first buy on the real manager with zero IMD, sell after a buy, settle, claimTeamFees to the real team wallet) before admission.","line":239,"path":"README.md","reproduction":"grep -rn 'createSelectFork\\|fork' test/ finds only comments in test/utils/WinGameFixture.sol:25; `forge test` runs 134 tests on a fresh local PoolManager; none exercise the selected chain's IMD or PoolManager.","severity":"info","snippet":"  unchanged against a fork with `forge test --fork-url <rpc>` once an RPC is available (the","title":"No fork test exists; the brief's 'Foundry tests on a fork of the selected chain' is unmet and the chain-specific IMD/PoolManager behaviour is unverified"}],"hash":"d38266d37f6fda2ea625c6f587bf605299cd93955d12fc454d27cea8cc66bfde","nodeId":"3cb5fec1-00fe-4d68-9449-f58c4412fb37","outcome":"completed","summary":"All four anchors now match the tree, and the working tree shows only the findings file plus my scratch tests, which are both permitted. The review is complete.\n\n## Review complete: `.imd-findings.json` written with 4 findings and 16 coverage rows\n\n**Scope covered.** I traced the hook's control flow through the Execution Trace, Periphery and First Principles guides: all four swap shapes in both pool orientations against the real `Hooks.beforeSwap`/`afterSwap` sign handling, the mint-versus-hookDelta offset, the unlock/burn/take payout path, the round state machine, and the identity decoder. I also verified the vendored `lib/v4-core` is byte-identical to upstream and ran both pinned protected floor suites against this creation code (all pass). The existing 134-test suite passes.\n\n**Findings, most severe first:**\n\n1. **Medium.** The qualifying minimum is read from the live bank at execution time, so the current leader can disqualify a challenger's exact-minimum buy with a dust trade. A 1e6-wei buy moved the minimum by ~300 wei in my repro, and the challenger still paid the full 3% fee without taking the lead. The brief asked for a defence against leader-sandwiching; this is the inverse and has none. Suggested fix: snapshot the prize-linked base per round and per qualifying buy, optionally with a \"must qualify\" hookData flag that reverts the swap.\n2. **Low.** After the deadline but before settle or the next buy, `minimumBuy()`, `roundNumber()`, `leader()` and `nextPrize()` report the dead round's numbers. A buy of 8.5 IMD qualified while the view showed 10.85 IMD.\n3. **Low.** A contract that buys without hookData hands the lead and prize to `tx.origin` (its keeper). The hookData path is unauthenticated, so a router can name itself. Both are documented trade-offs; recorded because the prize goes to a party other than the payer.\n4. **Info.** No fork test exists despite the brief's requirement; no chain was pinned. Recorded as an admission open item rather than a code defect.\n\n**What held.** Delta accounting sums to zero on every path, partial fills are rejected rather than overcharged, settlement cannot run inside a swap, the three payout entry points share a reentrancy guard, the team wallet is a compile-time constant, and nothing can leave the bank outside the game rules. No critical or high defect was found, so no proof files were attached.\n\n**Not reached.** Economic tuning of the escalator and first-round incentives, and block-stuffing cost on a specific chain, which belong to the economics specialist.","treeHash":null,"usage":{"cachedInputTokens":2947719,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":66462,"runtime":"claude","turns":47,"wallClockMs":996763}},{"artifacts":[],"attempt":1,"bundleHash":"2a35716ab5f6ff18c612beb76832c1e073fd6c29f5fe61f6930383688f4d7c39","device":"5ded77c3c883b7b0","findings":[],"hash":"d87bef714571b7e14587ab2313f25089ec3da3639f60606de5c8378732b4a243","nodeId":"9dfaa778-c065-4995-8b27-93b996778650","outcome":"completed","summary":"I updated `launch.json` to match the current hook. It validates against the launch manifest schema, the build passes, and all 179 repository tests pass. I also ran the two protected floor suites against the built hook declaring flags `0x20CC`, and all 10 tests passed. No other file is modified.\n\n**What stayed the same, because the current code still matches it:**\n- **Contracts:** `WinGameHook` and `WinToken`.\n- **Hook constructor arguments:** `[\"$poolManager\", \"$token\"]`.\n- **Permissions:** `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta` and `afterSwapReturnDelta`, which is exactly what `getHookPermissions` returns.\n- **Pool:** paired with `0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127`, fee 3000, tick spacing 60, and `initialPrice` `125262255113908064987203232`. That price is tick −129000, a 2,500 IMD starting market cap, and the hook's own price test checks the same number.\n\n**What changed: the notes (3,603 of 4,000 characters).** Since my last round the hook has gained several behaviours the notes didn't describe:\n- **Starting price check:** `beforeInitialize` now rejects a starting price more than 300 ticks from the briefed one for the token order it sees (error `WrongStartingPrice`).\n- **Buyer identity:** the hook takes the buyer from `hookData` first, then from the router's `msgSender()`, then from `tx.origin`.\n- **\"Must lead\" flag:** a buyer can set it so their buy reverts if it misses the minimum at execution, instead of paying the fee without taking the lead.\n- **Settlement:** `settle()` always closes the round. If IMD refuses the winner, the prize waits in a claimable balance (`claimPrize`).\n- **Review status:** `docs/REVIEW.md` now records an independent review's findings, E1–E10, and what was done about each.\n\n**Before deploying:**\n- **Token address order:** the price in the manifest is only correct if the deployed WIN address sorts below IMD. If it sorts above, the hook expects `50111677533496076234078224273595`, and pool creation will revert with the manifest's value rather than open a mispriced pool. The deployer must either place WIN below IMD or use the other value.\n- **Fork rehearsal still owed:** the tests use their own PoolManager and a mock IMD, so nothing has yet run against the selected chain's real PoolManager and IMD.\n- **Block stuffing:** this is only deterred economically. The review leaves it as a decision for the requester and recommends a fixed cap on the prize once the chain is known.","treeHash":"88692e80f90371be424ee3dffdd119c19c207159","usage":{"cachedInputTokens":708966,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":7873,"runtime":"claude","turns":14,"wallClockMs":117999}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7f1dec5ffcbde1d8","findings":[{"citation":"resolved","description":"The brief fixes the starting market cap at 2,500 IMD (2.5e-6 IMD per WIN). launch.json carries one `pool.initialPrice` = 125262255113908064987203232, which I verified is exactly getSqrtPriceAtTick(-129000) (the README's '1.2536e26' at README.md:300 is a misquote; the manifest value is the correct one). A v4 price is currency1/currency0, so that number means 2.5e-6 IMD per WIN only when WIN is currency0, i.e. only when the WIN address the factory deploys sorts below the paired currency 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127. If WIN sorts above it (every address > 0x5f7b..., roughly 63% of random addresses), the same number means 2.5e-6 WIN per IMD = 400,000 IMD per WIN. The manifest's `notes` tell the deployer to flip the value by hand, but notes are explanatory text, not deployment authority, and nothing else in the tree knows the ordering before the token exists (the fixture has to mine a salt to force it). The hook is the one component that does see both the ordering (`winIsCurrency0`) and the price (the unnamed `uint160` in beforeInitialize) at the moment it matters, and it checks nothing: it binds permanently to whatever price it is given. Outcome in the wrong ordering: either the factory's WIN-only seeding reverts (the range [-887220, 128940] from the notes straddles tick -129000 and needs IMD), or, if the factory derives the range from the price as the fixture does, the pool goes live with WIN at a 403,672,527,210,761 IMD market cap, no one can buy at that price, and because the hook refuses a second pool and has no owner, the launch is dead for this hook. Fix that keeps the design and needs no manifest field: in beforeInitialize, derive the expected sqrtPriceX96 for the observed ordering (getSqrtPriceAtTick(-129000) when winIsCurrency0, its mirror 50111677533496076234078224273595 otherwise, or a tolerance band around 2.5e-6 / 400000) and revert with a dedicated error when the supplied price is not within it. Then a wrong-ordering launch reverts loudly at initialization instead of creating an unusable pool. Alternatively the factory must guarantee ordering (CREATE2 salt for the token), which this tree cannot show. (Related, lower-severity gap on the same function: finding 5.)","line":223,"path":"src/WinGameHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {LiquidityAmounts} from \"v4-core/test/utils/LiquidityAmounts.sol\";\nimport {WinToken} from \"src/WinToken.sol\";\nimport {WinGameHook} from \"src/WinGameHook.sol\";\n\n/// Minimal stand-in for IMD (plain 18-decimal ERC-20).\ncontract ProofImd {\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// Proof: launch.json carries one `pool.initialPrice` (125262255113908064987203232 =\n/// getSqrtPriceAtTick(-129000)), which is the 2,500 IMD market cap only when WIN is currency0.\n/// When the deployed WIN address sorts above IMD, the hook accepts that same price and binds\n/// itself to a pool where the first WIN is sold at a ~4e14 IMD market cap. The hook must either\n/// refuse a starting price that is not the briefed market cap for its orientation, or the first\n/// buy must come out near 2,500 IMD.\ncontract ProofOrientation is Test {\n    uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);\n    uint160 constant MANIFEST_PRICE = 125262255113908064987203232;\n\n    PoolManager manager;\n    ProofImd imd;\n    WinToken win;\n    WinGameHook hook;\n    PoolKey key;\n    address alice = address(0xA11CE);\n\n    function _create2(address deployer, bytes32 salt, bytes32 initHash) internal pure returns (address) {\n        return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initHash)))));\n    }\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        manager = new PoolManager(address(this));\n        imd = new ProofImd();\n        // Deploy WIN so that it sorts ABOVE the IMD address: WIN is currency1.\n        bytes32 winHash = keccak256(type(WinToken).creationCode);\n        for (uint256 i = 0;; i++) {\n            if (_create2(address(this), bytes32(i), winHash) > address(imd)) {\n                win = new WinToken{salt: bytes32(i)}();\n                break;\n            }\n        }\n        require(address(win) > address(imd), \"WIN must be currency1 for this proof\");\n\n        bytes memory initCode = abi.encodePacked(type(WinGameHook).creationCode, abi.encode(manager, address(win)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i = 0;; i++) {\n            if (uint160(_create2(address(this), bytes32(i), initHash)) & ((1 << 14) - 1) != FLAGS) continue;\n            hook = new WinGameHook{salt: bytes32(i)}(IPoolManager(address(manager)), address(win));\n            break;\n        }\n        key = PoolKey(Currency.wrap(address(imd)), Currency.wrap(address(win)), 3_000, 60, IHooks(address(hook)));\n    }\n\n    function test_manifestInitialPriceWithWinAsCurrency1_isRefusedOrPricedAtTheBriefedCap() public {\n        // The deployer initializes with launch.json's pool.initialPrice as written.\n        try manager.initialize(key, MANIFEST_PRICE) {}\n        catch {\n            // The hook refused the mispriced orientation: that is the fix.\n            return;\n        }\n        assertFalse(hook.winIsCurrency0());\n\n        // Seed 900M WIN single-sided: with WIN as currency1 the only WIN-only range is below the tick.\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        win.approve(address(lp), type(uint256).max);\n        int24 lower = TickMath.minUsableTick(60);\n        int24 upper = TickMath.getTickAtSqrtPrice(MANIFEST_PRICE) - 60;\n        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(\n            TickMath.getSqrtPriceAtTick(lower), TickMath.getSqrtPriceAtTick(upper), 900_000_000 ether\n        );\n        lp.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, int256(uint256(liquidity)), 0), \"\");\n\n        // First buyer spends the 8.5 IMD minimum at launch (50% hook fee: 4.25 IMD reach the pool).\n        PoolSwapTest router = new PoolSwapTest(IPoolManager(address(manager)));\n        imd.mint(alice, 1_000 ether);\n        vm.prank(alice);\n        imd.approve(address(router), type(uint256).max);\n        vm.prank(alice, alice);\n        BalanceDelta d = router.swap(\n            key,\n            SwapParams(true, -int256(8.5 ether), TickMath.MIN_SQRT_PRICE + 1),\n            PoolSwapTest.TestSettings(false, false),\n            abi.encode(alice)\n        );\n        uint256 winOut = uint256(int256(d.amount1()));\n        // (IMD-wei paid to the pool / WIN-wei received) x 1e9 WIN supply = market cap in whole IMD.\n        uint256 impliedMarketCapImd = (8.5 ether / 2) * 1_000_000_000 / winOut;\n\n        // Brief: starting market cap 2,500 IMD. Allow the 60-tick gap and the LP fee (< 1%).\n        assertLt(impliedMarketCapImd, 2_600, \"first WIN sold far above the briefed 2,500 IMD market cap\");\n        assertGt(impliedMarketCapImd, 2_400, \"first WIN sold far below the briefed 2,500 IMD market cap\");\n    }\n}","reproduction":"Fresh PoolManager; mock 18-decimal IMD; WinToken deployed with a CREATE2 salt such that address(WIN) > address(IMD); hook at a mined 0x20CC address. key = PoolKey(IMD, WIN, 3000, 60, hook). manager.initialize(key, 125262255113908064987203232) (the manifest value, used as written). Expected: refused (price does not match the briefed market cap for this ordering) or a 2,500 IMD market cap. Actual: initialize succeeds, hook.winIsCurrency0() == false, pool tick == -129000. Seed 900M WIN single-sided in the only WIN-only range (below the tick: [minUsableTick(60), -129060]); no IMD is needed. First buyer spends 8.5 IMD exact-in at launch (50% hook fee, 4.25 IMD to the pool) and receives 10,528,335,998,900 wei = 1.05e-5 WIN, an implied market cap of 403,672,527,210,761 IMD against the brief's 2,500 (the correct ordering gives 2,514.7 IMD). Proof: test/scratch/Proof_Orientation.t.sol fails on this tree with 'first WIN sold far above the briefed 2,500 IMD market cap: 403672527210761 >= 2600' and passes once beforeInitialize refuses the price.","severity":"high","snippet":"    function beforeInitialize(address, PoolKey calldata key, uint160) external onlyPoolManager returns (bytes4) {","title":"beforeInitialize ignores the starting price, and launch.json's single initialPrice is right only when WIN sorts below IMD: in the other ordering the hook binds itself to a pool priced at ~4e14 IMD mar"},{"citation":"resolved","description":"Merged from audit_permissions #1, audit_math #1, audit_flow #1 and audit_economics #1/#2: one root cause with three triggers. afterSwap compares `gross` with minimumBuy() at execution time and, when the buy is short by even one wei, lets the swap through as an ordinary buy: the fee (3% after decay, up to 50%) is accrued, the buyer gets WIN at the moved price, no error, no lead, no timer reset. There is no input (hookData flag or otherwise) by which a buyer can say 'revert unless I take the lead'. Three things make the miss easy to cause by whoever lands first: (a) once 20% of nextPrize() exceeds the 8.5 IMD floor (bank > 850 IMD in rounds >= 2, > 212.5 IMD in round 1) minimumBuy() follows the live bank, so every fee-paying trade, including sells and sub-minimum buys that the brief says never touch the game, raises the threshold a pending buy is judged against; (b) in the floor regime any qualifying buy raises it 5%, and the leader can re-qualify and sell straight back for ~6.5% of the minimum (finding 7); (c) for exact-output buys `gross = poolIn + fee` depends on the pool price, so a sell placed in front lowers the IMD cost below the minimum, which contradicts README s5 ('price manipulation cannot disqualify a buy'). The current leader has the incentive and, with public-mempool front-running or same-block ordering on the launch chain, the means: each voided challenger pays 3% of the minimum (0.45 IMD at a 14.9 IMD minimum, >= 30 IMD at a 100k IMD bank) for nothing, and the leader keeps the lead and the original deadline. On an FCFS sequencer the same happens whenever two bids at the same displayed minimum land in one block: the second is converted into a donation to the first's prize. README s5 says front-running 'leaves the victim as leader' and 'Griefing with tiny buys ... Buys below the minimum change nothing except the bank'; through the bank they change everyone else's threshold. Fix that keeps the rules: accept an opt-in flag in hookData (e.g. abi.encode(buyer, mustLead) as a 64-byte payload beside the existing 32/20-byte forms) and revert with NotQualifying(minimum, gross) when a flagged buy does not take the lead, so a voided challenge costs gas and not a fee-bearing position; the website always sets it. Optionally, and as a requester decision because it changes 'upcoming prize' from live to snapshot, take the prize-linked base from the bank at round start / last qualifying buy so only qualifying buys (visible 5% steps) move the minimum.","line":324,"path":"src/WinGameHook.sol","reproduction":"Fixture test/utils/WinGameFixture.sol (WIN = currency0, LP fee 3000). Case A, prize-linked regime: warp past decay; alice buys minimumBuy(); warp 3h; settle(). carol buyExactIn(50_000 ether) -> carol leads round 2, bank 1,350 IMD. alice buyExactIn(minimumBuy()) -> alice leads; shown = hook.minimumBuy() = 14.889994306982865000 IMD. alice buyExactIn(1_000_000 wei) (1e-12 IMD) -> minimumBuy() = 14.889994306982865297. bob buyExactIn(shown) with hookData abi.encode(bob). Expected: bob is leader, deadline = now + 10 min. Actual: swap succeeds, bob's IMD balance falls by the full 14.889994306982865 (0.446699829209485950 IMD hook fee), hook.leader() is still alice and hook.deadline() is unchanged. Case B, floor regime: after settle, alice buys minimumBuy() (8.5) -> shown 8.925 IMD; bob intends shown*1.04 = 9.282 IMD; alice first buyExactIn(8.925) and sellExactIn(all WIN received) (net cost 0.577682661270364107 IMD) -> minimumBuy() = 9.37125; bob's 9.282 IMD buy executes and hook.leader() == alice. Case C, exact-output: alice buyExactIn(500 ether) at launch; warp 30 min; minimumBuy() = 9.45 IMD; carol prepares buyExactOut(winOut) where winOut is what 9.5445 IMD buys now (control: carol leads); alice sellExactIn(5% of her WIN) first; carol's identical buy costs gross 9.440369110833086836 < minimum 9.476261792309434066, executes, and hook.leader() == alice. All three reproduced in test/scratch/Repro.t.sol (test_f1_dustBuyVoidsExactMinimumChallenger, test_f1_floorRegime_requalifyAndSellBack, test_f1_exactOutputBuyVoidedBySell).","severity":"medium","snippet":"            uint256 minimum = minimumBuy();\n            _accrue(fee);\n            if (gross >= minimum) _qualify(trader, gross);","title":"A buy aimed at the lead that falls short of the minimum at execution time is silently executed and still charged the full fee; with the minimum read live from the bank, the leader (or any trade that l"},{"citation":"resolved","description":"Merged from audit_permissions #2 and audit_flow #3. Buyer identity decides who is paid. _resolveTrader takes it from hookData (32-byte ABI or 20-byte packed address) and otherwise from tx.origin. Any buy whose transaction is signed by someone other than the paying account (ERC-4337 accounts through a bundler, sponsored EIP-7702 accounts, meta-transactions, Safe executions sent by another owner, a vault or DAO contract whose keeper triggers the trade) and that does not go through the project website has empty hookData, so the lead, and later the whole prize pushed by settle() or claimPrize(), goes to the EOA that signed, not to the account that paid the minimum and the fee. The bundler/relayer can keep it. This is funds paid to the wrong party under a realistic condition (most wallet-native swap flows do not forward hookData), hence medium even though README s6 / REVIEW C4 document it as accepted. REVIEW C4 says the only alternative is an admin-kept router allowlist; it is not: the `sender` argument of afterSwap (discarded at line 273) is the router, and Uniswap's Universal Router and the v4-periphery routers expose msgSender() precisely so hooks can learn the account that opened the lock. A staticcall of msgSender() on `sender`, used when hookData is absent and before falling back to tx.origin, needs no owner and no list and is no weaker than hookData (a router that lies can only gift the lead). Alternative without any fallback: treat a buy as qualifying only when hookData names the buyer; buys without it pay the fee but cannot take the lead, so nobody is mis-credited. Either is a requester decision; what is reported is that the chosen fallback pays prizes to a party that did not buy.","line":605,"path":"src/WinGameHook.sol","reproduction":"Fixture, after round 1 is settled: min = hook.minimumBuy() (8.5 IMD); vm.prank(bob, bundler) (bob's account pays, `bundler` is tx.origin as with a 4337 UserOperation); swapRouter.swap(key, SwapParams(!winIsCurrency0, -int256(min), limit), TestSettings(false,false), \"\") with empty hookData. Expected: bob (who paid the 8.5 IMD) is the leader. Actual: hook.leader() == bundler; after warp +10 min, hook.settle() transfers 0.020655 IMD (5% of the bank in that state) to bundler and hook.unclaimedPrize(bob) == 0. Reproduced in test/scratch/Repro.t.sol::test_f2_txOriginFallback.","severity":"medium","snippet":"        if (trader == address(0)) trader = tx.origin;","title":"Buyer identity falls back to tx.origin when hookData is absent, so a smart-account, relayed or contract buy that does not come from the website makes the bundler/relayer/keeper the leader and pays it "},{"citation":"resolved","description":"From audit_permissions #3. The lazy path (_finalizeIfExpired in afterSwap) only credits unclaimedPrize[leader]; settle() credits it and in the same call runs _payPrize -> _payout -> unlock -> take(imd, leader, amount). If that transfer reverts the whole settle() reverts and the round stays open (roundActive and settleable() both true) until a later buy closes it. The leader is whatever address a buyer wrote into hookData, so any buyer can pick one the token refuses (a blocklisted address on a token with a blocklist, the token contract itself on tokens that forbid it, any contract without receive() if the pair were native). Impact is liveness only: 'anyone can call settle()' is broken for that round, the website's settle button reverts, the prize sits in unclaimedPrize forever and the escalated minimum stays in force until someone buys. README s7 says the opposite ('a blocklisted winner could not be paid (their own claimPrize would revert; nothing else would)'). Unreachable if the launch chain's IMD never reverts on transfer to a non-zero address, which the tree leaves unverified (finding 8). Fix: in settle(), finalize unconditionally and make the transfer best-effort (try the payout, leave the amount in unclaimedPrize on failure), or drop the push and let the winner use claimPrize.","line":343,"path":"src/WinGameHook.sol","reproduction":"Fixture with an IMD mock whose _transfer has require(to != blocked): finish round 1; set blocked = bad; buyExactIn(bob, hook.minimumBuy(), abi.encode(bad)) -> hook.leader() == bad; warp +10 min; hook.settleable() == true; hook.settle() from any account. Expected: round 2 closes, winner recorded, prize left claimable. Actual: settle() reverts (the take inside unlockCallback reverts), hook.roundActive() stays true on every further settle() call; only buyExactIn(carol, 1 ether) closes the round, after which unclaimedPrize(bad) > 0 and claimPrize(bad) reverts. Reproduced in test/scratch/Repro.t.sol::test_f3_settleRevertsForUnpayableLeader.","severity":"low","snippet":"        address winner = leader;\n        _finalize();\n        _payPrize(winner);","title":"settle() closes the round and pushes the prize in one step, so a leader address that the IMD token refuses to credit makes settle() revert for everyone until a buy closes the round lazily"},{"citation":"resolved","description":"Merged from audit_permissions #4 and write_foundry_tests #2. The single-use binding (poolInitialized, imd, _poolId, launchTime) is written by the first PoolManager.initialize that names this hook, and initialize is open to everyone and needs no tokens. The only conditions are an LP fee of 500/3000/10000 and WIN on one side: `sender` is ignored, the other currency is accepted whatever it is (it becomes `imd`), tick spacing and price are free (price: finding 1). The launch factory deploys and initializes in one transaction, which closes the window, so under the factory flow this is not reachable; it is reachable on the path the repository ships for rehearsals (script/DeployWin.s.sol run() deploys token and hook and stops) and on any other non-atomic deployment. Then (a) an outsider binds the hook to WIN/<worthless token>: `imd` is the attacker's token, the real WIN/IMD pool can never be created with this hook (PoolAlreadyInitialized) and the hook (mined address, fixed team wallet) is dead; or (b) the outsider initializes the correct key early so launchTime starts then: by the time liquidity is added the 50% anti-snipe fee has decayed and the 3-hour floor is partly spent. Fix without an owner: record the deployer as an immutable in the constructor and require `sender == deployer` in beforeInitialize, and/or take the paired currency as a constructor argument and require the other currency to equal it (the manifest already pins pairedCurrency).","line":224,"path":"src/WinGameHook.sol","reproduction":"Fixture: win2 = new WinToken(); h2 = deployHook(manager, address(win2)) (what DeployWin.run() leaves on chain); junk = new MockERC20. vm.prank(stranger); manager.initialize(PoolKey(sorted(win2, junk), fee 500, tickSpacing 1, hooks h2), sqrtPrice at tick 0). Expected: refused. Actual: succeeds, h2.imd() == junk, h2.poolInitialized() == true, and manager.initialize(PoolKey(sorted(win2, IMD), 3000, 60, h2), price) reverts (PoolAlreadyInitialized wrapped by the manager). Reproduced in test/scratch/Repro.t.sol::test_f4_strangerBindsHook.","severity":"low","snippet":"        if (poolInitialized) revert PoolAlreadyInitialized();\n        if (key.fee != 500 && key.fee != 3_000 && key.fee != 10_000) revert UnsupportedLPFee(key.fee);","title":"beforeInitialize binds the hook to whichever pool is initialized first: no check of the initializer or of the paired currency, so any non-atomic deployment (the repository's own DeployWin.run()) lets "},{"citation":"resolved","description":"Merged from audit_permissions #5, audit_math #2, audit_flow #2, audit_economics #3 and write_foundry_tests #1 (whose proof I ran: it fails on this tree with 'a buy below the displayed minimum became the leader'). afterSwap runs _finalizeIfExpired() before reading minimumBuy(), so a buy arriving at or after the deadline is judged against the next round's fresh minimum: escalator 1, bank minus the prize just credited, 5% prize share instead of 20% when round 1 is the one expiring. The views have no such step: between the deadline and settle()/the next buy, minimumBuy() keeps the dead round's escalator and prize share, nextPrize() the dead round's prize, roundNumber() and leader() the dead round. The NatSpec 'Gross IMD (fee included) a buy must spend right now to become the leader' and the brief's 'current minimum buy' view are therefore wrong in that window, which lasts until someone acts: honest users are told to pay several times the real price, and a bot that computes the post-close value opens the next round at the true, lower minimum. No funds are at risk and the displayed value is never lower than the enforced one. Fix: when roundActive && block.timestamp >= deadline, compute minimumBuy(), nextPrize(), roundNumber() and gameState() from the projected post-close state (bank - pending prize, next round's prize share and number, escalator = WAD), the same order afterSwap uses, and expose a flag so the UI can show 'round N ended, waiting for settle'.","line":391,"path":"src/WinGameHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {WinToken} from \"src/WinToken.sol\";\nimport {WinGameHook} from \"src/WinGameHook.sol\";\n\n/// Minimal stand-in for IMD (plain 18-decimal ERC-20).\ncontract ProofImd {\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// Proof: while a round has expired but has not been settled, `minimumBuy()` (and\n/// `gameState().minimumBuy`) report a minimum that is higher than the one a buy is actually judged\n/// against. A buy one wei UNDER the displayed minimum takes the lead of the next round.\ncontract ProofStaleMinimumBuyView is Test {\n    uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);\n\n    PoolManager manager;\n    ProofImd imd;\n    WinToken win;\n    WinGameHook hook;\n    PoolSwapTest router;\n    PoolKey key;\n    bool winIs0;\n    address alice = address(0xA11CE);\n    address bob = address(0xB0B);\n    address carol = address(0xCA201);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        manager = new PoolManager(address(this));\n        imd = new ProofImd();\n        win = new WinToken();\n        bytes memory initCode = abi.encodePacked(type(WinGameHook).creationCode, abi.encode(manager, address(win)));\n        bytes32 initHash = keccak256(initCode);\n        for (uint256 i = 0;; i++) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & ((1 << 14) - 1) != FLAGS) continue;\n            hook = new WinGameHook{salt: bytes32(i)}(IPoolManager(address(manager)), address(win));\n            break;\n        }\n        winIs0 = address(win) < address(imd);\n        (address c0, address c1) = winIs0 ? (address(win), address(imd)) : (address(imd), address(win));\n        key = PoolKey(Currency.wrap(c0), Currency.wrap(c1), 3_000, 60, IHooks(address(hook)));\n        router = new PoolSwapTest(manager);\n        PoolModifyLiquidityTest lp = new PoolModifyLiquidityTest(manager);\n        manager.initialize(key, TickMath.getSqrtPriceAtTick(winIs0 ? int24(-129_000) : int24(129_000)));\n        win.approve(address(lp), type(uint256).max);\n        // WIN-only liquidity on the side of the price that holds WIN.\n        (int24 lower, int24 upper) = winIs0\n            ? (int24(-128_940), TickMath.maxUsableTick(60))\n            : (TickMath.minUsableTick(60), int24(128_940));\n        lp.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, 1e24, 0), \"\");\n\n        address[3] memory who = [alice, bob, carol];\n        for (uint256 i = 0; i < 3; i++) {\n            imd.mint(who[i], 1_000_000 ether);\n            vm.prank(who[i]);\n            imd.approve(address(router), type(uint256).max);\n        }\n    }\n\n    function buy(address who, uint256 imdAmount) internal {\n        bool zeroForOne = !winIs0;\n        vm.prank(who, who);\n        router.swap(\n            key,\n            SwapParams(zeroForOne, -int256(imdAmount), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            abi.encode(who)\n        );\n    }\n\n    function test_displayedMinimumIsTheMinimumABuyIsJudgedAgainst() public {\n        vm.warp(block.timestamp + 30 minutes); // base fee\n        buy(alice, hook.minimumBuy()); // opens round 1\n        buy(bob, hook.minimumBuy()); // second qualifying buy: minimum is now 8.5 * 1.05^2\n        vm.warp(hook.deadline()); // round 1 is over; nobody has called settle() yet\n\n        uint256 displayed = hook.minimumBuy();\n        assertEq(hook.gameState().minimumBuy, displayed);\n\n        // \"Gross IMD (fee included) a buy must spend right now to become the leader\": one wei less\n        // must therefore NOT make carol the leader.\n        buy(carol, displayed - 1);\n        assertTrue(hook.leader() != carol, \"a buy below the displayed minimum became the leader\");\n    }\n}","reproduction":"Fixture: finish round 1 (buy, warp 3h, settle); alice makes 5 consecutive buys of hook.minimumBuy() in round 2 (escalator 1.05^5); vm.warp(hook.deadline()); hook.minimumBuy() returns 10.848393281250000000 IMD and roundNumber() 2. bob buyExactIn(8.5 ether), below the shown minimum. Expected per the view: not a qualifying buy. Actual: round 2 closes for alice, bob is leader and roundNumber() == 3. Attached proof (base fee, two qualifying buys in round 1, warp to the deadline, carol buys displayed - 1 wei and becomes leader) fails on this tree and passes once the view accounts for the pending close. Also test/scratch/Repro.t.sol::test_f5_staleMinimumAfterExpiry.","severity":"low","snippet":"    function minimumBuy() public view returns (uint256) {\n        uint256 base = nextPrize() * MIN_BUY_PRIZE_BPS / BPS;\n        if (base < MIN_BUY_FLOOR) base = MIN_BUY_FLOOR;\n        return base * escalator / WAD;\n    }","title":"minimumBuy(), nextPrize() and roundNumber() describe the expired round while it is unsettled, so the website overstates the minimum (by 1.05^n and up to a further 5x after round 1) in exactly the wind"},{"citation":"resolved","description":"Merged from audit_permissions #6 and audit_math #4. The qualifying amount is the gross IMD of one swap; nothing ties the lead to keeping the position, so a buyer can sell the WIN back, in the same transaction if they wish (inside one unlock the two deltas net out and only the fees are funded). The real price of a timer reset is 2 x 3% hook fee + 2 x 0.3% LP fee + slippage, about 6.5% of the minimum after decay, and 90% of the hook fees land in the bank the buyer is competing for. In later rounds the minimum is 1% of the bank and the prize 5%, so a reset costs ~0.065% of the bank against a prize 77x larger; the escalation still ends every round (the cost stays exponential) but bites roughly 89 overtakes in rather than the ~34 REVIEW A4 computes. README s5 says a buy-and-sell-back 'gains nothing'; it gains the lead and a full 10-minute timer, and it is what makes finding 2's floor-regime front-run and unattended-hours farming (open a round for 0.065% of the bank, collect 5% if nobody answers in 10 minutes) cheap. This is the brief's rule as literally written, so the fix is a requester decision: accept and document the true cost (and size the floor for it), void the lead if the leader's address sells during the round, or make part of a qualifying buy non-recoverable.","line":294,"path":"src/WinGameHook.sol","reproduction":"Fixture, after round 1 is settled (fee 3%): min = hook.minimumBuy() = 8.5 IMD. d = buyExactIn(bob, min); sellExactIn(bob, WIN received). Expected per README s5: nothing gained. Actual: hook.leader() == bob with deadline = now + 10 min, bob holds 0 WIN, and bob's IMD balance is down by 0.550177922494261455 IMD (6.47% of the minimum). Reproduced in test/scratch/Repro.t.sol::test_f6_buyAndSellBack.","severity":"low","snippet":"                gross = specified;","title":"Qualification is measured in swap volume, which is recoverable: a buy at the minimum sold straight back takes the lead for ~6.5% of the stated minimum (0.55 IMD instead of 8.5), contradicting README s"},{"citation":"resolved","description":"Merged from audit_math #3 and write_foundry_tests #3. MIN_BUY_FLOOR is compared with raw IMD amounts (`gross >= minimum`), so it means 8.5 IMD only if the paired currency has 18 decimals. beforeInitialize takes whatever is paired with WIN as `imd` and never reads decimals(); launch.json pins pairedCurrency 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 but no chain, and nothing in the tree verifies that token's decimals (the fork suite's test_fork_imdHasEighteenDecimals only runs with WIN_FORK_RPC_URL set, which the verifier does not). If that IMD has fewer than 18 decimals no buy ever qualifies, no round ever starts, settle() reverts NoActiveRound, and because the bank can only leave through a round prize and there is no owner, 90% of every trading fee is locked in the hook permanently. Low because I could not verify the real token's decimals offline and the check is one line: in beforeInitialize require decimals() == 18 on the paired ERC-20, or store a floor of 85 * 10**decimals / 10 at initialization.","line":65,"path":"src/WinGameHook.sol","reproduction":"hook.MIN_BUY_FLOOR() == 8_500_000_000_000_000_000 raw units regardless of the paired token (test/scratch/Repro.t.sol::test_f7_minimumIsRawConstant). Initialize WIN / X where X has decimals() == 6 (beforeInitialize accepts it: it checks only the LP fee and that one side is WIN): minimumBuy() returns 8.5e18 raw = 8,500,000,000,000 whole X. A buy of 100 X (100_000_000 raw) pays its fee into bank but gross >= minimum is false, so roundActive stays false forever and no function can move bank. Expected: floor of 8.5 X (8_500_000 raw) or a revert at initialization.","severity":"low","snippet":"    uint256 public constant MIN_BUY_FLOOR = 8.5 ether;","title":"The 8.5 IMD floor is the raw constant 8.5e18 and beforeInitialize never checks the paired currency's decimals; with a non-18-decimal IMD the floor is unreachable and 90% of every fee is locked forever"},{"citation":"resolved","description":"From audit_permissions #7. The brief: '10% to the team wallet ... (pull-based, the team claims it)'. The function has no caller check, so the share is pushed to TEAM_WALLET whenever any third party chooses. Funds cannot be redirected; the consequences are that the team does not control the timing of receipt and that, if 0x611F...4511 is a contract wallet not yet deployed on the launch chain, IMD can be forced onto a code-less address before the team is ready. REVIEW C7 accepts this; recorded because it departs from the letter of the brief. Fix if the letter matters: require(msg.sender == TEAM_WALLET), a fixed-address check, not an owner power.","line":356,"path":"src/WinGameHook.sol","reproduction":"After any fee-bearing swap (teamOwed > 0): vm.prank(address(0xBEEF)); hook.claimTeamFees(). Expected under 'the team claims it': revert for a caller that is not the team wallet. Actual: succeeds and transfers teamOwed IMD to 0x611F08c7226591708B5F53F29BF53f3830D54511 (the existing test_teamFees_arePullBasedToFixedWallet exercises exactly this path from a third party).","severity":"info","snippet":"    function claimTeamFees() external nonReentrant {","title":"claimTeamFees() is open to any caller although the brief makes the claim the team's own action"},{"citation":"resolved","description":"From audit_economics #4. The brief asks that block stuffing be explained and given a defence. README s5 and REVIEW A3 explain and quantify it (break-even: bank > 20x the stuffing cost) but the contract contains nothing that changes the arithmetic: flat 10-minute timer, 5% of an unbounded bank, no absolute prize cap and no link between the prize and what the leader paid. On a 2-second L2 ten minutes of full blocks can cost well under a thousand dollars, so the attack pays at a bank in the low tens of thousands of IMD. Recorded as a design residual the author accepted, beside the chain choice this tree does not pin. Mechanical options, each a requester decision: cap each round's prize at a multiple of the leader's qualifying buy, or at an absolute amount; lengthen the timer as the bank grows; require a minimum number of blocks as well as seconds between the last qualifying buy and settlement.","line":522,"path":"src/WinGameHook.sol","reproduction":"State, not a failing input: bank B, prize P = 0.05 B. Attacker makes a qualifying buy of max(8.5, 0.01 B) IMD, then fills every block for 600 s; no competing buy lands; attacker calls settle() at the deadline and receives P. Profitable whenever P exceeds the gas spent, i.e. B > 20 x (cost of ~300 full blocks at 2 s), independent of anything the hook does.","severity":"info","snippet":"        uint64 newDeadline = uint64(block.timestamp + ROUND_DURATION);","title":"Block stuffing has no mechanical defence: the prize is 5% of an uncapped bank and the timer a flat 10 minutes, so on a low-gas chain a stuffing bot wins once the bank exceeds ~20x the cost of ten minu"},{"citation":"resolved","description":"From audit_flow #4, updated for the tree as it stands: a fork suite now exists (test/WinGameHook.Fork.t.sol, four tests) and is honest about skipping (forge reports 2 skipped), but the brief's 'Foundry tests on a fork of the selected chain' is satisfied only when someone runs it with WIN_FORK_RPC_URL, WIN_FORK_POOL_MANAGER and WIN_FORK_IMD set, which the offline verifier does not. The assumptions findings 1, 4 and 8 depend on (IMD decimals and transfer semantics, the token ordering against the real IMD address, the deployed PoolManager matching the vendored v4-core) are therefore still open. Not a code defect; recorded so admission can require the rehearsal (initialize + single-sided seed on the real manager, first buy at 50% with zero IMD in the manager, sell, settle, claimTeamFees to the real team wallet) before funds are at stake.","line":42,"path":"test/WinGameHook.Fork.t.sol","reproduction":"`forge test` on this tree: 186 passed, 2 skipped; the two skipped are WinGameHookForkTest (both orientations), whose setUp returns at vm.skip(true) because WIN_FORK_RPC_URL is unset. No other test calls vm.createSelectFork.","severity":"info","snippet":"            vm.skip(true);","title":"The brief's fork tests are not exercised by the verifier: the fork suite skips itself without WIN_FORK_RPC_URL, so the real PoolManager, the real IMD (decimals, transfer behaviour) and the launch rehe"}],"hash":"e34fdc2a6b272ecc6cffe45559fa3feb20943695206d35d71492fa8922a77e0a","nodeId":"1ce4f137-e226-41c3-89ef-4a79151b56d1","outcome":"completed","summary":"Done. `git status` is clean apart from ignored paths, so no tracked file was touched; `.imd-findings.json` holds 11 findings and 13 coverage rows (all 9 entry points plus 4 invariants), and every path/line/snippet was verified against the tree.\n\n## Summary of the review\n\n**Reproduced and kept (specialist findings, merged):**\n\n| # | Sev | Finding | Merged from |\n|---|---|---|---|\n| 2 | medium | A short buy is silently executed and still charged; live minimum lets the leader void an exact-minimum challenger for dust (three triggers: live bank drift, 5% re-qualify + sell-back, exact-output price shift) | permissions #1, math #1, flow #1, economics #1+#2 |\n| 3 | medium | `tx.origin` fallback pays the prize to the bundler/relayer/keeper; router `msgSender()` never consulted | permissions #2, flow #3 |\n| 4 | low | `settle()` reverts for everyone when the leader can't be credited | permissions #3 |\n| 5 | low | `beforeInitialize` accepts any initializer / paired currency (non-atomic deploys only) | permissions #4, tests #2 |\n| 6 | low | Views report the expired round until settle (specialist's proof runs and fails as stated — attached) | 5 specialists |\n| 7 | low | Buy-and-sell-back takes the lead for ~6.5% of the minimum | permissions #6, math #4 |\n| 8 | low | 8.5e18 floor assumes 18-decimal IMD, unchecked | math #3, tests #3 |\n| 9–11 | info | open `claimTeamFees`, no mechanical block-stuffing defence, fork suite skips offline | — |\n\nAll reproductions were run in `test/scratch/Repro.t.sol` (9/9 pass, i.e. each attack succeeds on the current code), with the exact numbers quoted in the JSON.\n\n**New finding none of the four reported (#1, high, with proof):** `launch.json` carries one `initialPrice` (= `getSqrtPriceAtTick(-129000)`, so the math specialist's \"…231\" was off by one), which is the 2,500 IMD market cap *only if WIN's address sorts below the IMD address*. `beforeInitialize` ignores the `uint160` price entirely. In the other ordering — roughly 63% of random token addresses — the hook permanently binds to a pool where the first WIN sells at a **403,672,527,210,761 IMD** market cap, with no owner to fix it. The attached `Proof_Orientation.t.sol` fails on this tree for that reason and passes once `beforeInitialize` refuses a price that doesn't match the briefed cap for the observed ordering.\n\n**Dropped / folded:** the README sqrt misquote (math #5) was folded into finding 1; flow #4's \"no fork test\" was updated to reflect the fork suite that now exists but skips without an RPC.","treeHash":null,"usage":{"cachedInputTokens":2903063,"inputTokens":42,"model":"claude-fable-5-1","outputTokens":54729,"runtime":"claude","turns":29,"wallClockMs":759225}}],"verification":[{"checks":[{"durationMs":6456,"exitCode":0,"name":"build","output":"Compiling 87 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.33s\nCompiler run successful!\n","passed":true},{"durationMs":502,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 80722, ~: 81212)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 4.78ms (6.11ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 999909)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 799842)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 696467)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 19.36ms (1.83ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookUnpayableWinnerTest\n[PASS] test_claimTeamFeesStillRevertsWhenTheTeamWalletIsRefused() (gas: 710407)\n[PASS] test_settle_closesTheRoundAndDefersThePrizeWhenTheWinnerCannotBePaid() (gas: 1952906)\n[PASS] test_settle_paysDirectlyWhenTheWinnerCanBePaid() (gas: 1258023)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 29.23ms (2.74ms CPU time)\n\nRan 3 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployCanInitializeThePoolInTheSameCall() (gas: 190023991)\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 27075889)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 81.01ms (153.85ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 490988, ~: 505073)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 10999, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 668575, ~: 668728)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 490278)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412846)\n[PASS] test_feeDecay_appliesToTrades() (gas: 950380)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 768073)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 727065)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 463380)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 125103)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 583562)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 751033)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 736103)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 626097)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 99.18ms (94.97ms CPU time)\n\nRan 22 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8155)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 9686933)\n[PASS] test_disabledCallbacksRevert() (gas: 125670)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110045)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 219996143)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10690578)\n[PASS] test_initialize_acceptsPricesInsideTheTolerance() (gas: 212733849)\n[PASS] test_initialize_recordsLaunch() (gas: 36611)\n[PASS] test_initialize_refusesAPairedCurrencyWithoutEighteenDecimals() (gas: 11273073)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10885477)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 11090430)\n[PASS] test_initialize_refusesPricesOutsideTheTolerance() (gas: 10887973)\n[PASS] test_initialize_refusesThePriceOfTheOtherOrdering() (gas: 10854959)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52024)\n[PASS] test_launchPriceConstantsMatchTheManifest() (gas: 55848)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38814)\n[PASS] test_minimumBuyFloorIsEightAndAHalfWholeImd() (gas: 22108)\n[PASS] test_noAdminEntryPoints() (gas: 233800)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11047)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 8633155)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1099675)\n[PASS] test_settleInsideAManagerLockRevertsWithTheHooksOwnError() (gas: 890640)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 362.78ms (334.30ms CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5527642, ~: 5541333)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 469305)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 676116)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1250798)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 393000)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 937083)\n[PASS] test_gameState_matchesIndividualViews() (gas: 478229)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 397138)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1436420)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 399171)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1491764)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 648623)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 496787)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 399211)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 404750)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1442712)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1730886)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 551378)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 399629)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 397533)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1462667)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1247611)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 674089)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1020313)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 607045)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 903407)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1697087)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1330615)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1739098)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2099880)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1941835)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 268687)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2016644)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1525822)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566911)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1486309)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 405156)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368834)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619810)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1149692)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1623689)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678965)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1856550)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1282714)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1391383)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1461042)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1259860)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2557239)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1089541)\n[PASS] test_views_matchSettleOutcome() (gas: 1440760)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 362.67ms (384.68ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 490173, ~: 504127)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11002, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 667277, ~: 667512)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 489766)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 413426)\n[PASS] test_feeDecay_appliesToTrades() (gas: 948362)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 767964)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 726351)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 462615)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124968)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 582893)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 750902)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 734587)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 625620)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 362.92ms (162.63ms CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5545901, ~: 5551624)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 468531)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 675342)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1248780)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 392235)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 935065)\n[PASS] test_gameState_matchesIndividualViews() (gas: 477455)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 396364)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1435637)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 398397)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1490981)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 647228)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 494435)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 398437)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 403976)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1441929)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1729473)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 550604)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 398855)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 396759)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1460649)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1245593)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 672694)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1025173)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 605648)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 902094)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1693825)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1327974)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1735215)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2097237)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1937954)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 267894)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2012188)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1527546)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566819)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1483993)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 404382)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368060)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619094)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1148364)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1621321)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678191)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1853911)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1280694)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1388744)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1459033)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1257851)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2552125)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1087525)\n[PASS] test_views_matchSettleOutcome() (gas: 1438742)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 367.27ms (415.66ms CPU time)\n\nRan 9 test suites in 368.92ms (1.69s CPU time): 179 tests passed, 0 failed, 0 skipped (179 total tests)\n","passed":true},{"durationMs":76,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":426,\"docs/REVIEW.md\":287,\"foundry.toml\":28,\"remappings.txt\":3,\"script/DeployWin.s.sol\":73,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":874,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":91,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Game.t.sol\":954,\"test/WinGameHook.Security.t.sol\":553,\"test/WinToken.t.sol\":102,\"test/mocks/BlocklistERC20.sol\":21,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MsgSenderRouter.sol\":78,\"test/mocks/ReentrantERC20.sol\":23,\"test/mocks/SixDecimalERC20.sol\":45,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1661,"exitCode":0,"name":"slither","output":"[medium/medium] divide-before-multiply at src/WinGameHook.sol:502: WinGameHook.minimumBuy() (src/WinGameHook.sol#502-506) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/WinGameHook.sol:502: WinGameHook.minimumBuy() (src/WinGameHook.sol#502-506) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/WinGameHook.sol:637: WinGameHook._accrue(uint256) (src/WinGameHook.sol#637-642) uses a dangerous strict equality:\n[medium/medium] unused-return at src/WinGameHook.sol:718: WinGameHook._payout(address,uint256) (src/WinGameHook.sol#718-722) ignores return value by poolManager.unlock() (src/WinGameHook.sol#720)\n[medium/medium] unused-return at src/WinGameHook.sol:728: WinGameHook._tryPayout(address,uint256) (src/WinGameHook.sol#728-736) ignores return value by poolManager.unlock() (src/WinGameHook.sol#730-734)\n[low/medium] reentrancy-benign at src/WinGameHook.sol:728: Reentrancy in WinGameHook._tryPayout(address,uint256) (src/WinGameHook.sol#728-736):\n[low/medium] reentrancy-benign at src/WinGameHook.sol:718: Reentrancy in WinGameHook._payout(address,uint256) (src/WinGameHook.sol#718-722):\n[low/medium] reentrancy-benign at src/WinGameHook.sol:350: Reentrancy in WinGameHook._chargeAndPlay(SwapParams,BalanceDelta,address,bool) (src/WinGameHook.sol#350-369):\n[low/medium] reentrancy-events at src/WinGameHook.sol:350: Reentrancy in WinGameHook._chargeAndPlay(SwapParams,BalanceDelta,address,bool) (src/WinGameHook.sol#350-369):\n[low/medium] return-bomb at src/WinGameHook.sol:792: WinGameHook._routerMsgSender(address) (src/WinGameHook.sol#792-800) tries to limit the gas of an external call that controls implicit decoding\n[low/medium] timestamp at src/WinGameHook.sol:516: WinGameHook.roundNumber() (src/WinGameHook.sol#516-518) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:436: WinGameHook.settle() (src/WinGameHook.sol#436-453) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:373: WinGameHook._feeAndGross(SwapParams,BalanceDelta,bool,bool,uint256) (src/WinGameHook.sol#373-406) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:350: WinGameHook._chargeAndPlay(SwapParams,BalanceDelta,address,bool) (src/WinGameHook.sol#350-369) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:658: WinGameHook._qualify(address,uint256) (src/WinGameHook.sol#658-679) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:610: WinGameHook._feePipsAt(uint256) (src/WinGameHook.sol#610-616) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:681: WinGameHook._finalizeIfExpired() (src/WinGameHook.sol#681-683) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:521: WinGameHook.settleable() (src/WinGameHook.sol#521-523) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:637: WinGameHook._accrue(uint256) (src/WinGameHook.sol#637-642) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:586: WinGameHook.gameState() (src/WinGameHook.sol#586-604) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:411: WinGameHook._onBuy(address,bool,uint256,uint256) (src/WinGameHook.sol#411-425) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:509: WinGameHook.timeLeft() (src/WinGameHook.sol#509-512) uses timestamp for comparisons","passed":true},{"durationMs":504,"exitCode":0,"name":"aderyn","output":"[low] internal-function-used-once at src/HookFlags.sol:28: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/WinGameHook.sol:52: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/WinGameHook.sol:774: Literal Instead of Constant (13 places)\n[low] local-variable-shadowing at src/WinGameHook.sol:563: Local Variable Shadows State Variable (6 places)\n[low] missing-inheritance at src/WinToken.sol:9: Missing Inheritance\n[low] state-no-address-check at src/WinGameHook.sol:672: Address State Variable Set Without Checks (2 places)\n[low] unchecked-return at src/WinGameHook.sol:720: Unchecked Return\n[low] unused-public-function at src/WinGameHook.sol:306: Public Function Not Used Internally","passed":true},{"durationMs":2759,"exitCode":0,"name":"proof a30bd48ea51f","output":"Compiling 81 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.01s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-563d2fa6/Proof_a30bd48ea51f.t.sol:ProofOrientation\n[PASS] test_manifestInitialPriceWithWinAsCurrency1_isRefusedOrPricedAtTheBriefedCap() (gas: 70208)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.25ms (113.73µs CPU time)\n\nRan 1 test suite in 26.69ms (26.25ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7268cb1a98069e9fa027364da6af0e5df99d35faca1f4fff58e814af355b2072","verifiedTreeHash":"639aa277efd600d8c1847ea6744b3c3583a4ce85","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":4489,"exitCode":0,"name":"build","output":"Compiling 84 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.37s\nCompiler run successful!\n","passed":true},{"durationMs":497,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 80883, ~: 81206)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 8.41ms (7.28ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 996001)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 794714)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 693812)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 28.28ms (1.74ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 488136, ~: 502140)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 10994, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 665143, ~: 665360)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 487880)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412433)\n[PASS] test_feeDecay_appliesToTrades() (gas: 942032)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765625)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724120)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 460707)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124380)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 580440)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748693)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 732378)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 622948)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 99.24ms (183.95ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 489061, ~: 503100)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11001, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 666434, ~: 666565)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 488392)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 411853)\n[PASS] test_feeDecay_appliesToTrades() (gas: 944050)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765734)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724834)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 461472)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124515)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 581109)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748824)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 733894)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 623425)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 104.42ms (138.52ms CPU time)\n\nRan 2 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 19253318)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 331.61ms (62.76ms CPU time)\n\nRan 15 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8176)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 853451)\n[PASS] test_disabledCallbacksRevert() (gas: 125491)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110066)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 107656095)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10389023)\n[PASS] test_initialize_recordsLaunch() (gas: 36610)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10575305)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 10800730)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52001)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38770)\n[PASS] test_noAdminEntryPoints() (gas: 233732)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11069)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 5754798)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1141193)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 331.74ms (138.33ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5564017, ~: 5659700)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 461661)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673026)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1232903)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 390352)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 922369)\n[PASS] test_gameState_matchesIndividualViews() (gas: 466448)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 393697)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 396525)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 638757)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 396532)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 401390)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394203)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1446846)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1234974)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 663669)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1015255)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 602216)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 885668)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1676178)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1305724)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1685358)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2085089)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 366520)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 617746)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1142541)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1604640)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675171)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1838537)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1272035)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1372875)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1452129)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1251528)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 357.95ms (363.71ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5551366, ~: 5649461)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 462435)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673800)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1234921)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 391117)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 924387)\n[PASS] test_gameState_matchesIndividualViews() (gas: 467222)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 394471)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 397299)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 640152)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 397306)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 402164)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394977)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1448864)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1236992)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 665064)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1010395)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 603613)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 886981)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1679440)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1308365)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1689241)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2087732)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 367294)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 618462)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1143869)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1607008)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675945)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1841167)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1274053)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1375514)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1454138)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1253537)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 358.23ms (389.89ms CPU time)\n\nRan 8 test suites in 359.93ms (1.62s CPU time): 134 tests passed, 0 failed, 0 skipped (134 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":356,\"docs/REVIEW.md\":132,\"foundry.toml\":28,\"remappings.txt\":3,\"script/DeployWin.s.sol\":46,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":671,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":63,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Game.t.sol\":588,\"test/WinGameHook.Security.t.sol\":343,\"test/WinToken.t.sol\":102,\"test/mocks/MockERC20.sol\":60,\"test/mocks/ReentrantERC20.sol\":23,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1534,"exitCode":0,"name":"slither","output":"[medium/medium] divide-before-multiply at src/WinGameHook.sol:391: WinGameHook.minimumBuy() (src/WinGameHook.sol#391-395) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/WinGameHook.sol:506: WinGameHook._accrue(uint256) (src/WinGameHook.sol#506-511) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/WinGameHook.sol:286: WinGameHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).unspecifiedReturn (src/WinGameHook.sol#286) is a local variable never initialized\n[medium/medium] unused-return at src/WinGameHook.sol:581: WinGameHook._payout(address,uint256) (src/WinGameHook.sol#581-587) ignores return value by poolManager.unlock() (src/WinGameHook.sol#584)\n[low/medium] reentrancy-benign at src/WinGameHook.sol:272: Reentrancy in WinGameHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/WinGameHook.sol#272-332):\n[low/medium] reentrancy-benign at src/WinGameHook.sol:581: Reentrancy in WinGameHook._payout(address,uint256) (src/WinGameHook.sol#581-587):\n[low/medium] reentrancy-events at src/WinGameHook.sol:272: Reentrancy in WinGameHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/WinGameHook.sol#272-332):\n[low/medium] timestamp at src/WinGameHook.sol:409: WinGameHook.settleable() (src/WinGameHook.sol#409-411) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:521: WinGameHook._qualify(address,uint256) (src/WinGameHook.sol#521-542) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:340: WinGameHook.settle() (src/WinGameHook.sol#340-346) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:506: WinGameHook._accrue(uint256) (src/WinGameHook.sol#506-511) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:272: WinGameHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/WinGameHook.sol#272-332) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:479: WinGameHook._feePipsAt(uint256) (src/WinGameHook.sol#479-485) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:544: WinGameHook._finalizeIfExpired() (src/WinGameHook.sol#544-546) uses timestamp for comparisons\n[low/medium] timestamp at src/WinGameHook.sol:398: WinGameHook.timeLeft() (src/WinGameHook.sol#398-401) uses timestamp for comparisons","passed":true},{"durationMs":483,"exitCode":0,"name":"aderyn","output":"[low] internal-function-used-once at src/HookFlags.sol:28: Internal Function Used Only Once (3 places)\n[low] large-numeric-literal at src/WinGameHook.sol:43: Large Numeric Literal (6 places)\n[low] literal-instead-of-constant at src/WinGameHook.sol:599: Literal Instead of Constant (4 places)\n[low] local-variable-shadowing at src/WinGameHook.sol:445: Local Variable Shadows State Variable (6 places)\n[low] missing-inheritance at src/WinToken.sol:9: Missing Inheritance\n[low] state-no-address-check at src/WinGameHook.sol:535: Address State Variable Set Without Checks (2 places)\n[low] unchecked-return at src/WinGameHook.sol:584: Unchecked Return","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"730aa0bf0c35096134327dd9c402417dbc13f2f28b518c45809e7df5f8b09186","verifiedTreeHash":"1d80e5fdc52addd62e41878a434652a148f00649","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":10690,"exitCode":0,"name":"build","output":"Compiling 91 files with Solc 0.8.26\nSolc 0.8.26 finished in 10.57s\nCompiler run successful!\n","passed":true},{"durationMs":7987,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/WinGameHook.Fork.t.sol:WinGameHookForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 677.94µs (0.00ns CPU time)\n\nRan 1 test for test/WinGameHook.Fork.t.sol:WinGameHookForkFlippedTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 629.42µs (0.00ns CPU time)\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 80968, ~: 81236)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 3.93ms (6.20ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 999909)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 799842)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 696467)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 14.71ms (1.81ms CPU time)\n\nRan 2 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookSwapDuringPayoutTest\n[PASS] test_swapFromInsideThePrizePayout_isJustAnotherBuy() (gas: 1259565)\n[PASS] test_swapFromInsideTheTeamPayout_cannotTouchTheTeamShareTwice() (gas: 630007)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.32ms (1.32ms CPU time)\n\nRan 4 tests for test/WinGameHook.Fork.t.sol:WinGameHookRehearsalTest\n[PASS] test_rehearsal_firstBuyAtLaunchPaysHalfInFeeAndLeads() (gas: 499630)\n[PASS] test_rehearsal_fullLifecycle() (gas: 2522053)\n[PASS] test_rehearsal_hookIsBoundToTheManagerAndThePool() (gas: 94129)\n[PASS] test_rehearsal_imdHasEighteenDecimals() (gas: 5611)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 21.93ms (2.82ms CPU time)\n\nRan 4 tests for test/WinGameHook.Fork.t.sol:WinGameHookRehearsalFlippedTest\n[PASS] test_rehearsal_firstBuyAtLaunchPaysHalfInFeeAndLeads() (gas: 500408)\n[PASS] test_rehearsal_fullLifecycle() (gas: 2524362)\n[PASS] test_rehearsal_hookIsBoundToTheManagerAndThePool() (gas: 94057)\n[PASS] test_rehearsal_imdHasEighteenDecimals() (gas: 5611)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 34.91ms (2.07ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookUnpayableWinnerTest\n[PASS] test_claimTeamFeesStillRevertsWhenTheTeamWalletIsRefused() (gas: 710407)\n[PASS] test_settle_closesTheRoundAndDefersThePrizeWhenTheWinnerCannotBePaid() (gas: 1952906)\n[PASS] test_settle_paysDirectlyWhenTheWinnerCanBePaid() (gas: 1258023)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 93.65ms (2.84ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 491711, ~: 504877)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11012, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 668579, ~: 668728)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 490278)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412846)\n[PASS] test_feeDecay_appliesToTrades() (gas: 950380)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 768073)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 727065)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 463380)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 125103)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 583562)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 751033)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 736103)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 626097)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 260.88ms (408.70ms CPU time)\n\nRan 3 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployCanInitializeThePoolInTheSameCall() (gas: 190023991)\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 27075889)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 263.80ms (136.73ms CPU time)\n\nRan 21 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookAdversarialFlippedTest\n[PASS] testFuzz_firstRound_nothingClosesItBeforeThreeHours(uint256,uint256) (runs: 256, μ: 685197, ~: 694312)\n[PASS] testFuzz_minimum_exactAmountQualifies_oneWeiLessDoesNot(uint256,uint8) (runs: 256, μ: 2043515, ~: 2055624)\n[PASS] testFuzz_roundTrip_costsAtLeastBothFees(uint256) (runs: 256, μ: 874241, ~: 872003)\n[PASS] testFuzz_snipe_anyQualifyingBuyGivesTheFieldTenFullMinutes(uint256,uint256) (runs: 256, μ: 1647254, ~: 1647132)\n[PASS] test_callbacks_refuseAPoolThatIsNotTheirs() (gas: 71593)\n[PASS] test_callbacks_refuseSwapsBeforeThePoolExists() (gas: 39735312)\n[PASS] test_claimPrize_accumulatesAcrossLazilyClosedRounds() (gas: 1915362)\n[PASS] test_claimPrize_revertsForSomeoneWhoNeverWon() (gas: 725899)\n[PASS] test_claimPrize_theCurrentLeaderCannotPullBeforeTheRoundCloses() (gas: 933311)\n[PASS] test_claimTeamFees_leavesBankAndUnclaimedPrizesAlone() (gas: 1395090)\n[PASS] test_claimTeamFees_revertsWhenEmptyAndOnTheSecondCall() (gas: 618312)\n[PASS] test_donatedImdNeverBecomesPrizeMoney() (gas: 1239053)\n[PASS] test_dustTradesDoNotRevertOrBreakAccounting() (gas: 702115)\n[PASS] test_fee_splittingAnOrderSavesAtMostOneWeiPerSwap() (gas: 2277974)\n[PASS] test_gas_closingARoundDoesNotGetDearerWithHistory() (gas: 13276996)\n[PASS] test_identity_contractBuyerNamedInHookDataIsPaid() (gas: 1646587)\n[PASS] test_identity_namingSomeoneCannotTakeOrMoveTheLead() (gas: 1790573)\n[PASS] test_race_lastQualifyingBuyInTheBlockLeads_andEachOneEscalates() (gas: 1658441)\n[PASS] test_race_twoBuysAtTheSameMinimumInOneBlock_onlyTheFirstLeads() (gas: 1180884)\n[PASS] test_sell_largerThanThePoolsImd_feeOnlyOnWhatWasPaidOut() (gas: 1721785)\n[PASS] test_settle_oneSecondEarlyRevertsWithTheDeadline_thenSucceedsOnTheDeadline() (gas: 1197697)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 263.88ms (542.76ms CPU time)\n\nRan 21 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookAdversarialTest\n[PASS] testFuzz_firstRound_nothingClosesItBeforeThreeHours(uint256,uint256) (runs: 256, μ: 683983, ~: 693115)\n[PASS] testFuzz_minimum_exactAmountQualifies_oneWeiLessDoesNot(uint256,uint8) (runs: 256, μ: 2050012, ~: 2052346)\n[PASS] testFuzz_roundTrip_costsAtLeastBothFees(uint256) (runs: 256, μ: 873832, ~: 871496)\n[PASS] testFuzz_snipe_anyQualifyingBuyGivesTheFieldTenFullMinutes(uint256,uint256) (runs: 256, μ: 1644764, ~: 1644514)\n[PASS] test_callbacks_refuseAPoolThatIsNotTheirs() (gas: 71593)\n[PASS] test_callbacks_refuseSwapsBeforeThePoolExists() (gas: 44963455)\n[PASS] test_claimPrize_accumulatesAcrossLazilyClosedRounds() (gas: 1912719)\n[PASS] test_claimPrize_revertsForSomeoneWhoNeverWon() (gas: 725125)\n[PASS] test_claimPrize_theCurrentLeaderCannotPullBeforeTheRoundCloses() (gas: 931914)\n[PASS] test_claimTeamFees_leavesBankAndUnclaimedPrizesAlone() (gas: 1393081)\n[PASS] test_claimTeamFees_revertsWhenEmptyAndOnTheSecondCall() (gas: 617835)\n[PASS] test_donatedImdNeverBecomesPrizeMoney() (gas: 1237656)\n[PASS] test_dustTradesDoNotRevertOrBreakAccounting() (gas: 700888)\n[PASS] test_fee_splittingAnOrderSavesAtMostOneWeiPerSwap() (gas: 2270849)\n[PASS] test_gas_closingARoundDoesNotGetDearerWithHistory() (gas: 13251367)\n[PASS] test_identity_contractBuyerNamedInHookDataIsPaid() (gas: 1645190)\n[PASS] test_identity_namingSomeoneCannotTakeOrMoveTheLead() (gas: 1787311)\n[PASS] test_race_lastQualifyingBuyInTheBlockLeads_andEachOneEscalates() (gas: 1655181)\n[PASS] test_race_twoBuysAtTheSameMinimumInOneBlock_onlyTheFirstLeads() (gas: 1178851)\n[PASS] test_sell_largerThanThePoolsImd_feeOnlyOnWhatWasPaidOut() (gas: 1719189)\n[PASS] test_settle_oneSecondEarlyRevertsWithTheDeadline_thenSucceedsOnTheDeadline() (gas: 1196300)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 263.88ms (871.57ms CPU time)\n\nRan 22 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8155)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 9686933)\n[PASS] test_disabledCallbacksRevert() (gas: 125670)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110045)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 219996143)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10690578)\n[PASS] test_initialize_acceptsPricesInsideTheTolerance() (gas: 212733849)\n[PASS] test_initialize_recordsLaunch() (gas: 36611)\n[PASS] test_initialize_refusesAPairedCurrencyWithoutEighteenDecimals() (gas: 11273073)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10885477)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 11090430)\n[PASS] test_initialize_refusesPricesOutsideTheTolerance() (gas: 10887973)\n[PASS] test_initialize_refusesThePriceOfTheOtherOrdering() (gas: 10854959)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52024)\n[PASS] test_launchPriceConstantsMatchTheManifest() (gas: 55848)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38814)\n[PASS] test_minimumBuyFloorIsEightAndAHalfWholeImd() (gas: 22108)\n[PASS] test_noAdminEntryPoints() (gas: 233800)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11047)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 8633155)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1099675)\n[PASS] test_settleInsideAManagerLockRevertsWithTheHooksOwnError() (gas: 890640)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 346.36ms (291.35ms CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5534511, ~: 5535854)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 469305)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 676116)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1250798)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 393000)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 937083)\n[PASS] test_gameState_matchesIndividualViews() (gas: 478229)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 397138)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1436420)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 399171)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1491764)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 648623)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 496787)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 399211)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 404750)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1442712)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1730886)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 551378)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 399629)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 397533)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1462667)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1247611)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 674089)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1020313)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 607045)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 903407)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1697087)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1330615)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1739098)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2099880)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1941835)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 268687)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2016644)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1525822)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566911)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1486309)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 405156)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368834)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619810)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1149692)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1623689)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678965)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1856550)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1282714)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1391383)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1461042)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1259860)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2557239)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1089541)\n[PASS] test_views_matchSettleOutcome() (gas: 1440760)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 358.47ms (368.93ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 490395, ~: 503902)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11016, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 667285, ~: 667512)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 489766)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 413426)\n[PASS] test_feeDecay_appliesToTrades() (gas: 948362)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 767964)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 726351)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 462615)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124968)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 582893)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 750902)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 734587)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 625620)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 368.96ms (1.02s CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5530952, ~: 5544990)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 468531)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 675342)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1248780)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 392235)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 935065)\n[PASS] test_gameState_matchesIndividualViews() (gas: 477455)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 396364)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1435637)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 398397)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1490981)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 647228)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 494435)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 398437)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 403976)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1441929)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1729473)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 550604)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 398855)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 396759)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1460649)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1245593)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 672694)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1025173)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 605648)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 902094)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1693825)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1327974)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1735215)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2097237)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1937954)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 267894)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2012188)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1527546)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566819)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1483993)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 404382)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368060)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619094)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1148364)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1621321)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678191)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1853911)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1280694)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1388744)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1459033)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1257851)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2552125)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1087525)\n[PASS] test_views_matchSettleOutcome() (gas: 1438742)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 368.95ms (387.28ms CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookInvariantTest\n[PASS]\nWinGameHookInvariantTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookInvariantTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 920   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 914   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 951   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 924   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 897   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 889   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 901   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 926   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 879   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 921   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 892   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 8088902)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.49s (7.48s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookLaterRoundsInvariantFlippedTest\n[PASS]\nWinGameHookLaterRoundsInvariantFlippedTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookLaterRoundsInvariantFlippedTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 920   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 914   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 951   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 924   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 897   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 889   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 901   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 926   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 879   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 921   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 892   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 7669685)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.68s (7.65s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookInvariantFlippedTest\n[PASS]\nWinGameHookInvariantFlippedTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookInvariantFlippedTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 920   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 914   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 951   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 924   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 897   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 889   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 901   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 926   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 879   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 921   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 892   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 8099601)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.75s (7.72s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookLaterRoundsInvariantTest\n[PASS]\nWinGameHookLaterRoundsInvariantTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookLaterRoundsInvariantTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 920   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 914   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 951   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 924   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 897   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 889   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 901   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 926   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 932   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 879   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 921   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 892   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 7658692)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.87s (7.85s CPU time)\n\nRan 20 test suites in 7.87s (33.47s CPU time): 239 tests passed, 0 failed, 2 skipped (241 total tests)\n","passed":true},{"durationMs":65,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":426,\"docs/REVIEW.md\":287,\"foundry.toml\":28,\"remappings.txt\":3,\"script/DeployWin.s.sol\":73,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":874,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":91,\"test/WinGameHook.Adversarial.t.sol\":569,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Fork.t.sol\":258,\"test/WinGameHook.Game.t.sol\":954,\"test/WinGameHook.Security.t.sol\":553,\"test/WinToken.t.sol\":102,\"test/invariant/WinGameHandler.sol\":803,\"test/invariant/WinGameHook.Invariant.t.sol\":448,\"test/mocks/BlocklistERC20.sol\":21,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MsgSenderRouter.sol\":78,\"test/mocks/ReentrantERC20.sol\":23,\"test/mocks/SixDecimalERC20.sol\":45,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"878a141502937229edf1c970a3d010bd7c9fb67bc4b6c36398331fd4fc6be510","verifiedTreeHash":"1298331586049940ca25dcdea26379fe1c8576ee","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":12200,"exitCode":0,"name":"build","output":"Compiling 84 files with Solc 0.8.26\nSolc 0.8.26 finished in 11.85s\nCompiler run successful!\n","passed":true},{"durationMs":1530,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 81176, ~: 81212)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 34.76ms (33.17ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 996001)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 794714)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 693812)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 124.86ms (4.63ms CPU time)\n\nRan 15 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8176)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 853451)\n[PASS] test_disabledCallbacksRevert() (gas: 125491)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110066)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 107656095)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10389023)\n[PASS] test_initialize_recordsLaunch() (gas: 36610)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10575305)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 10800730)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52001)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38770)\n[PASS] test_noAdminEntryPoints() (gas: 233732)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11069)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 5754798)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1141193)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 206.50ms (314.78ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 488051, ~: 502818)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 10982, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 665124, ~: 665361)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 487880)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412433)\n[PASS] test_feeDecay_appliesToTrades() (gas: 942032)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765625)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724120)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 460707)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124380)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 580440)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748693)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 732378)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 622948)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 216.88ms (261.05ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5602910, ~: 5636326)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 461661)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673026)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1232903)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 390352)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 922369)\n[PASS] test_gameState_matchesIndividualViews() (gas: 466448)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 393697)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 396525)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 638757)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 396532)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 401390)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394203)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1446846)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1234974)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 663669)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1015255)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 602216)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 885668)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1676178)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1305724)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1685358)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2085089)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 366520)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 617746)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1142541)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1604640)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675171)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1838537)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1272035)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1372875)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1452129)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1251528)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 999.64ms (959.57ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 488837, ~: 503448)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 10992, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 666416, ~: 666564)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 488392)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 411853)\n[PASS] test_feeDecay_appliesToTrades() (gas: 944050)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765734)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724834)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 461472)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124515)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 581109)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748824)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 733894)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 623425)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 1.18s (401.71ms CPU time)\n\nRan 2 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 19253318)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.18s (198.91ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5605406, ~: 5618874)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 462435)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673800)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1234921)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 391117)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 924387)\n[PASS] test_gameState_matchesIndividualViews() (gas: 467222)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 394471)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 397299)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 640152)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 397306)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 402164)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394977)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1448864)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1236992)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 665064)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1010395)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 603613)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 886981)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1679440)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1308365)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1689241)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2087732)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 367294)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 618462)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1143869)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1607008)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675945)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1841167)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1274053)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1375514)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1454138)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1253537)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 1.18s (1.26s CPU time)\n\nRan 8 test suites in 1.19s (5.13s CPU time): 134 tests passed, 0 failed, 0 skipped (134 total tests)\n","passed":true},{"durationMs":189,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":356,\"docs/REVIEW.md\":132,\"foundry.toml\":28,\"launch.json\":30,\"remappings.txt\":3,\"script/DeployWin.s.sol\":46,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":671,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":63,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Game.t.sol\":588,\"test/WinGameHook.Security.t.sol\":343,\"test/WinToken.t.sol\":102,\"test/mocks/MockERC20.sol\":60,\"test/mocks/ReentrantERC20.sol\":23,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"bf1578c867035bedbffdfdc8ccce7da3a6afd49fe35475fca7ee112561d50616","verifiedTreeHash":"b81d8d411591008a4f8fe2a37dc508483e135ed3","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":8919,"exitCode":0,"name":"build","output":"Compiling 88 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.82s\nCompiler run successful!\n","passed":true},{"durationMs":7542,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/WinGameHook.Fork.t.sol:WinGameHookForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 761.88µs (0.00ns CPU time)\n\nRan 1 test for test/WinGameHook.Fork.t.sol:WinGameHookForkFlippedTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.12ms (0.00ns CPU time)\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 80829, ~: 81236)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 4.06ms (6.05ms CPU time)\n\nRan 2 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookSwapDuringPayoutTest\n[PASS] test_swapFromInsideThePrizePayout_isJustAnotherBuy() (gas: 1252118)\n[PASS] test_swapFromInsideTheTeamPayout_cannotTouchTheTeamShareTwice() (gas: 627335)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 20.55ms (1.04ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 996001)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 794714)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 693812)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 39.28ms (2.46ms CPU time)\n\nRan 2 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 19253318)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 56.50ms (62.12ms CPU time)\n\nRan 15 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8176)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 853451)\n[PASS] test_disabledCallbacksRevert() (gas: 125491)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110066)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 107656095)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10389023)\n[PASS] test_initialize_recordsLaunch() (gas: 36610)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10575305)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 10800730)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52001)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38770)\n[PASS] test_noAdminEntryPoints() (gas: 233732)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11069)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 5754798)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1141193)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 80.55ms (117.74ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5526888, ~: 5534038)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 462435)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673800)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1234921)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 391117)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 924387)\n[PASS] test_gameState_matchesIndividualViews() (gas: 467222)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 394471)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 397299)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 640152)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 397306)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 402164)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394977)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1448864)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1236992)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 665064)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1010395)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 603613)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 886981)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1679440)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1308365)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1689241)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2087732)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 367294)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 618462)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1143869)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1607008)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675945)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1841167)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1274053)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1375514)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1454138)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1253537)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 406.87ms (428.96ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 488151, ~: 502224)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11014, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 665118, ~: 665359)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 487880)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412433)\n[PASS] test_feeDecay_appliesToTrades() (gas: 942032)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765625)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724120)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 460707)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124380)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 580440)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748693)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 732378)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 622948)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 417.22ms (823.81ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 489202, ~: 503170)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11022, ~: 11225)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 666404, ~: 666562)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 488392)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 411853)\n[PASS] test_feeDecay_appliesToTrades() (gas: 944050)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 71094)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 765734)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 724834)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 461472)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124515)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 581109)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 748824)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 733894)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 623425)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 417.20ms (616.15ms CPU time)\n\nRan 21 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookAdversarialTest\n[PASS] testFuzz_firstRound_nothingClosesItBeforeThreeHours(uint256,uint256) (runs: 256, μ: 676850, ~: 672985)\n[PASS] testFuzz_minimum_exactAmountQualifies_oneWeiLessDoesNot(uint256,uint8) (runs: 256, μ: 1998945, ~: 1901479)\n[PASS] testFuzz_roundTrip_costsAtLeastBothFees(uint256) (runs: 256, μ: 868539, ~: 867063)\n[PASS] testFuzz_snipe_anyQualifyingBuyGivesTheFieldTenFullMinutes(uint256,uint256) (runs: 256, μ: 1628505, ~: 1628253)\n[PASS] test_callbacks_refuseAPoolThatIsNotTheirs() (gas: 71592)\n[PASS] test_callbacks_refuseSwapsBeforeThePoolExists() (gas: 152483959)\n[PASS] test_claimPrize_accumulatesAcrossLazilyClosedRounds() (gas: 1862536)\n[PASS] test_claimPrize_revertsForSomeoneWhoNeverWon() (gas: 722159)\n[PASS] test_claimPrize_theCurrentLeaderCannotPullBeforeTheRoundCloses() (gas: 926478)\n[PASS] test_claimTeamFees_leavesBankAndUnclaimedPrizesAlone() (gas: 1386124)\n[PASS] test_claimTeamFees_revertsWhenEmptyAndOnTheSecondCall() (gas: 615228)\n[PASS] test_donatedImdNeverBecomesPrizeMoney() (gas: 1228158)\n[PASS] test_dustTradesDoNotRevertOrBreakAccounting() (gas: 697951)\n[PASS] test_fee_splittingAnOrderSavesAtMostOneWeiPerSwap() (gas: 2259286)\n[PASS] test_gas_closingARoundDoesNotGetDearerWithHistory() (gas: 13123220)\n[PASS] test_identity_contractBuyerNamedInHookDataIsPaid() (gas: 1636603)\n[PASS] test_identity_namingSomeoneCannotTakeOrMoveTheLead() (gas: 1770596)\n[PASS] test_race_lastQualifyingBuyInTheBlockLeads_andEachOneEscalates() (gas: 1634207)\n[PASS] test_race_twoBuysAtTheSameMinimumInOneBlock_onlyTheFirstLeads() (gas: 1172037)\n[PASS] test_sell_largerThanThePoolsImd_feeOnlyOnWhatWasPaidOut() (gas: 1714714)\n[PASS] test_settle_oneSecondEarlyRevertsWithTheDeadline_thenSucceedsOnTheDeadline() (gas: 1190394)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 417.38ms (1.05s CPU time)\n\nRan 21 tests for test/WinGameHook.Adversarial.t.sol:WinGameHookAdversarialFlippedTest\n[PASS] testFuzz_firstRound_nothingClosesItBeforeThreeHours(uint256,uint256) (runs: 256, μ: 678067, ~: 674372)\n[PASS] testFuzz_minimum_exactAmountQualifies_oneWeiLessDoesNot(uint256,uint8) (runs: 256, μ: 2018317, ~: 1991172)\n[PASS] testFuzz_roundTrip_costsAtLeastBothFees(uint256) (runs: 256, μ: 869178, ~: 867570)\n[PASS] testFuzz_snipe_anyQualifyingBuyGivesTheFieldTenFullMinutes(uint256,uint256) (runs: 256, μ: 1631007, ~: 1630892)\n[PASS] test_callbacks_refuseAPoolThatIsNotTheirs() (gas: 71592)\n[PASS] test_callbacks_refuseSwapsBeforeThePoolExists() (gas: 46505994)\n[PASS] test_claimPrize_accumulatesAcrossLazilyClosedRounds() (gas: 1865177)\n[PASS] test_claimPrize_revertsForSomeoneWhoNeverWon() (gas: 722933)\n[PASS] test_claimPrize_theCurrentLeaderCannotPullBeforeTheRoundCloses() (gas: 927875)\n[PASS] test_claimTeamFees_leavesBankAndUnclaimedPrizesAlone() (gas: 1388133)\n[PASS] test_claimTeamFees_revertsWhenEmptyAndOnTheSecondCall() (gas: 615705)\n[PASS] test_donatedImdNeverBecomesPrizeMoney() (gas: 1229555)\n[PASS] test_dustTradesDoNotRevertOrBreakAccounting() (gas: 699178)\n[PASS] test_fee_splittingAnOrderSavesAtMostOneWeiPerSwap() (gas: 2266411)\n[PASS] test_gas_closingARoundDoesNotGetDearerWithHistory() (gas: 13148821)\n[PASS] test_identity_contractBuyerNamedInHookDataIsPaid() (gas: 1638000)\n[PASS] test_identity_namingSomeoneCannotTakeOrMoveTheLead() (gas: 1773858)\n[PASS] test_race_lastQualifyingBuyInTheBlockLeads_andEachOneEscalates() (gas: 1637467)\n[PASS] test_race_twoBuysAtTheSameMinimumInOneBlock_onlyTheFirstLeads() (gas: 1174070)\n[PASS] test_sell_largerThanThePoolsImd_feeOnlyOnWhatWasPaidOut() (gas: 1717310)\n[PASS] test_settle_oneSecondEarlyRevertsWithTheDeadline_thenSucceedsOnTheDeadline() (gas: 1191791)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 446.36ms (975.00ms CPU time)\n\nRan 36 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5498525, ~: 5529868)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 461661)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 673026)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1232903)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 390352)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 922369)\n[PASS] test_gameState_matchesIndividualViews() (gas: 466448)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 393697)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 396525)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 638757)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 396532)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 401390)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 394203)\n[PASS] test_initialState() (gas: 77599)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1446846)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1234974)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 663669)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1015255)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 602216)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 885668)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1676178)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1305724)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1685358)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2085089)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 366520)\n[PASS] test_poolViews() (gas: 43059)\n[PASS] test_sellsNeverStartARound() (gas: 617746)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1142541)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1604640)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36626)\n[PASS] test_settle_twiceReverts() (gas: 675171)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1838537)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1272035)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1372875)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1452129)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1251528)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 446.33ms (437.52ms CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookInvariantTest\n[PASS]\nWinGameHookInvariantTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookInvariantTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 880   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 927   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 876   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 892   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 925   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 906   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 913   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 950   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 953   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 942   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 918   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 6938049)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.79s (6.77s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookInvariantFlippedTest\n[PASS]\nWinGameHookInvariantFlippedTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookInvariantFlippedTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 880   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 927   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 876   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 892   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 925   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 906   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 913   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 950   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 953   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 942   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 918   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 6946865)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.86s (6.85s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookLaterRoundsInvariantFlippedTest\n[PASS]\nWinGameHookLaterRoundsInvariantFlippedTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookLaterRoundsInvariantFlippedTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 880   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 927   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 876   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 892   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 925   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 906   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 913   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 950   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 953   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 942   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 918   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 6520318)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.05s (7.03s CPU time)\n\nRan 2 tests for test/invariant/WinGameHook.Invariant.t.sol:WinGameHookLaterRoundsInvariantTest\n[PASS]\nWinGameHookLaterRoundsInvariantTest invariants:\n[PASS] invariant_game\n[PASS] invariant_money\n WinGameHookLaterRoundsInvariantTest invariants (runs: 256, calls: 12800, reverts: 0)\n\n╭----------------+-----------------+-------+---------+----------╮\n| Contract       | Selector        | Calls | Reverts | Discards |\n+===============================================================+\n| WinGameHandler | buyAtThreshold  | 922   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactIn      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | buyExactOut     | 880   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimPrize      | 898   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | claimTeamFees   | 927   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateClaims    | 876   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | donateRaw       | 892   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | modifyLiquidity | 925   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | pokeCallbacks   | 906   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactIn     | 913   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | sellExactOut    | 950   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | settle          | 953   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | snipe           | 942   | 0       | 0        |\n|----------------+-----------------+-------+---------+----------|\n| WinGameHandler | warp            | 918   | 0       | 0        |\n╰----------------+-----------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesEveryPath() (gas: 6511317)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.42s (7.38s CPU time)\n\nRan 17 test suites in 7.42s (30.87s CPU time): 186 tests passed, 0 failed, 2 skipped (188 total tests)\n","passed":true},{"durationMs":69,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":356,\"docs/REVIEW.md\":132,\"foundry.toml\":28,\"remappings.txt\":3,\"script/DeployWin.s.sol\":46,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":671,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":63,\"test/WinGameHook.Adversarial.t.sol\":563,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Fork.t.sol\":220,\"test/WinGameHook.Game.t.sol\":588,\"test/WinGameHook.Security.t.sol\":343,\"test/WinToken.t.sol\":102,\"test/invariant/WinGameHandler.sol\":709,\"test/invariant/WinGameHook.Invariant.t.sol\":397,\"test/mocks/MockERC20.sol\":60,\"test/mocks/ReentrantERC20.sol\":23,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"ce604e47e26e3564cc99b7c0a9568825574d7c69bedbf5f800cb3bb0d85c8739","verifiedTreeHash":"8a7e70d48b4d042310bdd0fcc831bf3d0274ec0b","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":5224,"exitCode":0,"name":"build","output":"Compiling 87 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.14s\nCompiler run successful!\n","passed":true},{"durationMs":421,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/WinToken.t.sol:WinTokenTest\n[PASS] testFuzz_transfer(address,uint256) (runs: 256, μ: 80857, ~: 81200)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 114980)\n[PASS] test_metadata() (gas: 23736)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 23673)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 202412)\n[PASS] test_revertsOnInsufficientBalance() (gas: 35196)\n[PASS] test_revertsOnTransferToZero() (gas: 29885)\n[PASS] test_runtimeHasNoDelegatecallOrSelfdestruct() (gas: 358434)\n[PASS] test_transferFromRespectsAllowance() (gas: 153496)\n[PASS] test_transferMovesExactAmount() (gas: 80225)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 3.10ms (4.62ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookReentrancyTest\n[PASS] test_lazyPrizeClaimCannotBeReentered() (gas: 999909)\n[PASS] test_prizePayoutCannotBeReentered() (gas: 799842)\n[PASS] test_teamPayoutCannotBeReentered() (gas: 696467)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 15.32ms (1.74ms CPU time)\n\nRan 3 tests for test/WinGameHook.Security.t.sol:WinGameHookUnpayableWinnerTest\n[PASS] test_claimTeamFeesStillRevertsWhenTheTeamWalletIsRefused() (gas: 710407)\n[PASS] test_settle_closesTheRoundAndDefersThePrizeWhenTheWinnerCannotBePaid() (gas: 1952906)\n[PASS] test_settle_paysDirectlyWhenTheWinnerCanBePaid() (gas: 1258023)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 17.68ms (3.02ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesFlippedTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 487432, ~: 504853)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 11001, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 668565, ~: 668728)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 490278)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 412846)\n[PASS] test_feeDecay_appliesToTrades() (gas: 950380)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 768073)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 727065)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 463380)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 125103)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 583562)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 751033)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 736103)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 626097)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 80.73ms (99.47ms CPU time)\n\nRan 3 tests for test/DeployWin.t.sol:DeployWinTest\n[PASS] test_deployCanInitializeThePoolInTheSameCall() (gas: 190023991)\n[PASS] test_deployProducesTokenAndHookBoundToThePoolManager() (gas: 27075889)\n[PASS] test_mineSaltIsDeterministicAndFindsTheFlags() (gas: 45615333)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 83.02ms (147.01ms CPU time)\n\nRan 16 tests for test/WinGameHook.Fees.t.sol:WinGameHookFeesTest\n[PASS] testFuzz_buyExactIn_feeIsRateOfGross(uint256,uint256) (runs: 256, μ: 486487, ~: 503945)\n[PASS] testFuzz_feeDecay_neverOutsideBounds(uint256) (runs: 256, μ: 10998, ~: 11203)\n[PASS] testFuzz_sellExactOut_userGetsExactlyWhatWasAsked(uint256) (runs: 256, μ: 667266, ~: 667525)\n[PASS] test_buyExactIn_feeTakenFromImdInput() (gas: 489766)\n[PASS] test_buyExactOut_feeChargedOnTopInImd() (gas: 413426)\n[PASS] test_feeDecay_appliesToTrades() (gas: 948362)\n[PASS] test_feeDecay_isLinearFrom50To3PercentOver30Minutes() (gas: 70940)\n[PASS] test_feeSplit_90PercentBank_10PercentTeam() (gas: 767964)\n[PASS] test_feesAtLaunchRateOnBothSides() (gas: 726351)\n[PASS] test_firstBuyWorksWhenManagerHoldsNoImd() (gas: 462615)\n[PASS] test_partialFill_exactInBuyReverts() (gas: 124968)\n[PASS] test_partialFill_exactOutSellReverts() (gas: 582893)\n[PASS] test_sellExactIn_feeTakenFromImdOutput() (gas: 750902)\n[PASS] test_sellExactOut_feeChargedOnTopInImd() (gas: 734587)\n[PASS] test_teamFees_arePullBasedToFixedWallet() (gas: 625620)\n[PASS] test_teamWalletIsTheBriefsAddress() (gas: 5627)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 312.47ms (360.04ms CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameFlippedTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5520305, ~: 5537313)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 469305)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 676116)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1250798)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 393000)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 937083)\n[PASS] test_gameState_matchesIndividualViews() (gas: 478229)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 397138)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1436420)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 399171)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1491764)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 648623)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 496787)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 399211)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 404750)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1442712)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1730886)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 551378)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 399629)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 397533)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1462667)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1247611)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 674089)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1020313)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 607045)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 903407)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1697087)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1330615)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1739098)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2099880)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1941835)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 268687)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2016644)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1525822)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566911)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1486309)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 405156)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368834)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619810)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1149692)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1623689)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678965)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1856550)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1282714)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1391383)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1461042)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1259860)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2557239)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1089541)\n[PASS] test_views_matchSettleOutcome() (gas: 1440760)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 314.88ms (328.01ms CPU time)\n\nRan 22 tests for test/WinGameHook.Security.t.sol:WinGameHookSecurityTest\n[PASS] test_constructorRefusesAddressWithoutTheFlags() (gas: 8155)\n[PASS] test_constructorRefusesZeroOrCodelessToken() (gas: 9686933)\n[PASS] test_disabledCallbacksRevert() (gas: 125670)\n[PASS] test_enabledCallbacksRefuseCallersOtherThanThePoolManager() (gas: 110045)\n[PASS] test_initialize_acceptsEveryLaunchTier() (gas: 219996143)\n[PASS] test_initialize_acceptsNativePairAndReadsItAsTheOtherCurrency() (gas: 10690578)\n[PASS] test_initialize_acceptsPricesInsideTheTolerance() (gas: 212733849)\n[PASS] test_initialize_recordsLaunch() (gas: 36611)\n[PASS] test_initialize_refusesAPairedCurrencyWithoutEighteenDecimals() (gas: 11273073)\n[PASS] test_initialize_refusesFeesOutsideTheLaunchTiers() (gas: 10885477)\n[PASS] test_initialize_refusesPoolWithoutWin() (gas: 11090430)\n[PASS] test_initialize_refusesPricesOutsideTheTolerance() (gas: 10887973)\n[PASS] test_initialize_refusesThePriceOfTheOtherOrdering() (gas: 10854959)\n[PASS] test_initialize_secondPoolWithThisHookIsRefused() (gas: 52024)\n[PASS] test_launchPriceConstantsMatchTheManifest() (gas: 55848)\n[PASS] test_managerCannotBeMadeToCallUnlockCallbackOutsideAPayout() (gas: 38814)\n[PASS] test_minimumBuyFloorIsEightAndAHalfWholeImd() (gas: 22108)\n[PASS] test_noAdminEntryPoints() (gas: 233800)\n[PASS] test_permissionsMatchTheMinedAddress() (gas: 11047)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 8633155)\n[PASS] test_settleCannotRunInsideAManagerLock() (gas: 1099675)\n[PASS] test_settleInsideAManagerLockRevertsWithTheHooksOwnError() (gas: 890640)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 319.37ms (304.85ms CPU time)\n\nRan 53 tests for test/WinGameHook.Game.t.sol:WinGameHookGameTest\n[PASS] testFuzz_fundsAreConserved(uint256) (runs: 256, μ: 5515468, ~: 5529173)\n[PASS] test_firstQualifyingBuyStartsRoundOne() (gas: 468531)\n[PASS] test_firstRound_cannotSettleBeforeThreeHours() (gas: 675342)\n[PASS] test_firstRound_prizeIsTwentyPercentOfBank() (gas: 1248780)\n[PASS] test_firstRound_qualifyingBuyAtLaunchPays50PercentFeeAndLeads() (gas: 392235)\n[PASS] test_firstRound_resetsRespectTheThreeHourFloor() (gas: 935065)\n[PASS] test_gameState_matchesIndividualViews() (gas: 477455)\n[PASS] test_identity_emptyHookDataFallsBackToTxOrigin() (gas: 396364)\n[PASS] test_identity_hookDataBeatsRouterMsgSender() (gas: 1435637)\n[PASS] test_identity_hookDataNamesTheBuyer() (gas: 398397)\n[PASS] test_identity_lyingRouterCanOnlyGiftTheLead() (gas: 1490981)\n[PASS] test_identity_malformedHookDataFallsBackToTxOrigin() (gas: 647228)\n[PASS] test_identity_malformedSixtyFourBytePayloadReverts() (gas: 494435)\n[PASS] test_identity_packedTwentyByteAddressIsAccepted() (gas: 398437)\n[PASS] test_identity_routerIsNeverTheLeader() (gas: 403976)\n[PASS] test_identity_routerMsgSenderIsUsedBeforeTxOrigin() (gas: 1441929)\n[PASS] test_identity_routerThatRevertsOrReturnsGarbageFallsBackToTxOrigin() (gas: 1729473)\n[PASS] test_identity_routerWithoutMsgSenderCreditsTheSigner() (gas: 550604)\n[PASS] test_identity_sixtyFourBytePayloadNamesTheBuyerWithoutTheFlag() (gas: 398855)\n[PASS] test_identity_zeroAddressFallsBackToTxOrigin() (gas: 396759)\n[PASS] test_initialState() (gas: 78540)\n[PASS] test_laterRounds_prizeIsFivePercent() (gas: 1460649)\n[PASS] test_laterRounds_tenMinuteTimerAndResets() (gas: 1245593)\n[PASS] test_minimumBuy_belowMinimumChangesNothingButTheBank() (gas: 672694)\n[PASS] test_minimumBuy_escalatorIsCapped() (gas: 1025173)\n[PASS] test_minimumBuy_floorIsEightAndAHalfImd() (gas: 605648)\n[PASS] test_minimumBuy_followsTwentyPercentOfPrizeWhenLarger() (gas: 902094)\n[PASS] test_minimumBuy_resetsEvenWhenRoundClosesLazily() (gas: 1693825)\n[PASS] test_minimumBuy_resetsWhenNewRoundStarts() (gas: 1327974)\n[PASS] test_minimumBuy_risesFivePercentPerQualifyingBuy() (gas: 1735215)\n[PASS] test_multipleRoundsAccumulateWinners() (gas: 2097237)\n[PASS] test_mustLead_buyAfterExpiryIsJudgedAgainstTheNextRound() (gas: 1937954)\n[PASS] test_mustLead_buyBelowTheMinimumRevertsAndCostsNoFee() (gas: 267894)\n[PASS] test_mustLead_dustBuyThatRaisesTheMinimumVoidsTheChallengeWithoutAFee() (gas: 2012188)\n[PASS] test_mustLead_exactOutputBuyUndercutByASellRevertsWithoutAFee() (gas: 1527546)\n[PASS] test_mustLead_flagOnASellReverts() (gas: 566819)\n[PASS] test_mustLead_leaderRequalifyingAheadVoidsTheChallengeWithoutAFee() (gas: 1483993)\n[PASS] test_mustLead_qualifyingBuyLeads() (gas: 404382)\n[PASS] test_nonQualifyingBuyFeedsBankButStartsNoRound() (gas: 368060)\n[PASS] test_poolViews() (gas: 43037)\n[PASS] test_sellsNeverStartARound() (gas: 619094)\n[PASS] test_sells_afterExpiryDoNotCloseTheRound() (gas: 1148364)\n[PASS] test_sells_neverResetTimerOrChangeLeader() (gas: 1621321)\n[PASS] test_settle_revertsWithoutActiveRound() (gas: 36605)\n[PASS] test_settle_twiceReverts() (gas: 678191)\n[PASS] test_snipe_buyAfterDeadlineCannotStealTheRound() (gas: 1853911)\n[PASS] test_snipe_buyExactlyAtDeadlineBelongsToNextRound() (gas: 1280694)\n[PASS] test_snipe_buyOneSecondBeforeDeadlineRestartsTheFullTimer() (gas: 1388744)\n[PASS] test_snipe_settleAndLazyCloseAgreeOnThePrize() (gas: 1459033)\n[PASS] test_snipe_tinyBuyAfterDeadlineStillClosesTheRoundForTheLeader() (gas: 1257851)\n[PASS] test_views_describeTheNextRoundOnceTheTimerHasRunOut() (gas: 2552125)\n[PASS] test_views_firstRoundWaitingForSettleShowsSecondRoundTerms() (gas: 1087525)\n[PASS] test_views_matchSettleOutcome() (gas: 1438742)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 320.86ms (350.37ms CPU time)\n\nRan 9 test suites in 321.87ms (1.47s CPU time): 179 tests passed, 0 failed, 0 skipped (179 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"WinGameHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WinGameHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WinGameHook.claimPrize(address)\",\"WinGameHook.claimTeamFees()\",\"WinGameHook.settle()\",\"WinGameHook.unlockCallback(bytes)\",\"WinToken.approve(address,uint256)\",\"WinToken.transfer(address,uint256)\",\"WinToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":426,\"docs/REVIEW.md\":287,\"foundry.toml\":28,\"launch.json\":30,\"remappings.txt\":3,\"script/DeployWin.s.sol\":73,\"src/HookFlags.sol\":58,\"src/WinGameHook.sol\":874,\"src/WinToken.sol\":68,\"test/DeployWin.t.sol\":91,\"test/WinGameHook.Fees.t.sol\":238,\"test/WinGameHook.Game.t.sol\":954,\"test/WinGameHook.Security.t.sol\":553,\"test/WinToken.t.sol\":102,\"test/mocks/BlocklistERC20.sol\":21,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MsgSenderRouter.sol\":78,\"test/mocks/ReentrantERC20.sol\":23,\"test/mocks/SixDecimalERC20.sol\":45,\"test/utils/WinGameFixture.sol\":231},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d87bef714571b7e14587ab2313f25089ec3da3639f60606de5c8378732b4a243","verifiedTreeHash":"88692e80f90371be424ee3dffdd119c19c207159","verifierVersion":"0.1.0+e6140b7a"}]}